0%

Python基础

Python 基础

1. Pyhton 简述

Pyhton 官网

1.1 IDLE

在 IDLE 的交互模式下,你给它一个指令,它立刻会还你一个反馈:
IDLE

打开 IDLE 的编辑器模式
打开 IDLE,在菜单栏中依次点击 File->New File,或者直接使用快捷键 Ctrl+N:
IDLE编辑器模式

1.2 BIF (Built-in Function)

Python 提供了很多内置函数,以对付各种不同的需求。

在 IDLE 的交互模式下,输入 dir(builtins),可以看到它们:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
dir(__builtins__)

['ArithmeticError', 'AssertionError', 'AttributeError', 'BaseException', 'BlockingIOError', 'BrokenPipeError', 'BufferError',
'BytesWarning', 'ChildProcessError', 'ConnectionAbortedError', 'ConnectionError', 'ConnectionRefusedError', 'ConnectionResetError', 'DeprecationWarning',
'EOFError', 'Ellipsis', 'EnvironmentError', 'Exception', 'False', 'FileExistsError', 'FileNotFoundError',
'FloatingPointError', 'FutureWarning', 'GeneratorExit', 'IOError', 'ImportError', 'ImportWarning', 'IndentationError', 'IndexError',
'InterruptedError', 'IsADirectoryError', 'KeyError', 'KeyboardInterrupt', 'LookupError', 'MemoryError', 'ModuleNotFoundError',
'NameError', 'None', 'NotADirectoryError', 'NotImplemented', 'NotImplementedError', 'OSError', 'OverflowError', 'PendingDeprecationWarning',
'PermissionError', 'ProcessLookupError', 'RecursionError', 'ReferenceError', 'ResourceWarning', 'RuntimeError', 'RuntimeWarning', 'StopAsyncIteration', 'StopIteration',
'SyntaxError', 'SyntaxWarning', 'SystemError', 'SystemExit', 'TabError', 'TimeoutError', 'True', 'TypeError', 'UnboundLocalError', 'UnicodeDecodeError',
'UnicodeEncodeError', 'UnicodeError', 'UnicodeTranslateError', 'UnicodeWarning', 'UserWarning', 'ValueError', 'Warning',
'WindowsError', 'ZeroDivisionError', '__build_class__', '__debug__', '__doc__', '__import__', '__loader__', '__name__', '__package__', '__spec__',
'abs', 'all', 'any', 'ascii', 'bin', 'bool', 'breakpoint', 'bytearray', 'bytes', 'callable', 'chr',
'classmethod', 'compile', 'complex', 'copyright', 'credits', 'delattr', 'dict', 'dir', 'divmod', 'enumerate', 'eval', 'exec', 'exit', 'filter', 'float', 'format', 'frozenset',
'getattr', 'globals', 'hasattr', 'hash', 'help', 'hex', 'id', 'input',
'int', 'isinstance', 'issubclass', 'iter', 'len', 'license', 'list', 'locals', 'map', 'max', 'memoryview', 'min', 'next', 'object', 'oct', 'open', 'ord', 'pow', 'print', 'property',
'quit', 'range', 'repr', 'reversed', 'round', 'set', 'setattr', 'slice', 'sorted', 'staticmethod', 'str', 'sum', 'super', 'tuple', 'type', 'vars', 'zip']

1.3 查看 Python 官方文档

打开 IDLE,依次点击右上角的 “Help” -> “Python Docs”(或者直接按下快捷键F1):
查看帮助文档

这就弹出了一个叫 Python Documentation 的帮助文档,点击左上角的“索引”,然后输入想要查询的关键字:
文档界面

或者直接在交互界面输出 help(obj) 查询:
help(obj)

2.变量与字符串

2.1.1 变量

在 Python 中,变量就是一个名字。

变量就是一个名字,一个标签,通过这个变量,你就能找到对应的数据。

2.1.2 创建一个变量

Python 的变量无需声明,只需要一次赋值,该变量就能够被成功创建:
>>> x = 8
这样我们就创建了一个变量,它的名字叫做x,它的值是8。
那么这个等于号(=),表示的是一个赋值操作,也就是将右边的数值8跟变量名x进行挂钩的意思。

2.1.3 访问一个变量

当一个变量被创建之后,使用变量名就可以直接访问该变量了:

1
2
>>> print(x)
3

2.1.4 变量名

变量名呢,通常是由字母、数字和下划线(_)构成,但千万不能以数字打头,比如 fuckyou567 是合法的变量名,而 789bitch 却是非法的。
变量名
另外,变量名是区分大小写的,也就是 FucK、fuck 在 Python 看来,是两个完全不同的名字。

Python3 还支持中文字符作为变量名,是的:

1
2
3
唐僧 = 81
print(唐僧)
81

2.2.1 字符串(Double quotes)

Double quotes 就是使用一对双引号将文本包含起来:

1
2
>>> print("I love Pyhton")
I love Python

混合使用 Single quotes 和 Double quotes 的技巧:
1
2
3
4
>>> print("Let's go!")
Let's go!
>>> print('"Life is short, you need Python."')
"Life is short, you need Python."

2.2.2 转义字符

转义字符

2.2.3 原始字符串

使用原始字符串,可以避免反斜杠(\)被当作转义字符解析:

1
2
3
4
5
6
7
>>> 未使用原始字符串
>>> print("D:\three\two\one\now")
D: hree wo\one
ow
>>> # 使用原始字符串
>>> print(r"D:\three\two\one\now")
D:\three\two\one\now

多次换行可以在换行符 (\n) 再加一个反斜杠 (\n\)
多次换行

2.2.4 长字符串(Triple quotes)

通常,使用三引号(单引号,双引号都可以——首尾呼应)字符串来引用多行文本:

1
2
3
4
>>> demo = """
知世故而不世故,
弥天真而芬芳。
"""

2.2.5 字符串加法和乘法

字符串相加我们叫做拼接,就是将字符串组合成一个长的新的字符串:

1
2
>>> '365' + '258'
'365258'

还可以使用乘法符号(*)进行复制。
比如被老师罚写名字三百遍:
字符串乘法

3. 运算符

3.1 赋值运算符

单独一个等于号(=)表示赋值运算符,作用是将右边的值跟左边的变量名进行挂钩。

3.2 将字符串转换为整数

使用 int() 函数将指定的值转换成整数。

但要注意,并不是所有的字符串都能够转换为整数,比如 int(“Fuck”) 是无法转换的

3.3 比较运算符

运算符

3.4 is 同一性运算符

is 运算符也称之为同一性运算符。
它是用于检验两个变量,是否指向同一个对象(内存)的运算符

1
2
3
x = 'mortal'
y = 'mortal'
x is y

4. 数字类型

Python 有三种不同的数字类型,分别是:整数、浮点数和复数。

4.1.1 整数 (integer)

Python 的整数长度是不受限制的,也就是说它是有无限大的精度。
所以,你可以随时随地的进行大数运算:

1
2
>>> 1568743148/115487896
13.583615273413589

4.1.2 浮点数

我们通常数学意义上的小数在编程里叫浮点数。
由于浮点数在计算机中的存储是存在 “误差” 的,所以有时候可能会闹出一些 “BUG”:

1
2
>>> 0.1 + 0.2
0.30000000000000004

由于浮点数并不是 100% 精确的,所以我们拿浮点数来做比较就要特别小心了:

1
2
>>> 0.3 == 0.1 + 0.2
False

可以借助decimal模块来进行十进制的运算
1
2
3
4
5
6
7
8
>>> import decimal
>>> a = decimal.Decimal("0.1")
>>> b = decimal.Decimal("0.2")
>>> print(a + b)
0.3
c = decimal.Decimal('0.3')
a + b == c
True

4.1.3 E记法

E 记法也就是平时我们所说的科学计数法,用于表示一些比较极端的数。

1
2
3
>>> x = 0.00005
>>> x
5e-05

4.1.4 复数

复数包含了一个实部和一个虚部:

1
2
>>> 1 + 2j
(1+2j)

它们都是以浮点数的形式存放的,如果将一个复数赋值给一个变量 x,则可以通过 x.real 访问该复数的实部,x.imag 访问其虚部:
1
2
3
4
5
>>> x = 1 + 2j
>>> x.real
1.0
>>> x.imag
2.0

4.2 数字运算

Python 支持的数字运算如下:
数字运算

4.2.1 四则运算

1
2
3
4
5
6
7
8
>>> 1 + 2
3
>>> 1 - 2
-1
>>> 1 * 2
2
>>> 1 / 2
0.5

4.2.2 地板除

双斜杠(//)表示一种特殊的除法 —— 地板除。
地板除原理是取比目标结果小的最大整数

1
2
3
4
>>> 3 // 2
1
>>> -3 // 2
-2

4.2.3 取余

百分号(%)用于求两数相除的余数,如果能够整除,则余数为 0:

1
2
3
4
>>> 3 % 2
1
>>> 6 % 2
0

4.2.4 被除数

地板除的结果乘以除数 + 余数 = 被除数:
x == (x // y) * y + (x % y)

4.2.5 divmod() 函数

Python 有个内置函数叫 divmod(),它的作用就是同时求出两参数地板除的结果和余数:

1
2
3
4
>>> divmod(3, 2)
(1, 1)
>>> divmod(-3, 2)
(-2, 1)

4.2.6 abs()函数

abs() 函数的作用是返回指定数值的绝对值:

1
2
3
4
5
6
>>> x = -520
>>> abs(x)
520
>>> y = -3.14
>>> abs(y)
3.14

如果传入的是一个复数,abs() 函数返回的结果就是复数的模
1
2
3
>>> z = 1 + 2j
>>> abs(z)
2.23606797749979

4.2.7 int() float() 和 complex() 函数

int() 函数是将指定的值转换成整数,比如我们传入一个字符串 ‘250’,那么得到结果就是一个整数 250:

1
2
>>> int('250')
250

不过如果参数是一个浮点数,那么就要注意了,因为它得到的将是一个截掉小数的整数:
1
2
3
4
>>> int(3.14)
3
>>> int(9.99)
9

  • 注意:它是直接截取整数部分,扔掉小数部分,而不是四舍五入。
    同样的道理,float() 和 complex() 函数是将指定的值转换成浮点数和复数

4.2.8 pow() 函数和幂运算符(**)

通常情况下,pow() 函数和幂运算符(**)这两个实现的效果是等价的:

1
2
3
4
5
6
7
8
>>> pow(2, 3)
8
>>> 2 ** 3
8
>>> pow(2, -3)
0.125
>>> 2 ** -3
0.125

不过,pow() 函数还留有一手,它支持第 3 个参数。
如果传入第 3 个参数,那么会将幂运算的结果和第 3 个参数进行取余数运算:
1
2
>>> pow(2, 3, 3)
2

相当于:
1
2
>>> 2 ** 3 % 3
2

5. 布尔类型

布尔类型的值只有两个:True 或者 False,也就是 “真” 或者 “假”。

5.1 bool() 函数

使用 bool() 函数可以直接给出 True 或者 False 的结果:

1
2
3
4
5
6
>>> bool(250)
True
>>> bool("假")
True
>>> bool("False")
True

5.2 真真假假

结果是 True 的情况非常多,但 False 却是屈指可数,下面这些几乎就是结果为 False 的所有情况:

  • 定义为False的对象:None 和 False
  • 值为 0 的数字类型:0, 0.0, 0j, Decimal(0), Fraction(0, 1)
  • 空的序列和集合:’’, (), [], {}, set(), range(0)

5.3 真值检测

Python 中任何对象都能直接进行真值检测(测试该对象的布尔类型值为 True 或者 False),用于 if 或者 while 语句的条件判断,也可以做为布尔逻辑运算符的操作数。

5.3 逻辑运算符

Python 总共有三个逻辑运算符:and、or 和 not。
逻辑运算符

对于 and 和 or 运算符,它的计算结果不一定是 True 或者 False。

这要看它的操作数是什么了,如果你给到操作数的是两个数值,那么它的运算结果也是数值:

1
2
3
4
>>> 3 and 4
4
>>> 4 or 5
4

如果你给到操作数的是两个字符串,那么它的结果也是字符串:
1
2
>>> "Mortal" and "LOVE"
'LOVE'

如歌你给到操作数是字符串和数值, and 时为数值, or 时为字符串
1
2
3
4
5
>>>"Mortal" and 250
250

>>> "Mortal" or 350
'Mortal'

5.4 短路逻辑

and 和 or 这两个运算符都是遵从短路逻辑的。
短路逻辑的核心思想就是:从左往右,只有当第一个操作数的值无法确定逻辑运算的结果时,才对第二个操作数进行求值。

  • and 同为真时 返回 True
  • or 两边只要有一个是 True 那么结果就是 True.
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    >>> 5 and 6         # 5 为True不能确定结果,继续执行 6 最终返回结果6
    6

    >>> 5 or 6 # 5 为 Ture 可以确定结果,直接返回 5
    5

    >>> 0 and 5 # 0 为 False 可以确定结果,直接返回 0
    0

    >>> 0 or 6 # 0 为 False 无法确定结果,继续执行 6 ,6 为True 返回结果 6
    6

6. 运算符优先级

这个表格从低到高()列出了 Python 的运算符优先级:

优先级 运算符 描述
1 lambda Lambda表达式
2 if - else 条件表达式
3 or 布尔“或”
4 and 布尔“与”
5 not x 布尔“非”
6 in, not in, is, is not, <, <=,>, >=, !=, == 成员测试,同一性测试,比较
7 ` ` 按位或
8 ^ 按位异或
9 & 按位与
10 <<, >> 移位
11 +, - 加法,减法
12 *, @, /, //, % 乘法,矩阵乘法,除法,地板除,取余数
13 +x,-x, ~x 正号,负号,按位翻转
14 ** 指数
15 await x Await表达式
16 x[index], x[index:index],x(arguments…), x.attribute 下标,切片,函数调用,属性引用
17 (expressions…), [expressions…],{key: value…}, {expressions…} 绑定或元组显示,列表显示,字典显示,集合显示

7. 分支和循环 (branch and loop)

7.1 分支结构

Python 的分支结构由 if 语句来操刀实现。
if 语句总共有 5 钟语法结构,其中前 4 种是比较常见的。

第 1 种是判断一个条件,如果这个条件成立,就执行其包含的某条语句或某个代码块。
语法结构如下:

1
2
if 条件:
某条语句或某个代码块

第 2 种同样是判断一个条件,跟第 1 种的区别是如果条件不成立,则执行另外的某条语句或某个代码块。
语法结构如下:

1
2
3
4
if 条件:
某条语句或某个代码块
else:
某条语句或某个代码块

第 3 种是判断多个条件,如果第 1 个条件不成立,则继续判断第 2 个条件,如果第 2 个条件还不成立,则接着判断第 3 个条件……
如果还有第 4、5、6、7、8、9 个条件,你还可以继续写下去。
语法结构如下:

1
2
3
4
5
6
if 第1个条件:
某条语句或某个代码块
elif 第2个条件:
某条语句或某个代码块
elif 第3个条件:
某条语句或某个代码块

第 4 种是在第 3 种的情况下添加一个 else,表示上面所有的条件均不成立的情况下,执行某条语句或某个代码块。
语法结构如下:

1
2
3
4
5
6
7
8
if 第1个条件:
某条语句或某个代码块
elif 第2个条件:
某条语句或某个代码块
elif 第3个条件:
某条语句或某个代码块
else:
某条语句或某个代码块

第 5 种其实是一个条件表达式,相当于将一个完整的 if-else 结构整合成一个表达式来使用。
语法结构如下:

1
条件成立时执行的语句 if 条件 else 条件不成立时执行的语句

它把条件放正中间,然后左右紧挨着关键字 if 和 else,最左侧是条件成立时执行的语句,最右侧是条件不成立时执行的语句。
示例如下:
1
2
3
4
5
>>> a = 3
>>> b = 5
>>> small = a if a < b else b
>>> print(small)
3

7.2 分支结构的嵌套(nested branches)

所谓嵌套,就是跟俄罗斯套娃一样,一层套一层。

1
2
3
4
5
6
7
8
9
10
11
>>> age = 18
>>> isGamer = True
>>> if age < 18:
... print("抱歉,本游戏不适合未成年。")
... else:
... if isGamer:
... print("游戏愉快!")
... else:
... print("抱歉,本游戏不适合非专业选手哦~")

游戏愉快!

7.3 循环结构

分支结构能让你的程序根据条件去做不同的事情,而循环机构能让你的程序去不断做同一件事情,这就是所谓的道不同而一样很牛逼啦!

Python 有两种循环语句:while 循环和 for 循环。

7.3.1 while 循环

它的语法结构结构如下:

1
2
while 条件:
某条语句或某个代码块

只要条件一直成立,那么其包含的某条语句或某个代码块就会一直被执行。

7.3.2 死循环

如果条件一直成立,那么循环体就一直被执行。

1
2
>>> while True:
... print("只要条件成立,我就循环到你死机!")

像这种倔强的循环,我们给他起了一个不大好听的名字:死循环。
所谓的死循环,就是打死也不会结束的循环。

7.3.3 break 语句

在循环体内,一旦遇到 break 语句,Python 二话不说马上就会跳出循环体,即便这时候循环体内还有待执行的语句。

7.3.4 continue 语句

实现跳出循环体还有另外一个语句,那就是 continue 语句。
continue 语句也会跳出循环体,但是,它只是跳出本一轮循环,它还会回到循环体的条件判断位置,然后继续下一轮循环(如果条件还满足的话)。

注意它和 break 语句两者的区别:

  • continue 语句是跳出本次循环,回到循环的开头
  • break 语句则是直接跳出循环体,继续执行后面的语句
    countine

7.3.5 else 语句

当循环的条件不再为真的时候,便执行 else 语句的内容。

1
2
3
4
5
6
7
8
9
10
11
>>> i = 1
>>> while i < 3:
... print("循环内,i 的值是", i)
... i += 1
... else:
... print("循环外, i 的值是", i)
...
...
循环内,i 的值是 1
循环内,i 的值是 2
循环外, i 的值是 3

小技巧

while-else 可以非常容易地检测到循环的退出情况。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
>>> this = 1
>>> while this <= 5:
... answer = input("是否this?")
... if answer != "是":
... break
... this += 1
... else:
... print("好的,你已经坚持了5次")
...
...
是否this?是
是否this?是
是否this?是
是否this?否

7.3.6 嵌套

循环也也可以嵌套,而且更简洁!

有时候,我们的需求可能要用到不止一层循环来实现。

比如我们要实现打印一个九九乘法表,就可以这么实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
>>> i = 1
>>> while i <= 9:
... j = 1
... while j <= i:
... print(j, "*", i, "=", j * i, end=" ")
... j += 1
... print()
... i += 1
...
1 * 1 = 1
1 * 2 = 2 2 * 2 = 4
1 * 3 = 3 2 * 3 = 6 3 * 3 = 9
1 * 4 = 4 2 * 4 = 8 3 * 4 = 12 4 * 4 = 16
1 * 5 = 5 2 * 5 = 10 3 * 5 = 15 4 * 5 = 20 5 * 5 = 25
1 * 6 = 6 2 * 6 = 12 3 * 6 = 18 4 * 6 = 24 5 * 6 = 30 6 * 6 = 36
1 * 7 = 7 2 * 7 = 14 3 * 7 = 21 4 * 7 = 28 5 * 7 = 35 6 * 7 = 42 7 * 7 = 49
1 * 8 = 8 2 * 8 = 16 3 * 8 = 24 4 * 8 = 32 5 * 8 = 40 6 * 8 = 48 7 * 8 = 56 8 * 8 = 64
1 * 9 = 9 2 * 9 = 18 3 * 9 = 27 4 * 9 = 36 5 * 9 = 45 6 * 9 = 54 7 * 9 = 63 8 * 9 = 72 9 * 9 = 81
  • 注意: 对于嵌套循环来说,无论是 break 语句还是 continue 语句,它们只能作用于一层循环体。

7.3.7 for 循环

语法结构如下:

1
2
for 变量 in 可迭代对象:
某条语句或某个代码块

什么是可迭代对象?
所谓可迭代对象,就是指那些元素能够被单独提取出来的对象。比如我们学过的字符串,它就是一个可迭代对象。

什么叫迭代呢?
比如说让你每一次从字符串 “Mortal” 里面拿一个字符出来,那么你依次会拿出 ‘M’、’o’、’r’、’t’、’a’、’l’ 六个字符,这个过程我们称之为迭代。

7.3.8 range()

range() 会帮你生成一个数字序列,它的用法有以下三种:

  • range(stop) - 将生成一个从 0 开始,到 stop(不包含)的整数数列
  • range(start, stop) - 将生成一个从 start 开始,到 stop(不包含)的整数数列
  • range(start, stop, step) - 将生成一个从 start 开始,到 stop(不包含)结束,步进跨度为 step 的整数数列

注意:无论你使用哪一种,它的参数都只能是整数。

7.3.9 for 循环和 while 循环的共通性

for 循环和 while 循环一样,都是可以支持嵌套的,同样它也可以搭配 break 和 continue 语句。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
>>> for n in range(2, 10):
... for x in range(2, n):
... if n % x == 0:
... print(n, "=", x, "*", n // x)
... break
... else:
... print(n, "是一个素数")
...
2 是一个素数
3 是一个素数
4 = 2 * 2
5 是一个素数
6 = 2 * 3
7 是一个素数
8 = 2 * 4
9 = 3 * 3

8. 列表

8.1 创建列表

创建一个列表非常简单,我们只需要使用中括号,将所有准备放入列表中的元素给包裹起来,不同元素之间使用逗号分隔:

1
2
3
>>> rhyme = [1, 2, 3, 4, 5, "上山打老虎"]
>>> print(rhyme)
[1, 2, 3, 4, 5, '上山打老虎']

8.2 访问列表中的元素

如果希望按顺序访问列表的每一个元素,可以使用 for 循环语句:

1
2
3
4
5
6
7
8
9
>>> for each in rhyme:
... print(each)
...
1
2
3
4
5
上山打老虎

如果希望随机访问其中一个元素,那么可以使用下标索引的方法:
1
2
3
4
5
6
>>> rhyme[0]
1
>>> rhyme[2]
3
>>> rhyme[5]
'上山打老虎'

8.3 下标索引

序列类型的数据都可以使用下标索引的方法,第一个元素的下标是 0,第二个的下标是 1,以此类推:
下表索引
Python 还支持你 “倒着” 进行索引:
下表索引2

8.4 列表切片

将原先的单个索引值改成一个范围即可实现切片:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
>>> rhyme[0:3]
[1, 2, 3]
>>> rhyme[3:6]
[4, 5, '上山打老虎']
>>> rhyme[:3]
[1, 2, 3]
>>> rhyme[3:]
[4, 5, '上山打老虎']
>>> rhyme[:]
[1, 2, 3, 4, 5, '上山打老虎']
>>> rhyme[0:6:2]
[1, 3, 5]
>>> rhyme[::2]
[1, 3, 5]
>>> rhyme[::-2]
['上山打老虎', 4, 2]
>>> rhyme[::-1]
['上山打老虎', 5, 4, 3, 2, 1]

切片是一个非常棒的技能

8.5 列表的增删改查

8.5.1 增(像列表添加数据)

向列表添加元素可以使用 append() 方法,它的功能是在列表的末尾添加一个指定的元素。

1
2
3
>>> food.append("egg")
>>> food
['milk', 'bread', 'egg']

append() 方法虽好,不过每次它只能添加一个元素到列表中,而 extend() 方法则允许一次性添加多个元素:

1
2
3
>>> food.extend(["cake", "rice", "vagetables"])
>>> food
['milk', 'bread', 'egg', 'cake', 'rice', 'vagetables']

  • 注意:extend() 方法的参数必须是一个可迭代对象,然后新的内容是追加到原列表最后一个元素的后面。

使用万能的切片语法,也可以实现列表元素的添加:

1
2
3
4
5
6
7
8
9
>>> s = [1, 2, 3, 4, 5]
>>> # 下面的做法等同于 s.append(6)
>>> s[len(s):] = [6]
>>> s
[1, 2, 3, 4, 5, 6]
>>> # 下面的做法等同于 s.extend([7, 8, 9])
>>> s[len(s):] = [7, 8, 9]
>>> s
[1, 2, 3, 4, 5, 6, 7, 8, 9]

insert() 方法允许你在列表的任意位置添加数据。
insert() 方法有两个参数,第一个参数指定的是插入的位置,第二个参数指定的是插入的元素:

1
2
3
4
>>> s = [1, 3, 4, 5]
>>> s.insert(1, 2)
>>> s
[1, 2, 3, 4, 5]

8.5.2 删(删除列表中的数据)

利用 remove() 方法,可以将列表中指定的元素删除:

1
2
3
>>> food.remove("vagetables")
>>> food
['milk', 'bread', 'egg', 'cake', 'rice']

有两点要注意:

  • 如果列表中存在多个匹配的元素,那么它只会删除第一个
  • remove() 方法要求你指定一个待删除的元素,如果指定的元素压根儿不存在,那么程序就会报错

有时候我们可能需要删除某个指定位置上的元素,那么可以使用 pop() 方法,它的参数就是元素的下标索引值:

1
2
3
4
>>> food.pop(0)
'milk'
>>> food
['bread', 'egg', 'cake', 'rice']

pop() 方法这个参数其实是可选的,如果你没有指定一个参数,那么它“弹”出来的就是最后一个元素:
1
2
3
4
>>> food.pop()
'rice'
>>> food
['bread', 'egg', 'cake']

如果想要一步到位清空列表,可以使用 clear() 方法:

1
2
3
>>> food.clear()
>>> food
[]

8.5.3 改(修改列表中的元素)

列表跟字符串最大区别就是:列表是可变的,而字符串是不可变的。
替换列表中的元素跟访问元素类似,都是使用下标索引的方法,然后使用赋值运算符就可以将新的值给替换进去了:

1
2
3
4
>>> fruit = ['apple', 'apricot', 'cherries', 'coconut', 'mango', 'peach']
>>> fruit[3] = 'lychees'
>>> fruit
['apple', 'apricot', 'cherries', 'lychees', 'mango', 'peach']

如果有连续的多个元素需要替换,可以利用切片来实现:

1
2
3
>>> fruit[3:] = ['blackberries', 'cranberries', 'blueberries']
>>> fruit
['apple', 'apricot', 'cherries', 'blackberries', 'cranberries', 'blueberries']

排序与翻转

1
2
3
4
5
6
7
8
>>> nums = [1, 4, 6, 9, 8, 2, 4, 7]
>>> nums.sort()
>>> nums
[1, 2, 4, 4, 6, 7, 8, 9]
>>> #如果想倒叙怎么做呢?
>>> nums.reverse() # reverse 是将数组翻转
>>> nums
[9, 8, 7, 6, 4, 4, 2, 1]

sort() 方法还可以实现排序后翻转(即从大到小的排序):

1
2
3
4
>>> nums = [1, 4, 6, 9, 8, 2, 4, 7]
>>> nums.sort(reverse=True)
>>> nums
[9, 8, 7, 6, 4, 4, 2, 1]

  • 这里可以参考 sort 的用法

8.5.4 查(定位列表中的元素)

如果我们想知道 nums 这个列表里面到底有多少个 4,可以使用 count() 方法:

1
2
3
4
5
>>> nums
[9, 8, 7, 6, 4, 4, 2, 1]

>>> nums.count(4)
2

如果我们要查找 fruit 列表中,”cherries”这个元素的索引值,可以使用 index() 方法:

1
2
3
4
5
>>> fruit
['apple', 'apricot', 'cherries', 'blackberries', 'cranberries', 'blueberries']

>>> fruit.index('cherries')
2

index() 还可以直接更换列表中的元素

1
2
3
>>> fruit[fruit.index('cherries')] = 'melon'
>>> fruit
['apple', 'apricot', 'melon', 'blackberries', 'cranberries', 'blueberries']

相当于 fruit[ 4 ] = ‘melon’

index() 方法有两个可选的参数 —— start 和 end,index(x, start, end) 就是指定查找的开始和结束的下标位置:

1
2
3
4
>>> nums = [1, 4, 6, 9, 8, 2, 4, 7]

>>> nums.index(4, 2, 7)
6

列表还有一个方法叫 copy(),用于拷贝一个列表:

1
2
3
>>> nums_copy1 = nums.copy()
>>> nums_copy1
[1, 4, 6, 9, 8, 2, 4, 7]

我们也可以使用切片的语法来实现列表拷贝:

1
2
3
>>> nums_copy2 = nums[:]
>>> nums_copy2
[1, 4, 6, 9, 8, 2, 4, 7]

上面这两种拷贝方法实现的效果是等同的。
这两种拷贝的方法,在 Python 中都称为浅拷贝。

8.6 列表的加法和乘法

列表的加法,其实也是拼接,所以要求加号(+)两边都应该是列表,举个例子:

1
2
3
4
5
>>> s = [4, 5, 6]
>>> t = [1, 2, 3]
>>> s + t
[4, 5, 6, 1, 2, 3]
>>> matix = [[1, 2, 3],

列表的乘法,则是重复列表内部的所有元素若干次:

1
2
>>> s * 3
[1, 2, 3, 1, 2, 3, 1, 2, 3]

8.7 嵌套列表

Python 是允许列表进行嵌套的
>>> matrix = [[1, 2, 3], [4, 5, 6], [7, 8, 9]]

可以把创建二维列表的语句这么写:

1
2
3
>>> matrix = [[1, 2, 3],
[4, 5, 6],
[7, 8, 9]]

这两种写法是等价的,只是后者在理解上更为直观。

8.8 访问嵌套列表

访问嵌套列表中的元素,可以使用嵌套的 for 语句来实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
>>> for i in matrix:
... for each in i:
... print(each)
...
1
2
3
4
5
6
7
8
9

通过下标同样可以访问嵌套列表:

1
2
3
4
5
6
7
8
9
10
11
12
>>> matrix[0]
[1, 2, 3]
>>> matrix[1]
[4, 5, 6]
>>> matrix[2]
[7, 8, 9]
>>> matrix[0][0]
1
>>> matrix[1][1]
5
>>> matrix[2][2]
9

8.9 通过 for 语句来创建并初始化二维列表

1
2
3
4
5
6
>>> A = [0] * 3
>>> for i in range(3):
... A[i] = [0] * 3
...
>>> A
[[0, 0, 0], [0, 0, 0], [0, 0, 0]]

8.10 浅拷贝和深拷贝

浅拷贝:利用列表的 copy() 方法或者切片来实现
深拷贝:利用 copy 模块的 deepcopy() 函数来实现
浅拷贝可以用于处理一维列表,对于嵌套列表的拷贝,只能拷贝第一层数据,其余仅拷贝其引用:

1
2
3
4
5
6
7
8
9
10
11
>>> x = [[1, 2, 3], [4, 5, 6], [7, 8, 9]]
>>> y = x.copy() # 列表的 copy 方法
>>> x[1][1] = 0
>>> x
[[1, 2, 3], [4, 0, 6], [7, 8, 9]]
>>> y
[[1, 2, 3], [4, 0, 6], [7, 8, 9]]

>>> import copy
>>> x = [[1, 2, 3], [4, 5, 6], [7, 8, 9]]
>>> y = copy.copy(x) # copy 模块的copy函数 列表、字符串、元组 都可以拷贝

浅拷贝

深拷贝可以用于处理多维列表:

1
2
3
4
5
6
7
>>> x = [[1, 2, 3], [4, 5, 6], [7, 8, 9]]
>>> y = copy.deepcopy(x)
>>> x[1][1] = 0
>>> x
[[1, 2, 3], [4, 0, 6], [7, 8, 9]]
>>> y
[[1, 2, 3], [4, 5, 6], [7, 8, 9]]

深拷贝

8.11 列表推导式

8.11.1 基础语法

[expression for target in iterable]

example:

掌握好列表推导式,会使代码变得更为简练和高效。
比如下面这个循环语句:

1
2
3
4
>>> this = [1, 2, 3, 4, 5]
...
>>> for i in range(len(this)):
... this[i] = this[i] * 2

写成列表推导式就是:

1
2
>>> this = [1, 2, 3, 4, 5]
>>> this = [i * 2 for i in this]

注意:这可不仅仅是少写了一行代码而已,从程序的执行效率上来说,列表推导式的效率通常是要比循环语句快上一倍左右的速度。 (因为列表推导式是用底层的C语言来执行)

8.11.2 处理矩阵

利用列表推导式处理矩阵也是非常方便,比如下面代码是将矩阵第 2 列的元素给提取出来:

1
2
3
4
5
6
>>> matrix = [[1, 2, 3],
... [4, 5, 6],
... [7, 8, 9]]
>>> col2 = [row[1] for row in matrix] #选取每行的第二个元素
>>> col2
[2, 5, 8]

又比如,下面代码是获取矩阵主对角线上的元素(就是从左上角到右下角这条对角线上的元素):

1
2
3
>>> diag = [matrix[i][i] for i in range(len(matrix))]   #len()是推导列表里元素的角标
>>> diag
[1, 5, 9]

8.11.3 列表推导式创建二维数组

利用列表推导式,就可以很轻松地创建一个二维列表:

1
2
3
4
5
6
>>> S = [[0] * 3 for i in range(3)]
>>> S
[[0, 0, 0], [0, 0, 0], [0, 0, 0]]
>>> S[1][1] = 1
>>> S
[[0, 0, 0], [0, 1, 0], [0, 0, 0]]

8.11.4 带条件筛选功能的列表推导式

列表推导式其实还可以添加一个用于筛选的 if 分句,完整语法如下:
[expression for target in iterable if condition1]

8.11.5 多层嵌套的列表推导式

列表推导式还可以变得更复杂一些,那就是实现嵌套,语法如下:

1
2
3
4
[expression for target1 in iterable1
for target2 in iterable2
...
for targetN in iterableN]

每层嵌套还可以附带一个用于条件筛选的 if 分句:
1
2
3
4
[expression for target1 in iterable1 if condition1
for target2 in iterable2 if condition2
...
for targetN in iterableN if conditionN]

9.元组

元组既能像列表那样同时容纳多种类型的对象,也拥有字符串不可变的特性。

9.1 元组和列表的不同点

  • 列表使用方括号,元祖则是圆括号(也可以不带圆括号)
  • 列表中的元素可以被修改,而元组不行
  • 列表中涉及到修改元素的方法元组均不支持
  • 列表的推导式叫列表推导式,元组的“推导式”叫生成器表达式

9.2 元组和列表的共同点

  • 都可以通过下标获取元素
  • 都支持切片操作
  • 都支持 count() 方法和 index() 方法
  • 都支持拼接(+)和重复(*)运算符
  • 都支持嵌套
  • 都支持迭代

9.3 圆括号的必要性

与其纠结什么时候省略圆括号会不会带来问题,还不如一直加上为妙。
这样也可以增加代码的可读性

9.4 当元组只有一个元素的时候

1
2
3
>>> x = (6,)
>>> type(x)
<class 'tuple'>

or

1
2
3
4
5
>>> x = 789,
>>> x
(789,)
>>> type(x)
<class 'tuple'>

9.5 打包和解包

生成一个元组有时候也称之为元组的打包:
>>> t = (123, 'Mortal', 2.713)
将他们一次性赋值给三个变量名的行为,我们称之为解包:

1
2
3
4
5
6
>>> x
123
>>> y
'Mortal'
>>> z
2.713

注意: 赋值号左侧的变量名数量,必须跟右侧序列的元素数量一致,否则通常都会报错

9.6 多重赋值的真相

1
2
3
4
5
6
>>> x, y = 5, 8
>>> x
5
>>> y
8

相当于

1
2
3
4
5
6
>>> _ = (5, 8)
>>> x, y = _
>>> x
5
>>> y
8

9.7 元组的修改

1
2
3
4
5
6
7
8
>>> s = [1, 2, 3]
>>> t = [4, 5, 6]
>>> w = (s, t)
>>> w
([1, 2, 3], [4, 5, 6])
>>> w[0][1] = '大笨蛋'
>>> w
([1, '大笨蛋', 3], [4, 5, 6])

10. 字符串

10.1.1大小写字母变换

capitalize() 返回将字符串中首字母大写,其余小写的新字符串

1
2
3
>>> x = "I love little Cat"
>>> x.capitalize()
'I love little cat'

casefold() 返回全部小写的新字符串
1
2
>>> x.capitalize()
'I love little cat'

title() 返回将字符串中每个单词首字母大写
1
2
>>> x.title()
'I Love Little Cat'

swapcase() 返回将原字符串大小反转的新字符串
1
2
>>> x.swapcase()
'i LOVE LITTLE cAT'

upper() 返回全部大写的新字符串
1
2
>>> x.upper()
'I LOVE LITTLE CAT'

lower() 返回全部小写的新字符串
1
2
>>> x.lower()
'i love little cat'

10.1.2 左中右对齐

center(width, fillchar=' ')

1
2
>>> x.center(15)
' 小猫爱吃鱼 '

ljust(width, fillchar=' ')
1
2
>>> x.ljust(15)
'小猫爱吃鱼 '

rjust(width, fillchar=' ')
1
2
>>> x.rjust(15)
' 小猫爱吃鱼'

zfill(15)0填充左侧
1
2
3
4
5
6
7
8
9
10
>>> "520".zfill(5)
'00520'
>>> "-520".zfill(5)
'-0520'
>>> x.center(15, "淦")
'淦淦淦有内鬼,停止交易!淦淦淦'
>>> x.ljust(15, "淦")
'有内鬼,停止交易!淦淦淦淦淦淦'
>>> x.rjust(15, "淦")
'淦淦淦淦淦淦有内鬼,停止交易!'

10.2.1 查找

count(sub[,start[,end]]) find(sub[, start[, end]]) rfind(sub[, start[, end]]) index(sub[, start[, end]]) rindex(sub[, start[, end]])

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
>>> x = "上海自来水来自海上"
>>> x.count("海")
2
>>> x.count("海", 0, 5)
1
>>> x.find("海")
1
>>> x.rfind("海")
7
>>> x.find("龟")
-1
>>> x.index("龟")
Traceback (most recent call last):
File "<pyshell#6>", line 1, in <module>
x.index("龟")
ValueError: substring not found

10.2.2 替换

expandtabs([tabsize=8]) replace(old, new, count=-1) translate(table)

首先是 expandtabs([tabsize=8]) 方法,它的作用是使用空格替换制表符并返回新的字符串。

比如你现在在路边捡到一段代码,里面混了着 Tab 和空格:

1
2
3
>>> code = """
print("I love MoralSec.")
print("I love my wife.")"""

那么使用 expandtabs(tabsize=4) 方法,就可以将字符串中的 Tab 转换成空格,其中 tabsize 参数指定的是一个 Tab 使用多少个空格来代替:

1
2
3
4
5
6
>>> new_code = code.expandtabs(4)
>>> print(new_code)

print("I love MoralSec.")
print("I love my wife.")
>>>

replace(old, new, count=-1) 方法返回一个将所有 old 参数指定的子字符串替换为 new 的新字符串。另外,还有一个 count 参数是指定替换的次数,默认值 -1 表示替换全部。

1
2
>>> "在吗!我在你家楼下,快点下来!!".replace("在吗", "想你")
'想你!我在你家楼下,快点下来!!'

translate(table) 方法,这个是返回一个根据 table 参数(用于指定一个转换规则的表格)转换后的新字符串。

需要使用 str.maketrans(x[, y[, z]]) 方法制定一个包含转换规则的表格。

1
2
3
>>> table = str.maketrans("ABCDEFG", "1234567")
>>> "YOU ARE AN APPLE OF MY EYE".translate(table)
'YOU 1R5 1N 1PPL5 O6 MY 5Y5'

这个 str.maketrans() 方法还支持第三个参数,表示将其指定的字符串忽略:

1
2
>>> "YOU ARE AN APPLE OF MY EYE".translate(str.maketrans("ABCDEFG", "1234567","ARE"))
'YOU N PPL O6 MY Y'

10.3.1 判断

startswith(prefix[, start[, end]]) endswith(suffix[, start[, end]]) istitle() isupper() islower() isalpha() isascii() isspace() isprintable() isdecimal() isdigit() isnumeric() isalnum() isidentifier()

这 14 个方法都是应对各种情况的判断,所以返回的都是一个布尔类型的值 —— 要么是 True,要么是 False。

startswith(prefix[, start[, end]]) 方法用于判断 prefix 参数指定的子字符串是否出现在字符串的起始位置:

1
2
3
4
5
>>> x = "我爱Python"
>>> x.startswith("我")
True
>>> x.startswith("小猫咪")
False

对应的,endswith(suffix[, start[, end]]) 方法则相反,用于判断 suffix 参数指定的子字符串是否出现在字符串的结束位置:

1
2
3
4
5
6
7
8
>>> x.startswith("我", 1)
False
>>> x.startswith("爱", 1)
True
>>> x.endswith("Py")
False
>>> x.endswith("Py", 0, 4)
True

这个 prefixsuffix 参数,其实是支持以元组的形式传入多个待匹配的字符串的:

1
2
3
4
5
>>> x = "她爱Pyhon"
>>> if x.startswith(("你", "我", "她")):
... print("总有人喜爱Pyhon")
...
总有人喜爱Pyhon

如果你希望判断一个字符串中的所有单词是否都是以大写字母开头,其余字母均为小写,那么可以使用 istitle() 方法进行测试:
1
2
3
>>> x = "I Love Python"
>>> x.istitle()
True

如果你希望判断一个字符串中所有字母是否都是大写,可以使用 isupper() 方法进行测试
1
2
3
4
>>> x.isupper()
False
>>> x.upper().isupper()
True

相反,判断是否所有字母都是小写,用 islower() 方法,我们这里就不再赘述了。

如果你希望判断一个字符串中是否只是由字母组成,可以使用 isalpha() 方法进行检测:

1
2
3
4
>>> x.isalpha()
False
>>> "IlovePython".isalpha()
True

如果你希望判断一个字符串中是否只是由 ASCII 字符组成,可以使用 isascii() 方法进行检测:
1
2
3
4
>>> x.isascii()
True
>>> "我爱Pyhon".isascii()
False

如果你希望判断是否为一个空白字符串,可以用 isspace() 方法进行检测:
1
2
>>> "    \t\n".isspace()
True

如果你希望判断一个字符串中是否所有字符都是可打印的,可以使用 isprintable() 方法:
1
2
3
4
>>> x.isprintable()
True
>>> "I love FishC\n".isprintable()
False

isdecimal()、isdigit() 和 isnumeric() 三个方法都是用来判断数字的。

首先是十进制数字:

1
2
3
4
5
6
7
>>> x = "12345"
>>> x.isdecimal()
True
>>> x.isdigit()
True
>>> x.isnumeric()
True

如果写成罗马数字:
1
2
3
4
5
6
7
>>> x = "ⅠⅡⅢⅣⅤ"
>>> x.isdecimal()
False
>>> x.isdigit()
False
>>> x.isnumeric()
True

或者中文数字:
1
2
3
4
5
6
7
>>> x = "一二三四五"
>>> x.isdecimal()
False
>>> x.isdigit()
False
>>> x.isnumeric()
True

isdecimal() 和 isdigit() 方法都败下阵来了,但 isnumeric() 方法,其实连繁体数字也难不倒它地:
1
2
3
4
5
6
7
>>> x = "壹贰叁肆伍"
>>> x.isdecimal()
False
>>> x.isdigit()
False
>>> x.isnumeric()
True

isalnum() 方法则是集大成者,只要 isalpha()、isdecimal()、isdigit() 或者 isnumeric() 任意一个方法返回 True,结果都为 True。

最后,isidentifier() 方法用于判断该字符串是否一个合法的 Python 标识符

1
2
3
4
5
6
7
8
>>> "I a good gay".isidentifier()
False
>>> "I_a_good_gay".isidentifier()
True
>>> "FishC520".isidentifier()
True
>>> "520FishC".isidentifier()
False

如果你想判断一个字符串是否为 Python 的保留标识符,就是像 “if”、“for”、“while” 这些关键字的话,可以使用 keyword 模块的 iskeyword() 函数来实现:

1
2
3
4
5
>>> import keyword
>>> keyword.iskeyword("if")
True
>>> keyword.iskeyword("py")
False

10.4.1 截取

lstrip(chars=None)rstrip(chars=None)strip(chars=None)removeprefix(prefix)removesuffix(suffix)

这几个方法都是用来截取字符串的:

1
2
3
4
5
6
>>> "    左侧不要留白".lstrip()
'左侧不要留白'
>>> "右侧不要留白 ".rstrip()
'右侧不要留白'
>>> " 左右不要留白 ".strip()
'左右不要留白'

例题:如果要从字符串 “https://ilovefishc.com/html5/index.html“ 中提取出 “ilovefishc.com”,使用 split() 方法应该如何实现呢?

1
2
>>> "https://ilovefishc.com/html5/index.html".split('//')[1].split('/')[0]
'ilovefishc.com'

解析:
1
2
3
4
5
6
7
8
>>> "https://ilovefishc.com/html5/index.html".split('//')
['https:', 'ilovefishc.com/html5/index.html']
>>> "https://ilovefishc.com/html5/index.html".split('//')[1] #[1]为返回列表的索引
'ilovefishc.com/html5/index.html'
>>> "https://ilovefishc.com/html5/index.html".split('//')[1].split('/')
['ilovefishc.com', 'html5', 'index.html']
>>> "https://ilovefishc.com/html5/index.html".split('//')[1].split('/')[0]
'ilovefishc.com'

这三个方法都有一个 chars=None 的参数, None 在 Python 中表示没有,意思就是去除的是空白。
那么这个参数其实是可以给它传入一个字符串的:

1
2
3
4
5
6
>>> "www.github.com".lstrip("wcom.")
'github.com'
>>> "www.github.com".rstrip("wcom.")
'www.github'
>>> "www.github.com".strip("wcom.")
'github'

removeprefix(prefix)removesuffix(suffix) 这两个方法,它们允许你指定将要删除的前缀或后缀:

1
2
3
4
>>> "www.github.com".removeprefix("www.")
'github.com'
>>> "www.github.com".removesuffix(".com")
'www.github'

10.4.2 拆分

partition(sep)rpartition(sep)split(sep=None, maxsplit=-1)rsplit(sep=None, maxsplit=-1)splitlines(keepends=False)

拆分字符串,言下之意就是把字符串给大卸八块,比如 partition(sep) 和 rpartition(sep) 方法,就是将字符串以 sep 参数指定的分隔符为依据进行切割,返回的结果是一个 3 元组(3 个元素的元组):

1
2
>>> "www.github.com".partition(".")
('www', '.', 'github.com')

partition(sep)rpartition(sep) 方法的区别是前者是从左往右找分隔符,后者是从右往左找分隔符:

1
2
>>> "github.com/python".partition("/")
('github.com', '/', 'python')

注意:它俩如果找不到分隔符,返回的仍然是一个 3 元组,只不过将原字符串放在第一个元素,其它两个元素为空字符串。

split(sep=None, maxsplit=-1)rsplit(sep=None, maxsplit=-1) 方法则是可以将字符串切成一块块:

1
2
3
4
5
6
7
8
>>> "苟日新,日日新,又日新".split(",")
['苟日新', '日日新', '又日新']
>>> "苟日新,日日新,又日新".rsplit(",")
['苟日新', '日日新', '又日新']
>>> "苟日新,日日新,又日新".split(",", 1)
['苟日新', '日日新,又日新']
>>> "苟日新,日日新,又日新".rsplit(",", 1)
['苟日新,日日新', '又日新']

splitlines(keepends=False) 方法会将字符串进行按行分割,并将结果以列表的形式返回

1
2
3
4
5
6
>>> "苟日新\n日日新\n又日新".splitlines()
['苟日新', '日日新', '又日新']
>>> "苟日新\r日日新\r又日新".splitlines()
['苟日新', '日日新', '又日新']
>>> "苟日新\r日日新\r\n又日新".splitlines()
['苟日新', '日日新', '又日新']

keepends 参数用于指定结果是否包含换行符,True 是包含,默认 False 则表示是不包含:

1
2
>>> "苟日新\r日日新\r\n又日新".splitlines(True)
['苟日新\r', '日日新\r\n', '又日新']

10.4.3 拼接

join(iterable) 方法是用于实现字符串拼接的。

虽然的它的用法在初学者看来是非常难受的,但是在实际开发中,它却常常是受到大神追捧的一个方法。

字符串是作为分隔符使用,然后 iterable 参数指定插入的子字符串:

1
2
3
4
5
6
>>> ".".join(["www", "ilovefishc", "com"])
'www.ilovefishc.com'
>>> "^".join(("F", "ish", 'C'))
'F^ish^C'
>>> "".join(("FishC", "FishC"))
'FishCFishC'

10.5.1 格式化字符串

在字符串中,格式化字符串的套路就是使用一对花括号({})来表示替换字段,就在原字符串中先占一个坑的意思,然后真正的内容被放在了 format() 方法的参数中。

1
2
3
4
5
>>> year = 2010
>>> "鱼C工作室成立于 year 年。"
'鱼C工作室成立于 year 年。'
>>> "鱼C工作室成立于 {} 年。".format(year)
'鱼C工作室成立于 2010 年。'

又比如:

1
2
>>> "1+2={}, 2的平方是{},3的立方是{}".format(1+2, 2*2, 3*3*3)
'1+2=3, 2的平方是4,3的立方是27'

在花括号里面,可以写上数字,表示参数的位置:

1
2
>>> "{1}看到{0}就很激动!".format("mortal", "漂亮的小姐姐")
'漂亮的小姐姐看到mortal就很激动!'

注意,同一个索引值是可以被多次引用的:

1
2
>>> "{0}{0}{1}{1}".format("是", "非")
'是是非非'

还可以通过关键字进行索引,比如:

1
2
>>> "我叫{name},我爱{fav}。".format(name="mortal", fav="Pyhon")
'我叫mortal,我爱Pyhon。'

当然,位置索引和关键字索引可以组合使用:

1
2
>>> "我叫{name},我爱{0}。喜爱{0}的人,运气都不会太差^o^".format("python", name="mortal")
'我叫mortal,我爱python。喜爱python的人,运气都不会太差^o^'

如果我只是想单纯的输出一个纯洁的花括号,那应该怎么办呢?
有两种办法可以把这个纯洁的花括号安排进去:

1
2
3
4
>>> "{}, {}, {}".format(1, "{}", 2)
'1, {}, 2'
>>> "{}, {{}}, {}".format(1, 2)
'1, {}, 2'

10.5.2 字符串格式化语法参考

以下所解锁的新知识,可以直接在字符串的 format() 方法上使用,也可以用于 Python3.6 后新添加的f-字符串

1
2
3
4
5
6
7
8
format_spec     ::=  [[fill]align][sign][#][0][width][grouping_option][.precision][type]
fill ::= <any character>
align ::= "<" | ">" | "=" | "^"
sign ::= "+" | "-" | " "
width ::= digit+
grouping_option ::= "_" | ","
precision ::= digit+
type ::= "b" | "c" | "d" | "e" | "E" | "f" | "F" | "g" | "G" | "n" | "o" | "s" | "x" | "X" | "%"

https://fishc.com.cn/thread-185807-1-1.html 字符串格式化语法参考

10.5.2.1 对齐选项([align])

对齐选项

1
2
3
4
5
6
7
8
>>> "{:^}".format(250)
'250'
>>> "{:^10}".format(250)
' 250 '
>>> "{1:>10}{0:<10}".format(520, 250)
' 250520 '
>>> "{left:>10}{right:<10}".format(right=520, left=250)
' 250520 '

"{1:>10}{0:<10}".format(520, 250) 1 ~ 位置索引 > ~ 对齐方向 10 ~ 显示宽度

10.5.2.2 填充选项([fill])

在指定宽度的前面还可以添加一个 ‘0’,则表示为数字类型启用感知正负号的 ‘0’ 填充效果:

1
2
3
4
>>> "{:010}".format(520)
'0000000520'
>>> "{:010}".format(-520)
'-000000520'

注意,这种用法只对数字有效:

1
2
3
4
5
>>> "{:010}".format("FishC")
Traceback (most recent call last):
File "<pyshell#39>", line 1, in <module>
"{:010}".format("FishC")
ValueError: '=' alignment not allowed in string format specifier

还可以在对齐([align])选项的前面通过填充选项([fill])来指定填充的字符:

1
2
3
4
5
6
>>> "{1:%>10}{0:%<10}".format(520, 250)
'%%%%%%%250520%%%%%%%'
>>> "{:0=10}".format(520)
'0000000520'
>>> "{:0=10}".format(-520)
'-000000520'

10.5.3 符号([sign])选项

符号([sign])选项仅对数字类型有效,可以使用下面3个值:

字符串符号

10.5.4 精度([.precision])选项

精度([.precision])选项是一个十进制整数,对于不同类型的参数,它的效果是不一样的:

  • 对于以 ‘f’ 或 ‘F’ 格式化的浮点数值来说,是限定小数点后显示多少个数位
  • 对于以 ‘g’ 或 ‘G’ 格式化的浮点数值来说,是限定小数点前后共显示多少个数位
  • 对于非数字类型来说,限定最大字段的大小(换句话说就是要使用多少个来自字段内容的字符)
  • 对于整数来说,则不允许使用该选项值

10.5.5 类型([type])选项

类型([type])选项决定了数据应该如何呈现。
以下类型适用于整数:
适用整数]
以下类型值适用于浮点数、复数和整数(自动转换为等值的浮点数)如下:
字符串类型

10.5.7 更灵活的玩法

Python 事实上支持通过关键参数来设置选项的值,比如下面代码通过参数来调整输出的精度:

1
2
>>> "{:.{prec}f}".format(3.1415, prec=2)
'3.14'

同时设置多个选项也是没问题的,只要你自己不乱,Python 就不会乱:
1
2
>>> "{:{fill}{align}{width}.{prec}{ty}}".format(3.1415, fill='+', align='^', width=10, prec=3, ty='g')
'+++3.14+++'

10.5.8 f-字符串

Python 随着版本的更迭,它的语法也是在不断完善的。“简洁胜于复杂”是 Python 之禅中强调的理念。

因此,在 Python3.6 的更新中,他们给添加了一个新的语法,叫 f-string,也就是 f-字符串。

f-string 可以直接看作是 format() 方法的语法糖,它进一步简化了格式化字符串的操作并带来了性能上的提升。

  • 注:语法糖(英语:Syntactic sugar)是由英国计算机科学家彼得·兰丁发明的一个术语,指计算机语言中添加的某种语法,这种语法对语言的功能没有影响,但是更方便程序员使用。语法糖让程序更加简洁,有更高的可读性。

来,我们使用 f-string 将前面讲解 format() 方法的例子给大家修改一遍,你就知道该怎么玩了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
>>> year = 2010
>>> f"鱼C工作室成立于 {year} 年"
'鱼C工作室成立于 2010 年'
>>>
>>> f"1+2={1+2}, 2的平方是{2*2},3的立方是{3*3*3}"
'1+2=3, 2的平方是4,3的立方是27'
>>>
>>> "{:010}".format(-520)
>>> f"{-520:010}"
'-000000520'
>>>
>>> "{:,}".format(123456789)
>>> f"{123456789:,}"
'123,456,789'
>>>
>>> "{:.2f}".format(3.1415)
>>> f"{3.1415:.2f}"
'3.14'

11 序列

11.1 列表、元组、字符串的共同点

  • 都可以通过索引获取每一个元素
  • 第一个元素的索引值都是 0
  • 都可以通过切片的方法获得一个范围内的元素的集合
  • 有很多共同的运算符

因此,列表、元组和字符串,Python 将它们统称为序列。
根据是否能被修改这一特性,可以将序列分为可变序列和不可变序列:比如列表就是可变序列,而元组和字符串则是不可变序列。

11.2 加号(+)和乘号(*)

首先是加减乘除,只有加号(+)和乘号(*)可以用上,序列之间的加法表示将两个序列进行拼接;乘法表示将序列进行重复,也就是拷贝:

1
2
3
4
5
6
7
8
9
10
11
12
>>> [1, 2, 3] + [4, 5, 6]
[1, 2, 3, 4, 5, 6]
>>> (1, 2, 3) + (4, 5, 6)
(1, 2, 3, 4, 5, 6)
>>> "123" + "456"
'123456'
>>> [1, 2, 3] * 3
[1, 2, 3, 1, 2, 3, 1, 2, 3]
>>> (1, 2, 3) * 3
(1, 2, 3, 1, 2, 3, 1, 2, 3)
>>> "123" * 3
'123123123'

11.3 关于 “可变” 和 “不可变” 的思考

可变序列

1
2
3
4
5
6
7
8
>>> s = [1, 2, 3]
>>> id(s)
2285532322944
>>> s *= 2
>>> s
[1, 2, 3, 1, 2, 3]
>>> id(s)
2285532322944

不可变序列
1
2
3
4
5
6
7
8
>>> t = (1, 2, 3)
>>> id(t)
2285532205952
>>> t *= 2
>>> t
(1, 2, 3, 1, 2, 3)
>>> id(t)
2285532393920 #观察这里

虽然可变序列和不可变序列看上去都是 “可变” 的,但实现原理却是天壤之别:可变序列是在原位置修改 “扩容”,而不可变序列则是将内容 “扩容” 后再放到一个新的位置上去。

11.4 是(is)和不是(is not)

是(is)和不是(is not)被称之为同一性运算符,用于检测两个对象之间的 id 值是否相等:

1
2
3
4
5
6
7
8
>>> x = "FishC"
>>> y = "FishC"
>>> x is y
True
>>> x = [1, 2, 3]
>>> y = [1, 2, 3]
>>> x is not y
True

11.5 包含(in)和不包含(not in)

in 运算符是用于判断某个元素是否包含在序列中的,而 not in 则恰恰相反:

1
2
3
4
5
6
>>> "Fish" in "FishC"
True
>>> "鱼" in "鱼C"
True
>>> "C" not in "FishC"
False

11.6 del 语句

del 语句用于删除一个或多个指定的对象:

1
2
3
4
5
6
7
8
9
10
11
12
13
>>> x = "FishC"
>>> y = [1, 2, 3]
>>> del x, y
>>> x
Traceback (most recent call last):
File "<pyshell#52>", line 1, in <module>
x
NameError: name 'x' is not defined
>>> y
Traceback (most recent call last):
File "<pyshell#53>", line 1, in <module>
y
NameError: name 'y' is not defined

11.7 list()、tuple() 和 str()

list()、tuple() 和 str() 这三个 BIF 函数主要是实现列表、元组和字符串的转换。

11.8 min() 和 max()

min() 和 max() 这两个函数的功能是:对比传入的参数,并返回最小值和最大值。

它们都有两种函数原型

1
2
min(iterable, *[, key, default])
min(arg1, arg2, *args[, key])

以及

1
2
max(iterable, *[, key, default])
max(arg1, arg2, *args[, key])

这第一种传入的是一个可迭代对象:
1
2
3
4
5
6
>>> s = [1, 1, 2, 3, 5]
>>> min(s)
1
>>> t = "Mortal"
>>> max(t)
't'

这第二种传入多个参数,它们会自动找出其中的最小值和最大值:
1
2
3
4
>>> min(1, 2, 3, 0, 6)
0
>>> max(1, 2, 3, 0, 6)
6

11.9 len() 和 sum()

len() 函数我们前面用过好多次了,基本用法不必啰嗦,大家都懂~

不过它有个最大的可承受范围,可能有些同学还不知道,比如说这样

1
2
3
4
5
>>> len(range(2 ** 100))
Traceback (most recent call last):
File "<pyshell#42>", line 1, in <module>
len(range(2 ** 100))
OverflowError: Python int too large to convert(转换) to C ssize_t

这个错误是由于 len() 函数的参数太大导致的,我们知道 Python 为了执行的效率,它内部几乎都是用效率更高的 C 语言来实现的。
而这个 len() 函数为了让 Python 自带的数据结构可以走后门,它会直接读取 C 语言结构体里面对象的长度。
所以,如果检测的对象超过某个数值,就会出错。

通常对于 32 位平台来说,这个最大的数值是 2**31 - 1;而对于 64 位平台来说,这个最大的数值是 2**63 - 1

sum() 函数用于计算迭代对象中各项的和:

1
2
3
>>> s = [1, 0, 0, 8, 6]
>>> sum(s)
15

它有一个 start 参数,用于指定求和计算的起始数值,比如这里我们设置为从 100 开始加起:
1
2
>>> sum(s, start=100)
115

11.10 sorted() 和 reverse()

sorted() 函数将重新排序 iterable 参数中的元素,并将结果返回一个新的列表:

1
2
3
>>> s = [1, 2, 3, 0, 6]
>>> sorted(s)
[0, 1, 2, 3, 6]

sorted() 函数也支持 key 和 reverse 两个参数,用法跟列表的 sort() 方法一致:

1
2
3
4
5
6
7
8
9
10
>>> sorted(s, reverse=True)
[6, 3, 2, 1, 0]
>>> s.sort(reverse=True)
>>> s
[6, 3, 2, 1, 0]
>>> t = ["FishC", "Apple", "Book", "Banana", "Pen"]
>>> sorted(t)
['Apple', 'Banana', 'Book', 'FishC', 'Pen']
>>> sorted(t, key=len)
['Pen', 'Book', 'FishC', 'Apple', 'Banana']

sorted(t, key=len) 这个,因为这个 key 参数,指定的是一个干预排序算法的函数。
比如这里我们指定为 len() 函数,那么 Python 在排序的过程中,就会先将列表中的每一个元素调用一次 len() 函数,然后比较的是 len() 返回的结果。
所以,sorted(t, key=len) 比较的就是每个元素的长度。

reverse() 函数将返回参数的反向迭代器。

举个例子:

1
2
3
>>> s = [1, 2, 5, 8, 0]
>>> reverse(s)
<list_reverseiterator object at 0x0000022926732AC0>

大家看,它不是直接返回所见即所得的结果,它返回的一串奇奇怪怪的英文……
刚刚我们说过,它返回的结果是一个迭代器,并且我们可以把它当可迭代对象处理。
既然如此,我们就可以使用 list() 函数将其转换为列表
1
2
>>> list(reverse(s))
[0, 8, 5, 2, 1]

reverse() 函数也同样支持任何形式的可迭代对象:
1
2
3
4
5
6
>>> list(reverse("FishC"))
['C', 'h', 's', 'i', 'F']
>>> list(reverse((1, 2, 5, 9, 3)))
[3, 9, 5, 2, 1]
>>> list(reverse(range(0, 10)))
[9, 8, 7, 6, 5, 4, 3, 2, 1, 0]

11.11 all()和any()

all() 函数是判断可迭代对象中是否所有元素的值都为真;

any() 函数则是判断可迭代对象中是否存在某个元素的值为真。

1
2
3
4
5
6
7
8
9
10
>>> x = [1, 1, 0]
>>> y = [1, 1, 9]
>>> all(x)
False
>>> all(y)
Ture
>>> any(x)
True
>>> any(y)
True

11.12 enumerate()

enumerate() 函数用于返回一个枚举对象,它的功能就是将可迭代对象中的每个元素及从 0 开始的序号共同构成一个二元组的列表:

1
2
3
>>> seasons = ["Spring", "Summer", "Fall", "Winter"]
>>> list(enumerate(seasons))
[(0, 'Spring'), (1, 'Summer'), (2, 'Fall'), (3, 'Winter')]

它有一个 start 参数,可以自定义序号开始的值:
1
2
3
4
5
6
7
>>> for i, j in enumerate(seasons, start=10):
... print(i, "->", j)
...
10 -> Spring
11 -> Summer
12 -> Fall
13 -> Winter

11.13 zip()

zip() 函数用于创建一个聚合多个可迭代对象的迭代器。

做法是将作为参数传入的每个可迭代对象的每个元素依次组合成元组,即第 i 个元组包含来自每个参数的第 i 个元素。

1
2
3
4
5
6
7
8
9
>>> x = [1, 2, 3]
>>> y = [4, 5, 6]
>>> zipped = zip(x, y)
>>> list(zipped)
[(1, 4), (2, 5), (3, 6)]
>>> z = [7, 8, 9]
>>> zipped = zip(x, y, z)
>>> list(zipped)
[(1, 4, 7), (2, 5, 8), (3, 6, 9)]

这里有一点需要大家注意的,就是如果传入的可迭代对象长度不一致,那么将会以最短的那个为准:

1
2
3
4
>>> z = "FishC"
>>> zipped = zip(x, y, z)
>>> list(zipped)
[(1, 4, 'F'), (2, 5, 'i'), (3, 6, 's')]

当我们不关心较长的可迭代对象多出的数据时,使用 zip() 函数无疑是最佳的选择,因为它自动裁掉多余的部分。

但是,如果那些值对于我们来说是有意义的,我们可以使用 itertools 模块的 zip_longest() 函数来代替:

1
2
3
4
>>> import itertools
>>> zipped = itertools.zip_longest(x, y, z)
>>> list(zipped)
[(1, 4, 'F'), (2, 5, 'i'), (3, 6, 's'), (None, None, 'h'), (None, None, 'C')]

11.14 map()

map() 函数会根据提供的函数对指定的可迭代对象的每个元素进行运算,并将返回运算结果的迭代器:

1
2
3
>>> mapped = map(ord, "FishC")
>>> list(mapped)
[70, 105, 115, 104, 67]

如果指定的函数需要两个参数,后面跟着的可迭代对象的数量也应该是两个:

1
2
3
>>> mapped = map(pow, [2, 3, 10], [5, 2, 3]))
>>> list(mapped)
[32, 9, 1000]

上面代码其实就相当于是:
1
2
>>> [pow(2, 5), pow(3, 2), pow(10, 3)]
[32, 9, 1000]

可以看出,如果数量一多,使用 map() 函数要方便许多。

如果可迭代对象的长度不一致,那么 Python 采取的做法跟 zip() 函数一样,都是在最短的可迭代对象终止时结束:

1
2
>>> list(map(max, [1, 3, 5], [2, 2, 2], [0, 3, 9, 8]))
[2, 3, 9]

11.15 filter()

与 map() 函数类似,filter() 函数也是需要传入一个函数作为参数,不过 filter() 函数是根据提供的函数,对指定的可迭代对象的每个元素进行运算,并将运算结果为真的元素,以迭代器的形式返回:

1
2
>>> filter(str.islower, "FishC")
<filter object at 0x000001B5170FEFA0>

上面代码我们传入的是字符串的 islower() 方法,作用就是判断传入的参数是否为小写字母,结合到 filter() 函数中使用,就是剔除大写字母,保留小写字母的作用。

如果提供的函数是 None,则会假设它是一个 “鉴真” 函数,即可迭代对象中所有值为假的元素会被移除:

1
2
>>> list(filter(None, [True, False, 1, 0]))
[True, 1]

11.16 可迭代对象和迭代器

最大的区别是:可迭代对象咱们可以对其进行重复的操作,而迭代器则是一次性的!

将可迭代对象转换为迭代器:iter() 函数。

1
2
>>> x = [1, 2, 3, 4, 5]
>>> y = iter(x)

通过 type() 函数,我们可以观察到这个区别:

1
2
3
4
>>> type(x)
<class 'list'>
>>> type(y)
<class 'list_iterator'>

最后,BIF 里面有一个 next() 函数,它是专门针对迭代器的。
它的作用就是逐个将迭代器中的元素提取出来:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
>>> next(y)
1
>>> next(y)
2
>>> next(y)
3
>>> next(y)
4
>>> next(y)
5
>>> next(y)
Traceback (most recent call last):
File "<pyshell#52>", line 1, in <module>
next(y)
StopIteration

现在如果不想它抛出异常,那么可以给它传入第二个参数:

1
2
3
4
5
6
7
8
9
10
11
12
13
>>> z = iter(x)
>>> next(z, "没啦,被你掏空啦~")
1
>>> next(z, "没啦,被你掏空啦~")
2
>>> next(z, "没啦,被你掏空啦~")
3
>>> next(z, "没啦,被你掏空啦~")
4
>>> next(z, "没啦,被你掏空啦~")
5
>>> next(z, "没啦,被你掏空啦~")
'没啦,被你掏空啦~'

12 字典

12.1 字典的关键特征

字典是 Python 中唯一实现映射关系的内置类型。
字典的关键符号是大括号({})和冒号(:):

1
2
3
>>> d = {"吕布":"口口布", "关羽":"关习习"}
>>> type(d)
<class 'dict'>

这里就是两对映射关系,我们将冒号的左边称为字典的 “键”,右边称为字典的 “值”。
在字典中,只要我们提供键,就可以获取其对应的值。方法跟序列类似,只不过这次在方括号中,咱们使用的是键,而非索引值:
1
2
>>> d["吕布"]
'口口布'

12.2 创建字典

创建字典有很多种方法,这里我们把官方文档列举的6种方法介绍给大家!
OK,第一种就是刚刚给大家演示过的,直接使用大括号和冒号的组合,将映射关系给“套牢”:
>>> a = {"吕布":"口口布", "关羽":"关习习", "刘备":"刘baby"}
第二种,使用dict()函数,跟list()、tuple()、str()类似,dict()函数用来生成字典,它的每个参数就是一个键值对,键与值直接使用等号
>>> b = dict(吕布="口口布", 关羽="关习习", 刘备="刘baby")
注意:这种写法要求你不能往键上面加引号,尽管它是一个字符串,但是你加引号就会出错.

第三种,使用列表作为参数,列表中的每个元素是使用元组包裹起来的键值对
>>> c = dict([("吕布","口口布"), ("关羽","关习习"), ("刘备","刘baby")])
第四种,属于“无病呻吟”版本,就是将第一种方法作为参数给到 dict() 函数:
>>> d = dict({"刘备": "刘baby", "关羽": "关习习", "吕布": "口口布"})
第五种,混合拳法:
>>> e = dict({"吕布":"口口布", "刘备":"刘baby"}, 关羽="关习习")
第六种,zip() 函数大家应该还有印象吧?它的作用是创建一个聚合多个可迭代对象的迭代器,对吧?那么,它也是可以作为参数传给 dict() 函数的:
>>> f = dict(zip(["吕布","关羽","刘备"], ["口口布","关习习","刘baby"]))

12.3 增

首先是 fromkeys(iterable[, value]) 方法,这个可以算是字典中最特殊的方法,它可以使用 iterable 参数指定的可迭代对象来创建一个新字典,并将所有的值初始化为 value 参数指定的值:

1
2
3
>>> d = dict.fromkeys("Fish", 250)
>>> d
{'F': 250, 'i': 250, 's': 250, 'h': 250}

如果不指定 value 参数,则采用默认值 None:
1
2
3
>>> d = dict.fromkeys("Fish")
>>> d
{'F': None, 'i': None, 's': None, 'h': None}

这种方法适用于从无到有,创建一个所有键的值都相同的字典。
这招对于快速初始化一个字典非常有用,如果需要修改某个键的值,这么做:
1
2
3
>>> d['F'] = 70
>>> d
{'F': 70, 'i': None, 's': None, 'h': None}

如果在字典中找不到对应的键,那么同样的操作就会变成增加一个新的键值对:
1
2
3
>>> d['C'] = 67
>>> d
{'F': 70, 'i': None, 's': None, 'h': None, 'C': 67}

12.4 删

删除字典中的指定元素我们可以使用 pop() 方法:

1
2
3
>>> d.pop('s')
>>> d
{'F': None, 'i': None, 'h': None, 'C': 67}

那么你会发现,如果 pop() 一个不存在的键,那么会抛出异常:
1
2
3
4
5
>>> d.pop("狗")
Traceback (most recent call last):
File "<pyshell#7>", line 1, in <module>
d.pop("狗")
KeyError: '狗'

如果你想让 Python 别这么激动,可以指定一个 default 参数:
1
2
>>> d.pop("狗", "没有~")
'没有~'

跟 pop() 方法类似的还有一个 popitem(),在 Python3.7 之前,它是随机删除一个键值对,在 Python3.7 之后,它删除的是最后一个加入字典的键值对:
1
2
3
4
>>> d.popitem()
('C', 67)
>>> d
{'F': None, 'i': None, 'h': None}

然后 del 关键字也可以删除一个指定的字典元素:
1
2
3
>>> del d['i']
>>> d
{'F': None, 'h': None}

当然,如果 del 直接加上字典的变量名就是将整个字典给干掉:
1
2
3
4
5
6
>>> del d
>>> d
Traceback (most recent call last):
File "<pyshell#14>", line 1, in <module>
d
NameError: name 'd' is not defined

如果我们只希望清空字典中的内容,可以使用 clear() 方法:
1
2
3
4
5
6
>>> d = dict.fromkeys("FishC", 250)
>>> d
{'F': 250, 'i': 250, 's': 250, 'h': 250, 'C': 250}
>>> d.clear()
>>> d
{}

12.5 改

类似于序列的操作,只需要指定一个存在于字典中的键,就可以修改其对应的值:

1
2
3
4
5
6
>>> d = dict.fromkeys("FishC")
>>> d
{'F': None, 'i': None, 's': None, 'h': None, 'C': None}
>>> d['s'] = 115
>>> d
{'F': None, 'i': None, 's': 115, 'h': None, 'C': None}

如果我们想要同时修改多个键值对,那么说实话,逐个操作就有点太麻烦了。
这时候,我们可以使用字典的 update() 方法,可以同时给它传入多个键值对,也可以直接给它传入另外一个字典,或者一个包含键值对的可迭代对象:
1
2
3
4
5
6
>>> d.update({'i':105, 'h':104})
>>> d
{'F': None, 'i': 105, 's': 115, 'h': 104, 'C': None}
>>> d.update(F='70', C='67')
>>> d
{'F': '70', 'i': 105, 's': 115, 'h': 104, 'C': '67'}

12.6 查

最简单的查方法就是你给它一个键,它返回你对应的值:

1
2
>>> d['C']
67

如果指定的键不存在于字典中,那么会报错:
1
2
3
4
5
>>> d['c']
Traceback (most recent call last):
File "<pyshell#6>", line 1, in <module>
d['c']
KeyError: 'c'

这在有些时候会被认为是用户体验不佳的表现,所以更好的方法是使用 get() 方法,它可以传入一个 default 参数,指定找不到键时返回的值:

1
2
>>> d.get('c', "这里没有c")
'这里没有c'

还有一种情况是我们需要查找一个键是否存在于字典中,如果在,返回它对应的值;如果不在,给它指定一个新的值:

1
2
3
4
5
6
7
8
>>> d.setdefault('C', "code")
67
>>> d
{'F': 70, 'i': 105, 's': 115, 'h': 104, 'C': 67}
>>> d.setdefault('c', "code")
'code'
>>> d
{'F': 70, 'i': 105, 's': 115, 'h': 104, 'C': 67, 'c': 'code'}

对比前面直接复制的操作,这么做的一个显而易见的好处就是不会破坏到已经存在的键值对。

12.7 视图对象

items()、keys() 和 values() 三个方法分别用于获取字典的键值对、键和值三者的视图对象。

什么是视图对象呢?

这个名字听着挺新鲜,字面上的解释是:视图对象就是字典的一个动态视图,这意味着当字典内容改变时,视图对象的内容也会相应地跟着改变。

举个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
>>> d
{'F': 70, 'i': 105, 's': 115, 'h': 104, 'C': 67, 'c': 'code'}
>>> items = d.items()
>>> keys = d.keys()
>>> values = d.values()
>>> items
dict_items([('F', 70), ('i', 105), ('s', 115), ('h', 104), ('C', 67), ('c', 'code')])
>>> keys
dict_keys(['F', 'i', 's', 'h', 'C', 'c'])
>>> values
dict_values([70, 105, 115, 104, 67, 'code'])
>>> d.pop('c')
'code'
>>> items
dict_items([('F', 70), ('i', 105), ('s', 115), ('h', 104), ('C', 67)])
>>> keys
dict_keys(['F', 'i', 's', 'h', 'C'])
>>> values
dict_values([70, 105, 115, 104, 67])

最后,为了方便地实现浅拷贝,字典也提供了一个 copy() 方法:
1
2
3
>>> e = d.copy()
>>> e
{'F': 70, 'i': 105, 's': 115, 'h': 104, 'C': 67}

12.8 字典妙用

使用 len() 函数来获取字典的键值对数量:

1
2
>>> len(d)
5

使用 in 和 not in 来判断某个键是否存在于字典中:
1
2
3
4
>>> 'C' in d
True
>>> 'c' not in d
True

字典也可以转化为列表,使用 list() 函数就可以了:
1
2
>>> list(d)
['F', 'i', 's', 'h', 'C']

那么 iter() 函数也可以作用于字典,它会将字典的键构成一个迭代器:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
>>> e = iter(d)
>>> next(e)
'F'
>>> next(e)
'i'
>>> next(e)
's'
>>> next(e)
'h'
>>> next(e)
'C'
>>> next(e)
Traceback (most recent call last):
File "<pyshell#15>", line 1, in <module>
next(e)
StopIteration

在 Python3.8 之后的版本中,咱们可以使用 reversed() 函数对字典内部的键值对进行逆向操作:
1
2
>>> list(reversed(d))
['C', 'h', 's', 'i', 'F']

可以看出,reversed(d) 其实相当于 reversed(d.keys()) 的缩写,那么如果我们想要获得值的逆向序列,可以这么做:
1
2
>>> list(reversed(d.values()))
[67, 104, 115, 105, 70]

12.9 嵌套

字典也是可以嵌套的,某个键的值是另外一个字典,并不是什么稀奇的事儿,举个例子,假如三国也有语数英:
>>> d = {"吕布": {"语文":60, "数学":70, "英语":80}, "关羽": {"语文":80, "数学":90, "英语":70}}
如果想要获取吕布的数学成绩,那么就需要进行两次索引:

1
2
>>> d["吕布"]["数学"]
70

那嵌套的也可以是一个列表:
>>> d = {"吕布": [60, 70, 80], "关羽": [80, 90, 70]}
第二次索引,我们当然也得换成下标索引:
1
2
>>> d["吕布"][1]
70

12.10 字典推导式

最后高阶的 —— 字典推导式:

1
2
3
4
5
6
>>> d = {'F':70, 'i':105, 's':115, 'h':104, 'C':67}
>>> b = {v:k for k,v in d.items()}
>>> b
{70: 'F', 105: 'i', 115: 's', 104: 'h', 67: 'C'}
>>> d
{'F': 70, 'i': 105, 's': 115, 'h': 104, 'C': 67}

看,这样我们轻而易举地将键和值给掉了个位置。
当然,我们也可以加上筛选的条件:
1
2
3
>>> c = {v:k for k,v in d.items() if v > 100}
>>> c
{105: 'i', 115: 's', 104: 'h'}

利用字典推导式,我们就可以轻易地让 Python 帮你求出字符串的编码值:
1
2
3
>>> d = {x:ord(x) for x in "FishC"}
>>> d
{'F': 70, 'i': 105, 's': 115, 'h': 104, 'C': 67}

13 集合

13.1 创建集合

创建一个集合通常有三种方法:

  • 使用花括号,元素之间以逗号分隔:{“FishC”, “Python”}
  • 使用集合推导式:{s for s in “FishC”}
  • 使用类型构造器,也就是 set():set(“FishC”)

13.2 集合具有随机性

1
2
>>> set("FishC")
{'i', 'C', 's', 'F', 'h'}

从这里我们不难发现,集合无序的特征,传进去的是 ‘F’、’i’、’s’、’h’、’C’,它这里显示的却是 ‘i’、’C’、’s’、’F’、’h’,在你们的电脑上结果还可能不一样,这就是随机性。
由于集合是无序的,所以我们不能使用下标索引的方式去访问它:

1
2
3
4
5
6
>>> s = set("FishC")
>>> s[0]
Traceback (most recent call last):
File "<pyshell#1>", line 1, in <module>
s[0]
TypeError: 'set' object is not subscriptable

不过我们可以使用 in 和 not in 来判断某个元素是否存在于集合中:
1
2
3
4
>>> 'C' in s
True
>>> 'c' not in s
True

13.3 访问集合

如果想要访问集合中的元素,可以使用迭代的方式:

1
2
3
4
5
6
7
8
>>> for each in s:
... print(each)
...
F
h
i
s
C

13.4 集合必杀技 —— 去重

集合另外一个特点就是唯一性,小甲鱼本鱼觉得,这也是集合最大的优势。比如利用集合,咱们就可以轻松地实现去重的操作:

1
2
>>> set([1, 1, 2, 3, 5])
{1, 2, 3, 5}

在实际开发中,我们经常需要去检测一个列表中是否存在相同的元素?
那么在没有学习过集合之前,我们很有可能需要通过迭代来统计每个元素出现的次数,从而判断是否唯一……
但是,现在,咱们只需要这么写:
1
2
3
4
5
6
>>> s = [1, 1, 2, 3, 5]
>>> len(s) == len(set(s))
False
>>> s = [1, 2, 3, 5]
>>> len(s) == len(set(s))
True

13.5 集合的方法

集合的各种方法大合集
列表、元组、字符串、字典它们都有一个 copy() 方法,那么集合也不例外:

1
2
3
>>> t = s.copy()
>>> t
{'h', 's', 'i', 'F', 'C'}

如果我们要检测两个集合之间是否毫不相干,可以使用 isdisjoint(other) 方法:

1
2
3
4
>>> s.isdisjoint(set("Python"))
False
>>> s.isdisjoint(set("JAVA"))
True

那么这个参数它并不要求必须是集合类型,可以是任何一种可迭代对象:

1
2
3
4
>>> s.isdisjoint("Python")
False
>>> s.isdisjoint("JAVA")
True

下面也是一样的,传入的参数,都只要求是可迭代对象的类型即可。
如果我们要检测该集合是否为另一个集合的子集,可以使用 issubset(other) 方法:
1
2
>>> s.issubset("FishC.com.cn")
True

如果我们要检测该集合是否为另一个集合的超集,可以使用 issuperset(other) 方法(对于两个集合 A、B,如果集合 B 中任意一个元素都是集合 A 中的元素,我们就说这两个集合有包含关系,称集合 A 为集合 B 的超集):
1
2
>>> s.issuperset("Fish")
True

除了检测子集和超集,我们还可以计算当前集合和其它对象共同构造的并集、交集、差集以及对称差集。
并集,就是将集合与其它集合的元素合并在一起,组成一个新的集合:
1
2
>>> s.union({1, 2, 3})
{1, 2, 3, 'h', 's', 'i', 'F', 'C'}

交集,就是找到多个集合之间共同的那些元素
1
2
>>> s.intersection("Fish")
{'h', 's', 'i', 'F'}

差集,就是找出存在于该集合,但不存在于其它集合中的元素:
1
2
>>> s.difference("Fish")
{'C'}

同时,上面的这三个都是支持多个参数的:
1
2
3
4
5
6
>>> s.union({1, 2, 3}, "Python")
{1, 2, 3, 'y', 'h', 'n', 'i', 'P', 's', 'o', 't', 'C', 'F'}
>>> s.intersection("Php", "Python")
{'h'}
>>> s.difference("Php", "Python")
{'s', 'C', 'F', 'i'}

最后一个是求对称差集,就是排除掉 s 集合和 other 容器中共有的元素后,剩余的所有元素,这个只能支持一个参数:
1
2
>>> s.symmetric_difference("Python")
{'t', 'y', 'F', 's', 'P', 'C', 'n', 'o', 'i'}

好了,那么上面这 6 种常见的操作,Python 也提供了相应的运算符,可以直接进行运算。
检测子集可以使用小于等于号(<=):
1
2
>>> s <= set("FishC")
True

那么检测真子集我们可以使用小于号(<):
1
2
3
4
>>> s < set("FishC")
False
>>> s < set("FishC.com.cn")
True

那么反过来,使用大于号(>)和大于等于号(>=)就是检测真超集和超集:
1
2
3
4
>>> s > set("FishC")
False
>>> s >= set("FishC")
True

并集使用管道符(|):
1
2
>>> s | {1, 2, 3} | set("Python")
{1, 2, 3, 'y', 'h', 'n', 'i', 'P', 's', 'o', 't', 'C', 'F'}

交集使用 and 符号(&):
1
2
>>> s & set("Php") & set("Python")
{'h'}

差集使用减号(-):
1
2
>>> s - set("Php") - set("Python")
{'s', 'C', 'F', 'i'}

对称差集使用脱字符(^):
1
2
>>> s ^ set("Python")
{'t', 'y', 'F', 's', 'P', 'C', 'n', 'o', 'i'}

注意:使用运算符的话,符号两边都必须是集合类型的数据才可以,不然会报错。
1
2
3
4
5
6
7
8
9
10
>>> s <= "FishC"
Traceback (most recent call last):
File "<pyshell#46>", line 1, in <module>
s <= "FishC"
TypeError: '<=' not supported between instances of 'set' and 'str'
>>> s | [1, 2, 3]
Traceback (most recent call last):
File "<pyshell#15>", line 1, in <module>
s | [1, 2, 3]
TypeError: unsupported operand type(s) for |: 'set' and 'list'

13.6 冻结的集合

Python 将集合细分为可变和不可变两种对象,前者是 set(),后者是 frozenset():

1
2
3
>>> t = frozenset("FishC")
>>> t
frozenset({'s', 'C', 'i', 'F', 'h'})

被冻结的集合(frozenset())是不支持修改的。
如果我们尝试修改它,那么可怕的事情就会发生:
1
2
3
4
5
>>> t.update([1, 1], "23")
Traceback (most recent call last):
File "<pyshell#6>", line 1, in <module>
t.update([1, 1], "23")
AttributeError: 'frozenset' object has no attribute 'update'

13.7 仅适用于 set() 对象的方法

update(*others) 方法使用 others 容器中的元素来更新集合:

1
2
3
4
5
6
>>> s = set("FishC")
>>> s
{'s', 'C', 'i', 'F', 'h'}
>>> s.update([1, 1], "23")
>>> s
{'s', 1, 'C', 'i', 'F', 'h', '3', '2'}

intersection_update(others)、difference_update(others) 和 symmetric_difference_update(other) 分别是使用前面讲过的交集、差集和对称差集的方式来更新集合:
1
2
3
4
5
6
7
8
9
>>> s.intersection_update("FishC")
>>> s
{'s', 'C', 'i', 'F', 'h'}
>>> s.difference_update("Php", "Python")
>>> s
{'s', 'C', 'i', 'F'}
>>> s.symmetric_difference_update("Python")
>>> s
{'s', 't', 'C', 'o', 'h', 'i', 'y', 'F', 'n', 'P'}

如果希望要单纯地往集合里添加数据,可以使用 add(elem) 方法:
1
2
3
>>> s.add("45")
>>> s
{'s', 't', 'C', 'o', 'h', 'i', 'y', 'F', '45', 'n', 'P'}

在集合中删除某个元素,可以使用 remove(elem) 或者 discard(elem) 方法:
1
2
3
4
5
6
>>> s.remove("瓦迈")
Traceback (most recent call last):
File "<pyshell#15>", line 1, in <module>
s.remove("瓦迈")
KeyError: '瓦迈'
>>> s.discard("瓦迈")

删除还有一个 pop() 方法,用于随机从集合中弹出一个元素:

1
2
3
4
5
6
7
8
>>> s.pop()
's'
>>> s.pop()
't'
>>> s.pop()
'C'
>>> s
{'o', 'h', 'i', 'y', 'F', '45', 'n', 'P'}

最后,clear() 方法就是将集合清空:

1
2
3
>>> s.clear()
>>> s
set()

13.8 可哈希

想要正确地创建字典和集合,是有一个刚性需求的 —— 那就是字典的键,还有集合的元素,它们都必须是可哈希的。
如果一个对象是可哈希的,那么就要求它的哈希值必须在其整个程序的生命周期中都保持不变。
通过 hash() 函数,可以轻松获取一个对象的哈希值:

1
2
3
4
5
6
>>> hash(1)
1
>>> hash(1.0)
1
>>> hash(1.001)
2305843009213441

这个哈希值有什么用呢?
对于我们来说可能没啥用,但对于字典和集合来说,却是 “木之根,水之源”,这里我们就不再展开论述了,再深挖下去就有点越俎代庖的感觉了……
有兴趣的童鞋可以看看这一篇扩展阅读 -> Python字典的实现原理
Python 中大多数不可变对象是可哈希的,而那些可变的容器则不哈希
1
2
3
4
5
6
7
>>> hash("FishC")
2090433017907150752
>>> hash([1, 2, 3])
Traceback (most recent call last):
File "<pyshell#36>", line 1, in <module>
hash([1, 2, 3])
TypeError: unhashable type: 'list'

如果我们把列表换成元组,元组是不可变的对象,那就应该是可哈希的:
1
2
>>> hash((1, 2, 3))
529344067295497451

前面我们说了,只有可哈希的对象,才有资格作为字典的键,以及集合的元素:
1
2
3
4
>>> {"Python":520, "FishC":1314}
{'Python': 520, 'FishC': 1314}
>>> {"Python", "FishC", 520, 1314}
{520, 1314, 'Python', 'FishC'}

13.9 嵌套的集合

如果要实现一个嵌套的集合,可不可行?

1
2
3
4
5
6
>>> x = {1, 2, 3}
>>> y = {x, 4, 5}
Traceback (most recent call last):
File "<pyshell#39>", line 1, in <module>
y = {x, 4, 5}
TypeError: unhashable type: 'set'

这样写是不行的,因为集合它是一个可变的容器,而可变的容器则是不可哈希。
那我们非要将集合嵌套,还有没有办法?
有!
没错,使用 “冰山美人” frozenset() 对象
1
2
3
4
>>> x = frozenset(x)
>>> y = {x, 4, 5}
>>> y
{frozenset({1, 2, 3}), 4, 5}

14 函数

Python 函数的主要作用就是打包代码。

有两个显著的好处:

可以最大程度地实现代码重用,减少冗余的代码
可以将不同功能的代码段进行封装、分解,从而降低结构的复杂度,提高代码的可读性。

14.1 创建和调用函数

我们使用 def 语句来定义函数,紧跟着的是函数的名字,后面带一对小括号,冒号下面就是函数体,函数体是一个代码块,也就是每次调用函数时将被执行的内容:

1
2
3
4
>>> def myfunc():
... pass
...
>>>
  • 注:pass 是一个空语句,表示不做任何事情,经常是被用来做一个占位符使用的。调用这个函数,只需要在名字后面加上一对小括号:
    1
    2
    >>> myfunc()
    >>>

14.3 函数的参数

从调用角度来看,参数可以细分为:形式参数(parameter)和实际参数(argument)。
其中,形式参数是函数定义的时候写的参数名字(比如下面例子中的 name 和 times);实际参数是在调用函数的时候传递进去的值(比如下面例子中的 “Python” 和 5)。

1
2
3
4
5
6
7
8
9
10
>>> def myfunc(name, times):
... for i in range(times):
... print(f"I love {name}.")
...
>>> myfunc("Python", 5)
I love Python.
I love Python.
I love Python.
I love Python.
I love Python.

14.4 函数的返回值

有时候,我们可能需要函数干完活之后能给一个反馈,这在 BIF 函数中也很常见,比如 sum() 函数会返回求和后的结果,len() 函数会返回一个元素的长度,而 list() 函数则会将参数转换为列表后返回……
只需要使用 return 语句,就可以让咱们自己定制的函数实现返回:

1
2
3
4
5
6
>>> def div(x, y):
... z = x / y
... return z
...
>>> div(4, 2)
2.0

最后,如果一个函数没有通过 return 语句返回,它也会自己在执行完函数体中的语句之后,悄悄地返回一个 None 值:
1
2
3
4
5
>>> def myfunc():
... pass
...
>>> print(myfunc())
None

14.5 位置参数

在通常的情况下,实参是按照形参定义的顺序进行传递的:

1
2
3
4
5
6
7
>>> def myfunc(s, vt, o):
... return "".join((o, vt, s))
...
>>> myfunc("我", "打了", "小甲鱼")
'小甲鱼打了我'
>>> myfunc("小甲鱼", "打了", "我")
'我打了小甲鱼'

由于在定义函数的时候,就已经把参数的名字和位置确定了下来,我们将 Python 中这类位置固定的参数称之为位置参数。

14.6 关键字参数

使用关键字参数,我们只需要知道形参的名字就可以:

1
2
>>> myfunc(o="我", vt="打了", s="小甲鱼")
'我打了小甲鱼'

尽管使用关键字参数需要你多敲一些字符,但对于参数特别多的函数,这一招尤其管用。
如果同时使用位置参数和关键字参数,那么使用顺序是需要注意一下的:

1
2
>>> myfunc(o="我", "清蒸", "小甲鱼")
SyntaxError: positional argument follows keyword argument

比如这样就不行了,因为位置参数必须是在关键字参数之前,之间也不行哈。

14.7 默认参数

Python 还允许函数的参数在定义的时候指定默认值,这样以来,在函数调用的时候,如果没有传入实参,那么将采用默认的参数值代替:

1
2
3
4
5
>>> def myfunc(s, vt, o="小甲鱼"):
... return "".join((o, vt, s))
...
>>> myfunc("香蕉", "吃")
'小甲鱼吃香蕉'

默认参数的意义就是当用户没有输入该参数的时候,有一个默认值可以使用,不至于造成错误。
如果用户指定了该参数值,那么默认的值就会被覆盖:
1
2
>>> myfunc("香蕉", "吃", "不二如是")
'不二如是吃香蕉'

这里也有一点是需要注意的,就是如果要使用默认参数,那么应该把它们摆在最后:
1
2
3
4
5
6
7
8
>>> def myfunc(s="苹果", vt, o="小甲鱼"):

SyntaxError: non-default argument follows default argument
>>> def myfunc(vt, s="苹果", o="小甲鱼"):
... return "".join((o, vt, s))
...
>>> myfunc("拱了")
'小甲鱼拱了苹果'

14.8 只能使用位置参数

咱们在使用 help() 函数查看函数文档的时候呢,经常会在函数原型的参数中发现一个斜杠(/),比如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
>>> help(abs)
Help on built-in function abs in module builtins:

abs(x, /)
Return the absolute value of the argument.

>>> help(sum)
Help on built-in function sum in module builtins:

sum(iterable, /, start=0)
Return the sum of a 'start' value (default: 0) plus an iterable of numbers

When the iterable is empty, return the start value.
This function is intended specifically for use with numeric values and may
reject non-numeric types.

这表示斜杠左侧的参数必须传递位置参数,不能是关键字参数,举个例子:
1
2
3
4
5
6
7
>>> abs(-1.5)
1.5
>>> abs(x = -1.5)
Traceback (most recent call last):
File "<pyshell#67>", line 1, in <module>
abs(x = -1.5)
TypeError: abs() takes no keyword arguments

那斜杠右侧的话呢,就随你了:
1
2
3
4
>>> sum([1, 2, 3], start=6)
12
>>> sum([1, 2, 3], 6)
12

14.9 只能使用关键字参数

既然有限制 “只能使用位置参数”,那有没有那种限制 “只能使用关键字参数” 的语法呢?
那就是利用星号(*):

1
2
>>> def abc(a, *, b, c):
... print(a, b, c)

这样,参数 a 既可以是位置参数也可以是关键字参数,但参数 b 和参数 c 就必须是关键字参数,才不会报错:
1
2
3
4
5
6
7
8
9
>>> abc(1, 2, 3)
Traceback (most recent call last):
File "<pyshell#98>", line 1, in <module>
abc(1, 2, 3)
TypeError: abc() takes 1 positional argument but 3 were given
>>> abc(1, b=2, c=3)
1 2 3
>>> abc(a=3, b=2, c=1)
3 2 1

14.10 收集参数

当我们在定义一个函数的时候,假如需要传入的参数的个数是不确定的,按照一般的写法可能需要定义很多个相同的函数然后指定不同的参数个数,这显然是很麻烦的,不能根本解决问题。
为解决这个问题,Python 就推出了收集参数的概念。所谓的收集参数,就是说只指定一个参数,然后允许调用函数时传入任意数量的参数。
定义收集参数其实也很简单,即使在形参的前面加上星号(*)来表示:

1
2
3
4
5
6
7
8
9
10
>>> def myfunc(*args):
... print("有%d个参数。" % len(args))
... print("第2个参数是:%s" % args[1])
...
>>> myfunc("小甲鱼", "不二如是")
有2个参数。
第2个参数是:不二如是
>>> myfunc(1, 2, 3, 4, 5)
有5个参数。
第二个参数是:2

如果在收集参数后面还需要指定其它参数,那么在调用函数的时候就应该使用关键参数来指定后面的参数:

1
2
3
4
5
>>> def myfunc(*args, a, b):
... print(args, a, b)
...
>>> myfunc(1, 2, 3, a=4, b=5)
(1, 2, 3) 4 5

对于这种情况,在传递参数的时候就必须要使用关键字参数了,因为字典的元素都是键值对嘛,所以等号(=)左侧是键,右侧是值:

1
2
>>> myfunc(a=1, b=2, c=3)
{'a': 1, 'b': 2, 'c': 3}

混合起来使用就更加灵活了:
1
2
3
4
5
>>> def myfunc(a, *b, **c):
... print(a, b, c)
...
>>> myfunc(1, 2, 3, 4, x=5, y=6)
1 (2, 3, 4) {'x': 5, 'y': 6}

14.11 解包参数

这一个星号()和两个星号(*)不仅可以用在函数定义的时候,在函数调用的时候也有特殊效果,在形参上使用称之为参数的打包,在实参上的使用,则起到了相反的效果,即解包参数:

1
2
3
4
5
6
>>> args = (1, 2, 3, 4)
>>> def myfunc(a, b, c, d):
... print(a, b, c, d)
...
>>> myfunc(*args)
1 2 3 4

那么两个星号(
)对应的是关键字参数:
1
2
3
>>> args = {'a':1, 'b':2, 'c':3, 'd':4}
>>> myfunc(**args)
1 2 3 4

14.12 局部作用域

如果一个变量定义的位置是在一个函数里面,那么它的作用域就仅限于函数中,我们将它称为局部变量。

1
2
3
4
5
6
>>> def myfunc():
... x = 520
... print(x)
...
>>> myfunc()
520

变量 x 是在函数 myfunc() 中定义的,所以它的作用域仅限于该函数,如果我们尝试在函数的外部访问这个变量,那么就会报错:
1
2
3
4
5
>>> print(x)
Traceback (most recent call last):
File "<pyshell#5>", line 1, in <module>
print(x)
NameError: name 'x' is not defined

14.13 全局作用域

如果是在任何函数的外部去定义一个变量,那么它的作用域就是全局的,我们也将其称为全局变量:

1
2
3
4
5
6
>>> x = 880
>>> def myfunc():
... print(x)
...
>>> myfunc()
880

如果在函数中存在一个跟全局变量同名的局部变量,会发生什么样的事情呢?
在函数中,局部变量就会覆盖同名的全局变量的值:
1
2
3
4
5
6
7
8
9
>>> x = 880
>>> def myfunc():
... x = 520
... print(x)
...
>>> myfunc()
520
>>> print(x)
880

注意:代码中两个 x 并非同一个变量,只是由于作用域不同,它们同名但并不同样。

14.14 global 语句

通常我们无法在函数内部修改全局变量的值,除非使用 global 语句破除限制:

1
2
3
4
5
6
7
8
9
10
>>> x = 880
>>> def myfunc():
... global x
... x = 520
... print(x)
...
>>> myfunc()
520
>>> print(x)
520

14.15 嵌套函数

函数也是可以嵌套的:

1
2
3
4
5
6
>>> def funA():
... x = 520
... def funB():
... x = 880
... print("In funB, x =", x)
... print("In funA, x =", x)

在外部函数 funA() 里面嵌套了一个内部函数 funB(),那么这个内部函数是无法被直接调用的:
1
2
3
4
5
>>> funB()
Traceback (most recent call last):
File "<pyshell#23>", line 1, in <module>
funB()
NameError: name 'funB' is not defined

想要调用 funB(),必须得通过 funA():
1
2
3
4
5
6
7
8
9
10
11
>>> def funA():
... x = 520
... def funB():
... x = 880
... print("In funB, x =", x)
... funB()
... print("In funA, x =", x)
...
>>> funA()
In funB, x = 880
In funA, x = 520

14.16 nonlocal 语句

通常我们无法在嵌套函数的内部修改外部函数变量的值,除非使用 nonlocal 语句破除限制:

1
2
3
4
5
6
7
8
9
10
11
12
>>> def funA():
... x = 520
... def funB():
... nonlocal x
... x = 880
... print("In funB, x =", x)
... funB()
... print("In funA, x =", x)
...
>>> funA()
In funB, x = 880
In funA, x = 880

14.17 LEGB 规则

只要记住 LEGB,那么就相当于掌握了 Python 变量的解析机制。

其中:

  • L 是 Local,是局部作用域
  • E 是 Enclosed,是嵌套函数的外层函数作用域
  • G 是 Global,是全局作用域
  • B 是 Build-In,也就是内置作用域

最后一个是 B,也就是 Build-In,最没地位的那一个。

比如说 Build-In Function —— BIF,你只要起一个变量名跟它一样,那么就足以把这个内置函数给 “毁了”:

1
2
3
4
5
6
>>> str = "小甲鱼把str给毁了"
>>> str(520)
Traceback (most recent call last):
File "<pyshell#1>", line 1, in <module>
str(520)
TypeError: 'str' object is not callable

是不是,它本来的功能是将参数转换成字符串类型,但由于我们将它作为变量名赋值了,那么 Python 就把它给覆盖了:
1
2
>>> str
'小甲鱼把str给毁了'

14.18 嵌套作用域的特性

对于嵌套函数来说,外层函数的作用域是会通过某种形式保存下来的,它并不会跟局部作用域那样,调用完就消失。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
>>> def funA():
... x = 520
... def funB():
... print(x)
... return funB
>>> funA()
<function funA.<locals>.funB at 0x0000014C02684040>
>>> funA()()
520
>>> funny = funA()
>>> funny
<function funA.<locals>.funB at 0x0000014C02684550>
>>> funny()
520

14.19 闭包

所谓闭包(closure),也有人称之为工厂函数(factory function)。

for example:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
>>> def power(exp):
... def exp_of(base):
... return base ** exp
... return exp_of
...
>>> square = power(2)
>>> cube = power(3)
>>> square
<function power.<locals>.exp_of at 0x000001CF6A1FAF70>
>>> square(2)
4
>>> square(5)
25
>>> cube(2)
8
>>> cube(5)
125

这里 power() 函数就像是一个工厂,由于参数不同,得到了两个不同的 “生产线”,一个是 square(),一个是 cube(),前者是返回参数的平方,后者是返回参数的立方。

14.20 闭包应用举例

比如说在游戏开发中,我们需要将游戏中角色的移动位置保护起来,不希望被其他函数轻易就能够修改,所以我们就可以利用闭包:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
origin = (0, 0)        # 这个是原点
legal_x = [-100, 100] # 限定x轴的移动范围
legal_y = [-100, 100] # 限定y轴的移动范围
# 好,接着我们定义一个create()函数
# 初始化位置是原点
def create(pos_x=0, pos_y=0):
# 然后我们定义一个实现角色移动的函数moving()
def moving(direction, step):
# direction参数设置方向,1为向右或向上,-1为向左或向下,如果是0则不移动
# step参数是设置移动的距离
# 为了修改外层作用域的变量
nonlocal pos_x, pos_y
# 然后我们真的就去修改它们
new_x = pos_x + direction[0] * step
new_y = pos_y + direction[1] * step
# 检查移动后是否超出x轴的边界
if new_x < legal_x[0]:
# 制造一个撞墙反弹的效果
pos_x = legal_x[0] - (new_x - legal_x[0])
elif new_x > legal_x[1]:
pos_x = legal_x[1] - (new_x - legal_x[1])
else:
pos_x = new_x
# 检查移动后是否超出y轴边界
if new_y < legal_y[0]:
pos_y = legal_y[0] - (new_y - legal_y[0])
elif new_y > legal_y[1]:
pos_y = legal_y[1] - (new_y - legal_y[1])
else:
pos_y = new_y
# 将最终修改后的位置作为结果返回
return pos_x, pos_y
# 外层函数返回内层函数的引用
return moving

程序实现如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
>>> move = create()
>>> print("向右移动20步后,位置是:", move([1, 0], 20))
向右移动20步后,位置是: (20, 0)
>>> print("向上移动120步后,位置是:", move([0, 1], 120))
向上移动120步后,位置是: (20, 80)
>>> print("向左移动66步后,位置是:", move([-1, 0], 66))
向左移动66步后,位置是: (-46, 80)
>>> print("向右下角移动88步后,位置是:", move([1, -1]), 88)
Traceback (most recent call last):
File "<pyshell#28>", line 1, in <module>
print("向右下角移动88步后,位置是:", move([1, -1]), 88)
TypeError: moving() missing 1 required positional argument: 'step'
>>> print("向右下角移动88步后,位置是:", move([1, -1], 88))
向右下角移动88步后,位置是: (42, -8)

14.21 装饰器

装饰器本质上也是一个函数,它可以让其他函数在不需要做任何代码变动的前提下增加额外的功能。

请看下面代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import time

def time_master(func):
def call_func():
print("开始运行程序...")
start = time.time()
func()
stop = time.time()
print("结束程序运行...")
print(f"一共耗费了 {(stop-start):.2f} 秒。")
return call_func

@time_master
def myfunc():
time.sleep(2)
print("I love FishC.")

myfunc()

程序实现如下:
1
2
3
4
开始运行程序...
I love FishC.
结束程序运行...
一共耗费了 2.01 秒

使用了装饰器,我们并不需要修改原来的代码,只需要在函数的上方加上一个 @time_master,然后函数就能够实现统计运行时间的功能了。

这个 @加上装饰器名字其实是个语法糖,装饰器原本的样子应该这么调用的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import time

def time_master(func):
def call_func():
print("开始运行程序...")
start = time.time()
func()
stop = time.time()
print("结束程序运行...")
print(f"一共耗费了 {(stop-start):.2f} 秒。")
return call_func

def myfunc():
time.sleep(2)
print("I love FishC.")

myfunc = time_master(myfunc)
myfunc()

这个就是装饰器的实现原理啦~

多个装饰器也可以用在同一个函数上:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
def add(func):
def inner():
x = func()
return x + 1
return inner

def cube(func):
def inner():
x = func()
return x * x * x
return inner

def square(func):
def inner():
x = func()
return x * x
return inner

@add
@cube
@square
def test():
return 2

print(test())

程序实现如下:
65

这样的话,就是先计算平方(square 装饰器),再计算立方(cube 装饰器),最后再加 1(add 装饰器)。

如何给装饰器传递参数呢?
答案是添加多一层嵌套函数来传递参数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import time

def logger(msg):
def time_master(func):
def call_func():
start = time.time()
func()
stop = time.time()
print(f"[{msg}]一共耗费了 {(stop-start):.2f}")
return call_func
return time_master

@logger(msg="A")
def funA():
time.sleep(1)
print("正在调用funA...")

@logger(msg="B")
def funB():
time.sleep(1)
print("正在调用funB...")

funA()
funB()

程序实现如下:

1
2
3
4
正在调用funA...
[A]一共耗费了 1.01
正在调用funB...
[B]一共耗费了 1.04

我们将语法糖去掉,拆解成原来的样子,你就知道原理了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
import time

def logger(msg):
def time_master(func):
def call_func():
start = time.time()
func()
stop = time.time()
print(f"[{msg}]一共耗费了 {(stop-start):.2f}")
return call_func
return time_master

def funA():
time.sleep(1)
print("正在调用funA...")

def funB():
time.sleep(1)
print("正在调用funB...")

funA = logger(msg="A")(funA)
funB = logger(msg="B")(funB)

funA()
funB()

程序实现如下:
1
2
3
4
正在调用funA...
[A]一共耗费了 1.02
正在调用funB...
[B]一共耗费了 1.01

这里其实就是给它裹多一层嵌套函数上去,然后通过最外层的这个函数来传递装饰器的参数。
这样,logger(msg=”A”) 得到的是 timemaster() 函数的引用,然后再调用一次,并传入 funA,也就是这个 logger(msg=”A”)(funA),得到的就是 call_func() 函数的引用,最后将它赋值回 funA()。

咱们对比一下没有参数的描述器,这里其实就是添加了一次调用,然后通过这次调用将参数给传递进去而已。

14.22.1 lambda 表达式

lambda 表达式,也就是大牛们津津乐道的匿名函数。
只要掌握了 lambda 表达式,你也就掌握了一行流代码的核心 —— 仅使用一行代码,就能解决一件看起来相当复杂的事情。
它的语法是这样的:

lambda arg1, arg2, arg3, ... argN : expression

lambda 是个关键字,然后是冒号,冒号左边是传入函数的参数,冒号后边是函数实现表达式以及返回值。
我们可以将 lambda 表达式的语法理解为一个极致精简之后的函数,如果使用传统的函数定义方式,应该是这样:

1
2
def <lambda>(arg1, arg2, arg3, ... argN):
... return expression

如果要求我们编写一个函数,让它求出传入参数的平方值,以前我们这么写:

1
2
3
4
5
>>> def squareX(x):
... return x * x
...
>>> squareX(3)
9

现在我们这么写:

1
2
3
>>> squareY = lambda y : y * y
>>> squareY(3)
9

传统定义的函数,函数名就是一个函数的引用:

1
2
>>> squareX
<function squareX at 0x0000015E06668F70>

而 lambda 表达式,整个表达式就是一个函数的引用:

1
2
>>> squareY
<function <lambda> at 0x0000015E06749EE0>

14.22.2lambda 表达式的优势

lambda 是一个表达式,因此它可以用在常规函数不可能存在的地方:

1
2
3
4
5
>>> y = [lambda x : x * x, 2, 3]
>>> y[0](y[1])
4
>>> y[0](y[2])
9

注意:这里说的是将整个函数的定义过程都放到列表中哦~

14.22.3 与 map() 和 filter() 函数搭配使用

利用 lambda 表达式与 map() 和 filter() 函数搭配使用,会使代码显得更加 Pythonic:

1
2
3
4
>>>  list(mapped = map(lambda x : ord(x) + 10, "FishC"))
[80, 115, 125, 114, 77]
>>> list(filter(lambda x : x % 2, range(10)))
[1, 3, 5, 7, 9]

14.22.4 总结

lambda 是一个表达式,而非语句,所以它能够出现在 Python 语法不允许 def 语句出现的地方,这是它的最大优势。
但由于所有的功能代码都局限在一个表达式中实现,因此,lambda 通常只能实现那些较为简单的需求。
当然,Python 肯定是有意这么设计的,让 lambda 去做那些简单的事情,我们就不用花心思去考虑这个函数叫什么,那个函数叫什么……def 语句则负责用于定义功能复杂的函数,去处理那些复杂的工作。

14.23 生成器

在 Python 中,使用了 yield 语句的函数被称为生成器(generator)。

与普通函数不同的是,生成器是一个返回生成器对象的函数,它只能用于进行迭代操作,更简单的理解是 —— 生成器就是一个特殊的迭代器。在调用生成器运行的过程中,每次遇到 yield 时函数会暂停并保存当前所有的运行信息,返回 yield 的值, 并在下一次执行 yield 方法时从当前位置继续运行。定义一个生成器,很简单,就是在函数中,使用 yield 表达式代替 return 语句即可。
举个例子:

1
2
3
4
5
>>> def counter():
... i = 0
... while i <= 5:
... yield i
... i += 1

现在我们调用 counter() 函数,得到的不是一个返回值,而是一个生成器对象:

1
2
>>> counter()
<generator object counter at 0x0000025835D0D5F0>

我们可以把它放到一个 for 语句中:

1
2
3
4
5
6
7
8
9
>>> for i in counter():
... print(i)
...
0
1
2
3
4
5

注意:生成器不像列表、元组这些可迭代对象,你可以把生成器看作是一个制作机器,它的作用就是每调用一次提供一个数据,并且会记住当时的状态。而列表、元组这些可迭代对象是容器,它们里面存放着早已准备好的数据。

生成器可以看作是一种特殊的迭代器,因为它首先是 “不走回头路”,第二是支持 next() 函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
>>> c = counter()
>>> next(c)
0
>>> next(c)
1
>>> next(c)
2
>>> next(c)
3
>>> next(c)
4
>>> next(c)
5
next(c)
Traceback (most recent call last):
File "<pyshell#51>", line 1, in <module>
next(c)
StopIteration

当没有任何元素产出的时候,它就会抛出一个 “StopIteration” 异常。
由于生成器每调用一次获取一个结果这样的特性,导致生成器对象是无法使用下标索引这样的随机访问方式:

1
2
3
4
5
6
>>> c = counter()
>>> c[2]
Traceback (most recent call last):
File "<pyshell#53>", line 1, in <module>
c[2]
TypeError: 'generator' object is not subscriptable

使用生成器来求出斐波那契数列:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
>>> def fib():
... back1, back2 = 0, 1
... while True:
... yield back1
... back1, back2 = back2, back1 + back2
...
>>> f = fib()
>>> next(f)
0
>>> next(f)
1
>>> next(f)
1
>>> next(f)
2
>>> next(f)
3
>>> next(f)
5
>>> next(f)
8
>>> next(f)
13
>>> next(f)
21

只要我们调用 next(f),就可以继续生成一个新的斐波那契数,由于我们在函数中没有设置结束条件,那么这样我们就得到了一个永恒的斐波那契数列生成器,薪火相传、生生不息。

14.24 生成器表达式

其实在前面讲解元组的时候,小甲鱼就给大家预告了这一章节的到来。
因为列表有推导式,元组则没有,如果非要这么写:

1
2
>>> (i ** 2 for i in range(10))
<generator object <genexpr> at 0x0000019A976CC5F0>

那么我们可以看到,它其实就是得到一个生成器嘛:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
>>> t = (i ** 2 for i in range(10))
>>> next(t)
0
>>> next(t)
1
>>> next(t)
4
>>> next(t)
9
>>> next(t)
16
>>> for i in t:
... print(i)
...
25
36
49
64
81

这种利用推导的形式获取生成器的方法,我们称之为生成器表达式。

14.25 递归

递归就是就是函数调用自身的过程,举个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
>>> def funC():
... print("AWBDYL")
... funC()
...
>>> funC()
AWBDYL
AWBDYL
AWBDYL
AWBDYL
AWBDYL
AWBDYL
AWBDYL
AWBDYL
AWBDYL
AWBDYL
...

上面代码会持续输出 “AWBDYL”,直到你把 IDLE 关闭或者使用 Ctrl + c 快捷键强制中断执行。
加上一个条件判断语句,让递归在恰当的时候进行回归,那么失控的局面就得到了控制:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
>>> def funC(i):
... if i > 0:
... print("AWBDYL")
... i -= 1
... funC(i)
...
>>> funC(10)
AWBDYL
AWBDYL
AWBDYL
AWBDYL
AWBDYL
AWBDYL
AWBDYL
AWBDYL
AWBDYL
AWBDYL

再次强调一下,要让递归正常工作,必须要有一个结束条件,并且每次调用都将向着这个结束条件推进。

14.25.1 使用递归求一个数的阶乘

一个正整数的阶乘,是指所有小于及等于该数的正整数的积,所以 5 的阶乘是 1×2×3×4×5,结果等于 120。
我们先来试试迭代的实现方法:

1
2
3
4
5
6
7
8
9
10
>>> def factIter(n):
... result = n
... for i in range(1, n):
... result *= i
... return result
...
>>> factIter(5)
120
>>> factIter(10)
3628800

那么递归来实现的话,代码则是像下面这样:

1
2
3
4
5
6
7
8
9
10
>>> def factRecur(n):
... if n == 1:
... return 1
... else:
... return n * factRecur(n-1)
...
>>> factRecur(5)
120
>>> factRecur(10)
3628800

14.25.2 使用递归求斐波那契数列

斐波那契数列由 0 和 1 开始,之后的斐波那契数就是由之前的两数相加而得出。

首几个斐波那契数是:
1、 1、 2、 3、 5、 8、 13、 21、 34、 55、 89、 144、 233、 377、 610、 987……
我们先来试试迭代的实现方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
>>> def fibIter(n):
... a = 1
... b = 1
... c = 1
... while n > 2:
... c = a + b
... a = b
... b = c
... n -= 1
... return c
...
>>> fibIter(12)
144

如果使用递归来实现,代码就是这样的:

1
2
3
4
5
6
7
8
>>> def fibRecur(n):
... if n == 1 or n == 2:
... return 1
... else:
... return fibRecur(n-1) + fibRecur(n-2)
...
>>> fibRecur(12)
144

14.26.1 汉诺塔的故事

汉诺塔其实是 1883 年的时候,由法国数学家卢卡斯发明的。不过这个游戏呢,与一个古老的印度传说有关:据说在世界中心贝拿勒斯的圣庙里边,有一块黄铜板,上边插着三根宝针。印度教的主神梵天在创造世界的时候,在其中一根针上从下到上地穿好了由大到小的 64 片金片,这就是所谓的汉诺塔原型。然后不论白天还是黑夜,总有一个僧侣按照下面的规则来移动这些金片:“一次只移动一片,不管在哪根针上,小片必须在大片上面。”另外僧侣们预言,当所有的金片都从梵天穿好的那根针上移到另外一根针上时,世界就将在一声霹雳中消灭,而梵塔、 和众生也都将同归于尽。

16.26.2 汉诺塔玩法分解

对于游戏的玩法,我们可以简单分解为三个步骤:

  1. 将顶上的 63 个金片从 A 移动到 B
  2. 将最底下的第 64 个金片从 A 移动到 C
  3. 将 B 上的 63 个金片移动到 C

看着跟没说一样……
那么先让我们把难度简化为婴儿等级 —— 3 个金片:

  1. 将顶上的 2 个金片从 A 移动到 B
  2. 将最底下的第 3 个金片从 A 移动到 C
  3. 将 B 上的 2 个金片移动到 C
    第 2 个步骤仍然是一步到位,难点就在于第 1 和第 3 个步骤,不过难度经过降级之后,我们可以简单看出:
    第 1 个步骤只需要借助 C,就可以将两个金片从 A 移到 B,第 3 个步骤只需要借助 A,就可以将 2 个金片从 B 移到 C。
    于是:
    1.将顶上的 2 个金片从 A 移动到 B 上,确保大片在小片下方
  • 将顶上的 1 个金片从 A 移到 C 上
  • 将底下的 1 个金片从 A 移到 B 上
  • 将 C 上的 1 个金片移动到 B 上
    2.将最底下的第 3 个金片从 A 移动到 C 上
    3.将 B 上的 2 个金片移动到 C 上
  • 将顶上的 1 个金片从 B 移到 A 上
  • 将底下的 1 个金片从 B 移到 C 上
  • 将 A 上的 1 个金片移动到 C 上

16.26.3 汉诺塔代码实现

1
2
3
4
5
6
7
8
9
10
def hanoi(n, x, y, z):
if n == 1:
print(x, '-->', z) # 如果只有 1 层,直接将金片从 x 移动到 z
else:
hanoi(n-1, x, z, y) # 将 x 上的 n-1 个金片移动到 y
print(x, '-->', z) # 将最底下的金片从 x 移动到 z
hanoi(n-1, y, x, z) # 将 y 上的 n-1 个金片移动到 z

n = int(input('请输入汉诺塔的层数:'))
hanoi(n, 'A', 'B', 'C')

16.27 函数文档

使用help()函数,我们可以快速查看到一个函数的使用文档:

1
2
3
4
5
6
7
8
9
10
11
12
>>> help(print)
Help on built-in function print in module builtins:

print(...)
print(value, ..., sep=' ', end='\n', file=sys.stdout, flush=False)

Prints the values to a stream, or to sys.stdout by default.
Optional keyword arguments:
file: a file-like object (stream); defaults to the current sys.stdout.
sep: string inserted between values, default a space.
end: string appended after the last value, default a newline.
flush: whether to forcibly flush the stream.

创建函数文档非常简单,使用字符串就可以了,举个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
>>> def exchange(dollar, rate=7.28):
... """
... 功能:汇率转换,美元 -> 人民币
... 参数:
... - dollar 美元数量
... - rate 汇率,默认值是7.28(2022-08-29)
... 返回值:
... - 人民币的数量
... """
... return dollar * rate
...
>>> exchange(20)
145.6

  • 注意:函数文档一定是在函数的最顶部。我们可以看到,函数开头的几行字符串并不会被打印出来,但它将作为函数的文档被保存起来。
    现在通过 help() 函数,就可以查看到 exchange() 的文档了:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    >>> help(exchange)
    Help on function exchange in module __main__:

    exchange(dollar, rate=7.28)
    功能:汇率转换,美元 -> 人民币
    参数:
    - dollar 美元数量
    - rate 汇率,默认值是7.28(2022-08-29)
    返回值:
    - 人民币的数量

    >>> def times(s:str, n:int) -> str:
    ... return s * n

16.28 类型注释

有时候,你可能会看到这样的代码:

1
2
>>> def times(s:str, n:int) -> str:
... return s * n

其实这里面多出来的东东,就是 Python 的类型注释啦~
比如上面代码表示该函数的作者,希望调用者传入到 s 参数的是字符串类型,传入到 n 参数的是整数类型,最后还告诉我们函数将会返回一个字符串类型的返回值:

1
2
>>> times("FishC", 5)
'FishCFishCFishCFishCFishC'

当然,这只不过是函数作者的寄望,如果调用者非要胡来,Python 也是不会出面阻止的:

1
2
>>> times(5, 5)
25

因为这只是类型注释,是给人看的,不是给机器看的哈。
如果需要使用默认参数,那么类型注释可以这么写:

1
2
3
4
5
>>> def times(s:str = "FishC", n:int = 5) -> str:
... return s * n
...
>>> times()
'FishCFishCFishCFishCFishC'

如果期望的参数类型是列表,可以这么写:

1
2
3
4
5
>>> def times(s:list, n:int = 5) -> list:
... return s * n
...
>>> times([1, 2, 3], 3)
[1, 2, 3, 1, 2, 3, 1, 2, 3]

如果还想更进一步,比如期望参数类型是一个整数列表(也就是列表中所有的元素都是整数),那么代码可以这么写:

1
2
>>> def times(s:list[int], n:int = 5) -> list:
... return s * n

映射类型也可以使用这种方法,比如我们期望字典的键是字符串,值是整数,可以这么写:

1
2
3
4
5
>>> def times(s:dict[str, int], n:int = 5) -> list:
... return list(s.keys()) * n
...
>>> times({'A':1, 'B':2, 'C':3}, 3)
['A', 'B', 'C', 'A', 'B', 'C', 'A', 'B', 'C']

16.29 mypy

Mypy 模块的安装及使用介绍 -> 传送门

16.30 内省

内省,其实最先是心理学的基本研究方法之一,又称为自我观察法。它是发生在内部的,我们自己能够意识到的主观现象。
Python 通过一些特殊的属性来实现内省,比如我们想知道一个函数的名字,可以使用 name

1
2
>>> times.__name__
'times'

使用 ___annotations__ 查看函数的类型注释:

1
2
>>> times.__annotations__
{'s': dict[str, int], 'n': <class 'int'>, 'return': list[str]}

查看函数文档,可以使用 __doc__

1
2
>>> exchange.__doc__
'\n\t功能:汇率转换,美元 -> 人民币\n\t参数:\n\t- dollar 美元数量\n\t- rate 汇率,默认值 6.32(2022-03-07)\n\t返回值:\n\t- 人民币数量\n\t'

阅读不友好,咱们使用 print() 函数给打印一下:

1
2
3
4
5
6
7
8
>>> print(exchange.__doc__)

功能:汇率转换,美元 -> 人民币
参数:
- dollar 美元数量
- rate 汇率,默认值 6.32(2022-03-07)
返回值:
- 人民币数量

16.31.1 高阶函数

在前面的学习中,我们发现,函数是可以被当作变量一样自由使用的,那么当一个函数接收另一个函数作为参数的时候,这种函数就称之为高阶函数。
高阶函数几乎就是函数式编程的灵魂所在,所以 Python 专程为此搞了一个模块 —— functools,这里面包含了非常多实用的高阶函数,以及装饰器。

友情提示,这是好东西,一定要收藏 -> functools — 高阶函数

16.31.2 reduce() 函数

1
2
3
4
5
>>> def add(x, y):
... return x + y
...
>>> functools.reduce(add, [1, 2, 3, 4, 5])
15

它的第一个参数是指定一个函数,这个函数必须接收两个参数,然后第二个参数是一个可迭代对象,reduce() 函数的作用就是将可迭代对象中的元素依次传递到第一个参数指定的函数中,最终返回累积的结果。

其实就相当于这样子:

1
2
>>> add(add(add(add(1, 2), 3), 4), 5)
15

另外,将 reduce() 函数的第一个参数写成 lambda 表达式,代码就更加极客了,比如我们要计算 10 的阶乘,那么可以这么写:

1
2
>>> functools.reduce(lambda x,y:x*y, range(1, 11))
3628800

16.31.3 偏函数(partial function)

偏函数是对指定函数的二次包装,通常是将现有函数的部分参数预先绑定,从而得到一个新的函数,该函数就称为偏函数。

1
2
3
4
5
6
7
8
9
10
>>> square = functools.partial(pow, exp=2)
>>> square(2)
4
>>> square(3)
9
>>> cube = functools.partial(pow, exp=3)
>>> cube(2)
8
>>> cube(3)
27

偏函数的实现原理大致等价于:

1
2
3
4
5
6
7
8
def partial(func, /, *args, **keywords):
def newfunc(*fargs, **fkeywords):
newkeywords = {**keywords, **fkeywords}
return func(*args, *fargs, **newkeywords)
newfunc.func = func
newfunc.args = args
newfunc.keywords = keywords
return newfunc

其实不难发现,它的实现原理就是闭包!
只不过使用偏函数的话更简单了一些,细节实现不用我们去费脑子了,直接拿来就用。

16.31.4 @wraps 装饰器

让我们先回到讲解装饰器时候的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import time

def time_master(func):
def call_func():
print("开始运行程序...")
start = time.time()
func()
stop = time.time()
print("结束程序运行...")
print(f"一共耗费了 {(stop-start):.2f} 秒。")
return call_func

@time_master
def myfunc():
time.sleep(2)
print("I love FishC.")

myfunc()

程序实现如下:

1
2
3
4
开始运行程序...
I love FishC.
结束程序运行...
一共耗费了 2.01 秒

这里的代码呢,其实是有一个 “副作用” 的:
1
2
>>> myfunc.__name__
'call_func'

竟然,myfunc 的名字它不叫 ‘my_func’,而是叫 ‘call_func’……
这个其实就是装饰器的一个副作用,虽然通常情况下用起来影响不大,但大佬的眼睛里哪能容得下沙子,对吧?
所以发明了这个 @wraps 装饰器来装饰装饰器:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import time
import functools

def time_master(func):
@functools.wraps(func)
def call_func():
print("开始运行程序...")
start = time.time()
func()
stop = time.time()
print("结束程序运行...")
print(f"一共耗费了 {(stop-start):.2f} 秒。")
return call_func

@time_master
def myfunc():
time.sleep(2)
print("I love FishC.")

myfunc()

程序实现如下:

1
2
3
4
5
6
开始运行程序...
I love FishC.
结束程序运行...
一共耗费了 2.01 秒
>>> myfunc.__name__
'myfunc'

16.35.1 永久储存

当我们在说 “永久存储” 的时候,是希望将数据保存到硬盘上,而非内存,因为内存在计算机断电后数据将会丢失。

16.35.2 打开文件

使用 Python 打开一个文件,我们需要用到 open() 函数:
>>> f = open("FishC.txt", "w")
第一个参数指定的是文件路径和文件名,这里我们没有添加路径的话,那么默认是将文件创建在 Python 的主文件夹下面,因为执行 IDLE 的程序就放在那里嘛(同样的道理,如果我们在桌面创建一个 test.py 的源文件,然后输入打开文件的代码,那么它就会在桌面创建一个 FishC.txt 的文本文件)。

第二个参数是指定文件的打开模式:
:-|:-|
字符串|含义
‘r’|读取(默认)
‘w’|写入(如果文件已存在则先截断清空文件)
‘x’|排他性创建文件(如果文件已存在则打开失败)
‘a’|追加(如果文件已存在则在末尾追加内容)
‘b’|二进制模式
‘t’|文本模式(默认)
‘+’|更新文件(读取和写入)

16.35.3 文件对象的各种方法大合集

open() 函数成功调用之后,会返回一个文件对象,那么通过这个文件对象,我们就可以往这个文件里面写入数据啦。
文件对象,提供了一系列方法,让你可以对它为所欲为。
文件对象的各种方法大合集 -> 传送门

有两个方法可以将字符串写入到文本对象种,一个是 write(),一个是 writelines():

1
2
>>> f.write("I love Python.")
14

使用 write() 方法,它有一个返回值,就是总共写入到文件对象中的字符个数。
使用 writelines() 方法,则可以将多个字符串同时写入:
1
2
>>> f.writelines(["I love FishC.\n", "I love my wife."])
>>>

注意:虽然 writelines() 方法支持传入多个字符串,但它不会帮你添加换行符,所以我们要自己添加才行。[/n]

16.35.4 关闭文件

我们使用 close() 方法来关闭文件:

1
2
>>> f.close()
>>>

注意,文件对象关闭之后,我们就没办法对它进行操作了。如果想要继续操作文件,那么我们必须重新打开它。

16.36 实用高效的速查手册(大家记得收藏哦)

pathlib 速查手册 -> 传送门

新旧路径处理模块大比拼(pathlib vs os.path)-> 传送门

16.37 pathlib.Path 实用功能讲解

使用 Path 里面的 cwd() 方法来获取当前的工作目录:

1
2
>>> Path.cwd()
WindowsPath('C:/Users/goodb/AppData/Local/Programs/Python/Python39')

创建路径对象:
>>> p = Path('C:/Users/goodb/AppData/Local/Programs/Python/Python39')

使用斜杠 / 直接进行路径拼接:

1
2
3
>>> q = p / "FishC.txt"
>>> q
WindowsPath('C:/Users/goodb/AppData/Local/Programs/Python/Python39/FishC.txt')

使用 is_dir() 方法可以判断一个路径是否为一个文件夹:

1
2
3
4
>>> p.is_dir()
True
>>> q.is_dir()
False

使用 is_file() 方法可以判断一个路径是否为一个文件:

1
2
3
4
>>> p.is_file()
False
>>> q.is_file()
True

通过 exists() 方法测试指定的路径是否真实存在:

1
2
3
4
5
6
>>> p.exists()
True
>>> q.exists()
True
>>> Path("C:/404").exists()
False

使用 name 属性去获取路径的最后一个部分:

1
2
3
4
>>> p.name
'Python39'
>>> q.name
'FishC.txt'

stem 属性用于获取文件名,suffix 属性用于获取文件后缀:

1
2
3
4
>>> q.stem
'FishC'
>>> q.suffix
'.txt'

parent 属性用于获取其父级目录:

1
2
3
4
>>> p.parent
WindowsPath('C:/Users/goodb/AppData/Local/Programs/Python')
>>> q.parent
WindowsPath('C:/Users/goodb/AppData/Local/Programs/Python/Python39')

加个复数,parents,还可以获得其逻辑祖先路径构成的一个不可变序列:

1
2
3
4
5
6
7
8
9
10
11
12
13
>>> p.parents
<WindowsPath.parents>
>>> ps = p.parents
>>> for each in ps:
... print(each)
...
C:\Users\goodb\AppData\Local\Programs\Python
C:\Users\goodb\AppData\Local\Programs
C:\Users\goodb\AppData\Local
C:\Users\goodb\AppData
C:\Users\goodb
C:\Users
C:\

还支持索引:

1
2
3
4
5
6
>>> ps[0]
WindowsPath('C:/Users/goodb/AppData/Local/Programs/Python')
>>> ps[1]
WindowsPath('C:/Users/goodb/AppData/Local/Programs')
>>> ps[2]
WindowsPath('C:/Users/goodb/AppData/Local')

parts 属性将路径的各个组件拆分成元组的形式:

1
2
3
4
>>> p.parts
('C:\\', 'Users', 'goodb', 'AppData', 'Local', 'Programs', 'Python', 'Python39')
>>> q.parts
('C:\\', 'Users', 'goodb', 'AppData', 'Local', 'Programs', 'Python', 'Python39', 'FishC.txt')

最后,还可以查询文件或文件夹的状态信息:

1
2
3
4
>>> p.stat()
os.stat_result(st_mode=16895, st_ino=281474976983758, st_dev=1289007019, st_nlink=1, st_uid=0, st_gid=0, st_size=4096, st_atime=1648462096, st_mtime=1648205377, st_ctime=1605695407)
>>> q.stat()
os.stat_result(st_mode=33206, st_ino=4503599627467517, st_dev=1289007019, st_nlink=1, st_uid=0, st_gid=0, st_size=0, st_atime=1648206152, st_mtime=1648206152, st_ctime=1648205377)

比如这个 st_size 就是文件或文件夹的尺寸信息:

1
2
3
4
>>> p.stat().st_size
4096
>>> q.stat().st_size
0

使用 resolve() 方法可以将相对路径转换为绝对路径:

1
2
3
4
>>> Path('./doc').resolve()
WindowsPath('C:/Users/goodb/AppData/Local/Programs/Python/Python39/Doc')
>>> Path('../FishC').resolve()
WindowsPath('C:/Users/goodb/AppData/Local/Programs/Python/FishC')

最后还可以通过 iterdir() 获取当前路径下面的所有子文件和子文件夹对象:

1
2
>>> p.iterdir()
<generator object Path.iterdir at 0x0000012D57CBE660>

最后还可以通过 iterdir() 获取当前路径下面的所有子文件和子文件夹对象:

1
2
>>> p.iterdir()
<generator object Path.iterdir at 0x0000012D57CBE660>

它生成的是一个迭代器对象,所以可以放到 for 语句中去提取数据:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
>>> for each in p.iterdir():
... print(each.name)
...
DLLs
Doc
FishC
FishC.txt
include
Lib
libs
LICENSE.txt
NEWS.txt
python.exe
python3.dll
python39.dll
pythonw.exe
Scripts
tcl
Tools
vcruntime140.dll
vcruntime140_1.dll

如果我们要将当前路径下面的所有文件整理成一个列表,可以这么做(注意,是文件,不包含文件夹,所以我们要加一个条件过滤):

1
2
>>> [x for x in p.iterdir() if x.is_file()]
[WindowsPath('FishC.txt'), WindowsPath('LICENSE.txt'), WindowsPath('NEWS.txt'), WindowsPath('python.exe'), WindowsPath('python3.dll'), WindowsPath('python39.dll'), WindowsPath('pythonw.exe'), WindowsPath('vcruntime140.dll'), WindowsPath('vcruntime140_1.dll')]

以上是用得比较多的,与路径查询相关的操作。

那么修改路径也是支持的,比如我们可以使用 mkdir() 方法来创建文件夹:

1
2
3
>>> n = p / "FishC"
>>> n.mkdir()
>>>

注意,如果需要创建的文件夹已经存在,那么它就会报错:
1
2
3
4
5
6
7
>>> n.mkdir()
Traceback (most recent call last):
File "<pyshell#16>", line 1, in <module>
n.mkdir()
File "C:\Users\goodb\AppData\Local\Programs\Python\Python39\lib\pathlib.py", line 1323, in mkdir
self._accessor.mkdir(self, mode)
FileExistsError: [WinError 183] 当文件已存在时,无法创建该文件。: 'C:\\Users\\goodb\\AppData\\Local\\Programs\\Python\\Python39\\FishC'

也可以避开这个报错信息,我们设置其 exist_ok 参数的值为 True 即可:
>>> n.mkdir(exist_ok=True)
还有一点需要注意的就是,如果路径中有存在多个不存在的父级目录,那么也会出错的,比如这样:
1
2
3
4
5
6
7
8
>>> n = p / "FishC/A/B/C"
>>> n.mkdir(exist_ok=True)
Traceback (most recent call last):
File "<pyshell#22>", line 1, in <module>
n.mkdir(exist_ok=True)
File "C:\Users\goodb\AppData\Local\Programs\Python\Python39\lib\pathlib.py", line 1323, in mkdir
self._accessor.mkdir(self, mode)
FileNotFoundError: [WinError 3] 系统找不到指定的路径。: 'C:\\Users\\goodb\\AppData\\Local\\Programs\\Python\\Python39\\FishC\\A\\B\\C'

它也定义了一个参数用于对付这种情况,将 parents 参数设置为 True 就可以了:
>>> n.mkdir(parents=True, exist_ok=True)

Path 内部其实还打包了一个 open() 方法,除了不用传入路径之外,其它参数跟 open() 函数是一摸一样的:

1
2
3
4
5
6
7
>>> n = n / 'FishC.txt'
>>> n
WindowsPath('C:/Users/goodb/AppData/Local/Programs/Python/Python39/FishC/A/B/C/FishC.txt')
>>> f = n.open('w')
>>> f.write("I love FishC.")
13
>>> f.close()

可以给文件或文件夹修改名字,使用 rename() 方法来实现:

1
2
>>> n.rename("NewFishC.txt")
WindowsPath('NewFishC.txt')

然后使用 replace() 方法替换文件或文件夹:

1
2
3
4
5
>>> m = Path("NewFishC.txt")
>>> n
WindowsPath('C:/Users/goodb/AppData/Local/Programs/Python/Python39/FishC/A/B/C/FishC.txt')
>>> m.replace(n)
WindowsPath('C:/Users/goodb/AppData/Local/Programs/Python/Python39/FishC/A/B/C/FishC.txt')

还有删除操作,rmdir()unlink() 方法,前者用于删除文件夹,后者用于删除文件:

1
2
3
4
5
6
7
>>> n.parent.rmdir()
Traceback (most recent call last):
File "<pyshell#6>", line 1, in <module>
n.parent.rmdir()
File "C:\Users\goodb\AppData\Local\Programs\Python\Python39\lib\pathlib.py", line 1363, in rmdir
self._accessor.rmdir(self)
OSError: [WinError 145] 目录不是空的。: 'C:\\Users\\goodb\\AppData\\Local\\Programs\\Python\\Python39\\FishC\\A\\B\\C'

可以看到,如果不是空文件夹,它是删不掉的,我们需要先把里面的文件删了:
>>> n.unlink()
现在再删除文件夹,就 OK 啦:
>>> n.parent.rmdir()
最后是功能强大的查找,由 glob() 方法来实现:
1
2
3
>>> p = Path('.')
>>> list(p.glob("*.txt"))
[WindowsPath('FishC.txt'), WindowsPath('LICENSE.txt'), WindowsPath('NEWS.txt')]

这就查找当前目录下的所有 .txt后缀的文件,如果要查找当前目录的下一级目录中的所有 .py 后缀的文件,可以这么写:

1
2
>>> list(p.glob('*/*.py'))
[WindowsPath('Lib/abc.py'), WindowsPath('Lib/aifc.py'), WindowsPath('Lib/antigravity.py'), ...]

好了,那么如果希望进行向下递归搜索,也就是查找当前目录以及该目录下面的所有子目录,可以使用两个星号**表示:

1
2
>>> list(p.glob('**/*.py'))
[WindowsPath('Lib/abc.py'), WindowsPath('Lib/aifc.py'), WindowsPath('Lib/antigravity.py'), ...]

16.37 with 语句和上下文管理器

上下文管理器为文件操作提供了一种更为优雅的实现方式。
我们先来看一下传统的文件操作实现:

1
2
3
4
>>> f = open("FishC.txt", "w")
>>> f.write("I love FishC.")
13
>>> f.close()

总结下来无非就是三板斧:打开文件 -> 操作文件 -> 关闭文件
那么使用 with 上下文管理器方案,应该如何实现呢?

1
2
3
4
>>> with open("FishC.txt", "w") as f:
... f.write("I love FishC.")
...
13

两者是等效的,通俗来讲,对于文件操作这样的三板斧来说,上文就是打开文件,下文就是关闭文件,这个就是上下文管理器做的事情。
使用上下文管理器,最大的优势是能够确保资源的释放(在这里就是文件的正常关闭)。

16.38 pickle

pickle 模块支持你将 Python 的代码序列化,解决的就是一个永久存储 Python 对象的问题。
说白了,就是将咱们的源代码,转变成 0101001 的二进制组合。
掌握 pickle,只需要学习两个函数的用法:一个是 dump(),另一个是 load()。
使用 dump() 函数将数据写入文件中(文件后缀要求是 .pkl):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import pickle

x, y, z = 1, 2, 3
s = "FishC"
l = ["小甲鱼", 520, 3.14]
d = {"one":1, "two":2}

with open("data.pkl", "wb") as f:
pickle.dump(x, f)
pickle.dump(y, f)
pickle.dump(z, f)
pickle.dump(s, f)
pickle.dump(l, f)
pickle.dump(d, f)

使用 load() 函数读取 pickle 文件中的数据:

1
2
3
4
5
6
7
8
9
10
11
import pickle

with open("data.pkl", "rb") as f:
x = pickle.load(f)
y = pickle.load(f)
z = pickle.load(f)
s = pickle.load(f)
l = pickle.load(f)
d = pickle.load(f)

print(x, y, z, s, l, d, sep="\n")

如果觉得反复写很多个 dump() 和 load() 太麻烦了,可以将多个对象打包成元组后再进行序列化:

1
2
3
4
5
...
pickle.dump((x, y, z, s, l, d), f)
...
x, y, z, s, l, d = pickle.load(f)
...

16.39 异常

16.39.1 编程时通常会遇到的两类错误

一类是语法错误,就是不按 Python 规定的语法来写代码,这也是初学者最容易犯的错误,比如:

1
2
>>> print(“I love FishC.”)
SyntaxError: invalid character '“' (U+201C)

另一类错误并非由于语法错误导致的:

1
2
3
4
5
>>> 1 / 0
Traceback (most recent call last):
File "<pyshell#3>", line 1, in <module>
1 / 0
ZeroDivisionError: division by zero

这里虽然 Python 的语法没有错,但由于没有过硬的小学数学知识,同样导致了代码无法正确执行,引发了 ZeroDivisionError 这个异常。

16.39.2 异常机制

Python 通过提供异常机制来识别及响应错误。
Python 的异常机制可以分离出程序中的异常处理代码和正常业务代码,使得程序代码更为优雅,并提高了程序的健壮性。

Python 内置异常大合集 -> 传送门(Python 的所有内置异常,全部都在这里了,大家遇到看不懂的异常信息,直接打开这个网页,然后 Ctrl+F,输入异常的名称就可以)

16.39.3 处理异常

利用 try-except 语句来捕获并处理异常语法如下:

1
2
3
4
try:
检测范围
except [expression [as identifier]]:
异常处理代码

举个例子:

1
2
3
4
5
6
>>> try:
... 1 / 0
... except:
... print("出错了~")
...
出错了~

我们可以在 except 后面指定一个异常:

1
2
3
4
5
6
>>> try:
... 1 / 0
... except ZeroDivisionError:
... print("除数不能为0。")
...
除数不能为0。

后面还有一个可选的 as,这样的话可以将异常的原因给提取出来:
1
2
3
4
5
6
>>> try:
... 1 / 0
... except ZeroDivisionError as e:
... print(e)
...
division by zero

其实就是把冒号后面的那部分异常原因给引用出来。

我们还可以将多个可能出现的异常使用元组的形式给包裹起来:

1
2
3
4
5
6
>>> try:
... 1 / 0
... 520 + "FishC"
... except (ZeroDivisionError, ValueError, TypeError):
... pass
...

在这个代码中,但凡检测到 try 语句中包含这三个异常中的任意一个,都会执行 pass 语句,直接忽略跳过。

最后也可以单独处理不同的异常,使用多个 except 语句就可以了:

1
2
3
4
5
6
7
8
9
10
11
>>> try:
... 1 / 0
... 520 + "FishC"
... except ZeroDivisionError:
... print("除数不能为0。")
... except ValueError:
... print("值不正确。")
... except TypeError:
... print("类型不正确。")
...
除数不能为0。

try-except-else

try-except 还可以跟 else 进行搭配,它的含义就是当 try 语句没有检测到任何异常的情况下,就执行 else 语句的内容:

1
2
3
4
5
6
7
8
>>> try:
... 1 / 0
... except:
... print("逮到了~")
... else:
... print("没逮到~")
...
逮到了~

如果在 try 语句中检测到异常,那么就执行 except 语句的异常处理内容:

1
2
3
4
5
6
7
8
9
>>> try:
... 1 / 1
... except:
... print("逮到了~")
... else:
... print("没逮到~")
...
1.0
没逮到~

try-except-finally

try-except 语句搭配的还有一个 finally,就是说无论异常发生与否,都必须要执行的语句:

1
2
3
4
5
6
7
8
9
10
11
>>> try:
... 1 / 0
... except:
... print("逮到了~")
... else:
... print("没逮到~")
... finally:
... print("逮没逮到都会咯吱一声~")
...
逮到了~
逮没逮到都会咯吱一声~

1
2
3
4
5
6
7
8
9
10
11
12
>>> try:
... 1 / 1
... except:
... print("逮到了~")
... else:
... print("没逮到~")
... finally:
... print("逮没逮到都会咯吱一声~")
...
1.0
没逮到~
逮没逮到都会咯吱一声~

finally 通常是用于执行那些收尾工作,比如关闭文件的操作:

1
2
3
4
5
6
7
>>> try:
... f = open("FishC.txt", "w")
... f.write("I love FishC.")
... except:
... print("出错了~")
... finally:
... f.close()

这样的话,无论 try 语句中是否存在异常,文件都能够正常被关闭。

现在我们的异常处理语法变成了这样:

1
2
3
4
5
6
7
8
9
10
try:
检测范围
except [expression [as identifier]]:
异常处理代码
[except [expression [as identifier]]:
异常处理代码]*
[else:
没有触发异常时执行的代码]
[finally:
收尾工作执行的代码]

或者:
1
2
3
4
try:
检测范围
finally:
收尾工作执行的代码

16.39.4 异常的嵌套

异常也是可以被嵌套的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
>>> try:
... try:
... 520 + "FishC"
... except:
... print("内部异常!")
... 1 / 0
... except:
... print("外部异常!")
... finally:
... print("收尾工作~")
...
内部异常!
外部异常!
收尾工作~

16.39.5 raise 语句

使用 raise 语句,我们可以手动的引发异常:

1
2
3
4
5
>>> raise ValueError("值不正确。")
Traceback (most recent call last):
File "<pyshell#23>", line 1, in <module>
raise ValueError("值不正确。")
ValueError: 值不正确。

注意,你不能够 raise 一个并不存在的异常哈:
1
2
3
4
5
>>> raise FishCError("小甲鱼说你不行你就不行~")
Traceback (most recent call last):
File "<pyshell#31>", line 1, in <module>
raise FishCError("小甲鱼说你不行你就不行~")
NameError: name 'FishCError' is not defined

由于这个 FishCError 未定义,所以是小甲鱼不行,不是你不行~

还有一种叫异常链,在 raise 后面加个 from:

1
2
3
4
5
6
7
8
9
>>> raise ValueError("这样可不行~") from ZeroDivisionError
ZeroDivisionError

The above exception was the direct cause of the following exception:

Traceback (most recent call last):
File "<pyshell#37>", line 1, in <module>
raise ValueError("这样可不行~") from ZeroDivisionError
ValueError: 这样可不行~

16.39.6 assert 语句

assert 语句跟 raise 类似,都是主动引发异常,不过 assert 语句只能引发一个叫 AssertionError 的异常。

这个语句的存在意义,通常是用于代码调试:

1
2
3
4
5
6
7
>>> s = "FishC"
>>> assert s == "FishC" # 得到期待的结果,通过
>>> assert s != "FishC" # 没有得到期待的结果,引发异常
Traceback (most recent call last):
File "<pyshell#72>", line 1, in <module>
assert s != "FishC"
AssertionError

16.39.7 利用异常来实现 goto

有学过 C 语言的同学应该听到过一个叫做 goto 的语句,虽然用的不多,但有时候,有这么一个可以指哪跳哪的功能,可以说是非常方便,比如说要在多个嵌套循环语句里面一把跳出来,就非常方便了……可惜 Python 没有!
但是,小甲鱼今天敢把话题撂在这,那就说明小甲鱼有想法了!

没错,通过异常,我们完全可以实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
>>> try:
... while True:
... while True:
... for i in range(10):
... if i > 3:
... raise
... print(i)
... print("被跳过~")
... print("被跳过~")
... print("被跳过~")
... except:
... print("到这儿来~")
...
0
1
2
3
到这儿来~

16.40 面向对象编程(OOP,Object-Oriented Programming)

所谓的面向对象编程,想要学好它,唯一的捷径就是像造物者一样去思考问题。因为,面向对象最初的灵感就是来源于真实世界:

对象 = 属性(对象的静态特征)+ 方法(所能做的事情)

16.40.1 类和对象

对象诞生之前,需要先创建一个类,再通过类来创造实际的对象。

创建一个类需要用到 class 关键字:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class Turtle:
head = 1
eyes = 2
legs = 4
shell = True

def crawl(self):
print("人们总抱怨我动作慢吞吞的,殊不知如不积硅步,无以至千里的道理。")

def run(self):
print("虽然我行动很慢,但如果遇到危险,还是会夺命狂奔的T_T")

def bite(self):
print("人善被人欺,龟善被人骑,我可是会咬人的!")

def eat(self):
print("谁知盘中餐粒粒皆辛苦,吃得好,不如吃得饱~")

def sleep(self):
print("Zzzz...")

类名的命名方式有一个约定俗成的标准,那就是使用大写字母开头,比如我们这里的 Turtle。
其实所谓的属性,就是写在类里面的变量,方法就是写在类里面的函数(实际上会有一点区别,后面我们会有仔细讲解)。
使用类名搭配上一对小括号,就像调用函数那样,就可以生成一个基于这个类的对象。

t1 就是一个 Turtle 类的对象,也叫实例对象(instance object),它就拥有了这个类所定义的属性和方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
>>> t1 = Turtle()
>>> t1.head
1
>>> t1.legs
4
>>> t1.crawl()
人们总抱怨我动作慢吞吞的,殊不知如不积硅步,无以至千里的道理。
>>> t1.bite()
人善被人欺,龟善被人骑,我可是会咬人的!
>>> t1.sleep()
Zzzz...
>>> t2 = Turtle()
>>> t2.head
1
>>> t2.legs
4
>>> t2.crawl()
人们总抱怨我动作慢吞吞的,殊不知如不积硅步,无以至千里的道理。
>>> t2.bite()
人善被人欺,龟善被人骑,我可是会咬人的!

当对象创建出来之后,我们可以随意修改它的属性值:

1
2
3
4
5
>>> t2.legs = 3
>>> t2.legs
3
>>> t1.legs
4

我们也可以动态的创建一个属性,这跟在字典中添加一个新的键值对一样:

1
2
3
>>> t1.mouth = 1
>>> t1.mouth
1

使用 dir() 函数,可以看到,t1 比 t2 多出了一个 mouth 的属性:

1
2
3
4
>>> dir(t1)
['__class__', '__delattr__', '__dict__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__le__', '__lt__', '__module__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', '__weakref__', 'bite', 'crawl', 'eat', 'eyes', 'head', 'legs', 'mouth', 'run', 'shell', 'sleep']
>>> dir(t2)
['__class__', '__delattr__', '__dict__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__le__', '__lt__', '__module__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', '__weakref__', 'bite', 'crawl', 'eat', 'eyes', 'head', 'legs', 'run', 'shell', 'sleep']

16.40.2 封装

封装,是面向对象编程的三个基本特征之一,另外两个是继承和多态,我们在后续的课程会给大家讲解。
像前面我们定义的 Turtle 类,它就把一个甲鱼的特征属性和行为能力封装到了一起。
当然,只要有时间,我们还可以添加更多的细节,使得这个 Turtle 更像一只真正意义上的甲鱼。

16.40.3 self 是什么?

我们编写一段代码,把 self 给打印出来:

1
2
3
4
5
6
7
>>> class C:
... def get_self(self):
... print(self)
...
>>> c = C()
>>> c.get_self()
<__main__.C object at 0x0000020C981BF0D0>

这是什么?

1
2
3
这就是类 [backcolor=#eee]C[/backcolor] 的实例对象小 [backcolor=#eee]c[/backcolor]:
>>> c
<__main__.C object at 0x0000020C981BF0D0>

原来传递给方法的是对象本身,那为什么要这么做呢?
我们知道,同一个类可以生成无数多个对象,那么当我们在调用类里面的一个方法的时候,Python 如何知道到底是哪个对象在调用呢?

没错,就是通过这个 self 参数传递的信息。
所以,类中的每一个方法,默认的第一个参数都是 self。

16.40.4 继承

Python 的类是支持继承的:它可以使用现有类的所有功能,并在无需重新编写代码的情况下对这些功能进行扩展。
通过继承创建的新类称为 “子类”,被继承的类称为 “父类”、“基类” 或 “超类”。
继承语法是将父类写在子类类名后面的小括号中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
>>> class A:
... x = 520
...
... def hello(self):
... print("你好,我是A~")
...
>>> class B(A):
... pass
...
>>> b = B()
>>> b.x
520
>>> b.hello()
你好,我是A~

基于上面代码的继承关系,类 A 就是父类,类 B 则是子类。

如果在子类 B 里面,存在跟父类 A 一样的属性和方法名,那么子类就会覆盖父类:

1
2
3
4
5
6
7
8
9
10
11
>>> class B(A):
... x = 880
...
... def hello(self):
... print("你好,我是B~")
...
>>> b = B()
>>> b.x
880
>>> b.hello()
你好,我是B~

16.40.5 isinstance() 和 issubclass()

isinstance() 函数用于判断一个对象是否属于某个类。
issubclass() 函数用于判断一个类是否属于某个类的子类。
友情提示:

在还没有学习 isinstance() 函数之前,我们使用 type() 函数判断对象的类型,其实这对 type() 函数来说真有点大材小用了(type 其实是 Python 中的神)。
对于检测对象类型(也就是检测对象所属的类)这件小事来说,使用 isinstance() 函数无疑是更名副其实的!
另外,使用 isinstance() 函数还会将父类考虑进去:

1
2
3
4
5
6
7
8
9
10
11
12
13
>>> class A:
... pass
...
>>> class B(A):
... pass
...
>>> b = B()
>>> isinstance(b, B)
True
>>> isinstance(b, A)
True
>>> type(b)
<class '__main__.B'>

16.40.6 多重继承

Python 的类是支持多重继承的,也就是一个子类同时可以继承多个父类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
>>> class A:
... x = 520
... def hello(self):
... print("你好,我是A~")
...
>>> class B:
... x = 880
... y = 250
... def hello(self):
... print("你好,我是B~")
...
>>> class C(A, B):
... pass
...
>>> issubclass(C, A)
True
>>> issubclass(C, B)
True

如果实例化一个类 C 的对象为 c,那么访问 c.x 和调用 c.hello() 得到的结果分别是:

1
2
3
4
5
6
7
>>> c = C()
>>> c.x
520
>>> c.y
250
>>> c.hello()
你好,我是A~

从例子中可以看出,对于多个父类拥有相同属性和方法的情况,它的访问顺序是按从左到右的。

16.40.7 组合

类的组合跟继承不同,继承是具有上下从属关系,而组合的多个类则是同级关系,下面代码演示的就是类的组合:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
>>> class Turtle:
... def say(self):
... print("不积跬步,无以至千里!")
...
>>> class Cat:
... def say(self):
... print("喵喵喵~")
...
>>> class Dog:
... def say(self):
... print("哟吼,我是一只小狗~")
...
>>> class Garden:
... t = Turtle()
... c = Cat()
... d = Dog()
... def say(self):
... self.t.say()
... self.c.say()
... self.d.say()
...
>>> g = Garden()
>>> g.say()
不积跬步,无以至千里!
喵喵喵~
哟吼,我是一只小狗~

16.40.8 绑定

如16.40.8的、末尾的案例,为什么要加上:self?
如果没有加上self,代码就会报错:

class Garden:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
...     t = Turtle()
... c = Cat()
... d = Dog()
... def say(self):
... t.say()
... c.say()
... d.say()
...
>>> g = Garden()
>>> g.say()
Traceback (most recent call last):
File "<pyshell#27>", line 1, in <module>
g.say()
File "<pyshell#25>", line 6, in say
t.say()
NameError: name 't' is not defined

想要弄清楚这个问题,我们就需要知道这个 self 到底是用来干嘛的?

在讲解类和对象第一节课的最后,这个 self 其实就是实例对象本身,当时我们求证的代码是这么写的:

1
2
3
4
5
6
7
8
9
>>> class C:
... def get_self(self):
... print(self)
...
>>> c = C()
>>> c.get_self()
<__main__.C object at 0x0000029F9E332850>
>>> c
<__main__.C object at 0x0000029F9E332850>

这里利用实例对象调用方法时,会自动传递 self 参数的原理,我们将 self 参数的值打印出来之后,知道它其实就是实例对象本身。
其实呢,这个 self 起到的作用就是俩字 —— 绑定。跟谁绑定?

没错,就是实例对象跟类的方法进行绑定!

因为类的实例对象可以有千千万,但这些实例对象却是共享类里面的方法,所以当我们在调用实例 c.get_self() 的时候,其实际的含义是调用类 C 的 get_self() 方法,并将实例对象作为参数给传递进去,进而实现绑定:

1
2
>>> C.get_self(c)
<__main__.C object at 0x0000029F9E332850>

这个绑定就像是骑共享单车,共享单车就是公共的方法,谁去骑它,那么就需要通过手机扫码绑定(这样它就知道在谁的钱包里扣钱了)。所以,现在大家应该知道绑定的必要性了吧!

16.40.9 只要通过绑定,就可以实现各个对象设置各个对象的属性

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
>>> class C:
... x = 100
... def set_x(self, v):
... self.x = v
...
>>> c1 = C()
>>> c2 = C()
>>> c1.x
100
>>> c2.x
100
>>> c1.set_x(250)
>>> c1.x
250
>>> c2.set_x(520)
>>> c2.x
520
>>> # 注意:如果对象同名属性未设置,会共享使用类的属性。
>>> c3 = C()
>>> c3.x
100
>>> c3.__dict__
{}
>>> c3.set_x(666)
>>> c3.x
666
>>> c3.__dict__
{'x': 666}

16.40.10 一个 “旁门左道” 的小技巧

最小的类,就是只有一个 pass 语句填充的类:

1
2
>>> class C:
... pass

那么这个什么都没有的类,它可以做什么呢?

我们就可以把它当字典来使用:

1
2
3
4
5
6
7
8
9
10
11
12
13
>>> class C:
... pass
...
>>> c = C()
>>> c.x = 250
>>> c.y = "小甲鱼"
>>> c.z = [1, 2, 3]
>>> print(c.x)
250
>>> print(c.y)
小甲鱼
>>> print(c.z)
[1, 2, 3]

也没啥问题,对吧,因为本来类和对象的属性就是通过字典进行存放的嘛~
对比一下,使用字典的话我们得多敲几个字符:

1
2
3
4
5
6
7
8
9
10
>>> d = {}
>>> d['x'] = 250
>>> d['y'] = "小甲鱼"
>>> d['z'] = [1, 2, 3]
>>> print(d['x'])
250
>>> print(d['y'])
小甲鱼
>>> print(d['z'])
[1, 2, 3]

虽然说是有点不按套路出牌,但有时候确实是很好用的。

16.40.11 构造函数(init(self[, …]))

在类中定义 __init__() 方法,可以实现在实例化对象的时候进行个性化定制:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
>>> class C:
... def __init__(self, x, y):
... self.x = x
... self.y = y
... def add(self):
... return self.x + self.y
... def mul(self):
... return self.x * self.y
...
>>> c = C(2, 3)
>>> c.add()
5
>>> c.mul()
6

16.40.12 重写

前面我们在 “继承” 中讲过,如果对于父类的某个属性或方法不满意的话,完全可以重新写一个同名的属性或方法对其进行覆盖。那么这种行为,我们就称之为是子类对父类的重写。
这里,我们可以定义一个新的类 —— D,继承自上面的类 C,然后对 add() 和 mul() 方法进行重写:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
>>> class D(C):
... def __init__(self, x, y, z):
... C.__init__(self, x, y)
... self.z = z
... def add(self):
... return C.add(self) + self.z
... def mul(self):
... return C.mul(self) * self.z
...
>>> d = D(2, 3, 4)
>>> d.add()
9
>>> d.mul()
24

16.40.13 钻石继承

下面代码中,类 B1 和 类 B2 都是继承自同一个父类 A,而类 C 又同时继承自它们,这种继承模式就被称之为钻石继承,或者菱形继承:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
>>> class A:
... def __init__(self):
... print("哈喽,我是A~")
...
>>> class B1(A):
... def __init__(self):
... A.__init__(self)
... print("哈喽,我是B1~")
...
>>> class B2(A):
... def __init__(self):
... A.__init__(self)
... print("哈喽,我是B2~")
...
>>> class C(B1, B2):
... def __init__(self):
... B1.__init__(self)
... B2.__init__(self)
... print("哈喽,我是C~")
...

钻石继承这种模式,一旦处理不好就容易带来问题:
1
2
3
4
5
6
>>> c = C()
哈喽,我是A~
哈喽,我是B1~
哈喽,我是A~
哈喽,我是B2~
哈喽,我是C~

看,“哈喽,我是A~” 这一句竟然打印了 2 次!
也就是说,类 A 的构造函数被调用了 2 次!

怎么解?
看下面~

16.40.14 super() 函数和 MRO 顺序

上面这种通过类名直接访问的做法,是有一个名字的,叫 “调用未绑定的父类方法”。通常使用其实没有多大问题,但是遇到钻石继承嘛,就容易出事儿了~那么其实 Python 还有一个更好的实现方案,就是使用 super() 函数。super() 函数能够在父类中搜索指定的方法,并自动绑定好 self 参数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
>>> class B1(A):
... def __init__(self):
... super().__init__()
... print("哈喽,我是B1~")
...
>>> class B2(A):
... def __init__(self):
... super().__init__()
... print("哈喽,我是B2~")
...
>>> class C(B1, B2):
... def __init__(self):
... super().__init__()
... print("哈喽,我是C~")
...
>>> c = C()
哈喽,我是A~
哈喽,我是B2~
哈喽,我是B1~
哈喽,我是C~

之所以 super() 函数能够有效避免钻石继承带来的问题,是因为它是按照 MRO 顺序去搜索方法,并且自动避免重复调用的问题。

那什么是 MRO 顺序呢?
MRO(Method Resolution Order),翻译过来就是 “方法解析顺序”。

想要查找一个类的 MRO 顺序有两种方法~
一种是通过调用类的 mro() 方法:

1
2
3
4
>>> C.mro()
[<class '__main__.C'>, <class '__main__.B1'>, <class '__main__.B2'>, <class '__main__.A'>, <class 'object'>]
>>> B1.mro()
[<class '__main__.B1'>, <class '__main__.A'>, <class 'object'>]

另一种则是通过 __mro__ 属性:

注:这里大家会看到它们都有一个 <class 'object'>,这是因为 object 是所有类的基类,所以就算你不写,它也是会被隐式地继承。

16.40.15 super() 也非全知全能!

由于 super() 函数是依赖于 MRO 顺序的,但 MRO 的排序方式,经常会让初学者感到迷惑,从而导致 super() 函数常常不能如大家预期那样去工作……

这篇扩展阅读 -> 传送门
大家一定要花点时间研究一下,最好是收藏起来,以后在使用 super() 函数时出现问题,随时可以在这里面找到答案。

16.40.16 Mixin

Mixin 即 Mix-in,翻译过来就是所谓的 “混入” 或者 “乱入”(也有音译为 “迷因”),它是一种设计模式。所谓设计模式,就是利用编程语言已有的特性,针对面向对象开发过程中,反复出现的问题,而设计出来的解决方案。

为了更好地解释什么是 Mixin,我们先来看一段代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class Animal:
def __init__(self, name, age):
self.name = name
self.age = age

def say(self):
print(f"我叫{self.name},今年{self.age}岁。")

class Pig(Animal):
def special(self):
print("我的技能是拱大白菜~")

p = Pig("大肠", 5)
p.say()
p.special()

代码实现如下:
1
2
我叫大肠,今年5岁。
我的技能是拱大白菜~

好,现在由于剧情需要,我们要让大肠起飞……问一下大家,咱们有没有办法在不修改原有类的代码结构的前提下,让大肠,也就是猪,飞起来?
其实仔细思考一下并不难,我们可以写一个类,它的功能就是起飞,然后让 Pig 类去继承它:
1
2
3
4
5
6
7
8
9
10
11
12
13
...
class FlyMixin:
def fly(self):
print("哦豁,我还会飞~")

class Pig(FlyMixin, Animal):
def special(self):
print("我的技能是拱大白菜~")

p = Pig("大肠", 5)
p.say()
p.special()
p.fly()

代码实现如下:
1
2
3
我叫大肠,今年5岁。
我的技能是拱大白菜~
哦豁,我还会飞~

16.40.17 多态

多态在编程中是一个非常重要的概念,它是指同一个运算符、函数或对象在不同的场景下,具有不同的作用效果,这么一个技能。

我们知道加号(+)和乘号(*)运算符在 Python 被广泛使用,但是它们并非只是单一的用途,比如当两边都是数字的时候,它们执行的是算术运算:

1
2
3
4
>>> 3 + 5
8
>>> 3 * 5
15

如果遇到字符串,又会是另外一番面孔:
1
2
3
4
>>> "Py" + "FishC"
'PyFishC'
>>> "FishC" * 3
'FishCFishCFishC'

执行的是拼接和重复~

这种 “见机行事” 的行为,我们就称之为多态。
除了运算符之外,Python 中有一些函数也是支持多态的,比如 len() 函数,它的功能是获取一个对象的长度:

1
2
3
4
5
6
>>> len("FishC")
5
>>> len(["Python", "FishC", "Me"])
3
>>> len({"name":"小甲鱼", "age":"18"})
2

你看,给它传递一个字符串,它帮你统计字符的个数;给它传递一个列表,它帮你统计列表中元素的个数;给它传递一个字典,它计算的是字典中键的个数。这就是函数的多态性。

多态的好处这样就一目了然了,尽管我们的接口是不变的,但它却可以根据不同的对象执行不同的操作。

16.40.18 类继承的多态

Python 允许我们在子类中定义与父类同名的方法。就是如果我们对于父类的某个方法不满意的话,完全是可以在子类中重新定义一个同名的方法进行覆盖,这种做法我们称为重写,这在前面的课程有讲解过了。

重写,其实就是实现类继承的多态:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
>>> class Shape:
... def __init__(self, name):
... self.name = name
... def area(self):
... pass
...
>>> class Square(Shape):
... def __init__(self, length):
... super().__init__("正方形")
... self.length = length
... def area(self):
... return self.length * self.length
...
>>> class Circle(Shape):
... def __init__(self, radius):
... super().__init__("圆形")
... self.radius = radius
... def area(self):
... return 3.14 * self.radius * self.radius
...
>>> class Triangle(Shape):
... def __init__(self, base, height):
... super().__init__("三角形")
... self.base = base
... self.height = height
... def area(self):
... return self.base * self.height / 2
...
>>> s = Square(5)
>>> c = Circle(6)
>>> t = Triangle(3, 4)
>>> s.name
'正方形'
>>> c.name
'圆形'
>>> t.name
'三角形'
>>> s.area()
25
>>> c.area()
113.03999999999999
>>> t.area()
6.0

正方形、圆形、三角形都继承自 Shape 类,但又都重写了构造函数和 area() 方法,这就是多态的体现。

16.40.19 自定义函数的多态

这个简单,直接看代码你们就懂了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
>>> class Cat:
... def __init__(self, name, age):
... self.name = name
... self.age = age
... def intro(self):
... print(f"我是一只沙雕猫咪,我叫{self.name},今年{self.age}岁~")
... def say(self):
... print("mua~")
...
>>> class Dog:
... def __init__(self, name, age):
... self.name = name
... self.age = age
... def intro(self):
... print(f"我是一只小狗,我叫{self.name},今年{self.age}岁~")
... def say(self):
... print("哟吼~")
...
>>> class Pig:
... def __init__(self, name, age):
... self.name = name
... self.age = age
... def intro(self):
... print(f"我是一只小猪,我叫{self.name},今年{self.age}岁~")
... def say(self):
... print("oink~") # 不要问我猪为什么这么叫,我是跟隔壁幼儿园的小朋友学的,oink~
...
>>> c = Cat("web", 4)
>>> d = Dog("布布", 7)
>>> p = Pig("大肠", 5)
>>> def animal(x):
... x.intro()
... x.say()
...
>>> animal(c)
我是一只沙雕猫咪,我叫宝儿,今年3岁~
mua~
>>> animal(d)
我是一只小狗,我叫布布,今年5岁~
哟吼~
>>> animal(p)
我是一只小猪,我叫大肠,今年5岁~
oink~

看,这个 animal() 函数就具有多态性,该函数接收不同对象作为参数,并在不检查其类型的情况下执行其方法。

16.40.20 鸭子类型

鸭子类型(Duck Typing)这个概念来源于美国印第安纳州的诗人詹姆斯·惠特科姆·莱利(James Whitcomb Riley, 1849 ~ 1916)的诗句:

“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.”

什么是鸭子类型?
举个例子,比如我们定义一个自行车:

1
2
3
4
5
6
>>> class Bicycle:
... def intro(self):
... print("我曾经跨过山和大海,也穿过人山人海~")
... def say(self):
... print("都有自行车了,要什么兰博基尼?")
...

自行车这个类,既有 intro() 方法,也有 say() 方法,所以它即便被前面的 animal() 函数所调用,也不会出错:
1
2
3
4
>>> b = Bicycle()
>>> animal(b)
我曾经跨过山和大海,也穿过人山人海~
都有自行车了,要什么兰博基尼?

编程中鸭子类型的概念就是:我们不需要关心对象具体是什么类型,只在乎它的行为方法是否符合要求即可。

16.40.21 私有变量

在大多数面向对象的编程语言中,都存在着私有变量(private variable)的概念,所谓私有变量,就是指通过某种手段,使得对象中的属性或方法无法被外部所访问。

Python 对于私有变量的实现是引入了一种叫 name mangling 的机制(翻译过来叫 “名字改编”、“名称改写” 或者 “名称修饰”),语法是在变量名前面加上两个连续下划线(__):

1
2
3
4
5
6
7
8
9
>>> class C:
... def __init__(self, x):
... self.__x = x
... def set_x(self, x):
... self.__x = x
... def get_x(self):
... print(self.__x)
...
>>> c = C(250)

此时,我们是无法直接通过变量名访问到该变量的:
1
2
3
4
5
>>> c.__x
Traceback (most recent call last):
File "<pyshell#13>", line 1, in <module>
c.__x
AttributeError: 'C' object has no attribute '__x'

想要访问变量的值,就需要使用指定的接口,比如这段代码中的 set_x() 和 get_x() 方法:
1
2
3
4
5
>>> c.get_x()
250
>>> c.set_x(520)
>>> c.get_x()
520

16.40.22 name mangling 机制的实现原理

我们看看 __dict__ 属性里面有啥:

1
2
>>> c.__dict__
{'_C__x': 250}

虽然这里面没有看到 __x,但是,却多了一个 _C__x 的属性对不对?
访问一下试试:
1
2
>>> c._C__x
520

果然如此……这个就是传说中的名字改编术!

做法其实也很简单,就是下横线(_)加上类名,再加上变量的名字。
方法名也是同样的道理:

1
2
3
4
5
6
7
8
9
10
11
12
>>> class D:
... def __func(self):
... print("Hello FishC.")
...
>>> d = D()
>>> d.__func()
Traceback (most recent call last):
File "<pyshell#12>", line 1, in <module>
d.__func()
AttributeError: 'D' object has no attribute '__func'
>>> d._D__func()
Hello FishC.

注意:name mangling 机制是发生在类实例化对象时候的事情,给对象动态添加属性则不会有同样的效果。

16.40.23 效率提升之道

Python 对象存储属性的工作原理 —— 字典 __dict__

1
2
3
4
5
6
7
8
9
>>> class C:
... def __init__(self, x):
... self.x = x
...
>>> c = C(250)
>>> c.x
250
>>> c.__dict__
{'x': 250}

对象动态添加属性,就是将键值对添加到 __dict__ 中:

1
2
3
>>> c.y = 520
>>> c.__dict__
{'x': 250, 'y': 520}

甚至你可以直接通过给字典添加键值对的形式来创建对象的属性:
1
2
3
4
5
>>> c.__dict__['z'] = 666
>>> c.__dict__
{'x': 250, 'y': 520, 'z': 666}
>>> c.z
666

但是,字典高效率的背后是以付出更多存储空间为代价的(字典和集合高效背后的玄机

如果我们明确知道一个类的对象设计出来,就只是需要那么固定的某几个属性,并且不需要有动态添加属性这样的功能,那么利用字典来存放属性,这种空间上的牺牲就是纯纯地浪费!
针对这个情况,Python 专门设计了一个 slots 类属性,避免了利用字典存放属性造成空间上的浪费。
举个例子:

1
2
3
4
5
6
>>> class C:
... __slots__ = ['x', 'y']
... def __init__(self, x):
... self.x = x
...
>>> c = C(250)

这样,我们就创建了一个属性受限制的对象。
访问 __slots__ 中列举的属性是没问题的:
1
2
3
4
5
>>> c.x
250
>>> c.y = 520
>>> c.y
520

如果想要动态地添加一个属性,那就不好意思了:
1
2
3
4
5
>>> c.z = 100
Traceback (most recent call last):
File "<pyshell#27>", line 1, in <module>
c.z = 100
AttributeError: 'C' object has no attribute 'z'

这种限制不仅体现在动态添加属性上,如果在类的内部,想创建一个 __slots__ 不包含的属性,也是不被允许的:

class D:
1
2
3
4
5
6
7
8
9
10
11
12
...     __slots__ = ['x', 'y']
... def __init__(self, x, y, z):
... self.x = x
... self.y = y
... self.z = z
>>> d = D(3, 4, 5)
Traceback (most recent call last):
File "<pyshell#8>", line 1, in <module>
d = D(3, 4, 5)
File "<pyshell#6>", line 6, in __init__
self.z = z
AttributeError: 'D' object has no attribute 'z'

甚至是 __dict__ 属性,也不存在了:
1
2
3
4
5
>>> d.__dict__
Traceback (most recent call last):
File "<pyshell#23>", line 1, in <module>
d.__dict__
AttributeError: 'D' object has no attribute '__dict__'

因为使用了 __slots__ 属性,那么对象就会划分一个固定大小的空间来存放指定的属性,这时候 __dict__ 属性就不需要了,空间也就节约了出来。

不过这里有一点是需要特别强调的,就是使用 __slots__ 属性的副作用其实也相当明显,那就是要以牺牲 Python 动态语言的灵活性,作为前提。使用了 __slots__ 属性,就没办法再拥有动态添加属性的功能了……这可以说是它的一个副作用,但实际上很多开发者却利用这个副作用,来限制类属性的滥用。

最后,还有一点需要大家知道的是,继承自父类的 __slots__ 属性是不会在子类中生效的,Python 只关注各个具体的类中定义的 __slots__ 属性:

1
2
3
4
5
6
7
8
9
10
>>> class E(C):
... pass
...
>>> e = E(250)
>>> e.x
250
>>> e.y = 520
>>> e.z = 666
>>> e.__slots__
['x', 'y']

对象 e 虽然拥有 __slots__ 属性,但它同时也拥有 __dict__ 属性:
1
2
>>> e.__dict__
{'z': 666}

16.40.23 魔法方法

魔法方法就如同它的名字一样,它让 Python 拥有了超凡的魔力。在面向对象开发的过程中,魔法方法总能在你需要的时候出现,并为帮助你轻松地实现你的想法。魔法方法是 Python 内部已经实现的一系列方法的统称,前后双下划线是它们身份的象征(比如 __init__())。毫不夸张地说,只有掌握了魔法方法,才能算得上是真正地学会了面向对象的 Python 开发。前面我们介绍过 __init__() 魔法方法,它的超能力,就是在类实例化对象的时候自动进行调用,咱们自己写的方法可以做不了这个。

16.40.23.1 new(cls[, …])

__init__()是对象构建的时候调用的魔法方法,其实参与构建对象还有一个 __new__() 方法,它是在 __init__() 之前被调用的。对象的诞生流程,是先调用 __new__() 方法,创建一个类的实例,然后将其传递给 __init__() 方法进行个性化定制,这么一个流程。需要重写 __new__() 方法的情况极少,通常会用到它不外乎两种情况:

一种情况是在元类中去定制类,元类是 Python 中最难理解的概念之一,是称之为魔法方法背后的魔法方法.
另一种情况比较特殊,是在继承不可变数据类型的时候,如果我们想要 “从中作梗”,就可以通过重写 __new__() 魔法方法进行拦截:

1
2
3
4
5
6
7
8
>>> class CapStr(str):
... def __new__(cls, string):
... string = string.upper()
... return super().__new__(cls, string)
...
>>> cs = CapStr("FishC")
>>> cs
'FISHC'

16.40.23.2 del(self)

相反,del() 魔法方法是在对象即将被销毁的时候所自动调用的。

1
2
3
4
5
6
7
8
9
10
>>> class C:
... def __init__(self):
... print("我来了~")
... def __del__(self):
... print("我走了~")
...
>>> c = C()
我来了~
>>> del c
我走了~

16.40.23.3 对象重生之旅

虽然官方不建议,但他又告诉你可以实现,咱们不妨来尝试一下吧~

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
>>> class D:
... def __init__(self, name):
... self.name = name
... def __del__(self):
... global x
... x = self
...
>>> d = D()
>>> d
<__main__.D object at 0x0000028B34990400>
>>> d.name
'小甲鱼'
>>> del d
>>> d
Traceback (most recent call last):
File "<pyshell#12>", line 1, in <module>
d
NameError: name 'd' is not defined
>>> x
<__main__.D object at 0x0000028B34990400>
>>> x.name
'小甲鱼'

这是一种方法。但是非迫不得已,尽量不要使用全局变量,因为它会污染命名空间。

还有一种方法是通过闭包的形式,将这个对象给保存起来。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
>>> class E:
... def __init__(self, name, func):
... self.name = name
... self.func = func
... def __del__(self):
... self.func(self)
...
>>> def outer():
... x = 0
... def inner(y=None):
... nonlocal x
... if y:
... x = y
... else:
... return x
... return inner
...
>>> f = outer()
>>> e = E("小甲鱼", f)
>>> e
<__main__.E object at 0x0000024775760790>
>>> e.name
'小甲鱼'
>>> del e
>>> e
Traceback (most recent call last):
File "<pyshell#37>", line 1, in <module>
e
NameError: name 'e' is not defined
>>> g = f()
>>> g
<__main__.E object at 0x0000024775760790>
>>> g.name
'小甲鱼'

16.40.23.4 运算相关的魔法方法

涉及到的方法数量之庞大,可能会让在座各位都大吃一鲸:
运算相关的魔法方法大合集.png

一共有 53 个,具体明细可以参考 -> 传送门
用到的时候,拿出来查一下就 OK,写过几次代码也就自然记住了。

涉及到运算相关的操作,Python 也都提供了相应的魔法方法。
比如说,我们希望两个字符串相加的结果不是拼接,而是统计两者的字符个数之和,代码可以这么写:

1
2
3
4
5
6
7
8
>>> class S(str):
... def __add__(self, other):
... return len(self) + len(other)
...
>>> s1 = S("FishC")
>>> s2 = S("Python")
>>> s1 + s2
11

只要重写对象的 __add__() 魔法方法,就可以实现对加法运算的拦截。
s1 + s2 就相当于 s1.__add__(s2)

16.40.23.5 反算术运算相关的魔法方法

“r” 开头的版本(比如 __radd__()),即 “反算术运算” 的这部分魔法方法,它们其实都是跟上面的算术运算是一一对应的。

那么它们的区别是什么呢?

仍然是拿加法来举例好了,当两个对象相加的时候,如果左侧的对象和右侧的对象不同类型,并且左侧的对象没有定义 __add__() 方法,或者其 __add__() 返回 NotImplemented,那么 Python 就会去右侧的对象中找查找是否有 __radd__() 方法的定义。

举个栗子:

1
2
3
4
5
6
7
8
9
10
11
12
>>> class S1(str):
... def __add__(self, other):
... return NotImplemented
...
>>> class S2(str):
... def __radd__(self, other):
... return len(self) + len(other)
...
>>> s1 = S1("Apple")
>>> s2 = S2("Banana")
>>> s1 + s2
12

这里能够成功调用到 __radd__() 方法,首先当然是因为 S2 实现了 __radd__() 方法;其次,s1 和 s2 是两个基于不同类的对象;再有一个条件,就是 S1 必须不能实现 __add__() 方法,不然还是会优先去执行左侧对象的 __add__() 方法(这里我们 __add__() 方法返回 NotImplemented,含义就是明确表示这个方法未实现;如果我们在 S1 中直接不去定义 __add__() 方法,也可以得到相同的结果)。

16.40.23.6 增强赋值运算相关的魔法方法

“i” 开头的版本(比如 __iadd__()),即 “增强赋值运算” 的这部分魔法方法,它们也是跟上面的算术运算也是对应的,不过呢,这个执行的是运算兼赋值的操作。
也就是说 s1 += s2,就相当于 s1 = s1.__iadd(s2)

注意,它有一个自我赋值的操作,也就是说它会修改对象自身:

1
2
3
4
5
6
7
8
9
10
>>> class S1(str):
... def __iadd__(self, other):
... return len(self) + len(other)
...
>>> s1 = S1("Apple")
>>> s1 += s2
>>> s1
11
>>> type(s1)
<class 'int'>

所以,用的时候一定要先想清楚,不然一个不小心,就容易把自己的对象给搞丢咯。

另外,如果增强赋值运算符的左侧对象没有实现相应的魔法方法,比如 += 的左侧对象没有实现 __iadd__() 方法,那么将退而求其次,使用相应的 __add__() 方法和 __radd__() 方法来实现:

1
2
3
4
5
6
7
8
9
10
11
12
>>> class S2(str):
... def __radd__(self, other):
... return len(self) + len(other)
...
>>> s2 = S2("FishC")
>>> type(s2)
<class '__main__.S2'>
>>> s2 += s2
>>> s2
'FishCFishC'
>>> type(s2)
<class 'str'>

16.40.23.7 一些内置函数也有相应的魔法方法

比如 int() 函数,它相应的魔法方法是 __int__()。那么 int(x) 就相当于调用了 x.__int__()
利用这个特性,我们就可以轻松地实现一个可以将中文转换为整数的对象,来大家请看代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class ZH_INT:
def __init__(self, num):
self.num = num

def __int__(self):
try:
return int(self.num)
except ValueError:
zh = {"零":0, "一":1, "二":2, "三":3, "四":4, "五":5, "六":6, "七":7, "八":8, "九":9,
"壹":1, "贰":2, "叁":3, "肆":4, "伍":5, "陆":6, "柒":7, "捌":8, "玖":9, }
result = 0

for each in self.num:
if each in zh:
result += zh[each]
else:
result += int(each)
result *= 10

return result // 10

我们通过拦截了 __int__() 魔法方法,让它在原来的基础上添加了对中文数字的识别:
1
2
3
4
5
6
>>> n = ZH_INT("五贰零")
>>> int(n)
520
>>> n = ZH_INT("五贰零1314")
>>> int(n)
5201314

16.40.23.8 位运算

常见的位运算包含按位与(&)、按位或(|)、按位非(^),还有按位异或。
前者是对两个整数进行位运算:

1
2
3
4
5
6
7
8
9
10
>>> bin(2)
'0b10'
>>> bin(3)
'0b11'
>>> bin(4)
'0b100'
>>> 3 & 2
2
>>> 3 & 4
0

这里 & 是按位进行 “与” 运算,就是只有当相同的位的值均为 1 的情况下,那么结果才为 1。

按位或也是相同的道理:

1
2
3
4
>>> 3 | 2
3
>>> 3 | 4
7

按位非则是将每个位进行取反,就是将 1 变成 0,0 变成 1,这样:

1
2
3
4
5
6
>>> ~2
-3
>>> ~3
-4
>>> ~4
-5

16.40.23.9 补码

按位与和按位或运算的结果相信大家都不会感觉到意外,但是按位取反的运算结果,估计会让很多鱼油摸不着头脑……
这个其实是涉及到补码的概念:
补码.jpg
感兴趣的童鞋可以看一下这篇扩展阅读 -> 为什么要使用补码(顺带讲讲二进制的前世今生及转换方法)
补码其实是在计算机底层对二进制数进行表示、运算和存储使用,对人类并不算太友好,所以这个概念在大多数 Python 教程中,都是不会涉及的,甚至压根儿都不会给你提到,所以感兴趣的童鞋,并且不怕掉头发的童鞋,可以看看,时间有限的童鞋呢,这个扩展阅读可以跳过,也无妨~

16.40.23.10 按位异或

按位异或,这个比较特殊,就是当两个相同的二进制位的值,不一样的时候,那么结果对应的二进制位的值为 1:

1
2
3
4
>>> 3 ^ 2
1
>>> 3 ^ 4
7

16.40.23.11 按位移动

还有一个左移(<<)和右移(>>)运算符,运算符的左侧是运算对象,运算符的右侧是指定移动的位数:

1
2
3
4
5
6
7
8
9
10
>>> bin(8)
'0b1000'
>>> 8 >> 2
2
>>> 8 >> 3
1
>>> 8 << 2
32
>>> 8 << 3
64

右移 n 位就是除以 2 的 n 次方:
1
2
3
4
>>> 8 // pow(2, 2)
2
>>> 8 // pow(2, 3)
1

注意,一定是地板除,因为移位它是会丢失数据的,比如说十进制的数字 9,它的二进制是:
1
2
3
4
>>> bin(9)
'0b1001'
>>> 9 >> 2
2

相反,左移 n 位就是乘以 2 的 n 次方:
1
2
3
4
>>> 8 * pow(2, 2)
32
>>> 8 * pow(2, 3)
64

另外,移位计数不能为负数,否则会引发 ValueError 异常:
1
2
3
4
5
>>> 8 >> -2
Traceback (most recent call last):
File "<pyshell#15>", line 1, in <module>
8 >> -2
ValueError: negative shift count

16.40.23.11 优先级

优先级
在同一个表达式里面,按位或、按位异或、按位与、还有移位,它们的优先级是依次递增的,然后按位非是和正、负号处于同一个优先级行列。

16.40.23.12 math 模块相关的魔法方法

为了方便大家日后查询,小甲鱼同样将 math 模块翻译了一份中文文档 -> 传送门

math.trunc(x) -> x.__trunc__()

math.floor(x) -> x.__floor__()

math.ceil(x) -> x.__ceil__()

math 模块里面还有一个 ulp() 函数,用于表示对应浮点数的最低有效位,所以:

1
2
3
>>> import math
>>> 0.1 + 0.2 == 0.3 + math.ulp(0.3)
True

16.40.23.13 特别魔法方法

就是__index__(self) 方法,

1
2
3
4
5
6
7
8
9
10
11
>>> class C:
... def __index__(self):
... print("被拦截了~")
... return 3
...
>>> c = C()
>>> c[2]
Traceback (most recent call last):
File "<pyshell#5>", line 1, in <module>
c[2]
TypeError: 'C' object is not subscriptable

其实啊,当对象作为索引值或参数的时候,才会触发 __index__() 方法:
1
2
3
4
5
6
7
>>> s = "FishC"
>>> s[c]
被拦截了~
'h'
>>> bin(c)
被拦截了~
'0b11'

这个方法如果不讲,是很容易被误解为由对象的索引访问触发,其实这个方法是让对象做为索引值,被访问才触发,真的是离了个大谱就是……

16.40.23.13 与对象的属性访问相关的 BIF
  • hasattr() — 用于判断对象是否拥有某个属性。
  • getarrt() — 用于返回对象中指定属性的值。
  • setattr() — 用于设置对象中指定属性的值。
  • delattr() — 于删除对象中指定属性的值。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
>>> class C:
... def __init__(self, name, age):
... self.name = name
... self.__age = age
...
>>> c = C("小甲鱼", 18)
>>> hasattr(c, "name")
True
>>> getattr(c, "name")
'小甲鱼'
>>> getattr(c, "_C__age")
18
>>> setattr(c, "_C__age", 19)
>>> getattr(c, "_C__age")
19
>>> delattr(c, "_C__age")
>>> hasattr(c, "_C__age")
False
16.40.23.14 与对象的属性访问相关的魔法方法

getarrt() 函数对应的是 __getattribute__() 这个魔法方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
>>> class C:
... def __init__(self, name, age):
... self.name = name
... self.__age = age
... def __getattribute__(self, attrname):
... print("拿来吧你~")
... return super().__getattribute__(attrname)
...
>>> c = C("小甲鱼", 18)
>>> getattr(c, "name")
拿来吧你~
>>> c._C__age
拿来吧你~
18
>>> c.FishC
拿来吧你~
Traceback (most recent call last):
File "<pyshell#28>", line 1, in <module>
c.y
File "<pyshell#23>", line 7, in __getattribute__
return super().__getattribute__(name)
AttributeError: 'C' object has no attribute 'FishC'

大家看,尽管最后这个 “FishC” 是一个不存在的属性,但 __getattribute__() 方法还是会作出响应,然后抛出异常。

那么 __getattr__() 这个魔法方法是干嘛的呢?
注意!一定要注意!!这个跟 getarrt() 函数长得像的,也是跟访问属性有关系,不过它是只有在用户试图获取一个不存在的属性时才会被触发:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
>>> class C:
... def __init__(self, name, age):
... self.name = name
... self.__age = age
... def __getattribute__(self, attrname):
... print("拿来吧你~")
... return super().__getattribute__(attrname)
... def __getattr__(self, attrname):
... if attrname == "FishC":
... print("I love FishC.")
... else:
... raise AttributeError(attrname)
...
>>> c = C("小甲鱼", 18)
>>> c.FishC
拿来吧你~
I love FishC.
>>> c.x
拿来吧你~
Traceback (most recent call last):
File "<pyshell#39>", line 1, in <module>
c.x
File "<pyshell#35>", line 12, in __getattr__
raise AttributeError(attrname)
AttributeError: x

属性赋值,对应的是 __setattr__() 魔法方法……

很容易,对不对?
大家不要掉以轻心,这里面也是有坑的,我举个例子你们就明白了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
>>> class D:
... def __setattr__(self, name, value):
... self.name = value
...
>>> d = D()
>>> d.name = "小甲鱼"
Traceback (most recent call last):
File "<pyshell#8>", line 1, in <module>
d.name = "小甲鱼"
File "<pyshell#6>", line 3, in __setattr__
self.name = value
File "<pyshell#6>", line 3, in __setattr__
self.name = value
File "<pyshell#6>", line 3, in __setattr__
self.name = value
[Previous line repeated 1022 more times]
RecursionError: maximum recursion depth exceeded

怎么回事?

怎么报错了?

大家仔细思考一下,self 是什么?

是对象自身,对吧?

那么捕获到赋值操作的时候,我们执行 self.name = value,那不就是相当于又给自己调用一次赋值操作,那么又会触发 setattr() 的这个魔法方法,然后又再次执行 self.name = value 的赋值操作……

这样就导致无限递归了!

遇到这个情况有两种解决方案:

  1. 交给 super()

  2. 通过字典来存放对象属性

对象的属性和值本来就是存放在一个叫 `__dict__ 的字典中:

1
2
3
4
5
6
7
8
>>> class D:
... def __setattr__(self, name, value):
... self.__dict__[name] = value
...
>>> d = D()
>>> d.name = "小甲鱼"
>>> d.name
'小甲鱼'

这样我们就绕开了死亡螺旋~

同样的道理,使用 __delattr__() 魔法方法的时候,也要注意这个死亡螺旋,我们还是得利用这个字典来实现属性的删除:

1
2
3
4
5
6
7
8
9
10
11
12
13
>>> class D:
... def __setattr__(self, name, value):
... self.__dict__[name] = value
... def __delattr__(self, name):
... del self.__dict__[name]
...
>>> d = D()
>>> d.name = "小甲鱼"
>>> d.__dict__
{'name': '小甲鱼'}
>>> del d.name
>>> d.__dict__
{}