01-第一阶段

第一章

1.1软件安装

如何安装Python,和安装PyCharm。

第二章

2.1 注释

我们在学习任何语言,都会有注释,注释的作用就是向别人解释我们编写的代码的含义和逻辑,使代码有更好的可读性。

注释不是程序,是不会被执行的

注释一般分类两类,单行注释多行注释

2.1.1 单行注释

# 我是单行注释
print("hello world")

注意:# 号和注释内容建议以空格隔开,这是代码规划。

2.1.2 多行注释

"""
我是
多行注释
"""

多行注释一般对:Python文件、类、方法进行解释,类和方法。

2.2 变量

在代码中,数据需要保存在变量中,变量是在程序运行的时候存储数据用的,可以想象变量为一个盒子。

整数、浮点数(小数)、字符串(文本)都可以放在变量中。

2.2.1 变量的定义

定义变量的格式:变量名称 = 变量值

# 定义变量
name = "张三"            # 定义一个字符串变量
age = 18             # 定义一个整数变量
height = 1.78         # 定义一个浮点数据类型
is_has_jj = True     # 定义一个bool类型

上面定义了4个变量,并赋值,值分别是字符串(string)、整数(int)、浮点型(float)、布尔型(bool)。

字符串(string)需要使用双引号 " 包围起来,可以包含任意字符。

布尔型(bool) 的值只有2个,即 TrueFalse,用来表示真和假,注意大小写不能错。

和 C 和 Java 语言不同,Python不是强类型变量,不需要指定变量的类型。

前面已经使用了 print 函数,用来输出数据,输出完数据会进行换行,所以每次输出都是新的一行,如果不想换行,可以使用如下方式:

print("人生苦短,",end=" ")        # 输出完不换行
print("我学Python")

输出结果是:人生苦短,我学Python

2.2.2 变量的特征

变量变量,从名字中可以看出,表示“量”是可变的。所以,变量被定义后,可以在后面的代码中使用了,而且还可以重新修改变量的值。

例如:

# 1.定义变量
name = "张三"
age = 18
height = 1.78
is_has_jj = True

# 2.修改变量的值
name = "李四"       # 修改name的值为逗比
age = age + 2      # 将age加2,重新赋值给age,可以简写为age += 2
height = 1.85
is_has_jj = False

print(name)       # 李四
print(age)        # 20
print(height)     # 1.85
print(is_has_jj)  # False

可以看到变量的值可以被修改,而且可以参与运算。

为什么要使用变量?

变量就是在程序运行时,记录数据用的,使用变量是为了重复的使用这些数据。

2.2.3 del 关键字

del的作用是删除变量,解除变量和堆中对象的关联。

举个栗子:

a = 10
del a
print(a)        # 代码执行到这里会报错,因为del a已经将变量a删除了,这里a是没有声明的,所以会报错。

2.2.4 总结

变量是什么,有什么作用

变量就是在程序运行时记录数据用的。

变量的定义格式是?

变量名 = 变量值

变量的特征是?

变量的值可以改变

print语句如何输出多份内容

print(内容1,内容2,……,内容N)

Python中如何做减法

使用符号 - 即可完成减法运算

2.3 数据类型

2.3.1 三大数据类型

入门阶段主要接触如下三大数据类型:

类型描述说明
string字符串类型用引号引起来的数据都是字符串
int整形(有符号)数字类型,存放整形,如 -1,10,0 等
float浮点型(有符号)数字类型,存放小数,如 -3.14,6.66 等

字符串(string),又称文本,是由任意数量的字符如中文、英文、各类符号、数字等组成。所以叫做字符的串

Python中常用的有6种值(数据)的类型

类型描述说明
数字(Number)整数(int)
浮点数(float)
复数(complex)
布尔(bool)
整数(int),如:10、-10
浮点数(float),如:13.14、-13.14
复数(complex),如:4+3j,以j结尾表示复数
布尔(bool)表达现实生活中的逻辑,即真和假 ,True表示真,False表示假。True本质上是一个数字记作1,False记作0
字符串(String)描述文本的一种数据类型字符串(string)由任意数量的字符组成
列表(List)有序的可变序列Python中使用最频繁的数据类型,可有序记录一堆数据
元组(Tuple)有序的不可变序列可有序记录一堆不可变的Python数据集合
集合(Set)无序不重复集合可无序记录一堆不重复的Python数据集合
字典(Dictionary)无序Key-Value集合可无序记录一堆Key-Value型的Python数据集合

2.3.2 type()语句

语法:type(被查看类型的数据)

具体使用方式

  • 在print语句中,直接输出类型信息:

    print(type("这是字符串"))
    peint(type(666))
    print(type(3.14))

    输出结果:

    <class 'str'>
    <class 'int'>
    <class 'float'>

    str是string的缩写

  • 用变量存储type()的结果(返回值):

    string_type = type("这是字符串")
    int_type = type(666)
    float_type = type(3.14)
    print(string_type, int_type, float_type)

    输出结果:

    <class 'str'> <class 'int'> <class 'float'>
  • 查看变量中存储的数据类型

    name = "张三"
    print(type(name))

    输出结果:

    <class 'str'>

2.3.3 总结

变量有类型吗?

我们通过type(变量)可以输出类型,这是查看变量的类型还是数据的类型?

查看的是:变量存储的数据的类型。因为,变量无类型,但是它存储的数据有。

使用什么语句可以查看数据的类型?

type()

如下代码,name_type变量可以存储变量name的类型信息,是因为?

name = "张三"
name_type = type(name)

因为type()语句会给出结果(返回值)

变量有没有类型?

没有,字符串变量表示变量存储了字符串而不是表示变量就是字符串

2.4 数据类型转换

2.4.1 常见的转换语句

数据类型之间,在特定的场景下,是可以相互转换的,如字符串转数字、数字转字符串等

那么,我们为什么要转换它们呢?

数据类型转换,将会是我们以后经常使用的功能。如:

  • 从文件中读取的数字,默认是字符串,我们需要转换成数字类型
  • 后续学习的input()语句,默认结果是字符串,若需要数字也需要转换
  • 将数字转换成字符串用以写出到外部系统
语句(函数)说明
int(x)将x转换为一个整数
float(x)将x转换为一个浮点数
str(x)将对象 x 转换为字符串

同前面学习的type()语句一样,这三个语句,都是带有结果的(返回值)我们可以用print直接输出或用变量存储结果值

2.4.2 类型转换注意事项

类型转换不是万能的,毕竟强扭的瓜不会甜,我们需要注意:

  1. 任何类型,都可以通过str(),转换成字符串
  2. ==字符串内必须真的是数字==,才可以将字符串转换为数字

错误示范:

name = "张三"
num = int(name)

输出结果:

Traceback (most recent call last):
  File "E:\python\python_go_learning\xuexi\1.py", line 2, in <module>
    num = int(name)
          ^^^^^^^^^
ValueError: invalid literal for int() with base 10: '张三'

==字符串内不是数字,是无法完成到数字的转换的==

2.4.3 总结

任何类型都可以转换成字符串,对不对?

正确

字符串可以随意转换成数字,对不对?

错误,字符串内必须只有数字才可以

浮点数转整数会丢失什么?

丢失精度,也就是小数部分

2.5 标识符

2.5.1 什么是标识符

在Python程序中,我们可以给很多东西起名字,比如:变量的名字、方法的名字、类的名字,等等。这些名字,我们把它统一的称之为标识符,用来做内容的标识。

所以,标识符就是用户在编程的时候所使用的一系列名字,用于给变量、类、方法等命名。

2.5.2 标识符命名规则

  • 内容限定

    标识符命名中,只允许出现:英文、中文、数字、下划线(_)这四类元素。其余任何内容都不被允许。

    ==不推荐使用中文,数字不能做开头==

  • 大小写敏感

    以定义变量为例:

    Andy = “安迪1”
    andy = “安迪2”

    字母a的大写和小写,是完全不一样的变量。

  • 不可使用关键字

    Python中有一系列单词,称之为关键字
    关键字在Python中都有特定用途
    我们不可以使用它们作为标识符

image-20240327002503898

2.5.3 变量命名规范

  • 见名知意

    变量的命名要做到:

    • 明了:尽量做到,看到名字,就知道是什么意思

      错误:

      a = "张三"
      b = 11

      正确:

      name = "张三"
      age = 11
    • 简洁:尽量在确保“明了”的前提下,减少名字的长度

      错误:

      a_person_name = "张三"

      正确:

      name = "张三"
  • 下划线命名法

    多个单词组合变量名,要使用下划线做分隔。

    错误:

    fistnumber = 1
    studentnickname = "小明"

    正确:

    fist_number = 1
    student_nickname = "小明"
  • 英文字母全小写

    命名变量中的英文字母,应全部小写:

    错误:

    Name = "张三"
    Age = 11

    正确:

    name = "张三"
    age = 11

2.5.4 总结

不遵守规则:==会出现问题==

不遵守规范:==不好阅读==

2.6 运算符

2.6.1 算术(数学)运算符

# 设a为10,b为20。
a = 10
b = 20
运算符描述实例
+两个对象相加 a + b 输出 30
-得到复数或是一个数减去另一个数 a - b 输出结果 -10
*两个数相乘或是返回一个被重复若干次的字符串 a * b 输出结果 200
/b / a 输出结果 2
//取整除返回商的整数部分 9//2 输出结果 4 , 9.0//2.0 输出结果 4.0
%取余返回除法的余数 b % a 输出结果 0
**指数a**b 为10的20次方, 输出结果 100000000000000000000

演示:

print("1 + 1的结果是:%d" % (1 + 1))
print("2 - 1的结果是:%d" % (2 - 1))
print("1 * 3的结果是:%d" % (1 * 3))
print("9 / 3的结果是:%d" % (9 / 3))
print("9整除2的结果是:%d" % (9 // 2))
print("9余2的结果是:%d" % (9 % 2))
print("2的6次方是:%d" % (2 ** 6))

输出结果:

1 + 1的结果是:2
2 - 1的结果是:1
1 * 3的结果是:3
9 / 3的结果是:3
9整除2的结果是:4
9余2的结果是:1
2的6次方是:64

2.6.2 赋值运算符

运算符描述实例
=赋值运算符把 = 号右边的结果 赋给 左边的变量,如 num = 1 + 2 * 3,结果num的值为7
+=加法赋值运算符c += a 等效于 c = c + a
-=减法赋值运算符c -= a 等效于 c = c - a
*=乘法赋值运算符c = a 等效于 c = c a
/=除法赋值运算符c /= a 等效于 c = c / a
%=取模赋值运算符c %= a 等效于 c = c % a
**=幂赋值运算符c = a 等效于 c = c a
//=取整除赋值运算符c //= a 等效于 c = c // a

2.6.3 总结

常见的算术(数学)运算符有:
加(+)、减(-)、乘(*)、除(/)、整除(//)、取余(%)、求平方(**
值运算符有
标准赋值: =
复合赋值:+=、-=、=、/=、//=、%=、*=

2.7 字符串扩展

2.7.1 字符串的三种定义方式

  1. 双引号定义法

    taxt1 = "我是字符串"
  2. 单引号定义法

    taxt2 = '我是字符串'
  3. 三引号定义法

    taxt3 = """我是字符串"""

三引号定义法,表示在一堆三个双引号的范围内,均是字符串,如下:

text = """
在三个引号的包围内
全部都是
字符串
"""

三引号定义法,和多行注释的写法一样,同样支持换行操作。
使用变量接收它,它就是字符串
不使用变量接收它,就可以作为多行注释使用。

要注意的是,包含范围是:==从三个引号开始,到下一个三个引号结束==

"""字符串"""字符串"""
#   ^这部分是字符串

2.7.2 字符串的引号嵌套

单引号定义法,可以内含双引号

print('我是"字符串"')

双引号定义法,可以内含单引号

print("我是'字符串'")

可以使用转移字符(\)来将引号解除效用,变成普通字符串

print("我是\"字符串\"")

2.7.3 字符串拼接

如果我们有两个字符串(文本)字面量,可以将其拼接成一个字符串,通过+号即可完成,如:

print("我是" + "字符串")

输出结果:

我是字符串

不过一般,单纯的2个字符串字面量进行拼接显得很呆,一般,字面量和变量或变量和变量之间会使用拼接,如:

name = "张三"
age = "18岁"
print("我的名字是" + name + "我今年" + age)

输出结果:

我的名字是张三我今年18岁

注意:==字符串无法和非字符串变量进行拼接,因为类型不一致,无法接上==,就像接力赛一样,不是队友,不能接力的哦

name = "张三"
age = 18
print("我的名字是" + name + "我今年" + age)

输出结果:

Traceback (most recent call last):
  File "E:\python\python_go_learning\xuexi\1.py", line 10, in <module>
    print("我的名字是" + name + "我今年" + age)
          ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~^~~~~
TypeError: can only concatenate str (not "int") to str

2.7.4 字符串格式化

我们会发现,这个拼接字符串也不好用啊

  • 变量过多,拼接起来实在是太麻烦了
  • 字符串无法和数字或其它类型完成拼接。

我们可以通过如下语法,完成字符串和变量的快速拼接。

name = "张三"
print("我的名字是%s" % name) 

其中的,%s

  • % 表示:我要占位
  • s 表示:将变量变成==字符串==放入占位的地方

所以,综合起来的意思就是:我先占个位置,等一会有个变量过来,我把它变成字符串放到占位的位置

多个变量占位,变量要用括号括起来,并按照占位的顺序填入。

name = "张三"
age = 18
height = 180
print("我的名字是%s,我今年%d岁了,身高是%dCM" % (name, age, height))

输出结果:

我的名字是张三,我今年18岁了,身高是180CM

==数字类型==变量也可以用%s占位,数字会被转换为==字符串==

Python最常用的三类数据类型占位符

格式符号转化
%s将内容转换成字符串,放入占位位置
%d将内容转换成整数,放入占位位置
%f将内容转换成浮点型,放入占位位置

2.7.5 格式化的精度控制

可以使用辅助符号"m.n"来控制数据的宽度和精度

  • m,控制宽度,要求是数字(很少使用),设置的宽度小于数字自身,m不生效
  • .n,控制小数点精度,要求是数字,会进行小数的四舍五入
  • m和.n均可省略

示例:

  • %5d:表示将整数的宽度控制在5位,如数字11,被设置为5d,就会变成:{空格}{空格}{空格}11,用三个空格补足宽度。
  • %5.2f:表示将宽度控制为5,将小数点精度设置为2
  • ==小数点和小数部分也算入宽度计算。==如,对11.345设置了%7.2f 后,结果是:{空格}{空格}11.35。2个空格补足宽度,小数部分限制2位精度后,四舍五入为 .35
  • %.2f:表示不限制宽度,只设置小数点精度为2,如11.345设置%.2f后,结果是11.35
num1 = 11
num2 = 11.345
print("数字11宽度限制5,结果是:%5d" % num1)
print("数字11宽度限制1,结果是:%1d" % num1)
print("数字11.345宽度限制7,小数精度2,结果是:%7.2f" % num2)
print("数字11.345不限制,小数精度2,结果是:%.2f" % num2)

输出结果:

数字11宽度限制5,结果是:   11
数字11宽度限制1,结果是:11
数字11.345宽度限制7,小数精度2,结果是:  11.35
数字11.345不限制,小数精度2,结果是:11.35

2.7.6 字符串格式化方式2

快速写法

除了%符号占位还能使用f"内容{变量}"的格式来快速格式化

name = "张三"
age = 18
height = 17.8
print(f"我的名字是{name},我今年{age}岁了,身高是{height}M")

输出结果:

我的名字是张三,我今年18岁了,身高是17.8M

这种写法不做精度控制,也不理会类型,适合对精度没有要求的时候快速使用

2.7.7 表达式的格式化

表达式是什么?

表达式就是一个具有明确结果的代码语句,如 1 + 1、type(“字符串”)、3 * 5等
在变量定义的时候,如 age = 11 + 11,等号右侧的就是表达式,也就是有具体的结果,将结果赋值给了等号左侧的变量

在无需使用变量进行数据存储的时候,可以直接格式化表达式,简化代码

print("1 * 1的结果是:%d" % (1 * 1))
print(f"1 * 1的结果是:{1 * 1}")
print("字符串在Python中的类型是:%s" % type('字符串'))

输出结果:

1 * 1的结果是:1
1 * 1的结果是:1
字符串在Python中的类型是:<class 'str'>

format()功能很强大,它把字符串当成一个模板,通过传入的参数进行格式化,并且使用大括号‘{}’作为特殊字符代替‘%’。

1、基本用法

  • (1)不带编号,即“{}”
  • (2)带数字编号,可调换顺序,即“{1}”、“{2}”
  • (3)带关键字,即“{a}”、“{tom}”
>>> print('{} {}'.format('千锋','教育'))  # 不带字段
千锋 教育

>>> print('{0} {1}'.format('千锋','教育'))  # 带数字编号
千锋 教育
 
>>> print('{0} {1} {0}'.format('千锋','教育'))  # 打乱顺序
千锋 教育 千锋

>>> print('{1} {1} {0}'.format('千锋','教育'))
教育 教育 千锋

>>> print('{a} {b} {a}'.format(a='千锋',b='教育'))  # 带关键字
千锋 教育 千锋

2、进阶用法

  • (1)< (默认)左对齐、> 右对齐、^ 中间对齐、= (只用于数字)在小数点后进行补齐
  • (2)取位数“{:4s}”、"{:.2f}"等
>>> print('{} and {}'.format('千锋','教育'))  # 默认左对齐
千锋 and 教育

>>> print('{:10s} and {:>10s}'.format('千锋','教育'))  # 取10位左对齐,取10位右对齐
千锋      and      教育

>>> print('{:^10s} and {:^10s}'.format('千锋','教育'))  # 取10位中间对齐
   千锋    and   教育   

>>> print('{} is {:.2f}'.format(1.123,1.123))  # 取2位小数
1.123 is 1.12

>>> print('{0} is {0:>10.2f}'.format(1.123))  # 取2位小数,右对齐,取10位
1.123 is       1.12
3、多个格式化
'b' - 二进制。将数字以2为基数进行输出。
>>> print('{0:b}'.format(3))
11

'c' - 字符。在打印之前将整数转换成对应的Unicode字符串。
>>> print('{:c}'.format(20))
4

'd' - 十进制整数。将数字以10为基数进行输出。
>>> print('{:d}'.format(20))
20

'o' - 八进制。将数字以8为基数进行输出。
>>> print('{:o}'.format(20))
24

'x' - 十六进制。将数字以16为基数进行输出,9以上的位数用小写字母。
>>> print('{:x}'.format(20))
14

'e' - 幂符号。用科学计数法打印数字。用'e'表示幂。
>>> print('{:e}'.format(20))
2.000000e+01

'g' - 一般格式。将数值以fixed-point格式输出。当数值特别大的时候,用幂形式打印。
>>> print('{:g}'.format(20.1))
20.1

'n' - 数字。当值为整数时和'd'相同,值为浮点数时和'g'相同。不同的是它会根据区域设置插入数字分隔符。
>>> print('{:f}'.format(20))
20.000000
>>> print('{:n}'.format(20))
20

'%' - 百分数。将数值乘以100然后以fixed-point('f')格式打印,值后面会有一个百分号。

>>> print('{:%}'.format(20))
2000.000000%

4、通过位置匹配参数

>>> '{0}, {1}, {2}'.format('北京', '千锋', '教育')
'北京,千锋,教育'
>>> '{}, {}, {}'.format('北京', '千锋', '教育')  # 3.1+版本支持
'北京,千锋,教育'
>>> '{2}, {1}, {0}'.format('北京', '千锋', '教育')
'教育,千锋,北京'
>>> '{2}, {1}, {0}'.format(*'北京千')  # 可打乱顺序
'千, 京, 北'
>>> '{0}{1}{0}'.format('千锋', '教育')  # 可重复
'千锋教育千锋'


5、通过名字匹配参数
>>> 'Coordinates: {latitude}, {longitude}'.format(latitude='37.24N', longitude='-115.81W')
'Coordinates: 37.24N, -115.81W'

>>> coord = {'latitude': '37.24N', 'longitude': '-115.81W'}
>>> 'Coordinates: {latitude}, {longitude}'.format(**coord)
'Coordinates: 37.24N, -115.81W'

另,可在字符串前加f以达到格式化的目的,在{}里加入对象,此为format的另一种形式:

name = 'qianfeng'
age = 18
sex = 'man'
job = "IT"
salary = 9999.99

print(f'my name is {name.capitalize()}.')
print(f'I am {age:*^10} years old.')
print(f'I am a {sex}')
print(f'My salary is {salary:10.3f}')

# 结果
my name is Qianfeng.
I am ****18**** years old.
I am a man
My salary is   9999.990

2.8 数据输入

我们前面学习过print语句(函数),可以完成将内容(字面量、变量等)输出到屏幕上。

在Python中,与之对应的还有一个input语句,用来获取键盘输入。

  • 数据输出:print
  • 数据输入:input

使用上也非常简单:

  • 使用input()语句可以从键盘获取输入
  • ()里面可以填入输出提示内容
  • 使用一个变量接收(存储)input语句获取的键盘输入数据即可

image-20240327185009493

注意:input语句无论键盘输入何种类型的数据,==最终的结果都是,字符串类型的数据==

print(type(input("请输入内容:")))

输出结果:

请输入内容:233
<class 'str'>

第三章

3.1 布尔类型和比较运算符

3.1.1 布尔类型

进行判断,只有2个结果:

布尔类型的字面量:

  • True 表示真(是、肯定)
  • False 表示假 (否、否定)

定义变量存储布尔类型数据:

变量名称 = 布尔类型字面量

布尔类型不仅可以自行定义,同时也可以通过计算的来。

也就是使用比较运算符进行比较运算得到布尔类型的结果。

match-case函数

该语法是 Python 3.10 版本引入的新语法。

只能在Python 3.10以上版本使用

语法格式:

match a:
    case 判断条件1:
        内容
    case 判断条件2 | 判断条件3: # 可以多选,满足2或3
        内容
    case if判断条件:  # 可以写if判断语句
        内容
    case x:  # 可以是_也可变量,如果上面无匹配则执行这条语句,如果是变量则把a赋值给x
        print('other,x')

第四章

4.1

循环跳过和结束

跳过当前循环

continue

结束

break

4.2 列表推导式

列表推导式(List Comprehension)是Python中一种非常强大且简洁的构造列表(List)的方法。它允许你通过对一个序列进行操作来快速生成一个新的列表。这里是列表推导式的基本结构和详细介绍:

基本结构:

[expression for item in iterable if condition]
  • expression:是您希望每个元素应用的表达式,它通常是对item的操作结果。
  • item:是从iterable中遍历出来的当前元素。
  • iterable:是一个序列,可以是列表、元组、集合等任何可迭代对象。
  • condition:是一个可选的条件语句,用于筛选出满足条件的元素。

列表推导式可以包含多个forif子句,它们可以嵌套使用,以处理更复杂的逻辑。

示例:

假设我们有一个数字列表,我们想要创建一个新列表,其中包含原列表中每个数字的平方。

不使用列表推导式的写法:

numbers = [1, 2, 3, 4, 5]
squares = []
for n in numbers:
    squares.append(n ** 2)

使用列表推导式的写法:

numbers = [1, 2, 3, 4, 5]
squares = [n ** 2 for n in numbers]

条件筛选:

我们还可以在列表推导式中添加条件语句来筛选元素。例如,如果我们只想要原列表中的偶数的平方:

numbers = [1, 2, 3, 4, 5]
even_squares = [n ** 2 for n in numbers if n % 2 == 0]

嵌套列表推导式:

列表推导式也可以嵌套,例如,如果我们有一个矩阵,我们想要将其转换为一个一维列表,包含矩阵中的所有元素:

matrix = [[1, 2, 3], [4, 5, 6], [7, 8, 9]]
flattened = [num for row in matrix for num in row]

列表推导式是Python编程中一个非常有用的特性,它可以使代码更加简洁和易于理解。

4.3 三元推导式

在Python中,三元推导式,也称为条件表达式,是一种基于条件的表达式选择器。它允许在单行内进行快速的条件判断和值赋值。其基本形式如下:

x if condition else y

这里,condition 是一个布尔表达式,x 是当条件为真时的结果,而 y 是当条件为假时的结果。这种表达式非常适合需要根据条件来决定变量值的情况。

例如,下面的代码使用三元推导式来决定变量 max_num 的值:

num1 = int(input('请输入第一个数字:'))
num2 = int(input('请输入第二个数字:'))
max_num = num1 if num1 >= num2 else num2
print(f'最大值是:{max_num}')

如果 num1 大于或等于 num2,则 max_num 的值为 num1,否则为 num2

三元推导式也可以与列表推导式结合使用,例如:

s = [i if i % 2 == 0 else 10 * i for i in range(10)]
print(s)

这段代码会生成一个列表,其中偶数保持不变,奇数则变为其原值的10倍。

三元推导式的优点包括代码简洁和可读性高。它们可以用来替换简单的条件语句,减少代码行数,使代码更加优雅和Pythonic

请注意,虽然三元推导式可以使代码更简洁,但过度使用或在复杂的条件下使用可能会降低代码的可读性。因此,建议在适当的情况下使用它们,以保持代码的清晰和易于维护。

4.4 match ... case语法

Python 3.10 版本引入了 matchcase 语句,这是一种结构模式匹配的新语法。它提供了一种更强大的模式匹配方法,可以使代码更简洁、易读。以下是 matchcase 语法的基本结构和使用示例:

基本语法结构:

match expression:
    case pattern1:
        # 处理pattern1的逻辑
    case pattern2 if condition:
        # 处理pattern2并且满足condition的逻辑
    case _:
        # 处理其他情况的逻辑
  • match 语句后跟一个表达式。
  • 使用 case 语句来定义不同的模式。
  • case 后跟一个模式,可以是具体值、变量、通配符等。
  • 可以使用 if 关键字在 case 中添加条件。
  • _ 通常用作通配符,匹配任何值。

下面是一些使用 matchcase 语法的实例:

  1. 简单的值匹配:
def match_example(value):
    match value:
        case 1:
            print("匹配到值为1")
        case 2:
            print("匹配到值为2")
        case _:
            print("匹配到其他值")
  1. 使用变量和条件:
def match_example(item):
    match item:
        case (x, y) if x == y:
            print(f"匹配到相等的元组: {item}")
        case (x, y):
            print(f"匹配到元组: {item}")
        case _:
            print("匹配到其他情况")
  1. 类型匹配:
class Circle:
    def __init__(self, radius):
        self.radius = radius

class Rectangle:
    def __init__(self, width, height):
        self.width = width
        self.height = height

def match_shape(shape):
    match shape:
        case Circle(radius=1):
            print("匹配到半径为1的圆")
        case Rectangle(width=1, height=2):
            print("匹配到宽度为1,高度为2的矩形")
        case _:
            print("匹配到其他形状")

matchcase 语句不仅可以用于简单的值匹配,还可以进行更复杂的模式匹配,如类型匹配、序列模式匹配等。这使得它成为处理多种数据结构和复杂条件的强大工具

第五章

5.1 函数

5.1.1 函数的基础定义语法

函数的定义:

def 函数名(传入参数):
    函数体
    return 返回值

传入参数和返回值可以省略。

函数必须先定义后使用。

函数定义完后,要调用才能生效。

函数的调用:

函数名(参数)

5.1.2 函数的传入参数

传入参数的功能是:在函数进行计算的时候,接受外部(调用时)提供的数据。

例子:

# 定义计算2个数相加的和的函数,通过参数接收被计算的2个数字。
def add(x, y):
    result = x + y
    print(f"{x} + {y}的结果是:{result}")
    
# 调用函数,传入被计算的2个数字
add(2, 3)  # 语法规范要求类和方法后面要隔两行。
add(10, 6)

函数定义中,提供的x和y,被称之为:形式参数(形参),表示函数声明将要使用2个参数

参数之间使用,进行分隔

函数调用中,提供的2和3,称之为:实际参数(实参),表示函数执行时真正使用的参数值

传入参数时,要和形式参数一一对应,使用,分隔

传入参数的数量是不受限制的。

  • 可以不使用参数
  • 也可以使用任意N个参数
def 函数名(a,b,c,……N)
    函数体
    return 返回值


函数名(a,b,c,……N)

5.1.3 函数返回值

程序中的返回值

def add(a, b):
    result = a + b
    return result
    
    
r = add(1, 2)  #result的值被r这个变量接收了
print(r)
# 输出结果:
# 3

所以”返回值“,就是程序中函数完成事后,最后给调用者的结果。

语法格式:

def 函数(参数……):
    函数体
    return 返回值


变量 = 函数(参数……)

如上所示,变量就能接收到函数的返回值

语法就是:通过return关键字,就能向调用者返回数值

注意:==函数体在遇到return后就结束了,所以写在return后的代码不会执行。==

5.1.4 None类型

如果函数没有使用return语句返回数据,函数会返回一个特殊字面量None,其类型是:<calss 'NoneType'>

可以使用return None主动返回None

None作为一个特殊的字面量,表示:空的,无实际意义的意思。

使用场景:

  • 用在函数无返回值上
  • 用在if判断上

    • 在if判断中,None等同于False
    • 一般用于在函数中主动返回None,配合if判断做相关处理
  • 用于声明无内容的变量上

    • 定义变量,但暂时不需要变量有具体值,可以用None来代替

      # 暂时不赋予变量具体值
      name = None

5.1.5 函数说明文档

对函数进行说明解释,帮助更好理解函数功能。

定义语法:

def func(x, y):
    """
    函数说明
    :param x: 行参的X的说明
    :param y: 行参的y的说明
    :return:  返回值的说明
    """
    函数体
    return 返回值

通过多行注释的形式,对函数进行说明解释

  • 内容应写在函数之前
  • :param 用于解释参数
  • :return 用于解释返回值

5.1.6 函数的嵌套调用

所谓的函数嵌套调用指的是一个函数里面又调用了另一个函数

def b():
    print("2")


def a():
    print("1")
    b()
    print("3")


a()

执行流程

函数A中执行调用函数B的语句,会将B全部执行完成后,继续执行函数A的剩余内容

5.1.7 变量的作用域

变量作用域指的是变量的作用范围(变量可以在哪里可用,在哪里不可用。)

主要分为两大类:局部变量和全局变量

局部变量

局部变量是定义在函数体内部的变量,即只在函数体内部生效

def testA():
    num = 100
    print(num)
    
    
tastA()  # 输出100
print(num)  # 报错 num变量没有被定义

局部变量的作用:在函数体内部,临时保存数据,函数调用完成后,局部变量会被销毁。

全局变量

全局变量,指的是在函数体内,外都能生效的变量

# 定义全局变量
num = 200


def test_a():
    print(f"teat_a:{num}")  # 访问全局变量,并打印变量num存储的数据


def test_b():
    num = 500  #局部变量,只能在函数体内部生效。若想修改全局变量请使用global关键字
    print(f"teat_b:{num}")  # 访问局部变量,并打印变量num存储的数据


test_a()  # 结果:200
test_b()  # 结果:500
print(num)  # 结果:200

使用global关键字可以在函数内部声明变量为全局变量,语法:gloal 变量名

num = 200


def test_a():
    print(f"teat_a:{num}")


def test_b():
    global num  # 设置内部定义的变量为全局变量
    num = 500  # 全局变量
    print(f"teat_b:{num}")


test_a()  # 结果:200
test_b()  # 结果:500
print(num)  # 结果:500

第六章

6.1数据容器入门


6.1.1 数据容器的概念

例子:

list1 = ['张三',123,True]

python中的数据容器是一种可以容纳多分数据的数据类型,容纳的每一份数据称之为1个元素,每一个元素,可以是任意类型的数据,如字符串、数字、布尔等。

数据容器根据特定点的不同,如:

  • 是否支持重复元素
  • 是否可以修改
  • 是否有序,等

可以分为5类,分别是:

列表(list)、元组(tuple)、字符串(str)、集合(set)、字典(dict)

6.2 list列表

基本语法

# 字面量
[元素1, 元素2, 元素3, 元素4, ……]

# 定义变量
变量名 = [元素1, 元素2, 元素3, 元素4, ……]

# 定义空列表
变量名 = []
变量名 = list[]

列表内的每一个数据,称之为元素

  • []作为标识
  • 列表内的每一个元素之间用,隔开

注意:列表可以一次存储多个数据,且==可以为不同的数据类型,支持嵌套==。


6.2.2 (list)列表的下标索引

列表的每一个元素,都有编号称之为下标索引

语法:列表[标号]

可以通过下标索引取出列表对应位置的元素

  • 正向:从0开始、依次递增

    name_list = ["张三", "李四", "王五"]
    print(name_list[0])  # 结果:张三
    print(name_list[1])  # 结果:李四
    print(name_list[2])  # 结果:王五
  • 反向:从-1开始,依次递减

    name_list = ["张三", "李四", "王五"]
    print(name_list[-1])  # 结果:王五
    print(name_list[-2])  # 结果:李四
    print(name_list[-2])  # 结果:张三

如果是嵌套列表,同样支持下标索引

my_list = [[1, 2], [3, 4]]
print(my_list[0][0])  # 结果:1
print(my_list[0][1])  # 结果:2
print(my_list[1][0])  # 结果:3
print(my_list[1][1])  # 结果:4

注意:==下标索引的范围不能超出列表范围,否则会报错==


6.2.2 list(列表)的常用操作

列表除了可以:

还可以:

等功能,我们称之为:列表的方法

1.查询某元素的下标

功能:查找某个值第一个匹配项的索引位置(正向),如果找不到,报错ValueError

语法:列表.index(元素)

index就是列表对象(变量)内置的方法(函数)

mylist = ["张三", "李四", "王五"]
index = mylist.index("张三")
print(f"张三在列表中的下标索引值是:{index}")
2.修改元素值

语法:列表[下标] = 值

可以使用如上语法,直接对指定下标(正向,反向下标均可)的值进行:重新赋值(修改)

# 正向
mylist = ["张三", "李四", "王五"]
mylist[0] = "陈二"
print(mylist)  # 结果:['陈二', '李四', '王五']

# 反向
mylist = ["张三", "李四", "王五"]
mylist[-1] = "赵六"
print(mylist)  # 结果:['张三', '李四', '赵六']
3.插入元素

语法:列表.insert(下标, 元素)

在指定的下标位置,插入指定元素

mylist = ["张三", "李四", "王五"]
mylist.insert(1, "赵六")
print(mylist)  # 结果:['张三', '赵六', '李四', '王五']
4.追加元素1

语法:列表.append(元素)

将指定元素,追加到列表的尾部

mylist = ["张三", "李四", "王五"]
mylist.append("赵六")
print(mylist)  # 结果:['张三', '李四', '王五', '赵六']
5.追加元素2

语法:列表.enxtend(其他数据容器)

将其他数据容器的内容取出,依次追加到列表尾部

mylist = ["张三", "李四", "王五"]
mylist.extend(["赵六", "孙七", "周八"])
print(mylist)  # 结果:['张三', '李四', '王五', '赵六', '孙七', '周八']
6.删除元素

删除指定下标元素:

语法:del 列表[下标]

mylist = ["张三", "李四", "王五"]
del mylist[2]
print(mylist)  # 结果:['张三', '李四']

删除某个元素在列表中的第一个匹配项:

语法:列表.remove(元素)

mylist = ["张三", "李四", "张三", "王五"]
mylist.remove("张三")
print(mylist)  # 结果:['李四', '张三', '王五']
7.取出元素

语法:变量 = 列表.pop(下标)

删除元素并取出,如果不用变量接收就只删除

mylist = ["张三", "李四", "王五"]
a = mylist.pop(2)
print(mylist)  # 结果:['张三', '李四']
print(a)  # 结果:王五
8.清空列表

语法:列表.clear()

删除列表内的元素,但保留列表

mylist = ["张三", "李四", "王五"]
mylist.clear()
print(mylist)  # 结果:[]

语法:del 列表

删除列表跟列表元素

mylist = ["张三", "李四", "王五"]
del mylist
print(mylist)  # 报错
9.统计元素个数

语法:列表.count(元素)

统计某元素在列表内的数量

mylist = ["张三", "李四", "张三", "王五", "张三","李四"]
count = mylist.count('张三')
print(count)  # 结果:3
10.统计列表长度

语法:len(列表)

可以得到一个int数字,表示列表内的元素数量

mylist = ["张三", "李四", "张三", "王五", "张三","李四"]
print(len(mylist))  # 结果:6

11.总结
使用方法作用
列表.append(元素)向列表中追加一个元素
列表.extend(容器)将数据容器的内容依次取出,追加到列表尾部
列表.insert(下标,元素)在指定下标处,插入指定的元素
del列表[下标]删除列表指定下标元素
列表.pop(下标)删除列表指定下标元素
列表.remove(元素)从前向后,删除此元素第一个匹配项
列表.clear()清空列表
列表.count(元素)统计此元素在列表中出现的次数
列表.index(元素)查找指定元素在列表的下标
找不到报错ValueError
len(列表)统计容器内有多少元素

列表有如下特点:

  • 可以容纳多个元素(上限为2**63-1、9223372036854775807个)
  • 可以容纳不同类型的元素(混装)
  • 数据是有序存储的(有下标序号)
  • 允许重复数据存在
  • 可以修改(增加或删除元素等)

6.3 list(列表)的遍历


6.3.1 遍历的概念

将容器内的元素依次取出,并处理,称之为遍历操作


6.3.2 语法

1.while循环
list1 = [1, 2, 3, 4, 5]
index = 0
while index < len(list1):
    print(list1[index])
    index += 1
2.for循环
list1 = [1, 2, 3, 4, 5]
for element in list1:
    print(element)

6.3.3 for循环和while循环的对比

while循环和for循环,都是循环语句,但细节不同:

在循环控制上:

  • while循环可以自定循环条件,并自行控制
  • for循环不可以自定循环条件,只可以一个个从容器内取出数据

在无限循环上:

  • while循环可以通过条件控制做到无限循环
  • for循环理论上不可以,因为被遍历的容器容量不是无限的

在使用场景上:

  • while循环适用于任何想要循环的场景
  • for循环适用于,遍历数据容器的场景或简单的固定次数循环场景

6.4 tuple 元组


6.4.1 元组的概念和定义

元组同列表一样,都是可以封装多个、不同类型的元素在内。

  • 元组也支持嵌套
  • 多数特性和list一致

但最大的不同点在于:==元组一旦定义完成,就不可以修改,但元组里的列表可以修改==

元组定义:定义元组使用(),且使用,隔开各个数据,数据可以是不同类型

# 定义元组的字面量
(元素1, 元素2, ……, 元素n)
# 定义单个元素的元组
(元素, )  # 定义单个元素的元组需要在后面添加逗号
# 定义元组变量
变量名称 = (元素1, 元素2, ……, 元素n)
# 定义空元组
变量名称 = ()  # 方式1
变量名称 = tuple()  # 方式2

6.4.2 元组的下标索引

列表相同

元组的每一个元素,都有编号称之为下标索引

语法:元组[标号]

可以通过下标索引取出元组对应位置的元素

  • 正向:从0开始、依次递增

    name_tuple = ("张三", "李四", "王五")
    print(name_tuple[0])  # 结果:张三
    print(name_tuple[1])  # 结果:李四
    print(name_tuple[2])  # 结果:王五
  • 反向:从-1开始,依次递减

    name_tuple = ("张三", "李四", "王五")
    print(name_tuple[-1])  # 结果:王五
    print(name_tuple[-2])  # 结果:李四
    print(name_tuple[-2])  # 结果:张三

如果是嵌套元组,同样支持下标索引

my_tuple = ((1, 2), (3, 4))
print(my_tuple[0][0])  # 结果:1
print(my_tuple[0][1])  # 结果:2
print(my_tuple[1][0])  # 结果:3
print(my_tuple[1][1])  # 结果:4

注意:==下标索引的范围不能超出元组范围,否则会报错==

6.4.3 元组的相关操作

使用方法作用
元组.count(元素)统计此元素在元组中出现的次数
元组.index(元素)查找指定元素在元组的下标
找不到报错ValueError
len(元组)统计元组内的元素个数

6.4.4 元组的遍历

列表相同

6.5 str 字符串的定义和操作

字符串可以看作字符的容器,支持下标索引等特性


6.5.1 特点

  • 只能存储字符串
  • 长度任意(取决于内存大小)
  • 支持下标索引
  • 允许重复字符串存在
  • ==不可以修改==(增加或删除元素等)
  • 支持while循环和for循环进行遍历

6.5.2 常用操作

操作说明
字符串[下标]根据下标索引取出特定位置字符
字符串.index(字符串)查找给定字符的第一个匹配项的下标
字符串.replace(字符串1,字符串2)将字符串内的全部字符串1,替换为字符2
不会修改原字符串,而是得到一个新的字符串
字符串.split(字符串)按照给定字符串,对字符串进行分隔,不会修改原字符串,而是得到一个新的列表
字符串.strip()
字符串.strip(字符串)
移除首尾的空格和换行符或指定字符串
字符串.count(字符串)统计字符串内某字符串的出现次数
len(字符串)统计字符串的字符个数

6.6 序列切片


6.6.1 序列的概念和语法

内容连续、有序,支持下标索引的一类数据容器

列表、元组、字符串都是序列

语法:序列[起始:结束:步长]

  • 起始可以省略,省略从头开始
  • 结束可以省略,不包含它本身,省略到尾结束
  • 步长可以省略,省略步长为1(可以为负数,表示倒叙执行)

例子:

str1 = '万过薪月,员序程马黑来,nohtyP学'
new_str = str1[str1.index('员'):(str1.index('黑') + 1)][::-1]
print(new_str)  # 输出结果:黑马程序员

6.7 set 集合

集合不属于序列


6.7.1 特点

  • 可以容纳多个数据
  • 可以容纳不同类型的数据(==但列表不能存储在集合里==)
  • 数据是无序存储(==不支持下标索引==)
  • 不允许重复数据存在
  • 可以修改(增加或者删除元素等)
  • 支持for循环遍历

6.7.2 语法

# 定义集合字面量
{元素, 元素, ……, 元素}
# 定义集合变量
变量名称 = {元素, 元素, ……, 元素}
# 定义空集合
变量名称 = set()

和列表、元组、字符串等定义基本相同


6.7.3 常用功能

操作说明
集合.add(元素)集合内添加一个元素
集合.remove(元素)移除集合内指定的元素
集合.pop()从集合中随机取出一个元素
集合.clear()将集合清空
集合1.difference(集合2)得到一个新集合,内含2个集合的差集 原有的2个集合内容不变
集合1.difference_update(集合2)在集合1中,删除集合2中存在的元素 集合1被修改,集合2不变
集合1.union(集合2)得到1个新集合,内含2个集合的全部元素 原有的2个集合内容不变
len(集合)得到一个整数,记录了集合的元素数量

6.8 dict 字典

可以提供基于key检索value的场景实现,就像查字典一样


6.8.1 特点

  • 可以容纳多个数据
  • 可以容纳不同类型的数据
  • ==每一份数据是KeyValue键值对==
  • ==可以通过Key获取到Value,Key不可重复(重复会覆盖)==
  • 不支持下标索引
  • 可以修改(增加或删除更新元素等)
  • 支持for循环

6.8.2 语法

# 定义字典字面量
{key: value, key: value, ……, key: value}
# 定义字典变量
变量名称 = {key: value, key: value, ……, key: value}
# 定义空字典
变量名称 = {}
变量名称 = dict()

6.8.3 常用操作

操作说明
字典[Key]获取指定Key对应的Value值
字典[Key] = Value添加或更新键值对
字典.pop(Key)取出Key对应的Value并在字典内删除此Key的键值对
字典.clear()清空字典
字典.keys()获取字典的全部Key,可用于for循环遍历字典
len(字典)计算字典内的元素数量
字典.get(key[, value])函数返回指定键的值,key表示字典中要查找的键,value可选,表示如果指定的键值不存在,返回该默认值,如果不设置则返回None

6.8.4 注意事项

  • 键值对的keyvalue可以是任意类型(==key不能是字典==)
  • 字典内key不能重复,重复添加等同于覆盖原数据
  • 新增和更新元素的语法一致,如果是key不存在即新增,如果key存在即更新(key不可重复)
  • 字典不可用下标索引,而是通过key检索value

6.9 数据容器总结


6.9.1 数据容器特点对比

..............列表元组字符串集合字典
元素数量支持多个支持多个支持多个支持多个支持多个
元素类型任意任意仅字符除列表外任意类型Key:Value
Key:只支持元组,字符串,数据
Value:任意类型
下标索引支持支持支持不支持不支持
重复元素支持支持支持不支持不支持
可修改性支持不支持不支持支持支持
数据有序
使用场景可修改、可重复的一批数据记录场景不可修改、可重复的一批数据记录场景一串字符的记录场景不可重复的数据记录场景以Key检索Value的数据记录场景

6.9.2 数据容器的通用操作

  1. 遍历

    • 5类数据容器都支持for循环遍历
    • 列表、元组、字符串支持while循环,集合、字典无法用下标索引遍历
  2. 统计功能

    • 统计容器的元素个数:len(容器)
    • 统计容器的最大元素:max(容器)
    • 统计容器的最小元素:min(容器)
  3. 转换功能

    • 将给定容器转换成列表:list(容器)
    • 将给定容器转换成元组:tuple(容器)
    • 将给定容器转换成字符串:str(容器)
    • 将给定容器转换成集合:set(容器)

    注意:==字典转列表、元组、集合会丢失value值==

  4. 排序功能

    • 将给定容器进行排序:sorted(容器, reverse=True)
    • reverse默认是:False从小到大排序
    • True从大到小排序

    注意:==排序完会被转换成列表容器,字典会丢失value值==


6.9.3 字符串比较大小的方式

通过ASCII码表,确定对应的码值数值来确定大小。

从头到尾,一位位进行比较,其中一位大,后面就无需比较了。

第七章

7.1函数进阶


7.1.1 函数多返回值

语法:

def test_return():
    return 1, 'hello', True


x, y, z = test_return()
print(x)  # 结果1
print(y)  # 结果hello
print(z)  # 结果True

按照返回值的顺序,写对应的顺序的变量接收即可

变量之间用,隔开

支持不同类型的数据return


7.1.2 函数数的多种参数使用形式

  1. 位置参数

    根据位置来传递参数:

    def user_info(name, age, gender):
        print(f"姓名是:{name}, 年龄是:{age}, 性别是:{gender}")
    
        
    user_info('小明', 20, '男')  # 结果:姓名是:小明, 年龄是:20, 性别是:男
  2. 关键字参数

    • 通过“键 = 值”形式传递参数,可以不限参数顺序

      def user_info(name, age, gender):
          print(f"姓名是:{name}, 年龄是:{age}, 性别是:{gender}")
      
      
      user_info(name='小王', age=11, gender='女')
      user_info(age=10, gender='女', name='潇潇')
      """
      结果:
      姓名是:小王, 年龄是:11, 性别是:女
      姓名是:潇潇, 年龄是:10, 性别是:女
      """ 
    • 可以和位置参数混用,位置参数需在前

      user_info('甜甜', gender='女', age=9)
      # 结果:姓名是:甜甜, 年龄是:9, 性别是:女
  3. 缺省参数

    • 不传递参数值时会使用默认的参数值
    • 默认值的参数必须定义在最后

      def user_info(name, age, gender='男'):
          print(f"姓名是:{name}, 年龄是:{age}, 性别是:{gender}")
      
          
      user_info('小天', 13)  # 结果:姓名是:小天, 年龄是:13, 性别是:男
  4. 不定长参数

    • 位置不定长传递以*号标记一个形式参数,以==元组==的形式接收参数,形式参数一般命名为args

      def user_info(*args):
          print(f"args参数的类型是:{type(args)},内容是:{args}")
      
      
      user_info(1, 2, 3, '小明', '男孩')
      # 结果:args参数的类型是:<class 'tuple'>,内容是:(1, 2, 3, '小明', '男孩')
    • 关键字不定长传递以**号标记一个形式参数,以==字典==的形式接受参数,形式参数一般命名为kwargs

      def user_info(**kwargs):
          print(f"args参数的类型是:{type(kwargs)},内容是:{kwargs}")
          
          
      user_info(name='小王', age=11, gender='男孩')
      # 结果:args参数的类型是:<class 'dict'>,内容是:{'name': '小王', 'age': 11, 'gender': '男孩'}

7.1.3 匿名函数

  1. 函数作为参数传递

    def test_func(compute):
        result = compute(1, 2)
        print(result)
    
    
    def compute(x, y):
        return x + y
    
    
    test_func(compute)  # 结果:3
    • 函数本身是可以作为参数,传入另一个函数中进行使用的。
    • 将函数传入的作用在于:==传入计算逻辑,而非传入数据==
  2. lambda匿名函数

    # 定义一个函数,接受其它函数输入
    def test_func(compute):
        result = compute(1, 2)
        print(f"结果是:{result}")
    
    
    # 通过lambda匿名函数的形式,将匿名函数作为参数传入
    test_func(lambda x, y: x + y)
    • 匿名函数使用lambda关键字进行定义
    • 定义语法:lambda 传入参数: 函数体(一行代码)
    • 注意事项:

      • 匿名函数用于临时构建一个函数,只用一次的场景
      • 匿名函数的定义中,函数体只能写一行代码,如果函数体要写多行代码,不可用lambda匿名函数,应使用def定义带名函数

第八章

8.1文件操作


8.1.1 文件编码概念

  1. 什么是编码

    • 编码是一种规则集合,记录了内容和二进制间进行相互转换的逻辑。
    • 编码中有许多种,最常用的编码是:UTF-8
  2. 为什么需要使用编码?

    • 计算机只认识0和1,所以需要将内容翻译成0和1才能保存在计算机中。
    • 同时也需要编码,将计算机保存的0和1,反向翻译回可识别的内容。

8.1.2 文件的读取

  1. open()打开函数

    可以打开一个已经存在的文件,或者创建一个新文件,语法如下:

    open(name, mode, encoding)
    • name:是要打开的目标文件名的字符串(可以包含文件所在的具体路径)。
    • mode:设置打开文件的模式(访问模式):只读、写入、追加等。
    • encoding:编码格式(推荐使用UTF-8)

    示例:

    f = open('python.txt', 'r', encoding=”UTF-8)
    # encoding的顺序不是第三位,所以不能用位置参数,用关键字参数直接指定

    mode常用的三种基础访问模式

    模式描述
    r以只读方式打开文件。文件的指针将会放在文件的开头。这是默认模式。
    w打开一个文件只用于写入。如果该文件已存在则打开文件,并从开头开始编辑,原有内容会被删除。 如果该文件不存在,创建新文件。
    a打开一个文件用于追加。如果该文件已存在,新的内容将会被写入到已有内容之后。 如果该文件不存在,创建新文件进行写入。
  2. read()方法:

    文件对象.read(num)

    num表示要从文件中读取的数据的长度(单位是字节),如果没有传入num,那么就表示读取文件中所有的数据。输出字符串

  1. readlines()方法:

    文件对象.readlines()

    readlines可以按照行的方式把整个文件中的内容进行一次性读取,并且返回的是一个==列表==,其中每一行的数据为一个元素。

  1. readline()方法:

    文件对象.readline()

    一次读取一行内容,输出字符串

  1. for循环读取文件行

    for line in 文件对象

    for循环文件行,一次循环得到一行数据

  1. close()关闭文件对象

    文件对象.close()

    关闭文件对象

  1. with open语法

    with open() as f

    通过在with open的语句块中对文件进行操作

    可以在操作完成后自动关闭close文件,避免遗忘掉close方法

注意:

  • 操作文件需要通过open函数打开文件得到文件对象
  • 文件读取完成后,要使用文件对象.close()方法关闭文件对象,否则文件会被一直占用

8.1.3 文件的覆写

  1. 写入文件使用open函数的w模式进行写入
  2. 写入的方法有:

    • wirte(),写入内容
    • flush(),刷新内容到硬盘中
  3. 注意事项:

    • w模式,文件不存在,会创建新文件
    • w模式,文件存在,会清空原有内容
    • close()方法,带有flush()方法的功能
    • 直接调用write,内容并未真正写入文件,而是会积攒在程序的内存中,称之为缓冲区
    • 当调用flush的时候,内容会真正写入文件,这样做是避免频繁的操作硬盘,导致效率下降(攒一堆,一次性写磁盘)

例:

# 1. 打开文件
f = open('python.txt', 'w')

# 2.文件写入
f.write('hello world')

# 3. 内容刷新
f.flush()

8.1.3 文件的追加

  1. 追加写入文件使用open函数的a模式进行写入
  2. 追加写入的方法有(和w模式一致):

    • wirte(),写入内容
    • flush(),刷新内容到硬盘中
  3. 注意事项:

    • a模式,文件不存在,会创建新文件
    • a模式,文件存在,会在原有内容后面继续写入
    • 可以使用\n来写出换行符

例:

# 1. 打开文件,通过a模式打开即可
f = open('python.txt', 'a')

# 2.文件写入
f.write('hello world')

# 3. 内容刷新
f.flush()

第九章

9.1 异常


9.1.1 异常的概念

  1. 什么是异常:

    • 异常就是程序运行的过程中出现了错误
  2. bug是什么:

    • bug就是指异常的意思,因为历史因为小虫子导致计算机失灵的案例,所以延续至今,bug就代表软件出现错误。

9.1.2 异常捕获


  1. 捕获异常的作用在于:提前假设某处会出现异常,做好提前准备,当真的出现异常的时候,可以有后续手段。
  2. 捕获异常的语法

    try:
        可能要发生异常的语句
    except[异常 as 别名]:
        出现异常的准备手段
    [else:]
        未出现异常时对应的事情
    [finally:]
        不管出不出现异常都会做的事情
  3. 捕获所有异常

    异常的种类多种多样,如果想要不管什么类型的异常都能捕获到,那么使用:

    • except:
    • except Exception:

    两种方式捕获全部的异常

9.1.3 异常的传递

异常是具有传递性的

当函数func01中发生异常, 并且没有捕获处理这个异常的时候, 异常会传递到函数func02, 当func02也没有捕获处理这个异常的时候,main函数会捕获这个异常, 这就是异常的传递性。

# 定义一个出现异常的方法
def func1():
    print('func1 开始执行')
    1/0     # 除零异常
    print('func1 结束执行')


# 定义一个无异常的方法,调用上面的方法
def func2():
    print('func2 开始执行')
    func1()
    print('func2 结束执行')


# 定义一个方法调用上面的方法
def main():
    try:
        func2()
    except Exception as e:
        print(f'出现异常,异常信息是:{e}')


main()
# 利用异常具有传递性的特点, 当我们想要保证程序不会因为异常崩溃的时候, 就可以在main函数中设置异常捕获, 由于无论在整个程序哪里发生异常, 最终都会传递到main函数中, 这样就可以确保所有的异常都会被捕获

9.2 Python模块(module)


9.2.1 模块的导入

Python 模块(Module),是一个 Python 文件,以 .py 结尾. 模块能定义函数,类和变量,模块里也能包含可执行的代码。

模块在使用前需要先导入 导入的语法如下:

[from 模块名] import [模块|类|变量|函数|*] {as 别名}

常用的组合方式:

  • import 模块名
  • from 模块名 import 类、变量、方法等 ==导入指定方法==
  • from 模块名 import * ==导入模块中所有的方法==
  • import 模块名 as 别名
  • from 模块名 import 功能名 as 别名

注意:

  • from可以省略,直接import即可
  • as别名可以省略
  • 通过”.”来确定层级关系
  • 模块的导入一般写在代码文件的开头位置

9.2.2 自定义模块

例子

  1. 如何自定义模块并导入?

    在Python代码文件中正常写代码即可,通过importfrom关键字和导入Python内置模块一样导入即可使用。

  2. __name__全局变量

    • 直接运行python文件(或者使用-m参数),则这个文件(模块)的__name__ = '__main__' (这种情况本质上就是将模块作为脚本执行);
    • 该模块被另外一个模块调用,则__name__ = 该模块名 (所谓“该模块名”,实际上是根据调用关系或者说嵌套关系得到的,一般都遵从“祖父包.父包.模块名”这样的结构,注意__init__.py__name__不包含模块名,层级结构之延伸到其所在包,其余的模块都要精确到其模块名)。
    • if __name__ == '__main__':表示,只有当程序是直接执行的才会进入if内部,如果是被导入的,则if无法进入
  3. __all__变量

    语法:__all__ = ['功能名']

    __all__变量可以控制import *的时候哪些功能可以被导入,__all__是列表。

注意事项

  • __all__只在使用import *的时候才有效,如果仅仅只是import 功能名的话,依然是按照默认导入。
  • 当==导入多个模块==的时候,且模块内有==同名功能==。当调用这个同名功能的时候,调用到的是==后面导入的模块的功能==
  • 自定义模块名必须要符合标识符命名规则
  • 模块名可以通过全局变量__name__来获得,一般来说,模块名就是其文件名去掉".py"。
  • 模块中的可执行代码会在模块第一次导入的时候执行,实际上也只有这一次。

9.3 Python包(package)

包就是一个文件夹,里面可以存放许多Python的模块(代码文件),通过包,在逻辑上将一批模块归为一类,方便使用。

从逻辑上看,包的本质依然是模块


9.3.1 自定义包

例子

导入包:

  1. import 包名.模块名
  2. form 包名 import 模块名
  3. from 包名 import *
  4. form 包名.模块名 import 方法名
  5. from 包名.模块名 import *

注意:

  • 新建包后,包内部会自动创建__init__.py文件,通过这个文件来表示一个文件夹是Python的包,而非普通的文件夹,这个文件控制着包的导入行为。
  • ==如果用from 包名 import *语法,必须在__init__.py文件中添加__all__ = [],控制允许导入的模块列表==
  • __all__只在使用import *的时候才有效,如果仅仅只是import 模块名的话,依然是按照默认导入。

9.3.2 安装第三方包

  1. 使用pip install命令

    安装包:

    pip install 包名              # 最新版本
    pip install 包名==1.0.4       # 指定版本
    pip install 包名>=1.0.4       # 最小版本

    设定版本版本范围:

    语法功能
    >=1.0大于或等于1.0的所有版本
    <=1.0小于或等于1.0的所有版本
    \>=1.0,<=2.0大于或等于1.0且小于或等于2.0的所有版本
    ~=1.4与1.4兼容的所有版本(允许小版本的更新)
    ==1.0仅安装版本1.0
    !=3.0排除版本3.0

    我们还可以在requirements.txt文件中指定。requirements.txt文件是一个文本文件,用于列出项目所依赖的包及其版本。

    ==requirements.txt示例:==

    pandas>=1.0
    matplotlib<=3.2
    numpy>=1.0,<=1.5
    requests~=2.25
    flask==1.2
    django!=3.0

    要使用requirements.txt安装依赖,可以执行以下命令:

    pip install -r requirements.txt

    临时使用pip源

    pip install 包名 -i 镜像源地址

    国内源镜像

    阿里云 http://mirrors.aliyun.com/pypi/simple/

    中国科技大学 https://pypi.mirrors.ustc.edu.cn/simple/

    豆瓣(douban) http://pypi.douban.com/simple/

    清华大学 https://pypi.tuna.tsinghua.edu.cn/simple/

    中国科学技术大学https://mirrors.ustc.edu.cn/pypi/web/simple

    卸载包:pip uninstall 包名

    升级包:pip install --upgrade 包名

    pip升级:pip install --upgrade pip

第十章

10.1 JSON


10.1.1 JSON的概念

  • JSON是一种轻量级的数据交互格式, 采用完全独立于编程语言的文本格式来存储和表示数据(就是字符串)
  • JSON本质上是一个带有特定格式的字符串
  • Python语言使用JSON有很大优势,因为:JSON无非就是一个单独的字典或一个内部元素都是字典的列表

    所以JSON可以直接和Python的字典或列表进行无缝转换。

  • JSON格式:

    # json数据的格式可以是:
    {"name":"admin","age":18}
    
    # 也可以是:
    [{"name":"admin","age":18},{"name":"root","age":16},{"name":"张三","age":20}]

10.1.2 Python数据和Json数据的相互转化

例子

语法:

# 导入json模块 
import json 

# 准备符合格式json格式要求的python数据 
data = [{"name": "老王", "age": 16}, {"name": "张三", "age": 20}]
 
# 通过 json.dumps(data) 方法把python数据转化为了 json数据 
data = json.dumps(data) 

# 通过 json.loads(data) 方法把json数据转化为了 python数据 
data = json.loads(data)

注意:如果有==中文==可以带上:ensure_ascii=False参数来确保中文正常转换

10.2 pyecharts 模块

pyecharts 是一个用于生成 Echarts 图表的类库。Echarts 是百度开源的一个数据可视化 JS 库。用 Echarts 生成的图可视化效果非常棒,pyecharts 是为了与 Python 进行对接,方便在 Python 中直接使用数据生成图。

模块主页 官方示例


10.2.1 基础入门

  • 基础折线图

    # 导包
    from pyecharts.charts import Line
    
    # 创建一个折线图对象
    line = Line()
    # 给折线图对象添加x轴的数据
    line.add_xaxis(["中国", "美国", "英国"])
    # 给折线图对象添加y轴的数据
    line.add_yaxis("GDP", [30, 20, 10])
    # 通过render方法,将代码生成为图像
    line.render()
  • pyecharts模块中有很多的配置选项, 常用到2个类别的选项:

  • 全局配置项能做什么?

    • 配置图表的标题
    • 配置图例
    • 配置鼠标移动效果
    • 配置工具栏
    • 等==整体配置项==

02-第二阶段

第一章

1.1 面向对象


1.1.1 初识对象

  1. 在程序中设计表格,我们称之为:设计类(class)

    class Student:
        name = None
        gander = None
        nationality = None
        native_place = None
        age = None
  2. 在程序中打印生产表格,我们称之为:创建对象

    # 基于类创建对象
    stu_1 = Student()
  3. 在程序中填写表格,我们称之为:对象属性赋值

    stu_1.name = '顶针'
    stu_1.gander = '男'
    stu_1.nationality = '中国'
    stu_1.native_place = '妈妈生的'
    stu_1.age = 18

1.1.2 成员方法


  1. 类由两部分组成

    • 类的属性,称之为:成员变量
    • 类的行为,称之为:成员方法

    ==注意:函数是写在类外的,定义在类内部,我们都称之为方法哦==

  2. 类和成员方法的定义语法

    class 类名称:
        成员变量
        
        def 成员方法(self,参数列表):
            成员方法体
            
            
    对象 = 类名称()
  3. self的作用

    • 表示类对象本身的意思
    • 只有通过self,成员方法才能访问类的成员变量
    • self出现在形参列表中,但是不占用参数位置,无需理会

1.1.3 类和对象


  1. 现实世界的事物由什么组成?

    • 属性
    • 行为

    类也可以包含属性和行为,所以使用类描述现实世界事物是非常合适的

  2. 类和对象的关系是什么?

    类是程序中的“设计图纸”

    对象是基于图纸生产的具体实体

  3. 什么是面向对象编程?

    面向对象编程就是,使用对象进行编程。

    即,设计类,基于类创建对象,并使用对象来完成具体的工作

1.1.4 构造方法


Python类可以使用:__init__()方法,称之为构造方法。

可以实现:

  • 在创建类对象(构造类)的时候,会自动执行。
  • 在创建类对象(构造类)的时候,将传入参数自动传递给__init__方法使用。

image-20240504222958776

注意:

  • 构造方法也是成员方法,不要忘记在参数列表中提供:==self==
  • 在构造方法内定义成员变量,需要使用==self==关键字

    image-20240504223348813

    • 这是因为:变量是定义在构造方法内部,如果要成为成员变量,需要用self来表示。

1.1.5 其它内置方法


常见的5个内置方法:

方法功能
__init__构造方法,可用于创建类对象的时候设置初始化行为
__str__用于实现类对象转字符串的行为
__lt__用于2个类对象进行小于或大于比较
__le__用于2个类对象进行小于等于或大于等于比较
__eq__用于2个类对象进行相等比较

1. __str__ 字符串方法

当类对象需要被转换为字符串之时,会输出内存地址(<__main__.Student object at 0x000001AD070F81D0>)

内存地址没有多大作用,我们可以通过__str__方法,控制类转换为字符串的行为。


语法:

def __str__(self):
    return f'Student类对象:name:{self.name},age:{self.age}'
  • 方法名:__str__
  • 返回值:字符串
  • 内容:自行定义
2. __lt__ 小于符号比较方法

直接对2个对象进行比较是不可以的,但是在类中实现__lt__方法,即可同时完成:==小于符号 和 大于符号 2种比较==

比较大于符号的魔术方法是:__gt__,不过,实现了__lt____gt__就没必要实现了


语法:

def __lt__(self, other):
    return self.age < other.age
  • 方法名:__lt__
  • 传入参数:other,另一个类对象
  • 返回值:True 或 False
  • 内容:自行定义
3. __le__ 小于等于比较符号方法

魔术方法:__le__可用于:<=、>=两种比较运算符上。

大于等于符号实现的魔术方法是:__ge__不过,实现了__le____ge__就没必要实现了


语法:

def __le__(self, other):
    return self.age <= other.age
  • 方法名:__le__
  • 传入参数:other,另一个类对象
  • 返回值:True 或 False
  • 内容:自行定义
4.__eq__ 比较运算符实现方法

不实现__eq__方法,对象之间可以比较,但是是比较内存地址,也即是:不同对象==比较一定是False结果。

实现了__eq__方法,就可以按照自己的想法来决定2个对象是否相等了。


语法:

def __eq__(self, other):
    return self.age == other.age
  • 方法名:__eq__
  • 传入参数:other,另一个类对象
  • 返回值:True 或 False
  • 内容:自行定义

1.1.6 封装


将现实世界事物在类中描述为属性和方法,即为封装。

私有成员:

在类中提供仅供内部使用的属性和方法,而不对外开放(类对象无法使用)

  1. 私用成员又分为两种:

    • 私有成员变量
    • 私有成员方法
  2. 定义语法:

    • 私有成员变量:变量名以__开头(2个下划线)

      __变量名 = None
    • 私有成员方法:方法名以__开头(2个下划线)

      def __方法名(self):
          成员方法体
  3. 私有成员的访问限制

    • 类对象无法访问私有成员
    • 类中的其它成员可以访问私有成员

1.1.7 继承


1. 继承的基础语法

继承表示将从父类那里继承(复制)来成员变量和成员方法(不含私有)

继承分为:单继承和多继承

单继承:

一个类继承另一个类

class 类名(父类名):
    类内容体

多继承:

一个类继承多个类,按照顺序从左向右依次继承

class 类名(父类名1, 父类名2, ……, 父类名n):
    类内容体

==注意:多个父类中,如果有同名的成员,那么默认以继承顺序(从左到右)为优先级。
即:先继承的保留,后继承的被覆盖==

pass关键字:

pass是占位语句,用来保证函数(方法)或类定义的完整性,表示无内容,空的意思

例:

class 类名(父类名1, 父类名2, ……, 父类名n):
    pass
2. 复写和使用父类成员

复写表示对父类的成员属性或成员方法进行重新定义

语法:

在子类中重新实现同名成员方法或成员属性即可,例:

class Phone:
    producer = "XIAOMI"
    

class MyPhone(Phone):
    producer = "HUAWEI"

在子类中调用父类成员:

方法一:

  • 使用成员变量:父类名.成员变量
  • 使用成员方法:父类名.成员方法(self)

方法二:

  • 使用成员变量:super().成员变量
  • 使用成员方法:super().成员方法()

==注意:只可以在子类内部调用父类的同名成员,子类的实体类对象调用默认是调用子类复写的==

1.1.8 类型注解


1. 变量的类型注解

Python在3.5版本的时候引入了类型注解,以方便静态类型检查工具,IDE等第三方工具。

类型注解:在代码中涉及数据交互的地方,提供数据类型的注解(显式的说明)。

主要功能:

  • 帮助第三方IDE工具(如PyCharm)对代码进行类型推断,协助做代码提示
  • 帮助开发者自身对变量进行类型注释

支持:

  • 变量的类型注解
  • 函数(方法)形参列表和返回值的类型注解

语法一:

变量: 类型

例:

# 基础数据类型注解
name: str = '张三'
# 基础容器类型注解
my_list: list = [1, 2, '好好好']
# 容器类型详细注解
my_list: list[int, str, bool] = [1, '好好好', True]

# 类对象类型注解
class Student:
    pass
stu: Student = Student()

注意:

  • 元组类型设置类型详细注解,需要将每一个元素都标记出来
  • 字典类型设置类型详细注解,需要2个类型,第一个是key第二个是value

语法二:

# type: 类型

例:

class Student:
    pass

var_1 = random.randit(1, 10)    # type: int
var_2 = json.loads(data)    # type: dict[str, int]
var_3 = func()        #type: Student

注意事项:类型注解只是提示性的,并非决定性的。数据类型和注解类型无法对应也不会导致错误

2. 函数(方法)的类型注解

分为:

  • 形参的类型注解
  • 返回值的类型注解

语法:

def 函数方法名(形参: 类型, ……, 形参: 类型) -> 返回值类型:
    方法内容体

注意:返回值类型注解的符号使用: ->

3. Union类型

Union可以定义联合类型注解

使用方式:

  • 导包:from typing import Union
  • 使用:Union[类型, ......, 类型]

例:

from typing import Union

my_list: [Union[str, int]] = [1, 2, '好好好', '呵呵呵']


def func(data: Union[int, str]) -> Union[int, str]:
    return data

1.1.9 多态


多态的概念:

多态指的是,同一个行为,使用不同的对象获得不同的状态。

如,定义函数(方法),通过类型注解声明需要父类对象,实际传入子类对象进行工作,从而获得不同的工作状态

什么是抽象类(接口):

包含抽象方法的类,称之为抽象类。抽象方法是指:没有具体实现的方法(pass)称之为抽象方法

抽象类的作用:

多用于做顶层设计(设计标准),以便子类做具体实现。

也是对子类的一种软性约束,要求子类必须复写(实现)父类的一些方法,并配合多态使用,获得不同的工作状态。

来源

Python中的抽象类
abc库实现抽象类和抽象函数

Python的内置库abc(abstract base class的简称)可以实现抽象类的实现。通过让你的类继承abc.ABC即可让这个类被声明为一个抽象类,比如我们现在来创建一个商品类Good,它用来派生具体的商品,要求如下:

  • 有一个静态变量TAX,代表税率
  • 私有变量price代表商品价格
  • 公有成员函数buy负责打印商品账单

简单实现如下:

import abc

class Good(abc.ABC):
    TAX = 0.01
    def __init__(self, price=None) -> None:
        self.__price = price
    
    # 通过装饰器申明抽象函数
    @abc.abstractmethod
    def buy(self):
        pass

如果是版本小于3.3,那么可以通过改变元类属性metaclass的方式实现:

import abc

class Good(metaclass=abc.ABCMeta):
    TAX = 0.01
    def __init__(self, price=None) -> None:
        self.__price = price
    
    @abc.abstractmethod
    def buy(self):
        pass

理想的效果是,我们实例化这个抽象类,解释器要报错:

g = Good()

out:

Traceback (most recent call last):
  File "e:\python draft\test.py", line 12, in <module>
    g = Good()
TypeError: Can't instantiate abstract class Good with abstract methods buy

需要注意,这个报错检查需要你编写的类同时满足两个条件:

  • 是抽象类ABC的子类
  • 含有抽象函数(即有成员函数被abc.abstractmethod装饰)

换句话说,在Python中,你编写的类需要满足以上两个条件,才能称得上一个抽象类。

collections.abc Python内置抽象类 实现自定义基本数据结构

熟悉Python内置库的同学可能还知道Python常用模块collection中有一个子库也叫abc,那么这个abc和之前的abc有什么区别吗?我们不妨打印一下__all__看看collection.abc中都有什么:

from collections import abc
print(abc.__all__)

out:

['Awaitable', 'Coroutine', 'AsyncIterable', 'AsyncIterator', 'AsyncGenerator', 'Hashable', 'Iterable', 'Iterator', 'Generator', 'Reversible', 'Sized', 'Container', 'Callable', 'Collection', 'Set', 'MutableSet', 'Mapping', 'MutableMapping', 'MappingView', 'KeysView', 'ItemsView', 'ValuesView', 'Sequence', 'MutableSequence', 'ByteString']

不卖关子了,collection.abc中定义的,都是基本数据类型的抽象父类。比如中间的Sequence,代表不可变序列类的抽象父类,很明显,Python中的基本数据结构tuple就是Sequence的子类,我们可以检查一下:

from collections import abc
print(issubclass(tuple, abc.Sequence))

out:

True

可以看到,tuple确实是Sequence的子类。

tuple是Sequence的子类不代表tuple是基于Sequence实现的,tuple,list这些内置数据类型是直接基于C源码中的PyObject结构体实现的,感兴趣的同学可以去看这篇文章zpoint's blog-CSDN博客_python tuple底层实现

这意味着,我们可以使用Sequence自己来实现一个tuple!那么我们可以试试看,首先,我们需要知道Sequence中有哪些抽象函数需要实现,最简单粗暴的方式就是直接实例化Sequence,所有的抽象函数都会在报错信息中展示出来:

from collections import abc
seq = abc.Sequence()

out:

Traceback (most recent call last):
  File "e:\python draft\test.py", line 3, in <module>
    seq = abc.Sequence()
TypeError: Can't instantiate abstract class Sequence with abstract methods __getitem__, __len__

可以看到,Sequence中我们只需要实现__getitem__, __len__即可。因此,当你想要实现属于自己的基本数据对象时,可以尝试使用collections.abc类预定好的抽象类作为父类来实现。


白鹅类型和鸭子类型

在Python中实现多态主要有两种机制:白鹅类型和鸭子类型。emm,请允许我解释清除:白鹅类型和鸭子类型不仅是两种机制,也是两种不同的编程风格。

我们先来举个例子来直观感受一下两者的区别:假设我们现在需要实现N个商品类,在入口函数中,我们希望调用这N个商品类实例对象的price方法来打印出对应的价格。这是非常典型的多态实现。

假设我们需要实现的类为Food, Clothes 和 Coffee。

我们先来写一下入口函数中需要执行的语句:

goods = [Food(), Clothes(), Coffee()]
for good in goods:
    good.price()
鸭子类型实现

我们希望上述代码可以打印出三种商品的价格,先来看看鸭子类型机制的实现:

class Food:
    def price(self):
        print("{} price:$4".format(__class__.__name__))

class Clothes:
    def price(self):
        print("{} price:$5".format(__class__.__name__))

class Coffee:
    def price(self):
        print("{} price:$6".format(__class__.__name__))

打印结果:

Food price:$4
Clothes price:$5
Coffee price:$6
白鹅类型实现

再来看看白鹅类型的实现:

import abc

class Good(abc.ABC):
    @abc.abstractmethod
    def price(self):
        pass

class Food(Good):
    def price(self):
        print("{} price:$4".format(__class__.__name__))

class Clothes(Good):
    def price(self):
        print("{} price:$5".format(__class__.__name__))

打印结果:

Food price:$4
Clothes price:$5
Coffee price:$6

Fine,我们可以看到白鹅类型和鸭子类型都是实现了完全相同的效果。我们接下来慢慢分析一下:可以看到,在鸭子类型的实现中,我们只需要保证调用price方法的对象身上有price方法即可,别的我们关心;而白鹅类型中,为了在一个对象列表中调用每一个对象的price方法,我们直接让所有对象的类派生自含有price虚函数的抽象类Good。

如果你学过强类型语言,譬如C++,Java,那么白鹅类型你应该丝毫不陌生,Python中的白鹅类型机制就是强类型语言中实现多态的标准模式,即通过调取父类的虚函数或者继承的函数来完成不同的行为。

但是Python是一门弱类型的语言,并不强制要求保证类型一致,因此,为了实现多态,白鹅类型和鸭子类型都只是一种手段或者项目开发时实现商量好的协议。特别是鸭子类型,由于鸭子类型不限制展现多态的对象的类型,所以鸭子类型更像是一种不受编程语言语法限制的“协议”。

比如我们在开发项目时,你现在要拿你的好朋友的模块来进行多态操作,那么如果你的朋友使用的是白鹅类型的编程风格,你只需要确保进行多态的各个对象的父类是同一个即可;但是如果你的好朋友是弱类型动态脚本大师,用了鸭子类型进行了模块编写,那么你不得不去确认后续多态涉及到的每个操作是否每个类中都有与之同名的类函数对应。

鸭子类型的历史由来

看到这个section的最后了,我们来闲聊几句吧。为什么要把弱类型索取不同类中同名函数的多态称为鸭子类型,而将索取同父类虚函数的多态称为白鹅类型呢?

img

鸭子类型(Duck Typing)源于一种名叫鸭子测试(Duck Test)的思想。鸭子测试有着非常悠久的历史,根据维基百科上的考据,最早发明这个词的人大概是美国诗人James Whitcomb Riley(1849–1916):

The American poet James Whitcomb Riley (1849–1916) may have coined the phrase when he wrote:
When I see a bird that walks like a duck and swims like a duck and quacks like a duck, I call that bird a duck.

翻译过来就是「当我看到一只像鸭子一样走路、像鸭子一样游泳、像鸭子一样叫的鸟时,我称那只鸟为鸭子。」

最早让这个词流行起来的人是美国驻危地马拉(拉丁美洲国家)大使Richard Cunningham Patterson Jr 。具体事宜大家可以去查wiki,在此我就不再赘述了。后来又在Douglas Adams的侦探小说中被提及,然后或许被某个喜欢侦探小说的程序员写入他自己喜欢的note或者blog中了吧hhh

Fine,所以说,鸭子类型是动态语言的一种特性。拿刚才商品类的实现来说,就是,我如何保证遍历goods列表中的对象都是good呢?因为good对象一定有price方法,那么我后面的程序就不大可能出错。

这个场景中,商品good就是“鸭子”,而我们列表中的商品都有price方法,也就是它们调用price的姿势很像商品(叫起来很像“鸭子”),那么这些对象就大概就是商品类(那么它大概就是一只鸭子!)

这种点对点对应的校验方式确实很适合动态语言的这种特性,因此,我们将这种只需蕴含执行操作方法实现的多态实现机制称为鸭子类型。

而白鹅类型则是相对于鸭子类型而言的,白鹅类型的对象必须有明确的接口定义。关于白鹅类型的历史,我暂时没有查到,但是从网上提及到过这个词的口吻而言,应该出自于某本Python编程教材的作者。知道的读者欢迎在评论区留言。


猴子补丁
什么是猴子补丁

Wiki上给出的解释为:

the term monkey patch only refers to dynamic modifications of a class or module at runtime, motivated by the intent to patch existing third-party code as a workaround to a bug or feature which does not act as desired

简单说,猴子补丁是动态为已经创建出的对象增加新的方法和属性成员的一种机制,也就是动态打补丁。

来举几个例子你就懂了。比如一个非常简单的例子:

class Test:
    def func1(self, x, y):
        print(x + y)

test = Test()
test.func1(1, 1)

out:

2

但是你发现环境发生了变化,我们需要修改这个func1的计算逻辑,由于我们很多时候是将Test类通过import导入的,Test的源代码对我们而言不可修改,甚至Test的源代码可能是二进制文件,我们也改不了,那么这个时候要怎么办呢?

答案很简单:直接移花接木!

实例化对象的猴子补丁

我们可以直接动态修改已经实例化出来的对象的func方法:

class Test:
    def func1(self, x, y):
        print(x + y)


test = Test()
test.func1 = lambda x, y : print(x + 2 * y)
test.func1(1, 1)

out:

3

是不是很爽?你可以在运行时直接瞎改对象成员,就好像对象就只是一个容器一样!

但是请注意,这种方式下“接上去”的函数是静态函数,这意味着我们无法直接通过猴子补丁函数来访问原本对象的内部成员变量,比如下面这个代码就会报错:

class Test:
    def __init__(self) -> None:
        self.a = 1
    def func1(self, x, y):
        print(x + y)


test = Test()
test.func1 = lambda x, y : print(x + 2 * y + self.a)
test.func1(1, 1)

out:

Traceback (most recent call last):
  File "e:\python draft\tempCodeRunnerFile.py", line 10, in <module>
    test.func1(1, 1)
  File "e:\python draft\tempCodeRunnerFile.py", line 9, in <lambda>
    test.func1 = lambda x, y : print(x + 2 * y + self.a)
NameError: name 'self' is not defined

我们可以这么改:

class Test:
    def __init__(self) -> None:
        self.a = 1
    def func1(self, x, y):
        print(x + y)


test = Test()
test.func1 = lambda self, x, y : print(x + 2 * y + self.a)
test.func1(test, 1, 1)

out:

4

但是说实话,这么改太不优雅了,而且函数接口都变了,如果我们只想改逻辑,不想改接口,这个方法就行不通,还有别的方法吗?

答案也很简单:直接改类!

类的猴子补丁

Python的猴子补丁的神奇的地方在于你甚至可以直接改类,因为Python中的类也是一个对象,读者可以简单理解为Python中的所有定义出来的class都是元类(meta class)的实例化对象。既然是对象,那当然可以打猴子补丁~~~

不过类的函数补丁一定要带指向实例化对象的self参数:

class Test:
    def __init__(self) -> None:
        self.a = 1
    def func1(self, x, y):
        print(x + y)


Test.func1 = lambda self, x, y : print(x + 2 * y)

test = Test()
test.func1(1, 1)

out:

3

你当然也可以优雅访问内部成员:

class Test:
    def __init__(self) -> None:
        self.a = 1
    def func1(self, x, y):
        print(x + y)


Test.func1 = lambda self, x, y : print(x + 2 * y + self.a)

test = Test()
test.func1(1, 1)

out:

4

是不是很爽?

除了给对象和类打补丁,你还可以给导入进__main__的模块,文件,动态链接库打补丁,做法都差不多,在此就不赘述了。

增加函数

既然猴子补丁可以修改函数,那自然也能增加函数:

class Test:
    def func1(self, x , y):
        print(x + y)
    
Test.func1 = lambda self, p, q: print(p + 2 * q)   # 修改函数
Test.func2 = lambda self, p, q: print(p + 3 * q)   # 增加函数
test = Test()
test.func1(1, 2)
test.func2(1, 3)

out:

5
10

同理,属性什么的也是可以随意修改和添加的。

补丁应用

gevent就是大规模使用猴子补丁的一个最好的例子,这个库能够在不改变第三方库源代码的情况下将大部分阻塞式库实现协程式运作,其中就是通过猴子补丁实现的,感兴趣的同学可以移步如下链接:

python并发编程gevent模块以及猴子补丁学习

禁用猴子补丁

虽然猴子补丁很好用,但是在实际开发中未免存在诸多的不安全,所以如何禁用猴子补丁呢?

这个需要具体情况具体分析,使用猴子补丁时,对象/类本质是在调用什么函数?

情况一:对象的猴子补丁

当我们对一个对象使用猴子补丁时,本质上就是在调用这个对象的类的setattr方法,所以以下两句话等价:

# 语句1
test.func1 = lambda x, y : print(x + 2 * y)

# 语句2
setattr(test, "func1", lambda x, y : print(x + 2 * y))

既然知道了是在调用setattr方法,那么我们直接复写对象的__setattr__即可禁用猴子补丁,锦恢简单做个示范:

class Test:
    def func1(self, x, y):
        print(x + y)

    def __setattr__(self, __name, __value) -> None:
        if __name in self.__dir__() and callable(getattr(self, __name)):
            print("Test的类函数不可在实例化中被覆盖!")
        else:
            super().__setattr__(__name, __value)


test = Test()
# 尝试使用猴子补丁,预想是失败的,但是你仍然可以创建新的函数并挂载在这个对象上,具体的逻辑你就可以自己定义啦!
test.func1 = lambda x, y : print(x + 2 * y)

out:

Test的类函数不可在实例化中被覆盖!

情况二:类的猴子补丁

类的猴子补丁禁用会麻烦一点,我们都知道Python中的class也是一个对象cls,那么我们只需要重写cls对象的__setattr__方法即可,那么问题来了——整个类编写的哪一个环节可以让我们操作cls对象呢?这个时候,对Python类创建生命周期熟悉的同学就忍不住蹦出来说了——在__new__方法中可以碰到cls!但是很抱歉,这不对,因为我们之前操作Test时,test实例还没被创建出来,__new__方法根本不会被调用到。

那么怎么做呢?给你们三秒时间思考 :D

答案就是,修改Test的元类,为了完成对类本身的更加抽象的操作,我们必须这么做。

对元类编程不熟悉的同学可以参考小明大佬写的文章,当然,后续我也会写一篇文章来讲讲元类编程

代码如下:

class MetaTest(type):
    def __setattr__(cls, __name, __value) -> None:
        if __name in dir(cls) and callable(getattr(cls, __name)):
            print("Test 元类函数不可覆盖!")
        else:
            super().__setattr__(__name, __value)

class Test(metaclass=MetaTest):
    def func1(self, x, y):
        print(x + y)

    
    def __setattr__(self, __name, __value) -> None:
        if __name in self.__dir__() and callable(getattr(self, __name)):
            print("Test的类函数不可在实例化中被覆盖!")
        else:
            super().__setattr__(__name, __value)

Test.func1 = lambda self, x, y : print(x + y)

out:

Test 元类函数不可覆盖!

但是你还是可以附加新的函数到类上的 :D

猴子补丁的说法来源

Wiki上给出的来源解释如下:

The term monkey patch seems to have come from an earlier term, guerrilla patch, which referred to changing code sneakily – and possibly incompatibly with other such patches – at runtime.The word guerrilla, homophonous with gorilla (or nearly so), became monkey, possibly to make the patch sound less intimidating. An alternative etymology is that it refers to “monkeying about”with the code (messing with it).

很有意思,用锦恢的奇妙比喻来说就是,有些程序员编码习经常更换代码、接口、工具,就像补给不充足的游击队一样,经常更换人员,武器和作战地点。所以这种频繁更换接口的坏习惯下造成的软件更新被人们称为游击队补丁,在英文中游击队(guerrilla,英式发音[ɡəˈrɪlə])和猩猩(gorilla,英式发音[ɡəˈrɪlə])发音完全相同,人们就玩梗,把“游击队补丁”戏称为“猩猩补丁”,后来又转化为更加amusing的“猴子补丁”。