Python学习笔记
Python自己用了快两年半了,很嫌弃自己很久以前写的代码,所以在此总结一下Python中的一些技巧+语法糖+坑。
表达式列表、解包
表达式列表可以在一行内初始化多个变量:
1 | a, b = 1, 2 |
可以用这个特性方便地交换变量顺序,而不必创建多余的临时变量:
1 | a, b = b, a |
等号右边的值可以来自可以迭代的对象:
1 | xs = [0, 1, 2, 3] |
等号左边也可以接受多个值:
1 | xs = [0, 1, 2, 3] |
连续赋值
如果需要初始化多个值为None
,用a, b, c = None, None, None
显得比较繁琐,可以这样做:
1 | a = b = c = None |
赋值表达式
从Python 3.8开始,可以使用赋值表达式作为判断的条件,可以在下面的场景使用:
1 | import re |
或者判断字典是否有指定值:
1 | data = {'x': 1, 'y': 2} |
列表推导式
可以产生一个生成器(某种程度上可以视为迭代器):
1 | xs = [x + 1 for x in range(100)] |
可以使用if
筛选一些值:
1 | xs = [x + 1 for x in range(100) if x % 2 == 0] |
对于集合/字典也有效:
1 | {x: 2 * x for x in range(3)} |
三元表达式
在Python中,根据一个条件给变量赋不同的值,可以这样做:
1 | if cond: |
但这样显得冗长,因此可以参考其他脚本语言的一种做法:
1 | x = cond and 123 or 456 |
这里利用了短路的特性实现三元表达式,然而上面这种方式在第一个值为假时会出现问题,原因是or
的第一个条件不会成立:
1 | cond = True |
在Python中,提供了另一种方式来进行这种操作:
1 | x = 0 if cond else 123 |
它也具有惰性求值的特性,在bool(cond)
为False
时,if
前面的表达式不会运行,所以下面的操作不会抛出KeyError
:
1 | data = {'x': 1} |
链式比较
不同于类C语言中的含义,Python支持链式比较:
1 | x = 50 |
它等价于:
1 | x = 50 |
推广:表达式value1 op1 value2 op2 value3
等价于value1 op1 value2 and value2 op2 value3
。
这个特性在表达式比较复杂或者有副作用时使用会很方便。
for-else
for
循环可以可选地加入一个else
分支,在循环正常结束时会进入而使用break
跳出循环则不进入,可以避免创建中间变量flag
:
1 | for i in (2, 3, 5, 7): |
它等价于下面的代码:
1 | flag = False |
字符串变换
字符串/字节集/字节数组支持批量替换,可以利用这个特性替换base64
的码表等:
1 | from string import \ |
另外也可以在AES
加密的实现中从S盒求出逆S盒,其中S盒是一个256字节长的bytes
,且值为0-255
的一个排列:
1 | S_i = bytes.maketrans(S, range(256)) |
列表切片
列表切片的形式:xs[start:stop:step]
。其中三个变量都可以被省略,并且遵循下面的原则:
- 如果
step
被省略,则默认值为1。如果提供值则不能为0 start
被省略时,如果step
为正,则默认值为0,否则默认值为len(xs) - 1
。stop
被省略时,如果step
为正,则默认值为len(xs) - 1
,否则默认值为0。- 如果
stop
大于len(xs)
,则它被修正为len(xs)
。 - 如果
start
小于-len(xs)
,则它被修正为-len(xs)
。 - 此时3个值都被确定,接着Python从列表中选取从
start
(包含)到stop
(不包含),每次递增step
的值。
1 | xs = list(range(10)) |
迭代器切片
在内置库itertools
中有一个islice
函数类似于列表切片,但它针对的是Iterable
对象,而且step
只能为正,例如:
1 | from itertools import islice |
用生成器替代递归
当在Python中使用递归时,可能由于递归过深而引起RecursionError
错误,此时我们可能会考虑调用内置库sys
中的setrecursionlimit
函数强制设置栈深度的最大值。但是这没有解决问题。一方面,它的值不好精确控制,只做一次调用则可能在递归继续深入时仍然报错,另一方面,即使这些处理好了,也可能由于操作系统的限制,底层的Python解释器不具有足够的栈空间,从而引起程序崩溃退出。
在很多地方都指出,程序应该尽量使用循环而不是递归,而把递归改写成循环并不简单,因为需要保存现场而和维护一个自己的栈。好在Python中可以使用yield
把递归转换成生成器,从而简化了转换过程。
例如在Python中用递归实现深度优先搜索的代码如下:
1 | from typing import Generator, Union |
我们可以把它转换成对应的循环版本:
1 | from typing import Generator, Union |
把只调用自身的递归函数f
转换成使用循环的函数g
的方法如下(其他情况可以稍作修改实现,核心就是Generator.send
方法):
- 创建
g
函数,参数和返回值与f
保持一致 - 把
f
的返回类型修改为Generator[f的参数类型, f的返回值类型, f的返回值类型]
,并修改函数名称 - 把
f
中的递归调用改为yield 参数列表
- 加入上面的循环调用代码
新的函数使用列表模拟一个栈,因此不会报递归错误。
坑
list.sort
, list.reverse
在列表中这些函数没有返回值,列表的内容就地更改。如果想生成副本可以调用sorted()
和list(reversed())
:
1 | xs = [0, 2, 4, 1, 3] |
nonlocal
在闭包中修改上层代码的变量需要使用nonlocal
来修饰此变量:
1 | def foo() -> None: |
闭包
猜猜下面这些代码的输出是什么?
1 | funcs = [lambda: i for i in range(5)] |
答案:[4, 4, 4, 4, 4]
而不是[0, 1, 2, 3, 4]
因为lambda
函数绑定了同一个变量i
,而i
是动态变化的,在第一个循环结束后i = 4
,因此会造成这种现象。
解决方法是使用内置库functools
中的partial
方法:
1 | from functools import partial |
函数的关键字参数
猜猜下面这些代码的输出是什么?
1 | class A: |
答案:
1 | A.__init__ |
可见,关键字参数在函数定义的时候已经被求值,而不是在调用函数前求值。
Python中这样做的原因是在函数被定义后就必须要能够从f.__defaults__
或者f.__kwdefaults__
中取得默认值,因此求值的时机只能是定义函数时。
一般在函数的关键字参数中只指定不可变的类,例如None
、str
、bytes
等等,而如果指定像list
这类值,则多次调用将会共享这个值。
列表嵌套
猜猜下面这些代码是否引发错误?
1 | xss = [[]] * 5 |
答案:AssertionError
因为第一句中的乘法在复制列表时是浅复制,所以实际结果是[[1, 2, 2, 3, 3, 3, 4, 4, 4, 4]]*5
。
应该修改为:
1 | xss = [[] for _ in range(5)] |