Python学习笔记

Python学习笔记(视频:韩顺平教育) O.0

一、Python中文文档

1.文档

官方中文文档参考

二、Pycharm使用技巧

1.快捷键

  1. 快捷键操作示意图:
快捷键效果
Ctrl+C/V/X拷贝/黏贴/剪切
Ctrl+F查找
Ctrl+R替换
Ctrl+D复制当前行
Shift+Delete删除当前行
Ctrl+/添加注释和取消注释
Ctrl+alt+L快速格式化代码
ctrl+H查看一个类的层级关系(对继承非常有用)
  1. 自定义快捷键:(这里我用run来演示):

在这里插入图片描述

右键修改你想要的快捷键,然后记得确定即可

2.断点调试

在这里插入图片描述
在这里插入图片描述

三、转义字符

1.功能一览

转义字符说明
\t一个制表符,实现对齐的功能
\n换行符
\反斜杠
单引号
"双引号
\r回车符
print("jack\t20") #/t制表符
print("hello jack\nhello tom") #\n换行符
print("D:\\Python\\转义字符") #转义输出\
print("我说:\"人生苦短,我用Python\"") #转义输出""
print("Hello World\rPython") #回车符会将光标移到当前行的开头,且不会换行

以上内容输出如下:

jack	20
hello jack
hello tom
D:\Python\转义字符
我说:"人生苦短,我用Python"
Python

四、注释(comment)

1.单行注释

#这是一个单行注释

2.多行注释

"""
这是一个多行注释
"""

3.文件编码声明注释(文件开头)

# coding: <encoding-name>(例如utf-8)

[!NOTE]

多行注释不要有多行注释嵌套

4.代码规范

  1. 注释

    • 如果注释函数或者其中的某个步骤,使用单行注释。
  2. 正确的缩进和空白

    • 使用一次 tab 操作,实现缩进,默认整体向右移动。使用 shift + tab 整体向左移

    • = 两边习惯性各加一个空格比较规整

    • 变量之间使用逗号隔开比较清晰

五、变量(variable)

1.基本使用

a = 60  # 定义变量
b = '男'
c = 90.4
print("a的值是", a, "类型是", type(a))  # 变量的输出
print(b, c)

输出为

a的值是 60 类型是 <class 'int'>

2.格式化输出

(1)%操作符
gender = "男"
age = 18
score = 99.5
print("个人信息:%s %d %.2f" % (gender, age, score)) #注意类型要一一对应
格式说明符描述示例输出
%s字符串"%s" % "Alice"Alice
%d十进制整数"%d" % 3030
%f浮点数(六位)"%f" % 99.599.500000
%.2f浮点数(两位小数)"%.2f" % 99.599.50
%e指数形式"%e" % 99.59.950000e+01
%x十六进制整数"%x" % 255ff
%o八进制整数"%o" % 810
%10s宽度为10的字符串"%10s" % "Alice"Alice
%-10s左对齐宽度10的字符串"%-10s" % "Alice"Alice
%04d宽度4的整数,左侧补零"%04d" % 420042
%+8.2f宽度8,保留两位小数,显示正负号"%+8.2f" % 1234.567+1234.57
(2)format()函数
gender = "男"
age = 18
score = 99.5
print("个人信息:{} {} {}".format(gender, age, score))
(3)f-strings(推荐使用)
gender = "男"
age = 18
score = 99.5
print(f"个人信息:{gender} {age} {score}")

3.加号的使用(数字与字符串)

  • 当左右两边都是数值型时,则做加法运算
name = "king"
score = 50.8
print(score + 90)
  • 当左右两边都是字符串,则做拼接运算
print("100" + "98")
  • 当左为字符串,右为数值型,则报错 TypeError: can only concatenate str (not "int") to str
print("100" + 3)

4.整型的进制表示

进制前缀
十进制
十六进制0x
八进制0o
二进制0b

5.整型的大小

python的整型是变长的,字节数随着数字增大而增大,每次增量是4个字节,可以使用**sys.getsizeof()** 来返回数据的大小(按照字节单位返回)

import sys
n1 = 0
print(sys.getsizeof(n1), "类型", type(n1))

6.浮点数的计算

浮点数类型计算会存在精度的损失,可以使用Decimal类进行精确计算

from decimal import Decimal
b = 8.1 / 3
c = Decimal("8.1") / Decimal("3")
print("b=", b)
print("c=", c)

输出结果如下:

b= 2.6999999999999997
c= 2.7

7.字符串的特殊输出

  1. 用三个单引号 '''内容''' 或三个双引号 """内容""" 可以使字符串内容保持原样输出,在输出格式复杂的内容时比较有用,例如输出一段代码:
code = '''def hello():
    print("Hello, World!")
'''
print(code) 

输出如下:

def hello():
    print("Hello, World!")
  1. 在字符串前面加 r 可以使整个字符串不被转义
raw_string = r'C:\new_folder\test.txt'  #\n不再被翻译为换行
print(raw_string)

8.字符串驻留机制

Python 仅保存一份相同且不可变字符串,不同的值被存放在字符串的驻留池中。Python 的驻留机制对相同的字符串只保留一份拷贝,后续创建相同字符串时,不会开辟新空间,而是把该字符串的地址赋给新创建的变量

str1 = "Hello"
str2 = "Hello"
str3 = "Hello"

# 打印变量地址,其中id()函数可以返回数据的内存地址
print(f"Address of str1: {id(str1)}")
print(f"Address of str2: {id(str2)}")
print(f"Address of str3: {id(str3)}")

# 比较字符串地址是否相同
print(f"str1 is str2: {str1 is str2}")
print(f"str1 is str3: {str1 is str3}")
print(f"str2 is str3: {str2 is str3}")

输出如下:

Address of str1: 2163951620080
Address of str2: 2163951620080
Address of str3: 2163951620080
str1 is str2: True
str1 is str3: True
str2 is str3: True

在此代码中,我们可以看到 str1str2str3 的地址是相同的,表明它们共享同一个字符串对象

驻留机制几种情况讨论(注意:需要在交互模式下(win + r -> python)进行测试,因为Pycharm对字符串进行了优化处理

  • 字符串是由26个英文字母大小写、0-9、_组成
a = "hello"
b = "hello"
print(a is b)  # True
  • 字符串长度为0或1时,此时特殊字符也行:
a = ""
b = ""
print(a is b)  # True
a = "a"
b = "a"
print(a is b)  # True
  • 字符串在编译时进行驻留,而非运行时:
# 编译期驻留
a = "hello" + "world"
b = "helloworld"
print(a is b)  # True

# 运行期驻留
part1 = "hello"
part2 = "world"
c = part1 + part2
print(b is c)  # False

ab 都是在编译期确定的字符串,因此它们指向同一个驻留字符串。

c 是在运行时通过字符串拼接生成的,因此它是一个新的字符串对象,与 b 不同。

  • [-5, 256]的整数数字:
a = 100
b = 100
print(a is b)  # True
a = 300
b = 300
print(a is b)  # False

短字符串和符合标识符命名规则的字符串通常会自动驻留。对于较长字符串或不符合标识符命名规则的字符串,可以使用 sys.intern() 函数强制驻留:

import sys
# 强制字符串驻留
long_str1 = sys.intern("This is a long string that we want to intern.")
long_str2 = sys.intern("This is a long string that we want to intern.")
print(f"long_str1 is long_str2: {long_str1 is long_str2}")

9.隐式类型转换

(1)数字类型之间的转换

在算术运算中,Python 会自动将较低精度的数字类型转换为较高精度的数字类型

# 整数和浮点数之间的隐式转换
a = 5        # 整数
b = 3.2      # 浮点数
result = a + b
print(result)       # 输出 8.2
print(type(result)) # 输出 <class 'float'>

在这个例子中,a 是整数类型,而 b 是浮点数类型。当它们相加时,Python 自动将 a 转换为浮点数,以匹配 b 的类型,结果是一个浮点数

(2)布尔类型和数字类型之间的转换

布尔类型 TrueFalse 在数值运算中会自动转换为 10

# 布尔类型和整数之间的隐式转换
a = True
b = 2

result = a + b
print(result)       # 输出 3
print(type(result)) # 输出 <class 'int'>

在这个例子中,True 被隐式转换为 1,然后与整数 2 相加。

(3)字符串和数字类型的拼接

在拼接字符串和数字时,Python 需要显式转换,隐式转换会导致错误。

# 字符串和数字之间的拼接
a = "Age: "
b = 25
# 隐式转换会导致错误
# result = a + b  # TypeError: can only concatenate str (not "int") to str
# 显式转换
result = a + str(b)
print(result)  # 输出 "Age: 25"

在这个例子中,我们必须显式地将整数 b 转换为字符串才能进行拼接。

10.显式类型转换

(1)转换为整数类型(int

可以将一个数值、字符串或布尔值转换为整数

# 将浮点数转换为整数
float_num = 3.14
int_num = int(float_num)
print(int_num)  # 输出 3
# 将字符串转换为整数
str_num = "42"
int_num = int(str_num)
print(int_num)  # 输出 42
# 将布尔值转换为整数
bool_val = True
int_num = int(bool_val)
print(int_num)  # 输出 1
(2)转换为浮点数类型(float

可以将一个整数、字符串或布尔值转换为浮点数。

# 将整数转换为浮点数
int_num = 42
float_num = float(int_num)
print(float_num)  # 输出 42.0
# 将字符串转换为浮点数
str_num = "3.14"
float_num = float(str_num)
print(float_num)  # 输出 3.14
# 将布尔值转换为浮点数
bool_val = False
float_num = float(bool_val)
print(float_num)  # 输出 0.0
(3)转换为字符串类型(str

可以将任何对象转换为字符串

# 将整数转换为字符串
int_num = 42
str_val = str(int_num)
print(str_val)  # 输出 "42"
# 将浮点数转换为字符串
float_num = 3.14
str_val = str(float_num)
print(str_val)  # 输出 "3.14"
# 将布尔值转换为字符串
bool_val = True
str_val = str(bool_val)
print(str_val)  # 输出 "True"
(4)转换为布尔类型(bool

可以将任何对象转换为布尔值。空值(如 00.0""None)会转换为 False,其余的值转换为 True

# 将整数转换为布尔值
int_num = 0
bool_val = bool(int_num)
print(bool_val)  # 输出 False
# 将浮点数转换为布尔值
float_num = 0.0
bool_val = bool(float_num)
print(bool_val)  # 输出 False
# 将字符串转换为布尔值
str_val = ""
bool_val = bool(str_val)
print(bool_val)  # 输出 False
# 将非空字符串转换为布尔值
str_val = "Hello"
bool_val = bool(str_val)
print(bool_val)  # 输出 True
(5)几点注意事项
  1. 类型转换的有效性:并不是所有的类型转换都是有效的。例如,将非数字字符串转换为整数或浮点数会导致 ValueError 异常。
str_val = "abc"
try:
    int_val = int(str_val)
except ValueError:
    print("转换失败")
  1. 数据精度:转换可能会导致数据的精度损失。例如,将浮点数转换为整数会截断小数部分。

  2. 对一个变量进行强制转换后并不会影响原变量的数据类型(即:不会影响原变量指向的数据/值的数据类型)

六、算术运算符

运算符名称描述示例
+加法对两个操作数进行加法运算3 + 2 结果为 5
-减法对两个操作数进行减法运算3 - 2 结果为 1
*乘法对两个操作数进行乘法运算3 * 2 结果为 6
/除法对两个操作数进行除法运算,结果为浮点数3 / 2 结果为 1.5
//整数除法对两个操作数进行除法运算,结果为整数(向下取整)3 // 2 结果为 1
%取余对两个操作数进行取余运算3 % 2 结果为 1
**幂运算对第一个操作数进行幂运算,底数为第一个操作数,指数为第二个操作数3 ** 2 结果为 9
-取负对操作数进行取负运算-3 结果为 -3
+正号对操作数不进行任何操作(主要用于表示正数)+3 结果为 3

注:对一个数取模时,对应的运算公式是:a % b = a - a // b * b,不要小看这个公式,比如计算 -10 % 3,输出结果并非-1,而是 2;然而计算 10 % -3 ,输出结果是 -2

七、比较运算符

运算符名称描述示例
==等于判断两个值是否相等3 == 2 结果为 False
!=不等于判断两个值是否不相等3 != 2 结果为 True
>大于判断左边的值是否大于右边的值3 > 2 结果为 True
<小于判断左边的值是否小于右边的值3 < 2 结果为 False
>=大于等于判断左边的值是否大于或等于右边的值3 >= 2 结果为 True
<=小于等于判断左边的值是否小于或等于右边的值3 <= 2 结果为 False
is身份运算符判断两个对象是否是同一个对象a is b 结果取决于 ab 的引用,即是否指向同一个数据空间,下同
is not身份运算符判断两个对象是否不是同一个对象a is not b 结果取决于 ab 的引用

八、逻辑/布尔运算符

运算符名称描述示例结果
and逻辑与当两个条件都为真时,结果为真True and FalseFalse
or逻辑或当至少一个条件为真时,结果为真True or FalseTrue
not逻辑非对条件取反not TrueFalse
  1. and 运算符:如果第一个操作数为 False,将不会评估第二个操作数,结果为 False

  2. or 运算符:如果第一个操作数为 True,将不会评估第二个操作数,结果为 True

九、赋值运算符

运算符描述示例解释
=简单赋值运算符a = 5将右边的值赋给左边的变量
+=加法赋值运算符a += 3等同于 a = a + 3
-=减法赋值运算符a -= 2等同于 a = a - 2
*=乘法赋值运算符a *= 4等同于 a = a * 4
/=除法赋值运算符a /= 2等同于 a = a / 2
//=整数除法赋值运算符a //= 3等同于 a = a // 3
%=取余赋值运算符a %= 2等同于 a = a % 2
**=幂赋值运算符a **= 3等同于 a = a ** 3

十、位运算符

运算符名称描述示例结果
&按位与对应位都为 1 时结果为 1,否则为 05 & 31
``按位或对应位有一个为 1 时结果为 1,否则为 0`5
^按位异或对应位不同则结果为 1,否则为 05 ^ 36
~按位取反每个位取反,即 0 变为 1,1 变为 0~5-6
<<左移各个位左移指定数量的位,右边补 05 << 110
>>右移各个位右移指定数量的位,左边补符号位(对于有符号数)或 0(对于无符号数)5 >> 12

注:计算机不管做什么运算都是用补码,结果都是看原码

十一、三元运算符

三元运算符语法为:

max = a if a > b else b

如果 a>b 成立,就把 a 作为整个表达式的值,并赋给变量 max

如果 a>b 不成立,就把 b 作为整个表达式的值,并赋给变量 max

十二、运算符的优先级

由高到低如下图所示:

优先级运算符描述示例
()圆括号中的表达式(3 + 2) * 4
**乘方2 ** 3
*, @, /, //, %乘法、矩阵乘法、除法、整数除法、取余3 * 2, 10 / 2, 5 % 2
+, -加法减法3 + 2, 5 - 2
>>, <<右移、左移运算符(移位)1 << 2
&按位与3 & 2
^按位异或3 ^ 2
|按位或`3
in, not in, is, is not, <, <=, >, >=, !=, ==比较运算,成员检测,标识号检测x in y, x is y, 3 < 5
not布尔逻辑非not x
and布尔逻辑与x and y
or布尔逻辑或x or y
=, %=, /=, //=, -=, +=, *=, **=赋值运算符a = 5, a += 2

综上可得:

()> 算术运算 > 位运算 > 比较运算 > 逻辑运算 > 赋值运算

十三、标识符的命名规则

1.标识符命名规则

  1. 大小写敏感:区分大小写。例如,myVariablemyvariable 是不同的标识符。

  2. 字符集:必须以字母(a-z 或 A-Z)或下划线(_)开头,后续字符可以是字母、数字(0-9)或下划线。

  3. 不能使用关键字:标识符不能是 Python 的关键字。

2.非法标识符示例

  1. 非法标识符:

    • 1variable(不能以数字开头)

    • my-variable(不能包含连字符)

    • my variable(不能包含空格)

    • class(关键字)

3.变量命名的规范

  1. 使用有意义的名称
  • 变量名应该清晰且具有描述性,能够准确表达变量的用途或含义。

    • 示例:total_price 而不是 tpuser_name 而不是 un
  1. 遵循命名约定

    • 变量名应使用小写字母和下划线间隔。

    • 示例:first_nametotal_sum

  2. 遵循特定的命名规则

    • 类名

      • 多个单词首字母用大写开头(大驼峰命名)。

      • 示例:MyClassEmployeeDetails

    • 常量名

      • 使用全部大写字母和下划线(UPPER_SNAKE_CASE)。

      • 示例:MAX_SIZEPI

  3. 不要使用内置函数名

    • 避免使用 Python 内置函数名作为变量名,以免引起混淆。

    • 示例:list = [1, 2, 3] 不建议使用,因为 list 是内置函数。

  4. 遵循特定前缀和后缀约定

    • 有时会使用特定前缀或后缀来表示变量的用途。

    • 示例:is_ 前缀表示布尔值,如 is_valid

十四、关键字

Python 的关键字是编程语言中保留的词汇,具有特殊的含义和用途。关键字不能用作标识符(变量名、函数名、类名等),因为它们是语言语法的一部分。具体查看文档或者命令行模式下输入help,再输入keywords查看全部关键字

十五、键盘输入:input()函数

Python 中的 input() 函数用于从用户那里获取输入。这个函数会暂停程序的执行,等待用户输入一些文本,然后返回这些文本作为字符串。

以下是 input() 函数的基本用法示例:

name = input("Enter your name: ")
print("Hello, " + name + "!")

在这个示例中,程序会显示提示信息 "Enter your name: ",然后等待用户输入。用户输入的文本将存储在变量 name 中,并在后面的 print() 语句中使用。

需要注意的是,input() 函数总是返回字符串。如果你需要输入数字,需要显式地将输入转换为合适的数值类型(例如 intfloat)。

# 输入整数
age = int(input("Enter your age: "))
print("Your age is:", age)
# 输入浮点数
height = float(input("Enter your height in meters: "))
print("Your height is:", height)

十六、顺序控制

顺序控制是编程中的一种基本概念,它涉及到控制程序执行的顺序。以下是一些常见的顺序控制结构:

1.顺序结构(Sequential Structure)

  • 程序按从上到下的顺序逐行执行。
  • 例如:
    print("第一行")
    print("第二行")
    print("第三行")
    
    输出:
    第一行
    第二行
    第三行
    

2.选择结构(Selection Structure)

  • 程序根据条件判断来决定执行哪一部分代码。

  • 主要包括ifif-elseif-elif-else等结构。

  • 例如:

    x = 10
    if x > 5:
        print("x 大于 5")
    else:
        print("x 小于或等于 5")
    

    输出:

    x 大于 5
    

3.循环结构(Loop Structure)

  • 程序反复执行某段代码,直到满足特定条件为止。
  • 主要包括for循环和while循环。
  • 例如for循环:
    for i in range(3):
        print("循环次数:", i)
    
    输出:
    循环次数: 0
    循环次数: 1
    循环次数: 2
    
  • 例如while循环:
    count = 0
    while count < 3:
        print("循环次数:", count)
        count += 1
    
    输出:
    循环次数: 0
    循环次数: 1
    循环次数: 2
    

4.嵌套结构(Nested Structure)

  • 在一个控制结构中嵌套另一个控制结构。
  • 例如在for循环中嵌套if条件判断:
    for i in range(5):
        if i % 2 == 0:
            print(i, "是偶数")
        else:
            print(i, "是奇数")
    
    输出:
    0 是偶数
    1 是奇数
    2 是偶数
    3 是奇数
    4 是偶数	
    

十七、for循环

1.for循环的基本语法

for 变量 in 可迭代对象:
    代码块

2.示例说明

(1)遍历列表
fruits = ["苹果", "香蕉", "樱桃"]
for fruit in fruits:
    print(fruit)

输出:

苹果
香蕉
樱桃
(2)遍历字符串
word = "Hello"
for letter in word:
    print(letter)

输出:

H
e
l
l
o
(3)使用range()函数:(包头不包尾)
  • range()函数生成一系列数字,可以指定起始、结束和步长。

  • 例如,从0到4的数字:

    for i in range(5):
        print(i)
    

    输出:

    0
    1
    2
    3
    4
    
  • 例如,从2到10,步长为2:

    for i in range(2, 11, 2):
        print(i)
    

    输出:

    2
    4
    6
    8
    10
    
(4)嵌套for循环
  • 在一个for循环中嵌套另一个for循环。

  • 例如,打印一个乘法表:

    for i in range(1, 10):
        for j in range(1, 10):
            print(f"{i} * {j} = {i*j}", end="\t")
        print()
    

    输出:

    1 * 1 = 1    1 * 2 = 2    1 * 3 = 3    1 * 4 = 4    1 * 5 = 5    1 * 6 = 6    1 * 7 = 7    1 * 8 = 8    1 * 9 = 9    
    2 * 1 = 2    2 * 2 = 4    2 * 3 = 6    2 * 4 = 8    2 * 5 = 10   2 * 6 = 12   2 * 7 = 14   2 * 8 = 16   2 * 9 = 18   
    3 * 1 = 3    3 * 2 = 6    3 * 3 = 9    3 * 4 = 12   3 * 5 = 15   3 * 6 = 18   3 * 7 = 21   3 * 8 = 24   3 * 9 = 27   
    4 * 1 = 4    4 * 2 = 8    4 * 3 = 12   4 * 4 = 16   4 * 5 = 20   4 * 6 = 24   4 * 7 = 28   4 * 8 = 32   4 * 9 = 36   
    5 * 1 = 5    5 * 2 = 10   5 * 3 = 15   5 * 4 = 20   5 * 5 = 25   5 * 6 = 30   5 * 7 = 35   5 * 8 = 40   5 * 9 = 45   
    6 * 1 = 6    6 * 2 = 12   6 * 3 = 18   6 * 4 = 24   6 * 5 = 30   6 * 6 = 36   6 * 7 = 42   6 * 8 = 48   6 * 9 = 54   
    7 * 1 = 7    7 * 2 = 14   7 * 3 = 21   7 * 4 = 28   7 * 5 = 35   7 * 6 = 42   7 * 7 = 49   7 * 8 = 56   7 * 9 = 63   
    8 * 1 = 8    8 * 2 = 16   8 * 3 = 24   8 * 4 = 32   8 * 5 = 40   8 * 6 = 48   8 * 7 = 56   8 * 8 = 64   8 * 9 = 72   
    9 * 1 = 9    9 * 2 = 18   9 * 3 = 27   9 * 4 = 36   9 * 5 = 45   9 * 6 = 54   9 * 7 = 63   9 * 8 = 72   9 * 9 = 81   
    
(5)使用enumerate()函数
  • enumerate()函数可以在遍历列表时获取元素的索引和值。

  • 例如:

    fruits = ["苹果", "香蕉", "樱桃"]
    for index, fruit in enumerate(fruits):
        print(f"第{index+1}个水果是{fruit}")
    

    输出:

    第1个水果是苹果
    第2个水果是香蕉
    第3个水果是樱桃
    

3.结束循环的控制语句

(1)break:用于提前结束循环
for i in range(5):
    if i == 3:
        break
    print(i)

输出:

0
1
2
(2)continue:用于跳过当前迭代,继续下一次循环。
for i in range(5):
    if i == 3:
        continue
    print(i)

输出:

0
1
2
4

十八、while循环

1.语法结构

while 条件:
    代码块

2.工作机制

  • 条件while语句首先检查条件。如果条件为True,则执行代码块。
  • 代码块:代码块是一个或多个语句,这些语句在每次迭代中被执行。
  • 循环结束:当条件变为False时,循环终止,程序执行循环后的代码。

3.示例说明

(1)有限循环
count = 0
while count < 5:
    print(count)
    count += 1

输出:

0
1
2
3
4

在这个例子中,count从0开始,每次循环增加1,当count达到5时,条件count < 5变为False,循环结束。

(2)无限循环
  • 如果条件始终为Truewhile循环将永远执行下去,形成无限循环。
  • 通常需要使用break语句来打破这种循环。
while True:
    user_input = input("请输入 'exit' 来退出:")
    if user_input == 'exit':
        break

在这个例子中,循环会持续运行,直到用户输入'exit'

(3)使用else子句
  • for循环类似,while循环也可以使用else子句。
  • else子句中的代码在循环条件变为False时执行,但如果循环被break打断,则else子句不会执行。
count = 0
while count < 5:
    print(count)
    count += 1
else:
    print("循环正常结束")

输出:

0
1
2
3
4
循环正常结束
(4)break语句
  • break:用于提前退出循环,不执行else子句。

    count = 0
    while count < 5:
        if count == 3:
            break
        print(count)
        count += 1
    else:
        print("循环正常结束")
    

    输出:

    0
    1
    2
    
(5)continue语句
  • continue:用于跳过当前迭代,直接进入下一次循环。

    count = 0
    while count < 5: 
        count += 1
        if count == 3:
            continue
        print(count)
    

    输出:

    1
    2
    4
    5
    
(6)使用场景
  • 等待条件变化:当程序需要等待某个条件变化时,比如等待用户输入有效数据。

  • 处理动态数据:处理实时数据流或不断变化的数据源。

  • 无限循环:某些服务器程序或后台任务需要持续运行,可以使用while True创建无限循环,并在适当时候打破循环。

(7)注意事项
  • 防止无限循环:确保循环条件最终会变为False,否则可能会导致程序卡死或资源耗尽。
  • 条件更新:在循环体内要注意更新条件变量,否则循环条件可能一直为True,导致无限循环。
  • 调试和测试:在编写和测试while循环时,注意调试,确保条件更新和break逻辑正确。

十九、函数

1.基本结构

定义一个函数的基本结构如下:

def 函数名(参数1, 参数2, ...):
    函数体
    return 返回值

在这里插入图片描述

2.示例

(1)简单函数
def greet():
    print("Hello, world!")
greet()  # 调用函数

输出:

Hello, world!
(2)带参数的函数
def greet(name):
    print(f"Hello, {name}!")
greet("Alice")  # 调用函数并传递参数

输出:

Hello, Alice!
  • 带返回值的函数
def add(a, b):
    return a + b
result = add(3, 5)  # 调用函数并接收返回值
print(result)

输出:

8

在这里插入图片描述

3.函数的参数

(1)位置参数

参数按位置顺序传递:

def subtract(a, b):
    return a - b
print(subtract(10, 5))  # 结果为5
(2)关键字参数

参数按名称传递:

def divide(a, b):
    return a / b
print(divide(a=10, b=2))  # 结果为5.0
(3)默认参数

参数可以有默认值,如果调用时未提供参数,则使用默认值:

def greet(name="world"):
    print(f"Hello, {name}!")
greet()  # 使用默认值,输出Hello, world!
greet("Alice")  # 输出Hello, Alice!
(4)可变参数

可以接受任意数量的位置参数和关键字参数:

def summarize(*args, **kwargs):
    print("位置参数:", args)
    print("关键字参数:", kwargs)

summarize(1, 2, 3, a=4, b=5)

输出:

位置参数: (1, 2, 3)
关键字参数: {'a': 4, 'b': 5}

4.函数的传参机制分析

来看下面这两段代码,研究一下不可变类型,如数值和字符串的传参机制:

# 定义函数f1
def f1(a):
    print(f"f1() a的值: {a} 地址是: {id(a)}")
    a += 1
    print(f"f1() a的值: {a} 地址是: {id(a)}")
# 定义变量 a = 10
a = 10
print(f"调用f1()前 a的值: {a} 地址是: {id(a)}")
# 调用f1(a)
f1(a)
print(f"调用f1()后 a的值: {a} 地址是: {id(a)}")

输出:

调用f1()前 a的值: 10 地址是: 140723308680264
f1() a的值: 10 地址是: 140723308680264  #驻留机制
f1() a的值: 11 地址是: 140723308680296
调用f1()后 a的值: 10 地址是: 140723308680264
def f2(name):
    print(f"f2() name的值: {name} 地址是: {id(name)}")
    name += " hi"
    print(f"f2() name的值: {name} 地址是: {id(name)}")
name = "tom"
print(f"调用f2()前 name的值: {name} 地址是: {id(name)}")
f2(name)
print(f"调用f2()后 name的值: {name} 地址是: {id(name)}")

输出:

调用f2()前 name的值: tom 地址是: 1919406723056
f2() name的值: tom 地址是: 1919406723056
f2() name的值: tom hi 地址是: 1919406722736
调用f2()后 name的值: tom 地址是: 1919406723056
  • 函数中不可变数据类型(如数值和字符串类型)传参机制

    • 在Python中,函数参数是通过“传值”的方式传递的,但这里的“值”是指对象的引用。

    • 当我们将变量a传递给函数f1时,函数f1中的参数a和外部的变量a引用的是同一个对象。

  • 不可变对象的特性

    • 整数是不可变对象。当我们在函数内部对整数a进行操作(如a += 1)时,会创建一个新的整数对象。

    • 因此,操作后的a指向了一个新的内存地址,而外部的a仍然指向原来的内存地址。

  • 具体分析

    • 调用f1之前,外部的a值为10,地址是140723308680264

    • 在函数f1内部,第一次打印时,a值为10,地址是140723308680264

    • 在函数f1内部,a += 1后,a值为11,地址变为140723308680296,这是因为创建了一个新的整数对象。

    • 调用f1之后,外部的a值仍为10,地址还是140723308680264,说明外部的a没有受到函数内部操作的影响。

总结:函数传参时传递的是对象的引用,但对于不可变对象(如整数),在函数内部的修改不会影响外部变量。这是因为对不可变对象的任何修改都会创建一个新的对象。

5.函数的返回值

函数可以返回一个值,也可以返回多个值(使用元组):

def add_and_subtract(a, b):
    return a + b, a - b
result = add_and_subtract(10, 5)
print(result)  # 输出(15, 5)

6.匿名函数(lambda表达式)

匿名函数是一种简短的函数定义方式,使用lambda关键字:

add = lambda a, b: a + b
print(add(3, 5))  # 输出8

7.函数的文档字符串

文档字符串(docstring)用于描述函数的功能,可以通过help函数查看:

def multiply(a, b):
    """
    返回a和b的乘积
    """
    return a * b

help(multiply)

输出:

Help on function multiply in module __main__:

multiply(a, b)
    返回a和b的乘积

8.函数的作用域

函数内部定义的变量具有局部作用域,函数外部定义的变量具有全局作用域:

x = 10  # 全局变量

def foo():
    x = 5  # 局部变量,根据就近原则,使用的是函数内部的x
    print("局部变量:", x)

foo()
print("全局变量:", x)

输出:

局部变量: 5
全局变量: 10

使用global关键字可以在函数内部修改全局变量:

x = 10

def foo():
    global x
    x = 5

foo()
print("全局变量:", x) n   # 输出5

9.递归函数(recursion)

(1)示例代码
def test(n):
    if n > 2:
        test(n - 1)
    print("n=", n)
test(4)

输出如下:

n= 2
n= 3
n= 4

示例图:(记住我们每执行一个函数会开辟一个新的栈空间,谁调用它的就返回给谁,牢牢把握这两点是解决递归问题的关键)
在这里插入图片描述

假如对这个代码稍作修改变成:

def test(n):
    if n > 2:
        test(n - 1)
    else:
        print("n=", n)
test(4)

输出结果将只有n = 2,因为返回其他递归函数的时候没法执行else

(2)计算阶乘

阶乘(Factorial)是最常见的递归例子之一。阶乘定义为n! = n * (n-1)!,并且0! = 1

def factorial(n):
    if n == 0: 
        return 1
    else:
        return n * factorial(n - 1)  # 递归步骤

# 测试
print(factorial(5))  # 输出120
(3)斐波那契数列

斐波那契数列(Fibonacci Sequence)是另一个经典的递归例子。斐波那契数列定义为:

  • F(0) = 0
  • F(1) = 1
  • F(n) = F(n-1) + F(n-2)
def fibonacci(n):
    if n <= 0: 
        return 0
    elif n == 1:  
        return 1
    else:
        return fibonacci(n - 1) + fibonacci(n - 2)  # 递归步骤
# 测试
print(fibonacci(6))  # 输出8

二十、数据容器(collection)

数据容器(Data Containers)是用于存储和组织数据的结构,Python 提供了几种内置的数据容器,每种都有不同的特性和用途。主要的数据容器包括列表、元组、字典和集合。

文档:https.//docs.python.org/zh-cn/3.12/library/stdtypes.html,从目录找到list等

1. 列表(List)

(1)特性
  • 有序集合
  • 可变,可以动态修改长度和内容
  • 允许重复元素
(2)定义和使用
# 定义列表
my_list = [1, 2, 3, 4, 5]

# 空列表
list1 = []
list2 = list()

# 访问元素
print(my_list[0])  # 输出1
print(my_list[-1])  # 输出5

# 修改元素
my_list[1] = 20

# 添加元素
my_list.append(6)

# 删除元素
del my_list[2]

# 列表操作
print(len(my_list))  # 输出5
print(my_list)       # 输出[1, 20, 4, 5, 6]
print(type(my_list)) #输出<class 'list'>

# 列表遍历
# 1.使用for循环
for element in my_list:
    print(element)
# 2.使用索引遍历
for i in range(len(my_list)):
    print(f"索引: {i}, 元素: {my_list[i]}")
# 3.使用while循环
i = 0
while i < len(my_list):
    print(f"索引: {i}, 元素: {my_list[i]}")
    i += 1
# 4.使用enumerate函数
for index, element in enumerate(my_list):
    print(f"索引: {index}, 元素: {element}")
# 定义列表
list1 = [1, 2.1, '韩顺平教育']
print(f"list: {list1} 地址: {id(list1)} {id(list1[2])} {list1[2]}")

# 修改列表的元素
list1[2] = 'python'  # 可以修改
print(f"list: {list1} 地址: {id(list1)} {id(list1[2])} {list1[2]}")

输出:

list: [1, 2.1, '韩顺平教育'] 地址: 2636491149888 2636490499984 韩顺平教育
list: [1, 2.1, 'python'] 地址: 2636491149888 2636491148976 python   

在这里插入图片描述

# 定义列表
list1 = [1, 2.1, '韩顺平教育']
list2 = list1  # list2 指向与 list1 相同的列表对象

# 修改 list2 中的元素
list2[0] = 500

# 打印 list2 和 list1
print("list2 =", list2)
print("list1 =", list1)

输出:

list2 = [500, 2.1, '韩顺平教育']
list1 = [500, 2.1, '韩顺平教育']

在这里插入图片描述

(3)其他常用方法
  • enumerate()

返回一个包含索引和值的元组迭代器。

my_list = ['apple', 'banana', 'cherry']
for index, value in enumerate(my_list):
    print(f"索引: {index}, 值: {value}")
# 输出:
# 索引: 0, 值: apple
# 索引: 1, 值: banana
# 索引: 2, 值: cherry
  • count()

返回元素在列表中出现的次数。

my_list = [1, 2, 3, 2]
count = my_list.count(2)
print(count)  # 输出: 2
  • extend()

通过添加一个可迭代对象中的所有元素来扩展列表。

my_list = [1, 2, 3]
my_list.extend([4, 5])
print(my_list)  # 输出: [1, 2, 3, 4, 5]
  • index()

返回列表中第一个匹配项的索引。(找不到会报错:ValueError)

my_list = [1, 2, 3, 2]
index = my_list.index(2)
print(index)  # 输出: 1
  • reverse()

反转列表中的元素。

my_list = [1, 2, 3]
my_list.reverse()
print(my_list)  # 输出: [3, 2, 1]
  • insert()

在指定位置插入一个元素。

my_list = [1, 2, 3]
my_list.insert(1, 1.5)
print(my_list)  # 输出: [1, 1.5, 2, 3]
  • sort()

对列表中的元素进行排序(默认是升序)。

my_list = [3, 1, 2]
my_list.sort()
print(my_list)  # 输出: [1, 2, 3]

# 降序排序
my_list.sort(reverse=True)
print(my_list)  # 输出: [3, 2, 1]
(4)列表生成式(List Comprehension)

用于从一个已有的列表或其他可迭代对象生成一个新的列表,也可以在生成列表的过程中对元素进行处理和筛选。

  1. 基本语法

列表生成式的基本语法如下:

[表达式 for 变量 in 可迭代对象]
  1. 示例
  • 生成一个平方数列表
squares = [x * x for x in range(10)]
print(squares)  # 输出: [0, 1, 4, 9, 16, 25, 36, 49, 64, 81]
  • 带条件的列表生成式
evens = [x for x in range(20) if x % 2 == 0]
print(evens)  # 输出: [0, 2, 4, 6, 8, 10, 12, 14, 16, 18]
  • 嵌套循环的列表生成式
pairs = [(x, y) for x in range(3) for y in range(3)]
print(pairs)  # 输出: [(0, 0), (0, 1), (0, 2), (1, 0), (1, 1), (1, 2), (2, 0), (2, 1), (2, 2)]
  • 对元素进行处理的列表生成式
words = ["hello", "world", "python"]
upper_words = [word.upper() for word in words]
print(upper_words)  # 输出: ['HELLO', 'WORLD', 'PYTHON']
  1. 带条件的列表生成式

可以在列表生成式中加入条件判断,只生成满足条件的元素。

  • 单个条件
evens = [x for x in range(10) if x % 2 == 0]
print(evens)  # 输出: [0, 2, 4, 6, 8]
  • 多个条件
special_numbers = [x for x in range(20) if x % 2 == 0 if x % 3 == 0]
print(special_numbers)  # 输出: [0, 6, 12, 18]
  1. 嵌套列表生成式

可以使用嵌套的列表生成式来处理多维列表(如二维矩阵)。

matrix = [
    [1, 2, 3],
    [4, 5, 6],
    [7, 8, 9]
]
flatten = [element for row in matrix for element in row]
print(flatten)  # 输出: [1, 2, 3, 4, 5, 6, 7, 8, 9]

2. 元组(Tuple)

(1)特性
  • 有序集合。
  • 不可变,一旦创建,不能修改长度和内容。
  • 允许重复元素。
(2)定义和使用
# 定义元组
my_tuple = (1, 2, 3, 4, 5)

# 空元组
tuple1 = ()
tuple2 = tuple()

# 特别注意的是,当元组中只有一个元素时,类型为int
tuple3 = (100)
print(f"tuple3的类型是{type(tuple3)}")
tuple4=(100,)
print(f"tuple4的类型是{type(tuple4)}")
# tuple3的类型是<class 'int'>
# tuple4的类型是<class 'tuple'>

# 访问元素 
print(my_tuple[0])  # 输出1

# 元组操作
print(len(my_tuple))  # 输出5
print(my_tuple)       # 输出(1, 2, 3, 4, 5)

# 元组不可变,不能修改或删除,添加元素,只能查找

3.字符串

(1)特性
  • 有序集合。
  • 不可变,一旦创建,不能修改内容。
  • 允许重复元素。
(2)定义和使用
# 定义字符串
my_string = "Hello, Python!"

# 访问元素
print(my_string[0])  # 输出: 'H'

# 字符串长度
print(len(my_string))  # 输出: 13

# 字符串切片
print(my_string[7:13])  # 输出: 'Python'
(3)字符串的方法
  • upper()

将字符串中的所有字符转换为大写,返回字符串副本(即原来的字符串不变,且返回一个新的字符串,下同)

my_string = "hello"
print(my_string.upper())  # 输出: 'HELLO'
  • lower()

将字符串中的所有字符转换为小写,返回字符串副本

my_string = "HELLO"
print(my_string.lower())  # 输出: 'hello'
  • strip()

移除字符串开头和结尾的空格或指定字符,返回字符串副本

my_string = "   Hello, Python!   "
print(my_string.strip())  # 输出: 'Hello, Python!'

my_string = "123Hello, Python!321"
print(my_string.strip('123'))  # 输出: 'Hello, Python!'
  • replace(old,new[,count]) 其中 [] 为可选操作

替换字符串中的指定子字符串,返回字符串副本

str_names = "jack tom mary hsp nono tom"
str_names_new = str_names.replace("jack", "杰克", 1)
print(f"新字符串:{str_names_new}") #输出:新字符串:杰克 tom mary hsp nono tom 
print(f"旧字符串:{str_names}") #输出:旧字符串:jack tom mary hsp nono tom 
  • split()

将字符串拆分为列表,返回字符串副本

my_string = "Hello, Python, World"
print(my_string.split(", "))  # 输出: ['Hello', 'Python', 'World']
  • join()

将列表中的字符串连接成一个字符串。

my_list = ['Hello', 'Python', 'World']
print(", ".join(my_list))  # 输出: 'Hello, Python, World'
  • find()

查找子字符串在字符串中的位置,找不到则返回 -1。

my_string = "Hello, Python!"
print(my_string.find("Python"))  # 输出: 7
print(my_string.find("Java"))    # 输出: -1
  • count()

返回子字符串在字符串中出现的次数。

my_string = "Hello, Python! Python is fun."
print(my_string.count("Python"))  # 输出: 2
  • ord()

返回单个字符的 ASCII(或 Unicode)码值。

character = 'A'
print(ord(character))  # 输出: 65
  • chr()

将整数(ASCII 或 Unicode 码值)转换为对应的字符。

code_point = 65
print(chr(code_point))  # 输出: 'A'
  • title()

将字符串的每个单词的首字母转换为大写。

my_string = "hello, python!"
print(my_string.title())  # 输出: 'Hello, Python!'
(4)字符串的比较
  1. 在Python中,字符串的比较是基于字典序(lexicographical order)的,即按字符的ASCII或Unicode码值逐个比较。字符串的比较运算符包括等于(==)、不等于(!=)、大于(>)、小于(<)、大于等于(>=)和小于等于(<=)。

  2. 字符串比较的基本原则

  • 逐字符比较

    • 从第一个字符开始,逐个字符进行比较,直到字符不同或到达字符串末尾。

    • 使用每个字符的ASCII或Unicode码值进行比较。

  • 短字符串优先

    • 如果两个字符串的前缀相同,短字符串小于长字符串。

4. 字典(Dictionary)

(1)特性
  • 无序集合。

  • 以键-值对形式存储数据。

  • 键必须唯一,值可以重复,通常情况下用数字或字符串作为键。

  • 可变,可以动态修改长度和内容。

(2)定义和使用
# 定义字典
my_dict = {'a': 1, 'b': 2, 'c': 3}

# 空字典
dict1 = {}
dict2=dict()

# 访问元素
print(my_dict['a'])  # 输出1

# 遍历
dict_b = {'one': 1, 'two': 2, 'three': 3}

# 遍历方式1 - 依次取出key,再通过dict[key]取出对应的value
print("--------遍历方式1--------")
for key in dict_b:
    print(f"key: {key} value: {dict_b[key]}")

# 遍历方式2 - 依次取出value
print("--------遍历方式2--------")
for value in dict_b.values():
    print(f"value: {value}")

# 遍历方式3 - 依次取出key-value
print("--------遍历方式3--------")
for key, value in dict_b.items():
    print(f"key: {key} value: {value}")



# 修改元素
my_dict['b'] = 20

# 添加元素
my_dict['d'] = 4

# 删除元素
del my_dict['c']

# 字典操作
print(len(my_dict))  # 输出3
print(my_dict)       # 输出{'a': 1, 'b': 20, 'd': 4}
(3)字典的方法
  • dict.clear()

移除字典中的所有元素。

my_dict = {'a': 1, 'b': 2, 'c': 3}
my_dict.clear()
print(my_dict)  # 输出: {}
  • dict.copy()

返回字典的浅拷贝。

my_dict = {'a': 1, 'b': 2, 'c': 3}
new_dict = my_dict.copy()
print(new_dict)  # 输出: {'a': 1, 'b': 2, 'c': 3}
  • dict.fromkeys()

创建一个新字典,以序列中的元素作为字典的键,指定的值作为所有键对应的初始值。

keys = ['a', 'b', 'c']
value = 0
new_dict = dict.fromkeys(keys, value)
print(new_dict)  # 输出: {'a': 0, 'b': 0, 'c': 0}
  • dict.items()

返回一个包含字典键值对的视图对象。

my_dict = {'a': 1, 'b': 2, 'c': 3}
items = my_dict.items()
print(items)  # 输出: dict_items([('a', 1), ('b', 2), ('c', 3)])
  • dict.keys()

返回一个包含字典所有键的视图对象。

my_dict = {'a': 1, 'b': 2, 'c': 3}
keys = my_dict.keys()
print(keys)  # 输出: dict_keys(['a', 'b', 'c'])
  • dict.pop()

移除指定键对应的值,并返回该值。如果键不存在,则引发 KeyError

my_dict = {'a': 1, 'b': 2, 'c': 3}
value = my_dict.pop('b')
print(value)  # 输出: 2
print(my_dict)  # 输出: {'a': 1, 'c': 3}
  • dict.update()

使用指定的键值对更新字典。如果键已存在,更新对应的值;如果键不存在,添加新的键值对。

my_dict = {'a': 1, 'b': 2}
my_dict.update({'b': 3, 'c': 4})
print(my_dict)  # 输出: {'a': 1, 'b': 3, 'c': 4}
  • dict.values()

返回一个包含字典所有值的视图对象。

my_dict = {'a': 1, 'b': 2, 'c': 3}
values = my_dict.values()
print(values)  # 输出: dict_values([1, 2, 3])
(4)字典生成式(Dictionary Comprehension)

使用内置函数 zip 可以方便地将两个或多个可迭代对象(如列表、元组等)打包成一个迭代器,生成成对的元组

  1. 基本语法
{key: value for key, value in zip(iterable1, iterable2)}
  • iterable1:第一个可迭代对象,生成字典的键。
  • iterable2:第二个可迭代对象,生成字典的值。
  1. 示例说明
  • 将两个列表合并成一个字典
fruits = ['apple', 'banana', 'cherry']
colors = ['red', 'yellow', 'red']
fruit_colors = {fruit: color for fruit, color in zip(fruits, colors)}
print(fruit_colors)  # 输出: {'apple': 'red', 'banana': 'yellow', 'cherry': 'red'}
  • 处理不同长度的可迭代对象
    当可迭代对象长度不同时,zip 会在最短的可迭代对象结束时停止配对。
keys = ['a', 'b', 'c', 'd']
values = [1, 2, 3]
truncated_dict = {key: value for key, value in zip(keys, values)}
print(truncated_dict)  # 输出: {'a': 1, 'b': 2, 'c': 3}
  • 带表达式
students = ["Alice", "Bob", "Charlie"]
math_scores = [85, 92, 78]
english_scores = [88, 90, 85]
total_scores = {student: math + english for student, math, english in zip(students, math_scores, english_scores)}
print(total_scores)  # 输出: {'Alice': 173, 'Bob': 182, 'Charlie': 163}

5.集合(Set)

(1)特性
  • 无序集合(输出的顺序和你定义时的顺序不一定一样)
  • 不允许重复元,会自动去重
  • 可变,可以动态修改长度和内容
  • 不支持索引
(2)定义和使用
# 定义集合
my_set = {1, 2, 3, 4, 5}

# 空集合
set1 = set()
print(f"set1:{set1} 类型:{type(set1)} ")

# 添加元素
my_set.add(6)

# 删除元素
my_set.remove(2)

# 集合操作
print(len(my_set))  # 输出5
print(my_set)       # 输出{1, 3, 4, 5, 6}

# 集合不允许重复元素
my_set.add(3)
print(my_set)  # 输出{1, 3, 4, 5, 6}
(3)集合的方法
  • add()

向集合中添加一个元素。如果元素已存在,则不进行任何操作,且添加后不一定在最后

my_set = {1, 2, 3}
my_set.add(4)
print(my_set)  # 输出: {1, 2, 3, 4}

my_set.add(3)
print(my_set)  # 输出: {1, 2, 3, 4} (元素3已存在)
  • update()

向集合中添加多个元素,可以是列表、元组、集合等。

my_set = {1, 2, 3}
my_set.update([4, 5])
print(my_set)  # 输出: {1, 2, 3, 4, 5}

my_set.update({6, 7})
print(my_set)  # 输出: {1, 2, 3, 4, 5, 6, 7}
  • remove()

从集合中删除一个元素。如果元素不存在,则引发 KeyError

my_set = {1, 2, 3}
my_set.remove(2)
print(my_set)  # 输出: {1, 3}

# my_set.remove(4)  # 会引发 KeyError
  • discard()

从集合中删除一个元素。如果元素不存在,则不进行任何操作

my_set = {1, 2, 3}
my_set.discard(2)
print(my_set)  # 输出: {1, 3}

my_set.discard(4)  # 不会引发错误
print(my_set)      # 输出: {1, 3}
  • pop()

随机移除并返回集合中的一个元素。如果集合为空,则引发 KeyError

my_set = {1, 2, 3}
element = my_set.pop()
print(element)  # 输出: 1 (或其他任一元素)
print(my_set)   # 输出: {2, 3} (或其他剩余元素)
  • clear()

移除集合中的所有元素。

my_set = {1, 2, 3}
my_set.clear()
print(my_set)  # 输出: set()
  • union()

返回两个集合的并集。

set1 = {1, 2, 3}
set2 = {3, 4, 5}
union_set = set1.union(set2)
print(union_set)  # 输出: {1, 2, 3, 4, 5}
  • intersection()

返回两个集合的交集。

set1 = {1, 2, 3}
set2 = {2, 3, 4}
intersection_set = set1.intersection(set2)
print(intersection_set)  # 输出: {2, 3}
  • difference()

返回两个集合的差集。

set1 = {1, 2, 3}
set2 = {2, 3, 4}
difference_set = set1.difference(set2)
print(difference_set)  # 输出: {1}
(4)集合生成式(Set Comprehension)

集合生成式用于从一个可迭代对象生成一个新的集合。它类似于列表生成式,但结果是一个集合,因此生成的元素是无序且唯一的。

  1. 基本语法
{表达式 for 变量 in 可迭代对象 if 条件}
  1. 示例说明
  • 基本集合生成式
numbers = [1, 2, 3, 4, 5]
squares = {x * x for x in numbers}
print(squares)  # 输出: {1, 4, 9, 16, 25}

squares = {x**2 for x in range(1, 11)}
print(squares)  # 输出: {1, 4, 9, 16, 25, 36, 49, 64, 81, 100}
  • 带条件的集合生成式
numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
evens = {x for x in numbers if x % 2 == 0}
print(evens)  # 输出: {2, 4, 6, 8, 10}
  • 从字符串生成集合
text = "hello"
unique_chars = {char for char in text}
print(unique_chars)  # 输出: {'h', 'e', 'l', 'o'}
  • 从列表中去重
numbers = [1, 2, 2, 3, 4, 4, 5]
unique_numbers = {x for x in numbers}
print(unique_numbers)  # 输出: {1, 2, 3, 4, 5}
  • 生成字母的大写集合
letters = {'a', 'b', 'c', 'd'}
uppercase_letters = {letter.upper() for letter in letters}
print(uppercase_letters)  # 输出: {'A', 'B', 'C', 'D'}
  1. 嵌套集合生成式
nested_list = [[1, 2, 3], [4, 5, 6], [7, 8, 9]]
flattened_set = {num for row in nested_list for num in row}
print(flattened_set)  # 输出: {1, 2, 3, 4, 5, 6, 7, 8, 9}

6.容器之间的转换

Python 提供了灵活的容器转换机制,可以在不同类型的容器之间进行转换:

# 列表转元组
my_list = [1, 2, 3]
my_tuple = tuple(my_list)

# 元组转集合
my_set = set(my_tuple)

# 集合转列表
my_new_list = list(my_set)

# 字典的键和值转换为列表或集合
my_dict = {'a': 1, 'b': 2}
keys = list(my_dict.keys())
values = list(my_dict.values())
keys_set = set(my_dict.keys())
values_set = set(my_dict.values())

7.总结

比较项列表 (list)元组 (tuple)字符串 (str)集合 (set)字典 (dict)
是否支持多个元素YYYYY
元素类型任意任意只支持字符串任意Key: 通常是字符串或数字,Value: 任意
是否支持元素重复YYYNKey不能重复,Value可以重复
是否有序YYYN3.6版本之前是无序的,3.6版本之后开始支持元素有序的功能
是否支持索引YYYNN
可修改性/可变性YNNYY
使用场景可修改、可重复的多个数据不可修改、可重复的多个数据字符串不可重复的多个数据通过键值对查询对应数据的需求
定义符号[]()''或者 ""{}{key:value}

二十一、切片(Slice)

字符串切片(Slicing)用于从字符串中提取子字符串。切片不仅适用于字符串,也可以用于列表、元组等序列类型,返回副本

1.基本语法

string[start:stop:step]
  • start:起始索引,包含在切片结果中。如果省略,默认为0。
  • stop:结束索引,不包含在切片结果中。如果省略,默认为字符串的长度。
  • step:步长,表示每次提取字符的间隔。如果省略,默认为1。

2.示例说明

(1)基本切片
text = "Hello, Python!"
list_a=["jack","tom","yoyo","nono","hsp"]
print(text[0:5])  # 输出: 'Hello'
print(list_a[1:4]) # 输出: ['tom', 'yoyo', 'nono']
# 元组同理
(2)省略起始或结束索引
text = "Hello, Python!"

print(text[:5])   # 输出: 'Hello' (从开始到索引5)
print(text[7:])   # 输出: 'Python!' (从索引7到结尾)
print(text[:])    # 输出: 'Hello, Python!' (整个字符串)
(3)使用步长
text = "Hello, Python!"

print(text[::2])  # 输出: 'Hlo yhn' (每隔一个字符取一个)
print(text[1::2]) # 输出: 'el,Pto!' (从索引1开始,每隔一个字符取一个)
(4)负索引
text = "Hello, Python "

print(text[-1])   # 输出: '!' (最后一个字符)
print(text[-7:-1])# 输出: 'Python' (倒数第7个到倒数第1个,不包含最后一个)
(5)负步长
text = "Hello, Python!"

print(text[::-1]) # 输出: '!nohtyP ,olleH' (反转字符串)
print(text[7:0:-1])# 输出: ' ,olleH' (从索引7倒着取到索引0,但不包括索引0)

二十二、冒泡排序(Bubble Sort)

冒泡排序是一种简单的排序算法,它重复地遍历要排序的列表,一次比较两个元素,如果它们的顺序错误就交换它们的位置。遍历列表的工作是重复进行的,直到不需要再交换为止,也就是说列表已经排序完成。

1.冒泡排序的基本思想

  • 从列表的第一个元素开始,依次比较相邻的两个元素。
  • 如果前一个元素比后一个元素大,则交换它们的位置。
  • 对每一对相邻元素做同样的工作,从开始第一对到列表的最后一对。这一趟遍历结束后,最后一个元素将是最大的元素。
  • 忽略最后一个元素,重复上述过程,直到整个列表有序。

2.详细步骤

  1. 初始化未排序部分的边界,例如设定为列表的长度。
  2. 从列表的第一个元素开始,比较每一对相邻元素。
  3. 如果前一个元素大于后一个元素,交换它们的位置。
  4. 继续比较下一对相邻元素,直到到达未排序部分的边界。
  5. 边界减1,因为此时最后一个元素已是最大的。
  6. 重复上述步骤,直到没有任何交换。

3.示例代码

以下是一个Python实现的冒泡排序算法:

def bubble_sort(arr):
    n = len(arr)
    # 遍历所有数组元素
    for i in range(n):
        # 最后 i 个元素已经排序
        for j in range(0, n-i-1):
            # 交换如果元素找到的顺序错误
            if arr[j] > arr[j+1]:
                arr[j], arr[j+1] = arr[j+1], arr[j]
                
# 测试代码
arr = [64, 34, 25, 12, 22, 11, 90]
bubble_sort(arr)
print("排序后的数组:", arr)

4.工作原理示例

假设有一个列表 [5, 1, 4, 2, 8],以下是每一步的结果:

  1. 初始列表:[5, 1, 4, 2, 8]
  2. 第一趟遍历:[1, 4, 2, 5, 8](5 和 1 交换,4 和 2 交换)
  3. 第二趟遍历:[1, 2, 4, 5, 8](4 和 2 交换)
  4. 第三趟遍历:[1, 2, 4, 5, 8](没有交换,已经有序)

5.冒泡排序的复杂度

(1)时间复杂度
  • 最坏情况:O(n^2)
  • 最好情况:O(n)(如果列表已经有序,优化后的冒泡排序可以检测到这一点)
  • 平均情况:O(n^2)
(2)空间复杂度
  • O(1)(原地排序)

6.优化冒泡排序

可以通过设置一个标志位来检测在一趟遍历中是否发生过交换,如果没有发生交换,说明列表已经有序,可以提前结束排序。

def bubble_sort_optimized(arr):
    n = len(arr)
    for i in range(n):
        swapped = False
        for j in range(0, n-i-1):
            if arr[j] > arr[j+1]:
                arr[j], arr[j+1] = arr[j+1], arr[j]
                swapped = True
        if not swapped:
            break
                
# 测试代码
arr = [64, 34, 25, 12, 22, 11, 90]
bubble_sort_optimized(arr)
print("排序后的数组:", arr)

二十三、查找

1.顺序查找(Sequential Search)

顺序查找适用于任何类型的数据结构(如数组、列表、链表等),并且不需要数据事先排序。顺序查找的基本思想是从数据结构的第一个元素开始,逐一检查每一个元素,直到找到目标元素或检查完所有元素为止。

(1)顺序查找的基本思想
  1. 从数据结构的第一个元素开始。
  2. 逐一检查每一个元素:
    • 如果当前元素等于目标元素,返回当前元素的索引。
    • 如果当前元素不等于目标元素,继续检查下一个元素。
  3. 如果遍历完所有元素都没有找到目标元素,返回一个表示未找到的值(如 -1None)。
(2)示例代码
def sequential_search(arr, target):
    """
    顺序查找算法
    :param arr: 待查找的列表
    :param target: 目标元素
    :return: 目标元素的索引,如果未找到则返回 -1
    """
    for index, element in enumerate(arr):
        if element == target:
            return index
    return -1

# 测试代码
arr = [5, 3, 8, 4, 2]
target = 4
result = sequential_search(arr, target)
if result != -1:
    print(f"元素 {target} 在索引 {result} 处找到。")
else:
    print(f"元素 {target} 未找到。")
(3)工作原理示例

假设有一个列表 [5, 3, 8, 4, 2],我们要查找元素 4,以下是每一步的结果:

  1. 检查第一个元素 5,不是目标元素。
  2. 检查第二个元素 3,不是目标元素。
  3. 检查第三个元素 8,不是目标元素。
  4. 检查第四个元素 4,是目标元素,返回索引 3
(4)顺序查找的复杂度
<1>时间复杂度
  • 最坏情况:O(n)(目标元素在最后一个位置或不存在)
  • 最好情况:O(1)(目标元素在第一个位置)
  • 平均情况:O(n)(目标元素在列表中的任意位置)
<2>空间复杂度
  • O(1)(只需要常数级别的额外空间)
(5)优缺点
<1>优点
  1. 简单易实现:顺序查找的算法逻辑简单,容易实现。
  2. 无需排序:顺序查找不需要数据结构事先排序,适用于数据结构是无序的,无法使用更高效的排序查找算法
  3. 适用范围广:可以用于链表、数组、列表等各种数据结构。
<2>缺点
  1. 效率低:对于大型数据集,顺序查找效率较低,时间复杂度为O(n)。
  2. 适合小型数据集:顺序查找适合数据量较小或者对性能要求不高的场合。
(6)实际应用
def find_first_even(arr):
    """
    查找列表中第一个偶数
    :param arr: 待查找的列表
    :return: 第一个偶数的索引,如果未找到则返回 -1
    """
    for index, element in enumerate(arr):
        if element % 2 == 0:
            return index
    return -1

# 测试代码
arr = [5, 3, 8, 4, 2]
result = find_first_even(arr)
if result != -1:
    print(f"第一个偶数在索引 {result} 处找到,值为 {arr[result]}。")
else:
    print("未找到偶数。")

2.二分查找(Binary Search)

二分查找适用于有序数据。它通过反复将查找范围减半来缩小目标值可能所在的范围,从而达到快速查找的目的。二分查找的时间复杂度为 O(log n),远优于顺序查找的 O(n)。

(1)二分查找的基本思想
  1. 确定中间元素:将查找范围的中间元素与目标值比较。
  2. 比较大小:(假设已经从小到大排序)
    • 如果中间元素等于目标值,查找成功,返回该元素的索引。
    • 如果中间元素大于目标值,目标值只可能在左半部分,更新查找范围为左半部分。
    • 如果中间元素小于目标值,目标值只可能在右半部分,更新查找范围为右半部分。
  3. 重复上述步骤,直到找到目标值或查找范围为空。
(2)示例代码

以下是一个Python实现的二分查找算法:

def binary_search(arr, target):
    left, right = 0, len(arr) - 1
    while left <= right:
        mid = (left + right) // 2
        if arr[mid] == target:
            return mid
        elif arr[mid] < target:
            left = mid + 1
        else:
            right = mid - 1
    return -1

# 测试代码
arr = [1, 2, 3, 4, 5, 6, 7, 8, 9]
target = 4
result = binary_search(arr, target)
if result != -1:
    print(f"元素 {target} 在索引 {result} 处找到。")
else:
    print(f"元素 {target} 未找到。")
(3)工作原理示例

假设有一个有序列表 [1, 2, 3, 4, 5, 6, 7, 8, 9],我们要查找元素 4

  1. 初始范围:left = 0, right = 8
  2. 中间索引:mid = (0 + 8) // 2 = 4
    • 中间元素:arr[4] = 5
    • 目标值 4 小于 5,更新范围:right = mid - 1 = 3
  3. 新范围:left = 0, right = 3
  4. 中间索引:mid = (0 + 3) // 2 = 1
    • 中间元素:arr[1] = 2
    • 目标值 4 大于 2,更新范围:left = mid + 1 = 2
  5. 新范围:left = 2, right = 3
  6. 中间索引:mid = (2 + 3) // 2 = 2
    • 中间元素:arr[2] = 3
    • 目标值 4 大于 3,更新范围:left = mid + 1 = 3
  7. 新范围:left = 3, right = 3
  8. 中间索引:mid = (3 + 3) // 2 = 3
    • 中间元素:arr[3] = 4
    • 找到目标值 4,返回索引 3
(4)优缺点
<1>优点
  1. 高效:时间复杂度为 O(log n),适合处理大规模数据。
  2. 实现简单:逻辑清晰,实现相对简单。
<2>缺点
  1. 需要有序数据:只能应用于已经排序的数据。
  2. 不适用于链表等线性访问的数据结构:因为需要随机访问数据。

二十四、模块(module)

模块是一个包含Python代码的文件,可以包含函数、类、变量以及可执行的代码。模块提供了一种组织代码的方式,将相关的代码放在一起,可以提高代码的可读性和重用性。

1.文档

模块文档

2.导入模块

要使用模块中的函数、类或变量,需要先导入模块。Python提供了多种导入模块的方法:

(1)使用 import 导入整个模块
import math # 移动光标到 math 然后输出 ctrl + b 可以查看模块的具体内容
from math import * # 另一种写法

print(math.sqrt(16))  # 输出: 4.0
print(fabs(-12)) # 输出:12.0
(2)使用 from ... import ... 导入模块中的特定部分
from math import sqrt

print(sqrt(16))  # 输出: 4.0
(3)使用 as 给模块或函数起别名
import math as m

print(m.sqrt(16))  # 输出: 4.0

from math import sqrt as square_root

print(square_root(16))  # 输出: 4.0

注:快捷键 alt + enter 或 shift + alt + enter :将鼠标移到一些模块中的函数并输入可以快速导入模块

3.创建模块

任何Python文件都可以作为一个模块使用。例如,创建一个名为 mymodule.py 的文件,其中包含以下代码:

# mymodule.py

def greet(name):
    print(f"Hello, {name}!") 

pi = 3.14159

然后,可以在另一个Python文件中导入并使用这个模块:

# main.py
import mymodule

print(mymodule.greet("Alice"))  # 输出: Hello, Alice!
print(mymodule.pi)              # 输出: 3.14159

4.内置模块

Python标准库包含了许多内置模块,这些模块提供了广泛的功能,例如操作系统接口、文件I/O、数学运算等。以下是一些常见的内置模块:

(1)math 模块:提供基本的数学运算函数
import math

print(math.pi)          # 输出: 3.141592653589793
print(math.factorial(5))# 计算阶乘的函数,输出: 120
(2)os 模块:提供操作系统相关的功能
import os

print(os.name)          # 输出操作系统名称
print(os.listdir('.'))  # 输出当前目录下的文件列表

在我的电脑上输出如下:

nt
['.idea', '.venv', '第二章']

注:nt: 这是 Windows 操作系统的标识符。nt 是 Windows NT 系列操作系统的简称,NT 代表 “New Technology”,包括 Windows NT、2000、XP、Vista、7、8、10 和 11。

posix: 这是类 Unix 操作系统的标识符,包括 Linux、macOS 以及其他类 Unix 系统。POSIX 是 “Portable Operating System Interface” 的缩写,是一系列 API 标准。

(3)sys 模块:提供与Python解释器相关的功能
import sys

print(sys.version)      # 输出Python版本信息
print(sys.path)         # 输出模块的搜索路径
(4)datetime 模块:提供日期和时间功能
import datetime

now = datetime.datetime.now()
print(now)              # 输出当前日期和时间

5.__name__ 变量

在Python中,__name__是一个内置变量,用于表示当前模块的名称。通过检查__name__的值,可以确定模块是被直接运行还是被导入到其他模块中。使用这种机制,可以避免在模块被导入时执行测试代码或其他不必要的代码。

  1. 当模块被直接运行,即自己运行自己时,__name__ 的值为 '__main__'

  2. 当模块被导入时,即由别的代码来运行自己时,__name__ 的值为模块的名称,通常这将是Python文件本身的名称去掉.py后缀

(1)示例

假设有一个名为 mymodule.py 的模块,包含一些函数和测试代码:

# mymodule.py

def greet(name):
    return f"Hello, {name}!"

def add(a, b):
    return a + b

# 测试代码
if __name__ == "__main__": # 注意,这里有两个_
    print(greet("Alice"))
    print(add(3, 5))

解释:

mymodule.py 中,定义了两个函数 greetadd,并在文件的末尾包含了一些测试代码。如果直接运行这个模块:

python mymodule.py

输出将是:

Hello, Alice!
8

因为 __name__ 的值是 '__main__',所以测试代码被执行。

然而,如果这个模块被导入到另一个模块中:

# main.py
import mymodule

print(mymodule.greet("Bob"))
print(mymodule.add(10, 20))

输出将是:

Hello, Bob!
30

此时,mymodule.py 中的测试代码不会执行,因为 __name__ 的值是 'mymodule',而不是 '__main__'

(2)使用 __name__ 的好处
  1. 避免测试代码被执行:确保测试代码或其他不必要的代码仅在模块被直接运行时执行,而不是在模块被导入时执行。
  2. 提高代码可重用性:模块可以被安全地导入到其他模块中,而不会执行测试代码。
  3. 便于调试和测试:可以在模块中包含测试代码进行独立测试,而不影响模块的导入和使用。

在Python中,__all__ 是一个列表,用于定义模块的公共接口。当使用 from module import * 语句导入模块时,只有在 __all__ 列表中指定的名称会被导入。如果没有定义 __all__,则默认导入所有不以下划线开头的名称。使用 import module 的方式导入模块时,不受 __all__ 的限制。

6.使用 __all__ 控制导入

假设有一个模块 mymodule.py,内容如下:

# mymodule.py

__all__ = ['public_function']

def public_function():
    print("This is a public function")

def _private_function():
    print("This is a private function")

在这个模块中,__all__ 定义了 public_function 为公共接口,因此就算import * 也只能导入这一个函数

(1)导入示例
<1>使用 from module import *
from mymodule import *

public_function()  # 输出: This is a public function

# 下面的代码会引发 AttributeError,因为 _private_function 不在 __all__ 中
# _private_function()  

在这种导入方式下,只有 __all__ 中的名称 public_function被导入。

<2>使用 import module
import mymodule

mymodule.public_function()  # 输出: This is a public function
mymodule._private_function()  # 输出: This is a private function

在这种导入方式下,模块中的所有名称都可以通过 mymodule 访问,不受 __all__ 限制。

7.第三方模块

除了Python标准库中的模块,还可以安装和使用第三方模块。第三方模块通常托管在Python包管理工具PyPI(Python Package Index)上,可以使用 pip 命令进行安装:

(1)进入到命令行控制台

(2)语法:pip install 库名/包名,注意安装时要确保网络连通

Usage:
    pip <command> [options]
命令说明
install安装软件包
download下载软件包
uninstall卸载软件包
freeze输出已安装软件包
inspect检查 Python 环境
list列出已安装软件包
show显示已安装软件包的信息
check验证已安装的软件包是否兼容
config管理本地和全局配置
search在 PyPI 中搜索软件包
pip install requests

安装后,可以在代码中导入并使用第三方模块:

import requests

在使用 pip 安装 Python 包时,默认情况下会从 Python 官方包管理仓库(PyPI)下载包。在某些情况下,例如网络速度慢或官方仓库访问不稳定时,可以使用国内的镜像源来加速下载和安装过程:

在命令行中使用 pip install 命令时,可以通过 -i 参数临时指定包源

pip install -i 指定源 库名/包名

例如:

pip install -i https://pypi.tuna.tsinghua.edu.cn/simple requests
  • 清华大学

    https://pypi.tuna.tsinghua.edu.cn/simple
    
  • 阿里云

    http://mirrors.aliyun.com/pypi/simple/
    
  • 中国科学技术大学

    https://pypi.mirrors.ustc.edu.cn/simple/
    
  • 华中理工大学

    http://pypi.hustunique.com/
    
  • 豆瓣

    http://pypi.douban.com/simple/
    

8.模块的组织结构:包(package)

大型项目通常将多个模块组织成包。包是一个包含 __init__.py 文件的目录,__init__.py 文件可以是空的,也可以包含包的初始化代码。

(1)示例包结构:
mypackage
    __init__.py
    module1.py
    module2.py

module1中代码:

def greet(name):
    return f"Hello, {name}!"

module2中代码:

def add(a, b):
    return a + b

导入包中的模块:

from mypackage import module1 # 写法一
import mypackage.module2 # 写法二

a= module1.greet("alice")  # 写法一不用带包名
print(a)
b = mypackage.test2.add(3,5) # 写法二要带包名
print(b)

"""
总之,用from写法后面写的越详细,调用时就越简洁,例如下面的引入代码:
from mypackage.module1 import greet
调用时可直接使用下面代码,可以看到连模块名都省略了
greet()  
"""
(2)使用 __init__.py 控制模块导入

mypackage 包的 __init__.py 文件中,可以使用 __all__ 控制允许导入的模块:

# __init__.py
__all__ = ['module1', 'module2']

这样可以在使用 from mypackage import * 时,控制哪些模块会被导入。

__init__.py中添加如下代码:

# __init__.py
__all__ = ['module1']

使用 from mypackage import * 导入,用 import 会不起作用:

from mypackage import *

a = module1.greet("alice")   # 不用带包名
print(a)                     # 输出: Hello, alice

b = module2.add(3, 5)
print(b)                     # AttributeError: module 'mypackage' has no attribute 'module2'
(3)带子包结构示例

子包可以帮助进一步组织代码。以下是如何定义和使用子包的示例。

<1>示例包结构:
mypackage/
    __init__.py
    module1.py
    module2.py
    subpackage/
        __init__.py
        module3.py
        module4.py

module1 中的代码:

# module1.py
def greet(name):
    return f"Hello, {name}!"

module2 中的代码:

# module2.py
def add(a, b):
    return a + b

subpackage/module3 中的代码:

# module3.py
def subtract(a, b):
    return a - b

subpackage/module4 中的代码:

# module4.py
def multiply(a, b):
    return a * b
<2>使用子包

subpackage 的 __init__.py

# subpackage/__init__.py
__all__ = ['module3', 'module4']

导入子包中的模块:

from mypackage.subpackage import module3  # 写法一
import mypackage.subpackage.module4       # 写法二

c = module3.subtract(10, 4)               # 写法一不用带包名
print(c)                                  # 输出: 6

d = mypackage.subpackage.module4.multiply(3, 5) # 写法二要带包名
print(d)                                       # 输出: 15
# __init__.py
__all__ = ['module1', 'subpackage']

二十五、面向对象编程(OOP)

面向对象程序设计(Object Oriented Programming)通过将数据和操作封装在对象中来实现程序的设计。这种设计方法的基本原则是,计算机程序由单个能够起到子程序作用的单元或对象组合而成。

1.类与实例

(1)文档

类与实例文档

(2)关系

  • :类是一个模板,用于定义对象的属性和行为。
  • 实例:实例是通过类创建的具体对象。每个实例都是类的一个具体实现。

(3)简单例子

在这里插入图片描述

注意事项和细节说明:

  • age,name,color等就是属性,属性的定义语法同变量,具体为:属性名 = 值,如果没有值,可以赋值为nonenone是Python的内置常量,通常用来代表空值的对象,具体见文档:https://docs.python.org/zh-cn/3.12/library/constants.html?highlight=none#None
  • 如果在类中给属性有指定具体值,那么创建的对象属性就有值,但你仍然在实例中更改

(4)对象的传递机制

来看下面这个代码:

class Person:
    age = None
    name = None

p1 = Person()
p1.age = 10
p1.name = "小明"
p2 = p1  # p1 赋值给 p2,因此 p2 指向 p1

print(p2.age)  # 输出: 10
print(f"p1.name 地址: {id(p1.name)} p2.name 地址: {id(p2.name)}")
# 输出:p1.name 地址: 2750273316944 p2.name 地址: 2750273316944

在Python中,对象的传递是通过引用传递的。这意味着,当我们将一个对象赋值给另一个变量时,实际上是将该对象的引用(地址)赋值给新的变量,而不是创建一个新的对象。这两个变量指向同一个内存地址,因此对其中一个变量进行的修改会影响到另一个变量。

在这里插入图片描述

2.对象的布尔值

在 Python 中,一切皆为对象,所有对象都有一个布尔值。通过内置函数 bool() 可以获取对象的布尔值。

(1)布尔值为 False的对象
  1. False
  2. 数值 0
  3. None
  4. 空字符串 ""
  5. 空列表 []
  6. 空字典 {}
  7. 空元组 ()
  8. 空集合 set()
(2)代码示例
print("---下面对象的布尔值为 False---")
print(bool(False))  # 输出: False
print(bool(0))      # 输出: False
print(bool(None))   # 输出: False
print(bool(""))     # 输出: False
print(bool([]))     # 输出: False  # 空列表
print(bool(()))     # 输出: False  # 空元组
print(bool({}))     # 输出: False  # 空字典
print(bool(set()))  # 输出: False  # 空集合

这些对象在布尔上下文中(如 if 语句)会被视为 False,可以直接在条件语句中使用对象进行判断

content = "hello"
if content:
    print(f"hi {content}")  # 输出: hi hello
else:
    print("空字符串")

lst = []
if lst:
    print(f"lst: {lst}")  
else:
    print("空列表") # 输出:空列表

在上述示例中:

  • content 是一个非空字符串,因此布尔值为 True,执行 if 分支。
  • lst 是一个非空列表,因此布尔值为 False,执行 else 分支。

3.成员方法

下面是带方法的类的定义与实例化,假设我们要定义一个表示“狗”的类,然后创建具体的狗的实例,其中__init__和 self 在后面会细讲

(1)定义类
class Dog:
    def __init__(self, name, age): 
        self.name = name  # 实例属性
        self.age = age    # 实例属性

    def bark(self):
        return f"{self.name} is barking."
(2)创建实例(实例对象)
# 创建两个狗的实例
dog1 = Dog("Buddy", 5)
dog2 = Dog("Lucy", 3)

# 访问实例属性和方法
print(dog1.name)  # 输出: Buddy
print(dog1.age)   # 输出: 5
print(dog1.bark())  # 输出: Buddy is barking.

print(dog2.name)  # 输出: Lucy
print(dog2.age)   # 输出: 3
print(dog2.bark())  # 输出: Lucy is barking.
  1. 定义类

    • Dog 类定义了狗的属性(nameage)和行为(bark 方法)。
    • __init__ 方法是初始化方法,当创建实例时会自动调用。
  2. 创建实例

    • dog1dog2 是通过 Dog 类创建的具体对象,称为实例。
    • 每个实例都有自己的 nameage 属性,可以通过实例访问。
  3. 访问实例属性和方法

    • 使用 dog1.namedog1.age 访问实例的属性。
    • 使用 dog1.bark() 调用实例的方法。
(3)动态给对象添加方法

Python 支持动态地给对象添加方法,即在程序运行时可以给某个实例对象添加新的方法。需要注意的是,这种方法仅对该实例对象有效,不会影响同类的其他实例。

def hi():
    print ("hi")
class Person:
    age = None
    name = None

# 创建两个对象 p 和 p2
p = Person()
p2 = Person()

# 动态地给 p 对象添加方法,取名为 m1
p.m1 = hi

# 调用 p 对象的 m1 方法
p.m1()  # 输出: hi

# 因为没有动态地给 p2 添加 m1 方法,会报错
p2.m1() 
(4)self

self 是类方法的第一个参数,用于指代实例对象本身。它允许我们在类的方法中访问实例的属性和其他方法。

<1>用法说明

在方法定义的参数列表中,必须包含 self

class Dog:
    name = "德牧"
    age = 2

    def info(self, name):
        print(f"name信息 -> {name}")
        
dog = Dog() # dog 是 Dog 类的实例。
dog.info("德牧") # 调用 info 方法时,Python 会自动将实例 dog 作为第一个参数传递给 self,同时传递字符串 "德牧" 作为 name 参数。

如果不写 self,则需要用 @staticmethod 装饰器来标注方法,否则会报错。

<2>静态方法 @staticmethod

静态方法是类中的方法,不会隐式传递实例或类作为第一个参数。静态方法不依赖实例或类属性,只是一种组织方法在类中的方式。静态方法不需要 self 参数,可以通过类名或实例调用。

class C:
    @staticmethod
    def f(arg1, arg2, argN):
        # 方法体
        ...

静态方法通常用于一些逻辑上归类在一起,但不需要访问类或实例属性的方法。

class Dog:
    name = "德牧"
    age = 2

    def info(self, name):
        print(f"name信息 -> {name}")

    @staticmethod
    def ok():
        print("ok()...")
# 调用静态方法 ok
# 方式1: 通过实例调用
dog.ok()  # 输出: ok()...

# 方式2: 通过类名调用
Dog.ok()  # 输出: ok()...
  • nameage 是类属性,所有实例共享。
  • info 是实例方法,需要 self 作为第一个参数。
  • ok 是静态方法,用 @staticmethod 装饰器标注。
(5)构造方法(构造器)

在 Python 中,__init__ 方法被称为构造器(constructor),用于初始化对象。每当创建类的实例时,__init__ 方法会自动被调用。它允许在实例化对象时设定初始属性值。

<1>__init__ 方法的特点
  1. 自动调用:在创建实例时,__init__ 方法会自动调用,即自动执行
  2. 设置初始值:通常用于设置对象的初始属性值。
  3. 参数传递:可以接受多个参数,以便在实例化时传递初始值。

注:一个类中只能有一个构造方法,即使你写了多个,也只有最后一个生效。同时,构造方法不能有返回值

<2>语法
class ClassName:
    def __init__(self, param1, param2, ...):
        self.attribute1 = param1
        self.attribute2 = param2
        ...
<3>示例

假设我们有一个表示“狗”的类,需要在创建实例时初始化狗的名字和年龄:

class Dog:
    def __init__(self, name, age):
        self.name = name
        self.age = age

    def info(self):
        print(f"My name is {self.name} and I am {self.age} years old.")

# 创建实例
dog1 = Dog("Buddy", 5)
dog2 = Dog("Lucy", 3)

# 调用方法
dog1.info()  # 输出: My name is Buddy and I am 5 years old.
dog2.info()  # 输出: My name is Lucy and I am 3 years old.

4.面向对象的三大特征

面向对象编程有三大特征:封装、继承、多态

(1)封装

封装是面向对象编程(OOP)的基本概念之一,它指的是将对象的属性和方法包装在一起,并限制外部对这些属性和方法的直接访问。通过封装,可以保护对象的内部状态,避免外部代码的直接修改,从而确保对象的内部数据一致性和完整性。

<1>生活例子:智能手机
  1. 外部操作
    • 我们通过屏幕触摸、按键等接口来操作手机,进行打电话、发短信、上网等功能。这些接口是公开的,允许我们自由使用。
  2. 内部保护
    • 手机内部的电池、处理器、电路板等组件是封装起来的,我们不能直接接触和修改它们。这样可以防止我们误操作导致手机损坏。
  3. 方法和属性
    • 当我们打开某个应用时,手机会调用内部的程序来执行这个操作,但我们不需要了解程序的具体实现细节。应用就是手机的方法,我们可以通过特定的方式调用这些方法。
    • 手机的电量、存储空间等信息是内部属性,我们可以通过手机的设置界面查看和管理这些属性,但不能直接修改它们,确保了手机的正常运作。
<2>封装的实现

在Python中,通过以下方式实现封装:

  1. 公开属性和方法:可以被外部代码访问和修改。
  2. 受保护的属性和方法:以单下划线 _ 开头,表示建议仅在类内部和子类中访问,不应从类外部直接访问。
  3. 私有属性和方法:以双下划线 __ 开头,表示强制性地只能在类内部访问,不能从类外部直接访问。
<3>快速入门示例代码

创建职员类(Clerk),属性有 name job salary,要求做到:

  • 不能随便查看职员Clerk的职位和工资等隐私,比如职员(“tiger”, “Python工程师”, “20000”)
  • 提供公共方法,可以对职位和工资进行操作
class Clerk:
    """
    公共属性
    name = None
    私有属性
    __job = None
    __salary = None
    这些代码可有可无
    """

    # 构造方法
    def __init__(self, name, job, salary):
        self.name = name
        self.__job = job
        self.__salary = salary

    # 提供公共的方法,对私有属性操作(根据实际业务编写即可)
    def set_job(self, job):
        self.__job = job

    def get_job(self):
        return self.__job
    
    # 私有方法
    def __hi(self):
        print("hi()")
        
    # 提供公共的方法,对私有方法操作(根据实际业务编写即可)
    def f1(self):
        self.__hi()


clerk = Clerk("tiger", "Python工程师", 20000)
# 如果是公共属性,在类的外部可以直接访问
print(clerk.name)  # 输出:tiger
# 如果是私有属性,在类的外部不可以直接访问
print(clerk.__job)  # 输出:AttributeError: 'Clerk' object has no attribute '__job'
# 只有授权了才能访问私有属性,即提供公共方法
clerk.set_job("Java工程师")  
print(clerk.get_job()) # 输出:Java工程师
clerk.__hi() # 输出:AttributeError: 'Clerk' object has no attribute '__hi'
clerk.f1() #输出:hi()
<4>注意事项和细节
  • Python语言的动态特性,会出现伪私有属性的情况

在 Python 中,伪私有属性(也称为名称改写或名称重整)是一种在类内部将属性或方法名称进行修改的方法,以避免名称冲突并提供一定程度的封装。虽然这种方式不能完全防止外部访问,但它使得直接访问变得不那么直观。

伪私有属性的定义是通过在属性或方法前添加双下划线 __,但不会以双下划线结尾。Python 会在内部对这些名称进行改写,以避免子类覆盖或意外访问。

以下是一个使用伪私有属性的示例:

class MyClass:
    def __init__(self, value):
        self.__value = value  # 伪私有属性

    def get_value(self):
        return self.__value

    def set_value(self, new_value):
        self.__value = new_value

# 创建一个实例
obj = MyClass(10)

# 访问伪私有属性的方法
print(obj.get_value())  # 输出: 10

# 修改伪私有属性的方法
obj.set_value(20)
print(obj.get_value())  # 输出: 20

# 直接访问伪私有属性
# print(obj.__value)  # AttributeError: 'MyClass' object has no attribute '__value'

# 通过名称改写后的名称访问伪私有属性(不推荐)
print(obj._MyClass__value)  # 输出: 20

虽然 Python 不允许直接访问伪私有属性,但可以通过 _类名__属性名 的方式访问改写后的属性名。在这个例子中,通过 obj._MyClass__value 访问伪私有属性,输出 20,所以简单来说,我们之前所说的其实全都是伪私有属性,有一种“投机取巧”的方式来直接访问并且不会报错,很神奇,通过网上查阅得知这中“投机取巧”其实是不能避免的,因为Python 的设计哲学是“我们都是成年人”,即 Python 倾向于信任程序员,认为他们不会故意去破坏封装性。然而,可以通过一些编程习惯和设计模式来进一步增强封装性和数据保护,我们后面再说

(2)继承(inheritance)

继承是面向对象编程(OOP)中的一个重要概念,它允许一个类(子类)从另一个类(父类)继承属性和方法。通过继承,子类可以复用父类的代码,同时也可以扩展和修改父类的行为。继承提高了代码的重用性和可维护性。

<1>基本概念
  1. 父类(基类):被继承的类。
  2. 子类(派生类):继承父类的类。
<2>继承的语法

在 Python 中,继承使用括号内的类名来表示:

class Parent:
    # 父类的属性和方法,即共有的
    def __init__(self, name):
        self.name = name

    def speak(self):
        print(f"{self.name} is speaking.")

class Child(Parent):
    # 子类继承父类
    def __init__(self, name, age):
        super().__init__(name)  # 调用父类的构造方法
        self.age = age

    def speak(self):
        print(f"{self.name} is speaking, and they are {self.age} years old.")

    def play(self):
        print(f"{self.name} is playing.")
<3>继承的示例
class Parent:
    def __init__(self, name):
        self.name = name
        self.__secret = "This is a secret!"  # 私有属性

    def speak(self):
        print(f"{self.name} is speaking.")

    def __reveal_secret(self):
        return self.__secret  # 私有方法

    def show_secret(self): # 由父类提供公有方法给子类来调用私有属性和私有方法
        print(f"{self.name}'s secret: {self.__reveal_secret()}")

class Child(Parent):
    def __init__(self, name, age):
        super().__init__(name)
        self.age = age

    def speak(self):
        print(f"{self.name} is speaking, and they are {self.age} years old.")

    def play(self):
        print(f"{self.name} is playing.")

    def show_secret(self):
        # 子类扩展了 show_secret 方法
        print(f"{self.name} is trying to reveal the secret...")
        super().show_secret()

# 创建父类的实例
parent = Parent("John")
parent.speak()  # 输出: John is speaking.
parent.show_secret()  # 输出: John's secret: This is a secret!

# 创建子类的实例
child = Child("Jane", 10)
child.speak()  # 输出: Jane is speaking, and they are 10 years old.
child.play()  # 输出: Jane is playing.
child.show_secret()  # 输出: Jane is trying to reveal the secret... Jane's secret: This is a secret!

# 尝试直接访问父类的私有属性和私有方法(会报错)
print(child.__secret)  # AttributeError: 'Child' object has no attribute '__secret'

print(child._reveal_secret())  # AttributeError: 'Child' object has no attribute '_reveal_secret'

# 正确访问父类的私有属性和私有方法
print(child._Parent__secret)  # 不推荐,用到前面的伪私有属性的输出: This is a secret!
<4>继承的特点
  1. 代码复用:子类继承了父类的属性和方法,避免了重复代码的编写。
  2. 方法重写(override):子类可以重写父类的方法,以实现不同的行为。

在这里插入图片描述

  1. super() 函数:用于调用父类的方法,特别是父类的构造方法。
  2. Python编程语言中,“object”是所有其他类的基类,通过 ctrl + h 可以查看类的继承
<5>多重继承与冲突解决

Python 支持多重继承,通过在类定义时列出多个父类来实现

以下示例展示了如何使用多重继承,以及如何处理可能的冲突:

class Animal:
    def __init__(self, name):
        self.name = name

    def eat(self):
        print(f"{self.name} is eating.")

class CanFly:
    def fly(self):
        print(f"{self.name} is flying.")

class CanSwim:
    def swim(self):
        print(f"{self.name} is swimming.")

class Duck(Animal, CanFly, CanSwim): # 多重继承
    def __init__(self, name):
        super().__init__(name)

    def quack(self):
        print(f"{self.name} is quacking.")

# 创建 Duck 类的实例
duck = Duck("Donald")

# 调用来自 Animal 类的方法
duck.eat()  # 输出: Donald is eating.

# 调用来自 CanFly 类的方法
duck.fly()  # 输出: Donald is flying.

# 调用来自 CanSwim 类的方法
duck.swim()  # 输出: Donald is swimming.

# 调用 Duck 类自己的方法
duck.quack()  # 输出: Donald is quacking.
  1. 定义多个父类

    • Animal 类:包含 name 属性和 eat 方法。
    • CanFly 类:包含 fly 方法。
    • CanSwim 类:包含 swim 方法。
  2. 定义子类 Duck

    • Duck 类继承了 AnimalCanFlyCanSwim,因此拥有所有父类的属性和方法。
    • __init__ 方法使用 super() 调用父类的构造方法。
  3. 创建实例并调用方法

    • 创建 Duck 类的实例 duck,并调用从各个父类继承的方法,以及 Duck 类自己的方法。

在多重继承中,如果多个父类中有同名的方法或属性,Python 会按照方法解析顺序(MRO,Method Resolution Order)来决定调用哪个父类的方法或属性。可以使用 super() 函数和 mro() 方法来查看和控制这种顺序。

class A:
    def do_something(self):
        print("A's method")

class B:
    def do_something(self):
        print("B's method")

class C(A, B):
    def do_something(self):
        super().do_something()
        print("C's method")

# 创建 C 类的实例
c = C()
c.do_something()
# 输出:
# A's method
# C's method

# 查看方法解析顺序
print(C.mro())
# 输出:
# [<class '__main__.C'>, <class '__main__.A'>, <class '__main__.B'>, <class 'object'>]
  1. 定义父类 A 和 B

    • A 类和 B 类都有一个 do_something 方法。
  2. 定义子类 C

    • C 类继承了 AB,并重写了 do_something 方法。
    • C 类的 do_something 方法中,通过 super() 调用了父类的方法。
  3. 方法解析顺序

    • C.mro() 返回方法解析顺序列表,表示在调用方法时,Python 按照这个顺序查找方法。对于 C 类,顺序是 C -> A -> B -> object(刚才说了“object”是所有其他类的基类)
  4. **总结:**可以看到在多重继承中,如果有同名的成员,遵守从左到右的继承优先级(写在越左边的父类优先级高,写在越右边的父类优先级低)

(3)类型注解

随着项目越来越大,代码也就越来越多,在这种情况下,如果没有类型注解,很容易不记得某一个方法的参数类型是什么,一旦传入了错误类型的参数,只有运行才能发现问题,这对大型项目来说是一个巨大的灾难

类型注解是 Python 3.5 引入的一种功能,用于在代码中显式声明变量、函数参数和返回值的类型。类型注解有助于代码的可读性、可维护性,并且可以与静态类型检查工具(如 mypy)配合使用,进行类型检查,且加入后并不会报正式的错误,只有提醒(在Pycharm中,出现类型错误会有黄色的警告)

<1>变量的类型注解
name: str = "Alice" # 标注name的类型为str,下同
age: int = 30
height: float = 5.7
is_student: bool = True

cat: Cat = Cat() # 对cat进行类型注解,标注cat的类型为Cat

my_list1: list = [100,200,300] # 对my_list1进行类型注解,标注my_list的类型为list
my_list2: list[int] = [100,200,300] # 对my_list2进行类型注解,标注my_list2的类型为list,且该list的元素是int类型
my_dict1: dict[str, int] = {"no1": 100, "no2": 200} # 对my_dict1进行类型注解,标注my_dict1的类型为dict,且key的元素是str类型,值是int类型
<2>注释的类型注解
# 注释中使用注解
# type: float 用于标注 变量 n3 的类型是 float
n3 = 89.9  # type: float
my_list3 = [100, 200, 300]  # type: list[int]
email = "hsp@sohu.com"  # type: str
<3>函数的类型注解
  1. 参数类型注解

    def greet(name: str) -> None:
        """
        参数name要是str类型
        返回:None
        """
        print(f"Hello, {name}")
    
  2. 返回值类型注解

    def add(x: int, y: int) -> int:
        """
        参数x (int): 第一个加数是int
        y (int): 第二个加数也是int
        返回int
        """
        return x + y
    
  3. 混合使用参数和返回值类型注解

    def introduce(name: str, age: int) -> str:
        """
        参数name是str类型
        age是int类型
        返回str
        """
        return f"Name: {name}, Age: {age}"
    
<4>联合类型注解

文档:https://docs.python.org/zh-cn/3.12/library/typing.html?highlight=union#typing.Union

使用 Union 来表示一个变量可以是多种类型之一

from typing import Union

def get_value(value: Union[int, str]) -> str:
    """
    参数:value (Union[int, str]): 可以是整数或字符串,满足其中之一即可,且不一定要两个,这里只是示例
    返回str
    """
    return str(value)
(4)多态(Polymorphism)

多态是面向对象编程中的一个重要概念,指的是不同的类可以通过相同的接口(方法)调用而产生不同的行为。多态使得对象能够以其具体类型的方式表现,从而增强了代码的灵活性和可扩展性。

<1>多态的核心
  1. 继承:子类继承父类的属性和方法。
  2. 方法重写:子类可以重写父类的方法。
  3. 接口一致性:不同类的对象可以通过相同的接口进行调用。
<2>生活中的多态例子

遥控器和电器

假设有一个通用的遥控器,它可以控制多种不同类型的电器(例如电视、空调、音响等)。尽管这些电器的具体实现不同,但它们都提供了相同的接口(例如开机、关机)。遥控器可以通过相同的按钮(方法)来控制这些电器,而电器根据自身的特性进行相应的操作,电视播放节目(调用自己的方法),空调制冷(调用自己的方法)

<3>补充点1

在Python可以将子类对象传递给父类类型的参数

  • 基类和子类定义
class Animal:
    def speak(self):
        print("Animal is speaking")

class Dog(Animal):
    def speak(self):
        print("Woof!")

class Cat(Animal):
    def speak(self):
        print("Meow!")

在这个示例中,Animal 是一个基类,它有一个 speak 方法。DogCat 是从 Animal 派生出来的子类,并且重写了 speak 方法。

  • 函数接受父类类型参数
def make_animal_speak(animal: Animal):
    animal.speak()
    # 其实可以看成是self,只是self不能用作独立函数
    print(id(animal))
    print(id(dog)) # 可以发现这两个地址其实是一样的

这个函数 make_animal_speak 接受一个 Animal 类型的参数,并调用它的 speak 方法。

  • 传递子类对象
dog = Dog()
cat = Cat()

make_animal_speak(dog)  # 输出: Woof!
make_animal_speak(cat)  # 输出: Meow!

在这个示例中,dogcat 对象都是 Animal 类型的子类对象,它们被传递给 make_animal_speak 函数,并正确地调用了各自的 speak 方法。

self区别如下:

  • self 是在类的方法中使用的惯例参数名,指向调用该方法的实例。

  • 在独立的函数中,不使用 self 作为参数名。

  • 独立的函数可以接受父类类型参数,并且调用子类方法时,利用多态性实现不同的行为。

<4>isinstance函数

isinstance() 函数是Python内置的一个函数,用于检查一个对象是否是指定类或其子类的实例。它返回一个布尔值 TrueFalse,表示对象是否是给定类型的实例。

语法

isinstance(object, classinfo)
  • object:要进行类型检查的对象。
  • classinfo:可以是一个类或类型,也可以是由类或类型构成的元组。
  1. 检查单一类型
class Animal:
    pass

class Dog(Animal):
    pass

dog = Dog()

# 检查 dog 是否是 Dog 类的实例
print(isinstance(dog, Dog))  # 输出: True

# 检查 dog 是否是 Animal 类的实例
print(isinstance(dog, Animal))  # 输出: True

# 检查 dog 是否是 str 类型的实例
print(isinstance(dog, str))  # 输出: False
  1. 检查多个类型

可以传递一个由多个类型组成的元组isinstance 会检查对象是否是其中之一的实例:

# 可以检查 dog 是否是 Dog 或 Animal 或 str 类型的实例
print(isinstance(dog, (Dog, Animal, str)))  # 输出: True

使用场景

  • 类型检查:在函数或方法中使用 isinstance() 来确保参数是期望的类型。

  • 多态支持:在使用继承和多态时,isinstance() 可以帮助判断对象的具体类型,从而执行不同的操作。

def process(value):
    if isinstance(value, int):
        print(f"Processing integer: {value}")
    elif isinstance(value, str):
        print(f"Processing string: {value}")
    else:
        print("Unsupported type")

process(100)   # 输出: Processing integer: 100
process("abc") # 输出: Processing string: abc
process([1, 2, 3]) # 输出: Unsupported type
  • 继承与多态
class Animal:
    def speak(self):
        print("Animal is speaking")

class Dog(Animal):
    def speak(self):
        print("Woof!")

def make_animal_speak(animal):
    if isinstance(animal, Animal):
        animal.speak()
    else:
        print("This is not an Animal!")

dog = Dog()
make_animal_speak(dog)  # 输出: Woof!
make_animal_speak("Not an animal")  # 输出: This is not an Animal!
<5>补充点2

在Python中调用对象的成员(属性或方法)时,实际上发生了一种叫做“动态绑定”(或“动态关联”)的过程。这种机制确保了在运行时,Python能够根据对象的具体类型动态地找到并调用相应的成员。这种动态性是Python作为动态语言的一个核心特性。

  1. 解释动态绑定

动态绑定是指在运行时,Python会根据对象的实际类型来决定调用哪个方法或访问哪个属性。也就是说,Python并不会在编译时(因为Python是解释型语言,没有传统意义上的编译过程)就决定哪个方法会被调用,而是等到运行时才做出这个决定。

  1. 具体过程
  • 成员查找顺序

    • 当你调用一个对象的方法或属性时,Python会先在对象的实例(即其类的实例)中查找该成员。

    • 如果在实例中没有找到,Python会继续在该对象的类(包括其父类,按照继承链)中查找。

    • 一旦找到符合的成员,Python就会“动态”地将这个成员与对象实例关联起来,并执行相应的操作。

  • 方法绑定

    • 当一个方法被调用时,它会被绑定到对象实例上,self 会指向这个对象实例。通过 self,方法可以访问和修改该对象的状态(即对象的属性)。
  1. 示例
class A:
    i = 10

    def sum(self):
        return self.getI() + 10

    def sum1(self):
        return self.i + 10

    def getI(self):
        return self.i


class B(A):
    i = 20

    def getI(self):
        return self.i


b = B()
print(b.sum()) # 输出:30
print(b.sum1()) # 输出:30
  • 分析过程
  1. AB 的定义
    • A 类中定义了类属性 i = 10,以及方法 sum(), sum1()getI()
    • B 类继承了 A 类,并且重写了 getI() 方法,同时定义了自己的类属性 i = 20
  2. 创建对象 b
    • bB 类的实例,因此 b 对象会优先使用 B 类中的方法和属性。
  3. 调用 b.sum()
    • sum() 方法定义在 A 类中,但因为 B 类继承了 A,所以 B 类的对象 b 也可以调用 sum() 方法。
    • sum() 方法内部调用了 self.getI()。这里的 self 是指 b 对象,虽然 sum() 方法定义在 A 类中,但因为 bB 类的实例,Python 在运行时会优先调用 B 类中的 getI() 方法(注意这里不是调用A的 getI 方法)
    • B 类中的 getI() 方法返回的是 B.i,即 20,所以 sum() 方法最终返回 20 + 10 = 30
  4. 调用 b.sum1()
    • sum1() 方法也定义在 A 类中,并且直接返回 self.i + 10。这里的 self.i 是指 b.i
    • 由于 bB 类的实例,而 B 类中有 i = 20,所以 b.i 返回 20
    • 因此 sum1() 方法最终返回 20 + 10 = 30
  • 动态关联的关键点

    • 方法调用:在调用 sum() 时,虽然 sum() 方法定义在 A 类中,但因为它使用了 self.getI(),而 self 是指 b 对象,因此调用的是 B 类中重写的 getI() 方法。

    • 属性访问:在 sum1() 中,self.i 直接访问了 B 类的 i 属性,返回 20。即使 sum1() 方法定义在 A 类中,它依然能够动态访问 B 类中的 i 属性。

(5)魔术方法(Magic Methods)

魔术方法也被称为“特殊方法”或“双下划线方法”(Dunder Methods),是Python中的一些具有特殊用途的内置方法。这些方法通常以双下划线 __ 开头和结尾,用于定义类的行为,如对象的初始化、表示、比较、运算等,魔术方法不需要调用就能执行

<1> 文档

在官方文档中查看所有魔术方法的详细介绍:魔术方法文档

<2> 常见的魔术方法及其用途

魔术方法使得对象可以表现得像内置类型一样,可以进行算术运算、比较、转换等操作。常见的魔术方法有 __init____str____repr____eq____lt____add____len__ 等。

  1. __init__(self, ...) — 初始化方法
  • 用途:当创建类的实例时,会自动调用 __init__ 方法,用于初始化对象的属性。

  • 示例

    class Person:
        def __init__(self, name, age):
            self.name = name
            self.age = age
    
    p = Person("Alice", 30)
    print(p.name, p.age)  # 输出: Alice 30
    
  1. __str__(self) — 字符串表示
  • 用途__str__ 方法用于定义当使用 print() 函数打印对象时,或者使用 str() 函数转换对象时,显示的字符串表示。
  • 未使用之前打印的是内存地址
class Person:
    def __init__(self, name, age):
        self.name = name
        self.age = age


p = Person("Alice", 30)
print(p)  # 输出: <__main__.Person object at 0x00000276C0DA8E90>
  • 使用后示例
class Person:
    def __str__(self):
        return f"Person(name={self.name}, age={self.age})"

p = Person("Alice", 30)
print(p)  # 输出: Person(name=Alice, age=30)
  1. __repr__(self) — 官方表示
  • 用途__repr__ 方法用于定义对象的官方表示,通常用于调试。__repr__ 返回的字符串应该尽量是合法的Python表达式,用于精确地重新创建对象。

  • 示例

    class Person:
        def __init__(self, name, age):
            self.name = name
            self.age = age
    
        def __repr__(self):
            return f"Person(name={self.name!r}, age={self.age!r})"
    
    p = Person("Alice", 30)
    print(repr(p))  # 输出: Person(name='Alice', age=30)
    print(p) #效果一样,因为这里没有__str__(self)这种也是打印对象的方法
    # !r 的使用是为了确保 name 和 age 的值以 repr() 的形式显示。对于字符串 'Alice',repr() 会在输出中包括引号,这就是为什么输出为 Person(name='Alice', age=30) 而不是 Person(name=Alice, age=30)。
    
  1. __eq__(self, other) — 相等比较
  • 用途__eq__ 方法定义了对象相等比较的行为,用于 == 操作符。

  • 示例

    class Person:
        def __init__(self, name, age):
            self.name = name
            self.age = age
    
        def __eq__(self, other):
            return self.name == other.name and self.age == other.age # 没有重写前比较的是内存地址,重写后是判断是否为同一个对象
    
    p1 = Person("Alice", 30)
    p2 = Person("Alice", 30)
    print(p1 == p2)  # 输出: True
    
  • 但是但是但是!这里的__eq__(self, other)比较笨,当两个对象不属于同一类对象,但是比较的属性均相同时还是会返回True(代码一),因此可以做出以下改进(代码二)

# 代码一(未改进)
class Person:
    def __init__(self, name, age):
        self.name = name
        self.age = age

    def __eq__(self, other):
        return self.name == other.name and self.age == other.age 
class Dog:
    def __init__(self, name, age):
        self.name = name
        self.age = age
        
dog = Dog("Alice", 30)
p1 = Person("Alice", 30)
print(dog == p1)  # 输出: True,但是人与狗不能比较
# 代码二(改进版)
class Person:
    def __init__(self, name, age):
        self.name = name
        self.age = age

    def __eq__(self, other):
        if isinstance(other, Person):  # 判断是否同类
            return self.name == other.name and self.age == other.age
        else:
            return False


class Dog:
    def __init__(self, name, age):
        self.name = name
        self.age = age


dog = Dog("Alice", 30)
p1 = Person("Alice", 30)
print(dog == p1)
  1. __lt__(self, other) — 小于比较
  • 用途__lt__ 方法定义了对象“小于”比较的行为,用于 < 操作符。

  • 示例

    class Person:
        def __init__(self, name, age):
            self.name = name
            self.age = age
    
        def __lt__(self, other):
            if isinstance(other, Person):  # 判断是否同类
                return self.age < other.age
            else:
                return False
    p1 = Person("Alice", 25)
    p2 = Person("Bob", 30)
    print(p1 < p2)  # 输出: True
    
  1. __add__(self, other) — 加法运算
  • 用途__add__ 方法定义了对象的加法运算行为,用于 + 操作符。

  • 示例

    class Vector:
        def __init__(self, x, y):
            self.x = x
            self.y = y
    
        def __add__(self, other):
            return Vector(self.x + other.x, self.y + other.y)
    
        def __repr__(self):
            return f"Vector({self.x}, {self.y})"
    
    v1 = Vector(1, 2)
    v2 = Vector(3, 4)
    v3 = v1 + v2
    print(v3)  # 输出: Vector(4, 6)
    
  1. __len__(self) — 长度
  • 用途__len__ 方法用于定义对象的长度,用于 len() 函数。

  • 示例

    class MyList:
        def __init__(self, items):
            self.items = items
    
        def __len__(self):
            return len(self.items)
    
    my_list = MyList([1, 2, 3, 4])
    print(len(my_list))  # 输出: 4
    
<3>其他常用的魔术方法(用到时请查阅文档)
  • __getitem__(self, key):定义对象的索引访问行为(用于 obj[key])。
  • __setitem__(self, key, value):定义对象的索引赋值行为(用于 obj[key] = value)。
  • __delitem__(self, key):定义对象的索引删除行为(用于 del obj[key])。
  • __iter__(self):定义对象的迭代行为(用于 for 循环)。
  • __call__(self, ...):使对象可以像函数一样被调用。
(6)Class对象和静态方法

在Python中,类不仅仅是用于创建对象的蓝图,它们本身也是一种对象,称为类对象。类对象可以用于创建实例,并且可以拥有方法和属性。静态方法是类中的一种特殊方法,它不依赖于类的实例,并且可以直接通过类名来调用。

<1> 文档

在官方文档中查看类对象和静态方法的详细介绍:Class对象文档

<2> Class对象

类对象 是 Python 中的一等对象,这意味着类本身可以像其他对象(如整数、字符串、函数)一样被操作。你可以将类对象赋值给变量、将其作为参数传递给函数,甚至可以在运行时动态创建类。

<3>类对象的属性和方法
  • 类属性:类属性是在类对象上定义的属性,它们在类的所有实例之间共享。类属性可以通过类名或实例来访问。

    class MyClass:
        class_attribute = "I am a class attribute"
        Myclass.name = "一个类"
    
    print(MyClass.class_attribute,Myclass.name)  # 输出: I am a class attribute
    
    instance = MyClass()
    print(instance.class_attribute)  # 输出: I am a class attribute
    
  • 实例属性:实例属性是在类的实例上定义的属性,它们是每个实例独有的。实例属性通常在 __init__ 方法中定义。

    class MyClass:
        def __init__(self, value):
            self.instance_attribute = value
    
    instance = MyClass("I am an instance attribute")
    print(instance.instance_attribute)  # 输出: I am an instance attribute
    
  • 类方法:类方法使用 @classmethod 装饰器定义,并且第一个参数是 cls,表示类对象。类方法可以通过类名或实例来调用,并且可以访问和修改类属性。

    class MyClass:
        class_attribute = 0
    
        @classmethod
        def increment_class_attribute(cls):
            cls.class_attribute += 1
    
    MyClass.increment_class_attribute()
    print(MyClass.class_attribute)  # 输出: 1
    
    instance = MyClass()
    instance.increment_class_attribute()
    print(MyClass.class_attribute)  # 输出: 2
    
<4> 静态方法(前面讲过,这里复习)

静态方法 是类中的一种方法,它不需要访问类对象或实例对象,因此不需要 self 参数。静态方法通常用于一些逻辑上属于类但不需要访问类或实例的上下文的功能。

  • 定义静态方法:使用 @staticmethod 装饰器来定义静态方法。

    class MyClass:
        @staticmethod
        def static_method1():
            print("I am a static method")
    
    
    class Person:
        def __init__(self, name, age):
            self.name = name
            self.age = age
    
    
    def static_method2(self): # 
        print(f"hi {self.name} im {self.age}")
    
    
    # 调用静态方法
    MyClass.static_method1()  # 输出: I am a static method,类名调用
    
    instance = MyClass()
    instance.static_method1()  # 输出: I am a static method,实例调用
    p1 = Person("小李", 20)
    static_method2(p1)
    
  • 使用场景:静态方法通常用于封装类相关的功能,但这些功能既不需要访问实例属性,也不需要访问类属性。例如,静态方法可以用于实现一些实用工具函数。

    class MathUtils:
        @staticmethod
        def add(a, b):
            return a + b
    
    print(MathUtils.add(3, 5))  # 输出: 8
    
(7)抽象类
<1>抽象类的引入
class Animal:
    def __init__(self, name, age):
        self.name = name
        self.age = age

    def cry(self):
        """
        动物都有叫唤的行为,但是这个行为不明确(即不能明确的实现)
        """
        print("不知道是什么动物,不知道是什么叫声...") # 这种输出毫无意义

对于以上代码,我们遇到如下问题:

  • 当父类的某些方法需要声明,但是又不确定如何实现,怎么办?
  • 不需要实例化父类对象,父类主要适用于设计和制定规范,让其它类来继承并实现,怎么办?

因此我们引出抽象类:

抽象类是面向对象编程中的一个重要概念,用于定义通用的接口或行为规范,但不能实例化。抽象类通常包含一个或多个抽象方法,这些方法没有具体的实现,子类必须重写这些抽象方法才能实例化和使用。Python通过abc模块提供对抽象类的支持。

<2>特点
  1. 不能实例化:抽象类不能被直接实例化,只能通过继承创建子类。
  2. 定义接口:抽象类定义了子类必须实现的方法接口,确保子类具有某些特定的行为。
  3. 部分实现:抽象类可以包含具体实现的方法,也可以包含抽象方法,抽象方法由子类实现。
<3>创建抽象类

在Python中,抽象类通过abc模块中的ABC类和@abstractmethod装饰器来定义。

from abc import ABC, abstractmethod

class Animal(ABC):
    @abstractmethod
    def speak(self):
        pass

    def move(self):
        print("Moving...")

class Dog(Animal):
    def speak(self):
        print("Woof!")

class Cat(Animal):
    def speak(self):
        print("Meow!")

解释

  1. Animal

    • Animal 是一个抽象类,继承自ABC,并使用了@abstractmethod装饰器来定义一个抽象方法speak
    • speak 方法没有具体实现,它只定义了一个接口,表示所有继承自Animal的子类都必须实现这个方法。
    • Animal 类还包含一个普通方法move,它有具体的实现,可以被子类直接继承和使用。
  2. DogCat

    • DogCat 类是具体类,它们继承自Animal
    • 由于Animal类定义了抽象方法speak,所以DogCat必须实现speak方法,(必须继承所有抽象方法,注意注意是所有,这里只有一个speak)否则它们也会成为抽象类,不能实例化。
    • 这些子类可以通过move方法继承自Animal的具体实现,同时也通过实现speak方法来满足抽象类的要求。
<4>使用抽象类

由于抽象类不能被实例化,因此下面的代码会抛出错误:

# 试图实例化抽象类将会引发错误
animal = Animal()  # TypeError: Can't instantiate abstract class Animal with abstract methods speak

但是我们可以实例化子类并调用其方法:

dog = Dog()
dog.speak()  # 输出: Woof!
dog.move()   # 输出: Moving...

cat = Cat()
cat.speak()  # 输出: Meow!
cat.move()   # 输出: Moving...
<5>抽象类的用途
  • 设计框架:抽象类常用于设计框架,定义一组必须由子类实现的方法,确保框架的一致性。
  • 实现接口:抽象类可以被看作接口的实现形式,它为子类提供了一种结构和约束。
  • 代码重用:通过在抽象类中定义部分实现,可以减少子类的重复代码,同时确保实现的统一性。
(8)模式设计之模板设计模式(Template Method Pattern)

模板设计模式是一种行为设计模式,它定义了一个算法的骨架,并允许子类在不改变算法结构的情况下重新定义算法的某些步骤。换句话说,模板方法模式将算法中的某些步骤推迟到子类中实现,使得在不修改原有代码的情况下可以扩展或变更某些算法的具体实现。

抽象类体现的就是一种模板设计模式,抽象类作为多个子类的通用模板,子类在抽象类的基础上进行拓展,改造,但子类总体会保留抽象类的行为方式

<1>模板设计模式能解决的问题
  • 当功能内部一部分是确定实现的,一部分实现是不确定的,这时可以把不确定的部分暴露出去,让子类去实现
  • 编写一个抽象父类,父类提供了多个子类的通用方法,并把一个或多个方法留给其子类实现,就是一种模板模式
<2>模板设计模式的引入
"""
1) 有多个类,完成不同的任务job
2) 要求统计得到各自完成任务的时间
3) 请编程实现  传统方式实现.py
"""

import time

class AA:
    def job(self):
        # 记录开始时间(秒)
        start = time.time() # time.time() 是 Python 标准库 time 模块中的一个函数。它用于返回当前时间的时间戳,即从 1970 年 1 月 1 日 00:00:00 到现在的秒数。这个时间戳是一个浮点数,可以精确到小数点后几位。
        
        # 执行任务逻辑,计算1到800000的和
        num = 0
        for i in range(1, 800001):
            num += i
        
        # 记录结束时间(秒)
        end = time.time()
        
        # 输出任务执行的时间
        print("计算任务 执行时间(秒) ", (end - start))


class BB:
    def job(self):
        # 记录开始时间(秒)
        start = time.time()
        
        # 执行任务逻辑,计算1到90000的差
        num = 1
        for i in range(1, 90001):
            num -= i
        
        # 记录结束时间(秒)
        end = time.time()
        
        # 输出任务执行的时间
        print("计算任务 执行时间 ", (end - start))

# 测试传统方式
if __name__ == '__main__':
    aa = AA()
    aa.job()  # 执行AA类的任务并输出时间
    bb = BB()
    bb.job()  # 执行BB类的任务并输出时间

不足之处:

  • 重复代码:在 AABB 类中,计算任务执行时间的逻辑是相同的,但这部分代码在每个类中都单独实现了。这种重复会导致代码冗余,使得代码不易维护。例如,如果以后需要修改时间统计的逻辑,就需要在每个类中分别修改,增加了出错的可能性。

  • 面向过程的编程风格:虽然使用了类,但代码仍然以过程为主,没有充分利用面向对象编程(OOP)的优势。具体来说,代码缺少抽象和封装,导致代码的灵活性不足。

  • 难以扩展:如果需要增加更多的任务类,比如 CCDD,这些类也需要分别实现统计时间的代码,重复劳动且容易出错。

<3>模板设计模式的示例(代码修改)
"""
1) 有多个类,完成不同的任务job
2) 要求统计得到各自完成任务的时间
3) 请编程实现  使用模板设计模式
"""

import time
from abc import ABC, abstractmethod

# 抽象类-模板类
class Template(ABC):
    """
    Template类作为一个抽象基类,定义了模板方法cal_time,用于计算任务执行的时间。
    具体的任务逻辑由子类通过实现抽象方法job来定义。
    """
    
    @abstractmethod  # 定义抽象方法,强制子类实现具体任务逻辑
    def job(self):
        pass

    # 模板方法:计算任务执行的时间
    def cal_time(self):
        # 记录开始时间(毫秒)
        start = time.time() * 1000
        
        # 调用子类实现的任务逻辑,这里具体的工作是不确定的,暴露出去
        self.job()
        
        # 记录结束时间(毫秒)
        end = time.time() * 1000
        
        # 输出任务执行的时间
        print("计算任务 执行时间(毫秒) ", (end - start))


class AA(Template):
    """
    AA类继承自Template类,实现了具体的任务逻辑。
    该类负责计算1到800000的和。
    """

    def job(self):
        num = 0
        for i in range(1, 800001):
            num += i


class BB(Template):
    """
    BB类继承自Template类,实现了具体的任务逻辑。
    该类负责计算1到90000的差。
    """

    def job(self):
        num = 1
        for i in range(1, 90001):
            num -= i


# 测试 , 模板设计模式
if __name__ == '__main__':
    aa = AA()
    aa.cal_time()  # 执行AA类的任务并输出时间
    
    bb = BB()
    bb.cal_time()  # 执行BB类的任务并输出时间

改进之处:

  • 引入抽象类:通过引入一个抽象类 Template,将时间统计的逻辑封装在一个通用的方法 cal_time 中。具体任务由子类实现 job 方法。这样就将通用逻辑与具体任务分离,避免了代码重复。

  • 代码复用性高:无论有多少个不同的任务类,只需要继承 Template 并实现 job 方法,就可以自动获得时间统计功能。减少了重复代码,提升了代码的复用性。

  • 易于维护和扩展:如果将来需要修改时间统计的逻辑,只需修改 Template 类中的 cal_time 方法,而不需要修改每个任务类,极大地方便了维护。同样,扩展时也只需添加新的子类,而不需重复实现时间统计代码。

  • 面向对象编程的优势:利用了继承和多态性,将通用行为放在父类中,子类只需关注特定的业务逻辑,这样既遵循了单一职责原则,又提高了代码的清晰度和可维护性。

二十六、项目一

1.房屋出租系统之基于模块开发

(1)项目设计-程序框架图
  • 在开发项目前,先进行设计,从功能上进行模块/文件的划分
  • 程序框架图:系统有哪些模块/文件,明确相互间的调用关系

在这里插入图片描述

(2)主菜单的设计(对房屋的各种操作)
<1>菜单
  • 先设计菜单
def main_menu():
    """
    显示主菜单,让用户选择
    """
    print()
    print("房屋出租系统菜单".center(32, "="))
    print("\t\t\t1 新 增 房 源")
    print("\t\t\t2 查 找 房 屋")
    print("\t\t\t3 删 除 房 屋 信 息")
    print("\t\t\t4 修 改 房 屋 信 息")
    print("\t\t\t5 房 屋 列 表")
    print("\t\t\t6 退     出")

# 测试
main_menu()
  • 在另一个模块设计怎么调用它
from house_operation import *


def main():
    """
    这是主函数,也就是程序的执行入口
    """
    # 调用main_menu函数显示主菜单
    # 循环显示菜单
    while True:
        main_menu()
        key = input("请输入你的选择(1-6): ")
        if key in ["1", "2", "3", "4", "5", "6"]:
            if key == "1":
                print("你输入了1,后面处理")
            elif key == "2":
                print("你输入了2,后面处理")
            elif key == "3":
                print("你输入了3,后面处理")
            elif key == "4":
                print("你输入了4,后面处理")
            elif key == "5":
                print("你输入了5,后面处理")
            elif key == "6":
                break
# 测试
if __name__ == "__main__":
    main()
    print("你退出了程序,欢迎下次使用...")
<2>设计“5”(显示房屋列表)
# 1. 使用字典来存储房屋信息
# 2. 一个字典对象就对应一个房屋信息
# 3. 多个字典/房屋信息, 就放到list中进行管理
# 4. 设计 houses = [{"id":2, "name":"tim","phone":"113","address":"海淀","rent":800,"state":"未出租"},{}...]

# 全局变量 即 houses, 存放所有的房屋信息
# 为了测试方便放了一个测试数据到该列表
houses = [{"id": 1, "name": "tim", "phone": "113", "address": "海淀", "rent": 800, "state": "未出租"}]
def list_houses():
    """
    显示房屋列表
    """
    print("房屋列表".center(60, "="))
    # 打印表头信息
    print("编号\t\t房主\t\t电话\t\t地址\t\t月租\t\t状态(未出租/已出租)")
    # 遍历houses这个列表
    # 取出的house就是一个字典{"id": 1, "name": "tim", "phone": "113", "address": "海淀", "rent": 800, "state": "未出租"}
    #
    for house in houses:
        # 取出house的values,并进行遍历显示
        for value in house.values():
            print(value, end="\t\t")
        # 输出一个完整的house信息后,换行
        print()
    print("房屋列表显示完毕".center(60, "="))
<3>设计“1”(新增房源)
# 全局变量id_counter 记录当前房屋的id
id_counter = 1

def add_house():
    """
    添加房屋信息
    """
    print("添加房屋".center(32, "="))
    name = input("姓名: ")
    phone = input("电话: ")
    address = input("地址: ")
    rent = int(input("租金: "))
    state = input("状态: ")
    # 由系统分配添加的房屋id
    global id_counter
    id_counter += 1
    # 构建房屋信息对应的字典, 加入到全局houses列表
    house = {"id": id_counter, "name": name, "phone": phone,
             "address": address, "rent": rent, "state": state}
    houses.append(house)
    print("添加房屋成功".center(32, "="))
<4>设计“3”(删除房源)
def del_house():
    """
    根据id删除房屋信息
    """
    print("删除房屋信息".center(32, "="))
    del_id = int(input("请输入待删除房屋的编号(-1退出): "))
    if del_id == -1:
        print("放弃删除房屋信息".center(32, "="))
        return

    # 让用户确认删除(Y/N), 如果输入的不是Y/N, 就一直提示输入
    while True:
        key = input("请输入你的选择(Y/N), 请确认选择: ")
        if key.lower() == 'y' or key.lower() == 'n':
            break
    if key.lower() == 'y':
        house = find_by_id(del_id)  # 根据del_id 去houses列表查找是否存在该房屋[字典]
        if house:
            # 执行删除
            houses.remove(house)
            print("删除房屋信息成功".center(32, "="))
        else:
            print("房屋编号不存在, 删除失败..".center(32, "="))
    else:
        print("放弃删除房屋信息".center(32, "="))
def find_by_id(find_id):
    """
    根据输入的find_id返回对应的房屋信息(字典), 如果没有就返回None
    """
    # 遍历houses列表
    for house in houses:
        if house["id"] == find_id:
            return house
    # 如果没有return ,默认就是返回None
    # return None

但是让用户确认删除那里可能很多代码都会用到,可以封装成函数,写在tool模块

def read_confirm_select():
    """
    确认用户输入的是(Y/N), 不区分大小写,
    如果用户输入的不是Y/N 就反复输入
    """
    print("请输入你的选择(Y/N), 请确认选择: ", end="")
    while True:
        key = input()
        if key.lower() == 'y' or key.lower() == 'n':
            break
        else:
            print("选择错误, 请重新输入: ", end="")
    return key.lower()

因此可以修改代码

def del_house():
    """
    根据id删除房屋信息
    """
    print("删除房屋信息".center(32, "="))
    del_id = int(input("请输入待删除房屋的编号(-1退出): "))
    if del_id == -1:
        print("放弃删除房屋信息".center(32, "="))
        return

    # 让用户确认删除(Y/N), 如果输入的不是Y/N, 就一直提示输入
    # while True:
    #     key = input("请输入你的选择(Y/N), 请确认选择: ")
    #     if key.lower() == 'y' or key.lower() == 'n':
    #         break

    choice = read_confirm_select()

    if choice == 'y':  # 如果真的要删除
        # 根据del_id 去houses列表查找是否存在该房屋[字典]
        house = find_by_id(del_id)
        if house:
            # 执行删除
            houses.remove(house)
            print("删除房屋信息成功".center(32, "="))
        else:
            print("房屋编号不存在, 删除失败..".center(32, "="))

    else:
        print("放弃删除房屋信息".center(32, "="))
<5>设计“6”(退出)
def exit_sys():
    """
    完成退出系统, 并确认(Y/N)
    :return: 如果是确认退出返回True, 否则返回False
    """
    choice = read_confirm_select()
    if choice == 'y':
        return True
    else:
        return False
<6>设计“2”(查找房屋信息)
def find_house():
    """
    查找房屋信息
    """
    print("查找房屋信息".center(32, "="))
    find_id = int(input("请输入要查找的id: "))
    # 调用函数返回对应的房屋
    house = find_by_id(find_id)
    if house:
        # 打印表头信息
        print("编号\t\t房主\t\t电话\t\t地址\t\t月租\t\t状态(未出租/已出租)")
        for value in house.values():
            print(value, end="\t\t")
        print()
    else:
        print(f"查找房屋信息id {find_id} 不存在")

<7>设计“4”(更新修改房屋信息)
def update():
    """
    修改房屋信息
    """
    update_id = int(input("请选择待修改的房屋编号(-1表示退出): "))
    if update_id == -1:
        print("你放弃修改房屋信息".center(32, "="))
        return
    # 根据id查找对应的房屋信息(字典)
    house = find_by_id(update_id)
    if not house:
        print("没有要修改的房屋信息".center(32, "="))
        return

    # 注意: 如果用户直接回车,表示不修改当前这个信息,保留原来的值
    # 如果用户输入的有内容,表示将接收到的name 赋给 house字典 key="name" 对应的值
    name = input(f"姓名({house['name']}):")
    if len(name) > 0:
        house['name'] = name
        
    phone = input(f"电话({house['phone']}):")
    if len(phone) > 0:
        house['phone'] = phone
        
    address = input(f"地址({house['address']}):")
    if len(address) > 0:
        house['address'] = address
        
    rent = input(f"租金({house['rent']}):")
    if len(rent) > 0:
        house['rent'] = int(rent)
     
    state = input(f"状态({house['state']}):")
    if len(state) > 0:
        house['state'] = state
    
    print("修改房屋信息成功"。center(32,"="))
# 代码的重复性过高,设计成函数,先按照简单的方式完成,后面优化-> 将输入新数据封装成函数

优化:(tools模块)

def read_str(tip, default_val):
    """
    读取用户的输入, 如果用户没有输入内容,则返回default_val
    :param tip:  提示信息
    :param default_val: 用户指定
    :return: 返回的就是需要的新数据
    """
    str = input(tip)
    if len(str) > 0:
        return str
    else:
        return default_val

则原代码修改为

def update():
    """
    修改房屋信息
    """
    update_id = int(input("请选择待修改的房屋编号(-1表示退出): "))
    if update_id == -1:
        print("你放弃修改房屋信息".center(32, "="))
        return
    # 根据id查找对应的房屋信息(字典)
    house = find_by_id(update_id)
    if not house:
        print("没有要修改的房屋信息".center(32, "="))
        return

    house['name'] = read_str(f"姓名({house['name']}):", house['name'])
    house['phone'] = read_str(f"电话({house['phone']}):", house['phone'])
    house['address'] = read_str(f"地址({house['address']}):", house['address'])
    house['rent'] = int(read_str(f"租金({house['rent']}):", house['rent']))
    house['state'] = read_str(f"姓名({house['state']}):", house['state'])
    
    print("修改房屋信息成功".center(32, "="))
<8>修改最开始的菜单
from house_operation import *


def main():
    """
    这是主函数,也就是程序的执行入口
    :return:
    """
    # 调用main_menu函数显示主菜单
    # 循环显示菜单
    while True:
        main_menu()
        key = input("请输入你的选择(1-6): ")
        if key in ["1", "2", "3", "4", "5", "6"]:
            if key == "1":
                add_house()
            elif key == "2":
                find_house()
            elif key == "3":
                del_house()
            elif key == "4":
                update()
            elif key == "5":
                list_houses()
            elif key == "6":
                if exit_sys():
                    break


# 测试
if __name__ == "__main__":
    main()
    print("你退出了程序,欢迎下次使用...")

2.房屋出租系统之基于OOP分层模式设计

(1)调用层:总的开始调用:main
"""
    说明: 主程序: 程序的执行入口
"""

from house_view import *


if __name__ == "__main__":
    # 创建HouseView对象,显示主菜单
    houseview = HouseView()
    houseview.main_menu()
    print("你退出了程序,欢迎下次使用...")
(2)逻辑层:与用户交互且显示信息模块:house_view
from house_service import *
from utility import *


class HouseView:
    # 定义属性house_operation【HouseService】
    house_operation: HouseService = HouseService()

    def update_house(self):
        """
        显示修改的界面,输入新的数据,修改对应的房屋信息即可
        :return:
        """
        update_id = int(input("请选择待修改的房屋编号(-1表示退出): "))
        if update_id == -1:
            print("你放弃修改房屋信息".center(32, "="))
            return
        # 根据id查找对应的房屋信息(对象)
        house = self.house_operation.find_by_id(update_id)
        if not house:
            print("没有要修改的房屋信息".center(32, "="))
            return

        # 因为返回的house是对象,所以修改该对象的属性即可
        house.name = Utility.read_str(f"姓名({house.name}): ", house.name)
        house.phone = Utility.read_str(f"电话({house.phone}): ", house.phone)
        house.address = Utility.read_str(f"地址({house.address}): ", house.address)
        house.rent = int(Utility.read_str(f"租金({house.rent}): ", house.rent))
        house.state = Utility.read_str(f"状态({house.state}): ", house.state)

        print("修改房屋信息成功".center(32, "="))

    def find_house(self):
        """
        显示查找的界面,接收用户输入id,查找并显示房屋信息
        :return:
        """
        print("查找房屋信息".center(32, "="))
        find_id = int(input("请输入要查找的id: "))
        # 调用方法返回对应的房屋
        house = self.house_operation.find_by_id(find_id)
        if house:
            # 打印表头信息
            print("编号\t\t房主\t\t电话\t\t地址\t\t月租\t\t状态(未出租/已出租)")
            print(house)
        else:
            print(f"查找房屋信息id {find_id} 不存在")

    def exit_sys(self):
        """
        退出系统(需求确认)
        :return:
        """
        key = Utility.read_confirm_select()
        if key == 'y':
            return True
        else:
            return False

    def del_house(self):
        """
        删除房屋的界面, 接收用户的输入
        :return:
        """
        print("删除房屋信息".center(32, "="))
        del_id = int(input("请输入待删除房屋的编号(-1退出): "))
        if del_id == -1:
            print("放弃删除房屋信息".center(32, "="))
            return

        choice = Utility.read_confirm_select()

        if choice == 'y':  # 如果真的要删除
            # 调用Service层的方法
            if self.house_operation.del_by_id(del_id):

                print("删除房屋信息成功".center(32, "="))
            else:
                print("房屋编号不存在, 删除失败..".center(32, "="))

        else:
            print("放弃删除房屋信息".center(32, "="))

    def add_house(self):
        """
        显示添加的界面,接收用户的输入,构建House对象
        :return:
        """
        print("添加房屋".center(32, "="))
        name = input("姓名: ")
        phone = input("电话: ")
        address = input("地址: ")
        rent = int(input("租金: "))
        state = input("状态: ")

        # 构建房屋对象
        new_house = House(0, name, phone, address, rent, state)
        # 调用service方法,添加new_house
        self.house_operation.add(new_house)
        print("添加房屋成功".center(32, "="))

    def list_houses(self):
        """
        显示房屋列表
        :return:
        """
        print("房屋列表".center(60, "="))
        # 打印表头信息
        print("编号\t\t房主\t\t电话\t\t地址\t\t月租\t\t状态(未出租/已出租)")
        # 得到houses列表
        houses = self.house_operation.get_houses()
        # 遍历houses这个列表
        for house in houses:
            print(house)  # 先看看效果

        print("房屋列表显示完毕".center(60, "="))

    def main_menu(self):
        """
        显示主菜单
        :return:
        """
        while True:
            print()
            print("房屋出租系统菜单".center(32, "="))
            print("\t\t\t1 新 增 房 源")
            print("\t\t\t2 查 找 房 屋")
            print("\t\t\t3 删 除 房 屋 信 息")
            print("\t\t\t4 修 改 房 屋 信 息")
            print("\t\t\t5 房 屋 列 表")
            print("\t\t\t6 退     出")
            key = input("请输入你的选择(1-6): ")
            if key in ["1", "2", "3", "4", "5", "6"]:
                if key == "1":
                    self.add_house()
                elif key == "2":
                    self.find_house()
                elif key == "3":
                    self.del_house()
                elif key == "4":
                    self.update_house()
                elif key == "5":
                    self.list_houses()
                elif key == "6":
                    if self.exit_sys():
                        break
(3)业务层:提供各种操作来回馈逻辑层:house_service
"""
    说明: 业务层: 提供对房屋操作方法
"""

from house import *


class HouseService:
    # 定义属性houses列表,存放房屋信息(对象)
    houses = []
    # 定义属性id_counter: 记录当前房屋的id
    id_counter = 1

    def find_by_id(self, find_id):
        """
        根据find_id返回对应的house对象,不存在返回None
        :param find_id:
        :return:
        """
        # 遍历houses
        for house in self.houses:
            if find_id == house.id:
                return house

    def del_by_id(self, del_id):
        """
        根据接收到的id删除房屋
        :param del_id:
        :return: 如果删除成功返回True,否则返回false
        """
        # 判断del_id是否存在
        house = self.find_by_id(del_id)
        if house is None:
            return False

        # 如果找到该house,就删除
        self.houses.remove(house)
        return True

    def add(self, new_house: House):
        """
        将接收到的new_house添加到houses
        :param new_house:
        :return:
        """
        # 分配id给new_house
        self.id_counter += 1
        new_house.id = self.id_counter
        self.houses.append(new_house)

    def __init__(self):
        # 为了测试方便,我们在houses列表中增加一个测试数据
        house = House(1, "tim", "118", "海淀", 800, "未出租")
        self.houses.append(house)

    def get_houses(self):
        """
        返回房屋列表
        :return:
        """
        return self.houses
(4)数据层:对具体对象编写:house
class House:
    def __init__(self, id, name, phone, address, rent, state):
        self.id = id
        self.name = name
        self.phone = phone
        self.address = address
        self.rent = rent
        self.state = state

    def __str__(self):
        return f'id: {self.id}\t\tname: {self.name}\t\tphone: {self.phone}\t\taddress: {self.address}\t\trent: {self.rent}\t\tstate: {self.state}'
(5)工具层:提供静态方法,与对象无关:utility
class Utility:

    @staticmethod
    def read_confirm_select():
        """
        确认用户输入的是(Y/N), 不区分大小写,
        如果用户输入的不是Y/N 就反复输入
        """
        print("请输入你的选择(Y/N), 请确认选择: ", end="")
        while True:
            key = input()
            if key.lower() == 'y' or key.lower() == 'n':
                break
            else:
                print("选择错误, 请重新输入: ", end="")
        return key.lower()

    @staticmethod
    def read_str(tip, default_val):
        """
        读取用户的输入, 如果用户没有输入内容,则返回default_val
        :param tip:  提示信息
        :param default_val: 用户指定
        :return: 返回的就是需要的新数据
        """
        str = input(tip)
        if len(str) > 0:
            return str
        else:
            return default_val

3.基于模块开发与基于OOP分层开发的比较

  1. 基于模块开发:将系统分为不同的模块,不同模块完成不同的任务/功能,用函数的方式进行管理和组织项目。
  2. OOP分层模式:将系统分为界面层、业务层、数据层,不同层完成不同的任务/功能,并以OOP的方式进行管理和组织项目。
  3. 两种方式从形式上看有区别,但编程思想是一样的,都是采用分而治之的方式,将一个系统/项目划分不同的部分,方便实现、扩展、可读和管理(即:不能把所有的代码/功能都放在一起,那样就乱了)。
  4. 两种方式没有严格的界限,比如基于模块开发时,也可以按照层来划分,同样OOP分层模式也可以调用模块中的函数。
  5. 如何选择的建议:根据业务需求和项目大小来定(比如完成一个很小的功能或者小项目,可以考虑模块开发方式,完成较大的项目,考虑分层模式)。

二十七、错误和异常

在Python中,错误和异常是程序处理过程中非常重要的一部分。它们帮助开发者捕获和处理程序运行中的意外情况,以避免程序崩溃。

1.文档

错误和异常文档

2.Python中的错误

错误通常是指在编译时或运行时遇到的严重问题,这些问题可能会导致程序无法继续执行。Python的错误通常包括语法错误(SyntaxError)和逻辑错误。

  • 语法错误 (SyntaxError)

    • 语法错误是指代码不符合Python语法规范,导致无法被解析。例如,缺少冒号、拼写错误等。
    • 例子:
      if True
          print("Hello, World!")
      
      上面的代码会抛出 SyntaxError,因为 if 语句后面缺少冒号。
  • 逻辑错误

    • 逻辑错误是指程序语法正确,但逻辑上不符合预期,导致程序行为异常。例如,意外的循环(死循环)或条件判断错误。

3.Python中的异常

异常是指在程序运行过程中发生的非预期事件,这些事件在运行时被捕获,并且可以通过代码来处理。Python中,异常是基于类的对象。常见的异常类型包括ValueErrorTypeErrorIndexError等。

(1)异常的类型
  • ValueError:当一个函数接收到的参数类型正确,但值不合适时,抛出此异常。

    int("abc")  # 这会抛出 ValueError,因为 "abc" 不能转换为整数
    
  • TypeError:当操作或函数应用于不合适的类型时,抛出此异常。

    len(5)  # 这会抛出 TypeError,因为整数没有长度
    
  • IndexError:当序列(如列表或字符串)的索引超出范围时,抛出此异常。

    my_list = [1, 2, 3]
    print(my_list[5])  # 这会抛出 IndexError,因为索引 5 超出列表范围
    
  • KeyError:当字典中查找一个不存在的键时,抛出此异常。

    my_dict = {"name": "Alice"}
    print(my_dict["age"])  # 这会抛出 KeyError,因为字典中没有 "age" 键
    
  • NameError:在你尝试访问一个未定义的变量或名称时发生。这通常是因为你引用了一个尚未声明的变量,或者拼写错误。

print(x)  # 这会引发 NameError,因为 x 未定义

解决方法:确保在使用变量之前先定义它,并且检查变量名的拼写是否正确。

  • FileNotFoundError:在你尝试打开一个不存在的文件时发生。通常出现在使用 open() 函数时,文件路径错误或者文件本身不存在都会导致此异常。
with open('non_existent_file.txt', 'r') as file:
    content = file.read()  # 这会引发 FileNotFoundError,因为文件不存在

解决方法:检查文件路径是否正确,确保文件确实存在

  • AttributeError:在你尝试访问一个对象不存在的属性或方法时发生。这通常是由于你误以为对象具有某个属性或方法。
my_list = [1, 2, 3]
my_list.append(4)
my_list.add(5)  # 这会引发 AttributeError,因为列表没有 add 方法

解决方法:检查对象的类型和可用的方法或属性,确保你尝试访问的属性或方法在对象中确实存在。如果不确定,可以使用 dir() 函数来查看对象的属性和方法列表。

(2)异常处理

Python提供了try-except语句来捕获和处理异常,从而防止程序崩溃。其基本形式如下:

try:
    # 可能出现异常的代码
except [异常类型 as 别名]:
    # 发生异常时,对异常处理的代码
[else:]
    # 没有发生异常时,执行的代码
[finally:]
    # 不管有没有异常,都会执行的代码

# 上面的语法结构带[]的是可选项
try:
    # 可能会引发异常的代码
    result = 10 / 0
except ZeroDivisionError:
    # 处理异常
    print("除数不能为零")
except Exception as e:
    # 处理其他所有异常
    print(f"发生了一个异常: {e}")
finally:
    # 无论是否发生异常,都会执行的代码块
    print("执行结束")
  • try:包含可能会引发异常的代码。
  • except:用于捕获和处理指定类型的异常。如果不指定异常类型,将捕获所有异常。
  • finally:无论是否发生异常,finally中的代码都会执行,常用于清理操作。
(3)制动触发异常之raise

在Python中,raise 语句用于显式地引发异常。当你希望在某些条件下手动触发一个异常时,可以使用 raise。这在调试、输入验证或强制终止程序执行时非常有用。

<1>文档

文档:https://docs.python.org/zh-cn/3.12/tutorial/errors.html#raising-exceptions

<2>基本用法
raise 异常类型(可选的异常信息)
  • 异常类型:这是你想要引发的异常的类型,它必须是一个继承自 BaseException 类的对象,通常是 Exception 的子类。
  • 可选的异常信息:你可以提供一个字符串来描述异常的原因,这有助于在捕获异常时进行调试。
<3>示例
  • 引发一个标准异常
raise ValueError("输入的值不符合要求")

这段代码会引发一个 ValueError 异常,并输出 "输入的值不符合要求" 的错误信息。

  • 条件触发异常

你可以在满足某些条件时使用 raise 来触发异常。例如,在用户输入不符合要求时手动抛出异常:

def check_age(age):
    if age < 0:
        raise ValueError("年龄不能为负数")
    else:
        print(f"输入的年龄是: {age}")

try:
    check_age(-1)
except ValueError as e:
    print(f"发生异常: {e}")

在这个例子中,如果 check_age 函数接收到的 age 是负数,它就会引发 ValueError 异常。try-except 块捕获了这个异常并输出相应的错误信息。

  • 重新引发异常

在捕获异常后,你可能希望在处理完一些逻辑后再次引发异常。可以通过 raise 语句不带参数来重新引发最近捕获的异常:

try:
    x = int("invalid")
except ValueError as e:
    print("捕获到 ValueError")
    # 做一些处理
    raise  # 重新引发异常

在这个例子中,程序捕获了一个 ValueError 异常,打印了错误信息,然后使用 raise 重新引发了该异常。

  • 自定义异常

你可以定义自己的异常类,并在需要时使用 raise 来引发它:

class MyCustomError(Exception):
    pass

def check_value(value):
    if value > 100:
        raise MyCustomError("值不能大于100")

try:
    check_value(200)
except MyCustomError as e:
    print(f"捕获到自定义异常: {e}")

在这个例子中,MyCustomError 是一个自定义的异常类。当 value 大于100时,check_value 函数会引发 MyCustomError 异常,异常被 try-except 块捕获并处理。

class MyCustomError(Exception):
    def __init__(self, message, code):
        self.message = message
        self.code = code
        super().__init__(self.message)  # 调用父类的构造函数

    def __str__(self):
        return f"[Error {self.code}]: {self.message}"

    def log_error(self):
        # 自定义的方法,用于记录或处理异常
        print(f"Logging error: {self.code} - {self.message}")

# 使用自定义异常类
def risky_operation(value):
    if value > 100:
        raise MyCustomError("Value cannot exceed 100", 400)

try:
    risky_operation(150)
except MyCustomError as e:
    print(e)
    e.log_error()  # 调用自定义方法处理异常

#输出:
[Error 400]: Value cannot exceed 100
Logging error: 400 - Value cannot exceed 100
  1. __init__ 方法
    • 这个方法是异常类的构造函数,用于初始化异常对象。它接受两个参数:message(错误信息)和 code(错误码)。
    • super().__init__(self.message) 调用了父类 Exception 的构造函数,这样异常的基本信息会被正确地初始化和传递。
  2. __str__ 方法
    • 这个方法定义了当异常对象被转换为字符串时应该返回的内容。在这里,返回了一个包含错误码和错误信息的字符串。
    • 当你打印异常对象时,比如在 print(e) 中,这个方法会被自动调用。
  3. 自定义方法 log_error
    • 你可以定义自己的方法来处理异常相关的逻辑。在这个例子中,log_error 方法被用来记录错误信息。
    • 在异常被捕获后,可以调用这个方法来执行额外的操作,比如日志记录或错误报告。
<4>异常的传递

异常传递机制是指当一个异常发生时,它会沿着调用栈向上传递,直到遇到第一个能够处理它的异常处理程序(通常是 try-except 块)。如果没有找到处理程序,程序将终止,并输出一个错误消息(堆栈跟踪)。

先来看一下这个代码的结构:

def f3():  # f3() 没有捕获处理 10/0 异常
    print("---f3() start----")
    print(10 / 0)  # 这里会引发 ZeroDivisionError 异常
    print("---f3() end----")


def f2():
    print("---f2() start---")
    f3()  # 调用 f3(),f3() 中的异常会在这里抛出
    print("---f2() end---")


def f1():
    try:
        f2()  # 调用 f2(),并尝试捕获任何在 f2() 中发生的异常
    except Exception as e:
        print(f"f1()捕获异常->{e}")  # 捕获并处理异常


f1()  # 启动异常传递过程

# 输出:
---f2() start---
---f3() start----
f1()捕获异常->division by zero

异常传递过程

  1. f3() 函数:

    • f3() 函数开始执行,并打印 "---f3() start----"
    • 然后,执行 print(10 / 0),此处会引发 ZeroDivisionError,因为在数学中不能除以零。
    • 因为 f3() 内部没有任何异常处理(即没有 try-except 块),所以这个异常会向上传递到调用它的函数 f2()
  2. f2() 函数:

    • f2() 函数调用 f3(),在 f3() 内部发生的异常会在 f2() 中抛出。
    • f2() 也没有捕获这个异常,所以异常继续向上传递到调用它的 f1() 函数。
  3. f1() 函数:

    • f1() 函数调用 f2(),并在 try 块中捕获可能发生的异常。
    • f2() 中的异常传递到 f1() 时,except 块捕获到这个异常,并将异常信息通过 print(f"f1()捕获异常->{e}") 输出。
    • 因此,f1() 是这个异常传递链中唯一处理异常的地方。

输出解析:

  • "---f2() start---"f2() 函数的输出。
  • "---f3() start----"f3() 函数的输出。
  • "f1()捕获异常->division by zero"f1() 中捕获并处理异常的输出。

注意"---f3() end----""---f2() end---" 并没有被输出,因为在 f3() 中,发生异常后,程序控制流立即转到异常处理,而不会继续执行 f3()f2() 中的剩余代码。

<5>练习一下

在这里插入图片描述

分析一下三段代码分别输出什么,然后去Pycharm里运行,这里就不一一列举了,注意一个点,处理过的异常就不会再处理,因此f1()通常不会输出

二十八、文件

在Python中,文件操作是一个非常常见的任务,Python提供了丰富的功能来处理文件的读取、写入、创建和删除等操作。下面我将详细介绍文件操作的基本概念和常用方法。

1.打开文件

在Python中,使用 open() 函数来打开文件。open() 函数有两个主要参数:文件名和模式。

(1)语法
file_object = open(filename, mode)
  • filename: 要打开的文件的名称(包括路径,如果文件不在当前目录)。
  • mode: 打开文件的模式,常见模式有:
    • 'r': 以读模式打开文件(默认)。
    • 'w': 以写模式打开文件。如果文件不存在,则创建一个新文件;如果文件存在,则覆盖文件内容。
    • 'a': 以追加模式打开文件。如果文件不存在,则创建一个新文件;如果文件存在,新的内容会被写入文件末尾。
    • 'b': 以二进制模式打开文件(如 'rb''wb' 等),用于读写二进制文件(如图片、视频)。
    • 't': 以文本模式打开文件(默认),适用于文本文件。
    • 'x': 以写模式打开文件,但如果文件已存在则会引发异常。
(2)示例
# 以读模式打开文件
file = open('example.txt', 'r')

"""
    如果我们要创建一个文件, 只需要以 mode="w" 形式打开文件即可
    如果, 该文件不存在, 系统就会创建该文件
    注意: encoding="utf-8" 是关键字参数, 不能少 encoding, 因为 encoding 并不是open() 方法的第3个形参
"""
f1 = open("d://a//hi.txt", "w", encoding="utf-8")
print(f"文件创建成功类型是: {type(f1)}")

2. 读取文件

(1)文件常用操作一览
序号操作说明
1f.read(size)可用于读取文件内容。它会读取一些数据,并返回字符串(文本模式)或字节串对象(在二进制模式下)。size 是可选的数值参数。省略 sizesize 为负数时,读取并返回整个文件的内容。
2f.readline()从文件中读取单行数据;字符串末尾保留换行符 (\n)。
3list(f)f.readlines()如果需要以列表形式读取文件中的所有行,可以使用 list(f)f.readlines()
4for line in f:从文件中读取多行时,可以用循环遍历整个文件对象。这种操作能高效利用内存,快速,且代码简单。 示例: for line in f: print(line, end='')
5f.write(string)string 的内容写入文件,并返回写入的字符数。
6f.flush()刷新流的写入缓冲区。
7f.close()刷新并关闭此流。即释放文件占用的系统资源,如果文件已经关闭,则此方法无效。文件关闭后,对文件的任何操作(例如读取或写入)都会引发 ValueError
8with open() as f:在处理文件对象时,最好使用 with 关键字。优点是,子句体结束后,文件会自动关闭。
(2)示例
# 打开文件
f = open("d://a//hello.txt", "r", encoding="utf-8")

# 读取方式1: read(),没有参数,一次返回整个文件的内容
content = f.read()
content = f.read(6) # 读取前六个字符
# 关闭文件, 释放文件占用的系统资源
f.close()

print("==========hello.txt内容============")
print(content)
# 读取方式2: f.readline(), 注意readline, 字符串末尾保留换行符\n
# print("==========hello.txt内容 方式2============")
# f.readline()的使用
line1 = f.readline()  # 读第一行,然后换行
line2 = f.readline()  # 读第二行,然后换行
print(f"第1行数据是{line1}")
print(f"第2行数据是{line2}")
f.close()

# 循环读取整个文件, 一行行的读取
while True:
    line_content = f.readline()
    if line_content == "": 
        # 如果为"" ,表示文件读取完毕
        break
    print(line_content, end="") # 没这个end会换两行

f.close()
# 读取方式3: f.readlines()列表形式读取文件中的所有行
lines = f.readlines()
print(f"lines类型是->{type(lines)}") #list
print(f"lines内容是->{lines}") #["第一行的数据\n", "第2行的数据\n"...]
for line in lines:
    print(line, end="")
f.close()
# 读取方式4: for line in f形式读取文件
for line in f:
    print(line, end="")
f.close()

3. 写入文件

可以使用 write() 方法将内容写入文件。注意,如果文件以写模式 ('w') 打开,文件中的原内容将被清除。

示例
# 需求:
# 1) 在d盘的a目录下创建  abc.txt 文件, 并写入10句 "hello, 韩顺平教育" 到文件
# 2) 将abc.txt 内容覆盖成新的内容10句 "hi, 老韩"
# 3) 在abc.txt 原有内容基础上追加10句 '你好! python'

"""
    说明细节:
    1. mode = "w" 打开文件, 如果文件不存在,会创建, 如果文件已经存在,
       会先截断打开的文件, 也就是清空文件内容(!!!)
    2. 如果我们希望以追加的方式写入, 需要 mode = "a"
"""
# w模式打开文件, 文件截断原来的内容
f = open("d://a//abc.txt", "w", encoding="utf-8")
# a模式打开文件, 是在原来的基础上追加内容
f = open("d://a//abc.txt", "a", encoding="utf-8")

# 1) 在d盘的a目录下创建  abc.txt 文件, 并写入10句 "hello, 韩顺平教育" 到文件
i = 1
while i <= 10:
    f.write(f"hello, 韩顺平教育~\n")
    i += 1
f.close()

# 2) 将abc.txt 内容覆盖成新的内容10句 "hi, 老韩"
i = 1
while i <= 10:
    f.write(f"hi, 老韩\n")
    i += 1
f.close()

# 3) 在abc.txt 原有内容基础上追加10句 '你好! python', mode 指定为 "a"追加模式即可
i = 1
while i <= 10:
    f.write(f"你好! python\n")
    i += 1
f.close()

4. 关闭文件

文件操作完成后,应该使用 close() 方法关闭文件。这是一个良好的编程习惯,可以释放系统资源。

file.close()

5. 上下文管理器 (with 语句)

使用 with 语句可以自动管理文件的打开和关闭,避免忘记调用 close() 导致文件未被关闭。

示例
with open('example.txt', 'r') as file:
    content = file.read()
    print(content)
# 文件在这里已经自动关闭

with 语句确保文件在使用完毕后自动关闭,即使在处理中间发生了异常。

6. 文件操作异常处理

文件操作可能会引发异常,例如文件不存在、权限不足等。在处理文件时,建议使用 try-except 块来捕获和处理这些异常。

示例
try:
    with open('example.txt', 'r') as file:
        content = file.read()
        print(content)
except FileNotFoundError:
    print("文件不存在,请检查文件名。")
except IOError:
    print("文件无法读取,可能存在权限问题。")

7. 文件的其他操作

  • 删除文件: 使用 os.remove() 方法删除文件。
  • 重命名文件: 使用 os.rename() 方法重命名文件。
  • 检查文件是否存在: 使用 os.path.exists() 方法检查文件是否存在。
示例
import os

# 删除文件
os.remove('example.txt')

# 重命名文件
os.rename('old_name.txt', 'new_name.txt')

# 检查文件是否存在
if os.path.exists('example.txt'):
    print("文件存在")
else:
    print("文件不存在")

8.目录操作

(1)文档

目录操作文档:https://docs.python.org/zh-cn/3.12/library/os.html#module-os

(2)创建一级目录
import os

if os.path.isdir("d://aaa"):
    print("d://aaa 目录已经存在...")
else:
    os.mkdir("d://aaa")
  • os.path.isdir(path):检查指定路径是否存在且是一个目录。如果是,返回 True,否则返回 False
  • os.mkdir(path):用于创建一个新的目录。在这个示例中,它尝试在 d:// 路径下创建一个名为 aaa 的目录。如果目录已存在,程序会输出一条消息;否则,创建该目录。
(3)创建多级目录
if os.path.isdir("d://bbb//ccc"):
    print("d://bbb//ccc 目录已经存在...")
else:
    os.makedirs("d://bbb//ccc")
  • os.makedirs(path):用于创建多级目录。如果中间的某些目录不存在,makedirs 会递归创建所有必要的目录。在这个示例中,尝试在 d:// 路径下创建一个名为 bbb//ccc 的多级目录。如果多级目录已存在,程序会输出一条消息;否则,创建这些目录。
(4)删除目录
<1>删除单级目录
if os.path.isdir("d://aaa"):
    os.rmdir("d://aaa")
else:
    print("d://aaa 目录不存在, 无法删除")
  • os.rmdir(path):用于删除单级空目录。如果目录中有文件或其他目录,rmdir 将无法删除该目录,并抛出异常。在这个示例中,程序首先检查 d://aaa 是否存在且是一个目录,如果存在且为空,就删除它;否则,输出一条消息。
<2>删除多级目录
if os.path.isdir("d://bbb//ccc"):
    os.removedirs("d://bbb//ccc")
else:
    print("d://bbb//ccc 目录不存在, 无法删除")
  • os.removedirs(path):用于递归删除目录。如果目录是多级目录且全部为空,removedirs 会删除路径上所有空的目录。在这个示例中,程序首先检查 d://bbb//ccc 是否存在且是一个目录,如果存在且为空,就递归删除它及其上级目录;否则,输出一条消息。
(5)总结
  • os.path.isdir():用于检查给定路径是否为一个目录。
  • os.mkdir():创建单级目录。
  • os.makedirs():创建多级目录。
  • os.rmdir():删除单级空目录。
  • os.removedirs():递归删除多级空目录。

9.获取文件相关信息

获取文件的相关信息在编程中是一个常见的需求,尤其是在文件管理、日志记录和系统维护等场景中。Python 提供了多种方法来获取文件的元数据,包括文件大小、创建时间、访问时间、修改时间等。

(1)使用 os 模块获取文件信息

Python 的 os 模块提供了一些与操作系统交互的功能,包括获取文件或目录的详细信息。

<1> os.stat(path)

os.stat() 函数返回一个包含文件相关信息的对象(os.stat_result)。这个对象中包含以下重要属性:

  • st_size: 文件的大小(以字节为单位)。

  • st_atime: 文件的最近访问时间(以时间戳表示)。

  • st_mtime: 文件的最近修改时间(以时间戳表示)。

  • st_ctime: 文件的创建时间(以时间戳表示)。在某些操作系统上,如 Unix,st_ctime 表示文件元数据的最后修改时间。

  • 示例代码:

import os
import time

# 获取文件信息
f_stat = os.stat("d:/a/hello.txt")

# 打印文件相关信息
print("--------文件信息---------")
print(f"文件大小: {f_stat.st_size} 字节")
print(f"最近的访问时间: {time.ctime(f_stat.st_atime)}")
print(f"最近的修改时间: {time.ctime(f_stat.st_mtime)}")
print(f"文件创建时间: {time.ctime(f_stat.st_ctime)}")
<2>time.ctime() 函数

文件的时间信息通常以时间戳的形式存储(自1970年1月1日起的秒数),time.ctime() 函数用于将这些时间戳转换为人类可读的字符串格式,例如 'Mon Aug 1 10:14:39 2022'

示例代码:

import time

timestamp = 1627841620  # 一个示例时间戳
readable_time = time.ctime(timestamp)
print(f"可读时间: {readable_time}")
(2)常见的文件信息说明
  1. 文件大小 (st_size)

    • 表示文件的字节数。例如,一个文本文件可能是 1024 字节。
  2. 最近访问时间 (st_atime)

    • 表示文件最后一次被读取或访问的时间。例如,当用户或程序读取文件内容时,该时间会更新。
  3. 最近修改时间 (st_mtime)

    • 表示文件内容最后一次被修改的时间。例如,当文件的内容被更改时,该时间会更新。
  4. 文件创建时间 (st_ctime)

    • 通常表示文件被创建的时间。在某些文件系统中,它可能表示文件元数据的最后修改时间。
(3)使用 os.path 模块获取额外信息

os.path 模块提供了一些额外的实用函数,用于获取文件路径相关的信息:

  • os.path.getsize(path): 获取文件大小(以字节为单位)。

  • os.path.getatime(path): 获取文件的最近访问时间(以时间戳表示)。

  • os.path.getmtime(path): 获取文件的最近修改时间(以时间戳表示)。

  • os.path.getctime(path): 获取文件的创建时间(以时间戳表示)。

  • 示例代码:

import os
import time

file_path = "d:/a/hello.txt"

file_size = os.path.getsize(file_path)
last_access_time = time.ctime(os.path.getatime(file_path))
last_modified_time = time.ctime(os.path.getmtime(file_path))
creation_time = time.ctime(os.path.getctime(file_path))

print(f"文件大小: {file_size} 字节")
print(f"最近访问时间: {last_access_time}")
print(f"最近修改时间: {last_modified_time}")
print(f"文件创建时间: {creation_time}")
(4)文件操作的注意事项及其他细节
<1>f.flush():刷新流的写入缓冲区到文件
  • 缓冲区的概念

在文件写入操作中,数据并不是立即写入到磁盘的。为了提高效率,操作系统通常会将写入的数据暂时存放在内存中的缓冲区中,当缓冲区满了或者文件关闭时,才会一次性将数据写入到磁盘中。这种机制减少了磁盘I/O操作的次数,从而提高了性能。

  • f.flush() 的作用

f.flush() 函数的作用是将文件的写入缓冲区内容立即刷新到磁盘,而不是等待缓冲区满或文件关闭时才写入。这在某些情况下是非常重要的,比如在程序崩溃之前确保数据已写入磁盘。

  • 示例代码解释
import time

f = open("d://a//hi.txt", "w", encoding="utf-8")
f.write("你好1, python~\n")
f.write("你好2, python~\n")
f.write("你好3, python~\n")

# 测试f.flush()将缓冲区数据刷新, 写入文件
f.flush()
print("----等待----")
time.sleep(100000)
print("----等待 end----")
  • 文件写入操作

    • 打开文件 "d://a//hi.txt" 进行写操作("w" 模式),编码设置为 "utf-8"
    • 通过 f.write() 方法,将三行数据写入文件。
  • 使用 f.flush()

    • 调用 f.flush() 后,程序会强制将当前缓冲区中的数据立即写入磁盘,而不会等待缓冲区满或文件关闭。
    • 随后的 time.sleep(100000) 会让程序进入长时间的等待状态,此时即使程序停留在等待状态,文件内容也已经写入磁盘。
  • 测试场景

    • 如果没有 f.flush(),数据可能仍在缓冲区中,未实际写入磁盘。如果此时程序崩溃或强制退出,那么数据可能丢失。
    • 通过 f.flush(),确保在程序进入长时间等待或可能发生崩溃前,数据已经安全地写入磁盘。
  • f.close:

    • f.close()内置了flush功能,当执行close会刷新并关闭此流
<2>使用 with open() 自动管理文件关闭
  • with open() 的作用

with open() 语句是一种上下文管理器,用于在处理文件时自动管理文件的打开和关闭。使用 with open() 打开的文件在代码块(即 with 子句体)结束后,会自动关闭,无需手动调用 f.close()

  • 示例代码解释
# 3、with open() as f:
# 在处理文件对象时,子句体结束后,文件会自动关闭

with open("d:/a/hello.txt", "r", encoding="UTF-8") as f:
    lines = f.readlines()
    print("----文件内容-----")
    for line in lines:
        print(line, end="")

print("\n文件是否关闭->", f.closed)
  • 读取文件内容

    • 使用 with open("d:/a/hello.txt", "r", encoding="UTF-8") as f: 打开文件 hello.txt 进行读取。
    • f.readlines() 读取文件的所有行,并存储在 lines 列表中。
    • 通过循环遍历 lines 列表,逐行输出文件内容。
  • 文件自动关闭

    • with 语句块执行完毕后,文件对象 f 自动关闭。
    • f.closed 用于检查文件是否已关闭,输出为 True 表示文件已成功关闭。
(5)目录分隔符

在操作系统中,目录分隔符(也称为路径分隔符)是用来分隔文件路径中不同目录的符号。不同的操作系统使用不同的目录分隔符:

  1. Windows:使用反斜杠 (\) 作为目录分隔符。例如:C:\Users\YourName\Documents\file.txt

  2. Unix/Linux 和 macOS:使用正斜杠 (/) 作为目录分隔符。例如:/home/yourname/documents/file.txt

<1>Python 中的目录分隔符

在 Python 中,如果你直接在代码中书写文件路径,最好使用以下几种方式来处理目录分隔符,以确保代码的跨平台兼容性:

  1. 正斜杠 (/):即使在 Windows 上,Python 也支持使用正斜杠作为路径分隔符,这使得代码在不同操作系统之间更具可移植性。

    path = "C:/Users/YourName/Documents/file.txt"
    
  2. 使用 os.path.join():Python 的 os 模块提供了 os.path.join() 函数,它会根据操作系统自动使用正确的目录分隔符。

    import os
    path = os.path.join("C:", "Users", "YourName", "Documents", "file.txt")
    print(path) # 输出:C:Users\YourName\Documents\file.txt
    
  3. 使用 os.sepos.sep 是一个常量,表示当前操作系统的目录分隔符。在需要动态处理路径的场景中,可以使用它。

    import os
    path = "C:" + os.sep + "Users" + os.sep + "YourName" + os.sep + "Documents" + os.sep + "file.txt"
    print(path)
    # 输出:C:\Users\YourName\Documents\file.txt
    
(6)递归遍历目录
<1>代码分析:递归遍历目录
import os
dir_path = "d:/a"
def print_dir_all_content(dir_path):
    # 获取文件夹(目录)的所有内容(元素)
    content_list = os.listdir(dir_path)
    # 遍历content_list, 输出对应的信息
    for ele in content_list:
        child_ele = dir_path + "/" + ele
        if os.path.isdir(child_ele):
            print(f"目录: {child_ele}")
            # 递归的操作
            print_dir_all_content(child_ele)
        else:
            print(f"文件: {child_ele}")
  • 功能与工作原理:
  1. 获取目录内容

    • 使用 os.listdir(dir_path) 获取指定目录 dir_path 中的所有内容,返回一个列表 content_list,包含了该目录中的所有文件和子目录的名称。
  2. 遍历内容

    • 使用 for ele in content_list: 遍历 content_list 中的每一个元素(文件或目录名)。
    • 对于每个元素,构造完整的路径 child_ele = dir_path + "/" + ele
  3. 判断是否为目录

    • if os.path.isdir(child_ele): 用于判断当前元素是否是一个目录。
    • 如果是目录,首先打印该目录的路径 print(f"目录: {child_ele}"),然后递归调用 print_dir_all_content(child_ele),继续遍历该子目录的内容。
    • 如果是文件,则直接打印文件的路径 print(f"文件: {child_ele}")
  4. 递归调用

    • 当遇到一个目录时,函数会递归调用自身,这样可以深入到该目录下的所有子目录,直到遍历完整个目录树的所有文件和子目录。
<2>递归的作用:
  • 递归使得这段代码能够遍历整个目录树,包括所有的子目录及其内容。每当遇到一个子目录,程序就会进入这个目录并继续遍历其中的内容,直到所有子目录和文件都被遍历完毕。
(7)登陆日志综合案例
<1>代码
# 1) 实现登录验证,如果用户名是列表中的元素 ["smith", "tom", "hsp"]  , 密码 "888" 则登录成功 , 否则失败
# 2) 不管登录是否成功, 都需要在文件中记录登录的信息
# 3) 登录成功, 可以看到相应的操作菜单提示, 请实现相应的功能

"""
    思路分析
    1. 根据业务需求, 编写相应的函数
    2. 编写函数operation_menu() 显示操作的菜单
    3. 编写函数record_log() 记录登录信息
    4. 编写函数read_log() 读取登录信息
"""
import time


# 编写函数operation_menu() 显示操作的菜单
def operation_menu():
    print()
    print("请选择操作".center(32, "="))
    print("\t\t\t1 查看当前登录用户")
    print("\t\t\t2 查看登录日志")
    print("\t\t\t3 退出系统")


# 编写函数record_log() 记录登录信息

def record_log(user, info):
    """
    记录登录信息
    :param user: 当前登录用户名
    :param info: 登录成功还是失败的信息
    """
    with open("log.txt", mode="a", encoding="utf-8") as f:
        # 构建写入的登录信息
        s = f"登录用户: {user}, {info} 登录时间:{time.strftime('%m-%d %H:%M:%S', time.localtime())}\n"
        f.write(s)


# 编写函数read_log() 读取登录信息
def read_log():
    with open("log.txt", mode="r", encoding="utf-8") as f:
        for line in f:
            print(line, end="")


# 测试
if __name__ == "__main__":
    user = input("请输入用户名: ")
    pwd = input("请输入密码: ")
    if user in ["smith", "tom", "hsp"] and pwd == "888":
        # 记录登录信息
        record_log(user, "登录成功")
        while True:
            operation_menu()
            key = input("请输入你的选择: ")
            if key == "1":
                print(f"当前登录用户: {user}")
            elif key == "2":
                read_log()
            elif key == "3":
                print("退出系统...")
                break
            else:
                print("你的选择有误, 请重新选择..")
    else:
        record_log(user, "登录失败")
        print("用户名/密码有误, 重新登录")

<2>功能概述
  • 登录验证:用户输入用户名和密码,程序验证是否匹配预定义的用户名列表和密码。如果匹配,登录成功;否则,登录失败。
  • 日志记录:无论登录成功还是失败,程序都会将登录尝试的信息(包括用户名、结果、时间)记录到日志文件中。
  • 操作菜单:登录成功后,用户可以通过一个菜单执行以下操作:
    1. 查看当前登录的用户名。
    2. 查看登录日志。
    3. 退出系统。
<3>函数详细分析
  • operation_menu()
def operation_menu():
    print()
    print("请选择操作".center(32, "="))
    print("\t\t\t1 查看当前登录用户")
    print("\t\t\t2 查看登录日志")
    print("\t\t\t3 退出系统")
  • 作用:显示一个操作菜单,让用户在登录成功后选择要执行的操作。

  • 实现:使用 print 函数输出带有居中和对齐效果的菜单选项。

  • record_log(user, info)

def record_log(user, info):
    with open("log.txt", mode="a", encoding="utf-8") as f:
        s = f"登录用户: {user}, {info} 登录时间:{time.strftime('%m-%d %H:%M:%S', time.localtime())}\n"
        f.write(s)
  • 作用:记录登录尝试的信息到日志文件 log.txt 中。

  • 参数

    • user:当前登录的用户名。
    • info:登录结果的信息(如“登录成功”或“登录失败”)。
  • 实现:打开或创建日志文件 log.txt,并以追加模式 ("a") 写入登录信息。登录信息包括用户名、登录结果和登录时间。使用 time.strftime() 函数将当前时间格式化为可读的字符串。

  • read_log()

def read_log():
    with open("log.txt", mode="r", encoding="utf-8") as f:
        for line in f:
            print(line, end="")
  • 作用:读取并显示日志文件中的内容。
  • 实现:以读取模式 ("r") 打开 log.txt,逐行读取内容并打印。使用 end="" 参数防止 print 函数在输出每一行后自动添加额外的换行符。
<4>主程序 (__main__) 分析
if __name__ == "__main__":
    user = input("请输入用户名: ")
    pwd = input("请输入密码: ")
    if user in ["smith", "tom", "hsp"] and pwd == "888":
        record_log(user, "登录成功")
        while True:
            operation_menu()
            key = input("请输入你的选择: ")
            if key == "1":
                print(f"当前登录用户: {user}")
            elif key == "2":
                read_log()
            elif key == "3":
                print("退出系统...")
                break
            else:
                print("你的选择有误, 请重新选择..")
    else:
        record_log(user, "登录失败")
        print("用户名/密码有误, 重新登录")
  • 登录验证

    • 用户输入用户名和密码,程序检查用户名是否在预定义的列表 ["smith", "tom", "hsp"] 中,且密码是否为 "888"
    • 如果验证通过,记录“登录成功”的日志信息,并进入操作菜单循环。
    • 如果验证失败,记录“登录失败”的日志信息,并提示用户登录失败。
  • 操作菜单

    • 如果登录成功,程序进入一个无限循环,显示操作菜单并等待用户选择。
    • 用户可以选择查看当前登录的用户名、查看日志文件内容,或退出系统。
    • 如果用户选择退出(选择“3”),程序会结束循环并退出。
    • 对于无效输入,程序提示用户重新选择。
<5>日志记录
  • 日志文件 log.txt
    • 每次用户尝试登录(无论成功与否),都会记录在 log.txt 文件中。
    • 日志记录的格式为:登录用户: <用户名>, <登录结果> 登录时间: <时间>

二十九、pyecharts

1.文档

pyecharts 是一个基于 Python 的数据可视化库,旨在为 Python 用户提供简单而强大的图表生成工具。它是对 ECharts(一个流行的 JavaScript 可视化库)的封装,使得 Python 用户可以轻松生成高质量的交互式图表,并将它们嵌入到各种应用中,如 Jupyter Notebook、网页和报告中。

pyecharts文档:https://pyecharts.org/#/zh-cn/

pyecharts-gallery文档:https://gallery.pyecharts.org/#/README

2.主要特点

  1. 多种图表类型

    • pyecharts 支持多种图表类型,包括折线图、柱状图、饼图、散点图、雷达图、地图、热力图等,适用于不同的可视化需求。
  2. 交互性

    • pyecharts 生成的图表是交互式的,用户可以在浏览器中与图表进行互动,比如悬停显示详细数据、缩放、拖拽等。
  3. 高扩展性

    • pyecharts 基于 ECharts,提供了强大的扩展能力,用户可以通过配置项自定义图表的外观和行为,甚至可以直接使用 ECharts 的配置项。
  4. 丰富的主题

    • pyecharts 提供了多种主题供用户选择,用户可以根据自己的需求更改图表的主题风格。
  5. 多平台支持

    • pyecharts 可以在多个平台上使用,如 Jupyter Notebook、Web 应用、Flask、Django 等。

3.安装

可以通过 pip 安装 pyecharts

pip install pyecharts

4.使用场景

  • 数据分析与展示:在数据分析中,用于可视化分析结果,帮助更好地理解数据。

  • 报告生成:生成图表并嵌入到报告中,使报告内容更加生动和易于理解。

  • Web 应用:在 Web 应用中动态展示数据,通过 pyecharts 提供的接口轻松集成图表。

  • 自定义图表:通过 pyechartsoptions 配置项,用户可以精细地控制图表的每个细节。

  • 联动与交互:可以创建多个图表之间的联动,或者使用鼠标事件与图表进行交互。

  • 集成第三方库pyecharts 可以与 FlaskDjango 等 Web 框架集成,生成动态交互式图表。

5.快速入门

(1)pyecharts 中 Bar 图的基本用法
<1>基本的 Bar 图创建与渲染

首先,我们可以使用 pyecharts.charts 模块中的 Bar 类来创建一个简单的柱状图,并添加数据。然后,通过 render() 方法将图表渲染为 HTML 文件。

from pyecharts.charts import Bar

bar = Bar()
bar.add_xaxis(["衬衫", "羊毛衫", "雪纺衫", "裤子", "高跟鞋", "袜子"])
bar.add_yaxis("商家A", [5, 20, 36, 10, 75, 90])
bar.render()  # 默认会生成 render.html 文件
  • add_xaxis:用于设置 x 轴的标签,这里表示商品类别。
  • add_yaxis:用于设置 y 轴的数据,这里表示不同商品的销售数量。
  • render():将图表渲染并输出为 HTML 文件,默认会在当前目录生成 render.html

你也可以通过传入路径参数来指定生成的 HTML 文件名:

bar.render("mycharts.html")
<2>链式调用的使用

pyecharts V1 版本开始,支持链式调用。这使得代码更加简洁和流畅。以下是同样的 Bar 图,通过链式调用的实现:

from pyecharts.charts import Bar

bar = (
    Bar()
    .add_xaxis(["衬衫", "羊毛衫", "雪纺衫", "裤子", "高跟鞋", "袜子"])
    .add_yaxis("商家A", [5, 20, 36, 10, 75, 90])
)
bar.render("myrender.html")
  • 链式调用:通过将方法调用链式连接起来,可以在一行代码中完成多个操作。这使代码更加清晰和易读。
<3>使用全局配置设置图表标题

pyecharts 允许使用 set_global_opts 方法来设置图表的全局配置项,例如标题、图例等。以下示例展示了如何通过链式调用设置图表的主标题和副标题:

from pyecharts import options as opts
from pyecharts.charts import Bar

bar = (
    Bar()
    .add_xaxis(["衬衫", "羊毛衫", "雪纺衫", "裤子", "高跟鞋", "袜子"])
    .add_yaxis("商家A", [5, 20, 36, 10, 75, 90])
    .set_global_opts(title_opts=opts.TitleOpts(title="主标题", subtitle="副标题"))
)
bar.render()
  • set_global_opts:用于设置图表的全局选项。这里使用了 TitleOpts 来设置主标题和副标题。
  • title_opts:可以使用 opts.TitleOpts 对象,也可以直接传入字典参数来设置标题信息。
<4>不使用链式调用的方式

对于不习惯链式调用的开发者,pyecharts 仍然支持传统的单独调用方法的方式。以下示例展示了如何通过单独调用方法来生成图表,并且使用了自定义主题:

from pyecharts import options as opts
from pyecharts.charts import Bar
from pyecharts.globals import ThemeType

bar = Bar(init_opts=opts.InitOpts(theme=ThemeType.HALLOWEEN))
bar.add_xaxis(["衬衫", "羊毛衫", "雪纺衫", "裤子", "高跟鞋", "袜子"])
bar.add_yaxis("商家A", [5, 20, 36, 10, 75, 90])
bar.set_global_opts(title_opts=opts.TitleOpts(title="主标题", subtitle="副标题"))
bar.render()
  • 自定义主题:通过 InitOpts 设置 theme 参数,可以选择预定义的主题,这里选择了 HALLOWEEN 主题。
  • 单独调用方法:这种方式更接近传统的编程风格,适合那些习惯逐步调用方法的开发者。
(2)其他常用图代码示例(饼图)
data = [['衬衫', 138], ['毛衣', 40], ['领带', 74], ['裤子', 112],
        ['风衣', 147], ['高跟鞋', 104], ['袜子', 65]]
c = (
    Pie()
    .add("", data)
    .set_colors(["blue", "green", "yellow", "red", "pink", "orange", "purple"])
    .set_global_opts(title_opts=opts.TitleOpts(title="Pie-商品销售情况"), toolbox_opts=opts.ToolboxOpts(is_show=True))
    .set_series_opts(label_opts=opts.LabelOpts(formatter="{b}: {c}", font_size=12))
    .render("pie_sale.html")
)
  1. 数据准备
data = [['衬衫', 138], ['毛衣', 40], ['领带', 74], ['裤子', 112],
        ['风衣', 147], ['高跟鞋', 104], ['袜子', 65]]
  • 这里的数据 data 是一个嵌套列表,每个子列表包含两个元素:商品名称和对应的销售数量。
  • 例如,['衬衫', 138] 表示 “衬衫” 的销售数量为 138。
  1. 创建饼图对象并添加数据
c = (
    Pie()
    .add("", data)
  • Pie():创建一个饼图对象。
  • add("", data):将数据添加到饼图中。这里的第一个参数为空字符串 "",表示不设置系列名称。data 则是之前定义的商品销售数据。
  1. 设置颜色
.set_colors(["blue", "green", "yellow", "red", "pink", "orange", "purple"])
  • set_colors([...]):设置饼图中各部分的颜色。这里指定了七种颜色(蓝色、绿色、黄色、红色、粉色、橙色、紫色),依次对应数据中的七个商品。
  1. 设置全局选项
.set_global_opts(
    title_opts=opts.TitleOpts(title="Pie-商品销售情况"), 
    toolbox_opts=opts.ToolboxOpts(is_show=True)
)
  • set_global_opts(...):设置图表的全局配置选项。
    • title_opts=opts.TitleOpts(title="Pie-商品销售情况"):设置图表的主标题为 "Pie-商品销售情况"
    • toolbox_opts=opts.ToolboxOpts(is_show=True):启用工具箱(Toolbox),工具箱通常包含保存、缩放、重置等功能。is_show=True 表示显示工具箱。
  1. 设置系列选项
.set_series_opts(label_opts=opts.LabelOpts(formatter="{b}: {c}", font_size=12))
  • set_series_opts(...):设置系列的配置选项,这里主要配置标签(label)。
    • label_opts=opts.LabelOpts(formatter="{b}: {c}", font_size=12)
      • formatter="{b}: {c}":设置标签的显示格式,其中 {b} 是商品名称,{c} 是对应的销售数量。这样,标签会显示成类似 "衬衫: 138" 的格式。
      • font_size=12:设置标签字体的大小为 12。
  1. 渲染图表
.render("pie_sale.html")
  • render("pie_sale.html"):将饼图渲染为 HTML 文件,并保存为 pie_sale.html。可以通过浏览器打开这个 HTML 文件查看生成的图表。
(3)折线图项目
<1>官方代码解析
from pyecharts import options as opts
from pyecharts.charts import Line
from pyecharts.globals import ThemeType

x_data = ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"]
y_data = [820, 932, 901, 934, 1290, 1330, 1320]
y_data_2 = [8201, 9321, 9011, 9341, 12901, 13301, 13201]


(
    Line()
    .set_global_opts(
        tooltip_opts=opts.TooltipOpts(is_show=False),
        xaxis_opts=opts.AxisOpts(type_="category"),
        yaxis_opts=opts.AxisOpts(
            type_="value",
            axistick_opts=opts.AxisTickOpts(is_show=True),
            splitline_opts=opts.SplitLineOpts(is_show=True),
        ),
    )
    .add_xaxis(xaxis_data=x_data)
    .add_yaxis(
        series_name="111",
        y_axis=y_data,
        symbol="emptyCircle",
        is_symbol_show=True,
        label_opts=opts.LabelOpts(is_show=False),
    )
    .add_yaxis(
        series_name="",
        y_axis=y_data_2,
        symbol="emptyCircle",
        is_symbol_show=True,
        label_opts=opts.LabelOpts(is_show=False),
    )
    .render("basic_line_chart.html")
)
<2>自制分省年度数据折线图

在这里插入图片描述

**拿到的数据如下图所示:**难的是数据的处理,好好分析!

在这里插入图片描述

import pyecharts.options as opts
from pyecharts.charts import Line

"""
Gallery 使用 pyecharts 1.1.0
参考地址: https://echarts.apache.org/examples/editor.html?c=line-simple

# 创建Line对象 折线图对象
line = Line()

# 准备X轴数据
# 打开文件
f = open("分省年度数据.csv", "r", encoding="gbk")
# 为了方便小伙伴理解, 先输出文件的内容
# for line in f:
#     print(line, end="")
# f.close()

# 给X轴添加数据,经分析, 数据就是[2003, 2004, ....2022]
"""

# 读取所有的行数据
line_datas = f.readlines()
f.close()

# 先删除前面三个行(元素)
for _ in range(3):
    line_datas.pop(0)

# 得到x轴的数据
# 得到 ['地区', '2022年', '2021年', '2020年', '2019年', '2018年', '2017年', '2016年', '2015年',..]
x_data_year = line_datas.pop(0).replace("\n", "").split(",")
x_data_year.pop(0)  # ['2022年', '2021年',... '2003年']
x_data_year.reverse()  # ['2003年', '2004年', '2005年',... '2022年']
# print(x_data_year)


# 给Y轴添加数据
"""
分析: 这里有四组数据,分别是北京、上海、天津、重庆的近20年的人口数据
"""
# 创建四个列表,存放 北京 上海 天津, 重庆的近20年的人口数据
y_data_bj = []
y_data_sh = []
y_data_tj = []
y_data_cq = []

# 遍历 line_datas 得到 北京 上海 天津, 重庆的近20年的人口数据
for line_data in line_datas:
    line_data = line_data.replace("\n", "").split(",")
    if line_data[0] == "北京市":
        line_data.pop(0)
        line_data.reverse()  # 从2003-2022年的排序
        y_data_bj = line_data
    elif line_data[0] == "上海市":
        line_data.pop(0)
        line_data.reverse()
        y_data_sh = line_data
    elif line_data[0] == "天津市":
        line_data.pop(0)
        line_data.reverse()
        y_data_tj = line_data
    elif line_data[0] == "重庆市":
        line_data.pop(0)
        line_data.reverse()
        y_data_cq = line_data

# 添加X轴的数据
line.add_xaxis(x_data_year)
# 添加Y轴的数据
line.add_yaxis("重庆市历年人口", y_data_cq, label_opts=opts.LabelOpts(is_show=False))
line.add_yaxis("北京市历年人口", y_data_bj, label_opts=opts.LabelOpts(is_show=False))
line.add_yaxis("上海市历年人口", y_data_sh, label_opts=opts.LabelOpts(is_show=False))
line.add_yaxis("天津市历年人口", y_data_tj, label_opts=opts.LabelOpts(is_show=False))

# 设置全局配置项
line.set_global_opts(
    title_opts=opts.TitleOpts(title="2003-2022年直辖市总人口折线图", pos_left="center", pos_bottom="1%"))

# 生成文件
line.render("line_4city_population.html")
<3>效果图

在这里插入图片描述

(4)地图项目
<1>官方代码解析
from pyecharts import options as opts
from pyecharts.charts import Map
from pyecharts.faker import Faker

c = (
    Map()
    .add("商家A", [list(z) for z in zip(Faker.provinces, Faker.values())], "china")
    .set_global_opts(title_opts=opts.TitleOpts(title="Map-基本示例"))
    .render("map_base.html")
)

print([list(z) for z in zip(Faker.provinces, Faker.values())])

这里只解释数据添加的部分:

.add("商家A", [list(z) for z in zip(Faker.provinces, Faker.values())], "china")
  • add("商家A", ...):向地图中添加数据。
    • "商家A":这是系列名称,表示数据的标签。在地图上,这个标签会显示在图例中。
    • [list(z) for z in zip(Faker.provinces, Faker.values())]:这是一个列表,包含了各省份的数据。具体解释如下:
      • Faker.provincesFakerpyecharts 提供的一个工具模块,用于生成假数据。Faker.provinces 生成的是一个中国省份名称的列表。
      • Faker.values():生成一组随机的数值,通常与 Faker.provinces 一一对应,表示每个省份的数据值。
      • zip(Faker.provinces, Faker.values()):将省份名称与随机数值配对在一起,形成一个二元组的列表。
      • list(z) for z in zip(...):将每个二元组转换为列表,从而生成形如 [['省份1', 值1], ['省份2', 值2], ...] 的数据结构。
    • "china":指定绘制中国地图。
<2>自制2022年人口地图
from pyecharts import options as opts
from pyecharts.charts import Map
from pyecharts.faker import Faker

"""准备数据"""
with open("分省年度数据.csv", "r", encoding="gbk") as f:
    data_lines = f.readlines()

# 删除data_lines列表的前4个元素(行)
for _ in range(4):
    data_lines.pop(0)

# 创建一个空的列表存放地图数据
# 分析map_data_list 格式 [[省市名, 人口数量], [省市名, 人口数量]....]
map_data_list = []

for data_line in data_lines:
    data_line_list = data_line.split(",")
    try:
        map_data_list.append([data_line_list[0], data_line_list[1]])
    except Exception as e:
        # 如果在添加数据到map_data_list出现异常,我们就continue
        continue

print("map_data_list", map_data_list)

"""创建Map对象"""
map = Map()

"""添加数据并配置"""
map.add("2022年各省市的人口分布情况", map_data_list, "china")

# 全局配置
map.set_global_opts(
    title_opts=opts.TitleOpts(title="2022年各省市的人口分布情况"),
    # VisualMapOpts:视觉映射配置项
    visualmap_opts=opts.VisualMapOpts(
        # 指定 visualMapPiecewise 组件的最小值。
        min_=100,
        # 指定 visualMapPiecewise 组件的最大值。
        max_=15000,
        # 指定 visualMapPiecewise 组件的位置。
        pos_left="10%",
        pos_bottom="30%"
    )
)

# 系列配置-标签字体大小配置
map.set_series_opts(label_opts=opts.LabelOpts(font_size=8))

"""生成文件"""
map.render("2022年各省市的人口分布情况.html")
<3>效果图

在这里插入图片描述

(5)轮播图项目
<1>官方代码解析
from pyecharts import options as opts
from pyecharts.charts import Bar, Timeline
from pyecharts.faker import Faker
from pyecharts.globals import ThemeType
x = Faker.choose()  # week = ["周一", "周二", "周三", "周四", "周五", "周六", "周日"]
tl = Timeline()  # 创建Timeline对象
for i in range(2015, 2020):  # 循环5次,创建了5个Bar柱状图
    bar = (
        Bar()
        .add_xaxis(x)  # 添加X轴数据week = ["周一", "周二", "周三", "周四", "周五", "周六", "周日"]
        .add_yaxis("商家A", Faker.values())  # [值1,值2....]
        .add_yaxis("商家B", Faker.values())  # [值1,值2....]
        .set_global_opts(title_opts=opts.TitleOpts("某商店{}年营业额".format(i)))
    )
    tl.add(bar, "{}年~~~".format(i))
tl.render("timeline_bar.html")

这段代码使用 pyecharts 创建了一个带有时间轴(Timeline)的柱状图(Bar Chart),展示了某商店在不同年份(2015-2019)的营业额变化情况。

  1. 导入必要的模块
from pyecharts import options as opts
from pyecharts.charts import Bar, Timeline
from pyecharts.faker import Faker
from pyecharts.globals import ThemeType
  • options:提供了各种图表配置项的工具模块,如设置标题、图例、工具箱等。
  • Bar:柱状图的类,用于创建柱状图。
  • Timeline:时间轴类,用于创建带有时间轴的图表,可以在不同的时间点之间切换显示不同的数据。
  • Faker:用于生成假数据的工具模块,帮助快速创建示例数据。
  • ThemeType:提供各种主题样式的常量,可以用于更改图表的主题。
  1. 生成 X 轴数据
x = Faker.choose()  # week = ["周一", "周二", "周三", "周四", "周五", "周六", "周日"]
  • Faker.choose():生成一组类别数据,在这个例子中,它生成了一个week = [“周一”, “周二”, “周三”, “周四”, “周五”, “周六”, “周日”]作为 X 轴的分类标签。
  1. 创建时间轴对象
tl = Timeline()  # 创建Timeline对象
  • Timeline():创建一个时间轴对象。时间轴可以在不同时间点显示不同的数据,使得图表能够动态展示随时间变化的数据。
  1. 循环创建柱状图
for i in range(2015, 2020):  # 循环5次,创建了5个Bar柱状图
    bar = (
        Bar()
        .add_xaxis(x)  # week = ["周一", "周二", "周三", "周四", "周五", "周六", "周日"]
        .add_yaxis("商家A", Faker.values())  # [值1,值2....]
        .add_yaxis("商家B", Faker.values())  # [值1,值2....]
        .set_global_opts(title_opts=opts.TitleOpts("某商店{}年营业额".format(i)))
    )
    tl.add(bar, "{}年~~~".format(i))
  • for i in range(2015, 2020):循环遍历 2015 到 2019 五个年份。
  • Bar():在每次循环中,创建一个新的柱状图对象。
  • add_xaxis(x):为柱状图添加 X 轴数据。
  • add_yaxis("商家A", Faker.values()):为柱状图添加 Y 轴数据,这里表示“商家A”在该年的数据。Faker.values() 生成一组随机数作为数据值。
  • add_yaxis("商家B", Faker.values()):同样地,为“商家B”添加 Y 轴数据。
  • set_global_opts(title_opts=opts.TitleOpts("某商店{}年营业额".format(i))):设置图表的标题,显示为“某商店2015年营业额”、“某商店2016年营业额”等。
  • tl.add(bar, "{}年~~~".format(i)):将每个年份对应的柱状图添加到时间轴中,并标记为相应的年份。
  1. 渲染图表
tl.render("timeline_bar.html")
  • render("timeline_bar.html"):将带有时间轴的柱状图渲染为 HTML 文件,并保存为 timeline_bar.html。你可以在浏览器中打开这个文件,查看图表,并通过时间轴切换查看不同年份的数据。
<2>自制各省人口人数变化轮播图
from pyecharts import options as opts
from pyecharts.charts import Bar, Timeline
from pyecharts.faker import Faker
from pyecharts.globals import ThemeType

"""准备数据"""

# 确定需要创建多少个Bar对象,根据文件提供的年份 2003-2022
with open("分省年度数据.csv", "r", encoding="gbk") as f:
    data_lines = f.readlines()

# 删除data_lines列表的前3个元素(行)
for _ in range(3):
    data_lines.pop(0)

# 删除data_lines列表的后2个元素(行)
for _ in range(2):
    data_lines.pop(-1)

# print("data_lines", data_lines)
years = data_lines.pop(0).replace("\n", "").split(",")
# 去掉第一个元素"地区"
years.pop(0)

# 遍历 data_lines 生成我们需要的数据
# 难度-需求我们设计一下
# 把数据放到一个字典对象中 data_dict = {年份:[[省市名,人口数],[省市名,人口数]...],年份:[[省市名,人口数],[省市名,人口数]...]...}
# 看一个具体的案例: {2003:[["北京市",1456],["天津市",1011]..],2004:[["北京市",1493],["天津市",1024]..]..}

# 创建字典对象
data_dict = {}

for data_line in data_lines:
    # print("data_line", data_line)
    data_line_list = data_line.replace("\n", "").split(",")
    # 遍历years 给各个城市的各个年份的人口数据添加到data_dict
    index = 0
    for year in years:
        index += 1
        try:
            data_dict[year].append([data_line_list[0], float(data_line_list[index])])
        except Exception as e:
            # 如果出现了异常,说明是第一次添加数据
            data_dict[year] = []
            data_dict[year].append([data_line_list[0], float(data_line_list[index])])

"""创建Timeline对象"""
timeline = Timeline({"theme": ThemeType.ESSOS})
years.reverse()

"""创建Bar对象 并加入到Timeline对象 还有进行配置"""
for year in years:
    # 下面我们需要取出每一年按照人口数量排序的前12省市
    # 1. 先排序 2. 切片
    data_dict[year].sort(key=lambda ele: ele[1], reverse=True)
    rank_12_city_data = data_dict[year][0:12]
    # 定义Bar的X轴数据
    x_data = []
    # 定义Bar的Y轴数据
    y_data = []
    for city in rank_12_city_data:
        x_data.append(city[0])
        y_data.append(city[1])
    # 创建Bar对象
    bar = Bar()
    # 对x_data数据和y_data数据翻转
    x_data.reverse()
    y_data.reverse()
    bar.add_xaxis(x_data)
    bar.add_yaxis("人口(万)", y_data)
    # 转换X和Y轴
    bar.reversal_axis()
    # 全局配置
    bar.set_global_opts(title_opts=opts.TitleOpts(title=f"{year}年中国省市人口排名前12的情况"))

    # 将创建好的bar添加到TimeLine对象
    timeline.add(bar, str(year))

# 对时间线进行配置
timeline.add_schema(
    play_interval=500,
    is_auto_play=True
)

"""生成对应的文件"""

timeline.render("2003-2022年中国省市人口排名前12的情况.html")
<3>效果图

在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值