Python自己用了快两年半了,很嫌弃自己很久以前写的代码,所以在此总结一下Python中的一些技巧+语法糖+坑。

表达式列表、解包

表达式列表可以在一行内初始化多个变量:

1
2
3
a, b = 1, 2
assert a == 1
assert b == 2

可以用这个特性方便地交换变量顺序,而不必创建多余的临时变量:

1
a, b = b, a

等号右边的值可以来自可以迭代的对象:

1
2
3
4
xs = [0, 1, 2, 3]
a, b, _, _ = xs
assert a == 0
assert b == 1

等号左边也可以接受多个值:

1
2
3
4
xs = [0, 1, 2, 3]
a, *xs = xs
assert a == 0
assert xs == [1, 2, 3]

连续赋值

如果需要初始化多个值为None,用a, b, c = None, None, None显得比较繁琐,可以这样做:

1
a = b = c = None

赋值表达式

从Python 3.8开始,可以使用赋值表达式作为判断的条件,可以在下面的场景使用:

1
2
3
import re
if match := re.fullmatch(r'he(.*)d', 'hello world'):
print(match.groups())

或者判断字典是否有指定值:

1
2
3
4
data = {'x': 1, 'y': 2}
if value := data.get('x'):
print(value)
else: print('no value')

列表推导式

可以产生一个生成器(某种程度上可以视为迭代器):

1
2
3
xs = [x + 1 for x in range(100)]
# equiv. to xs == list(x + 1 for x in range(100))
assert xs == list(range(1, 101))

可以使用if筛选一些值:

1
2
xs = [x + 1 for x in range(100) if x % 2 == 0]
assert xs == list(range(1, 101, 2))

对于集合/字典也有效:

1
2
{x: 2 * x for x in range(3)}
#-> {0: 0, 1: 2, 2: 4}

三元表达式

在Python中,根据一个条件给变量赋不同的值,可以这样做:

1
2
3
4
if cond:
x = 123
else:
x = 456

但这样显得冗长,因此可以参考其他脚本语言的一种做法:

1
x = cond and 123 or 456

这里利用了短路的特性实现三元表达式,然而上面这种方式在第一个值为假时会出现问题,原因是or的第一个条件不会成立:

1
2
3
cond = True
x = cond and 0 or 123
assert x == 0 # AssertionError

在Python中,提供了另一种方式来进行这种操作:

1
x = 0 if cond else 123

它也具有惰性求值的特性,在bool(cond)False时,if前面的表达式不会运行,所以下面的操作不会抛出KeyError

1
2
3
data = {'x': 1}
value = data['y'] if 'y' in data else 0
print(value)

链式比较

不同于类C语言中的含义,Python支持链式比较:

1
2
3
x = 50
if 0 <= x < 100:
print(x)

它等价于:

1
2
3
x = 50
if 0 <= x and x < 100:
print(x)

推广:表达式value1 op1 value2 op2 value3等价于value1 op1 value2 and value2 op2 value3

这个特性在表达式比较复杂或者有副作用时使用会很方便。

for-else

for循环可以可选地加入一个else分支,在循环正常结束时会进入而使用break跳出循环则不进入,可以避免创建中间变量flag

1
2
3
4
5
for i in (2, 3, 5, 7):
if i == 3:
print(i)
break
else: print('找不到3')

它等价于下面的代码:

1
2
3
4
5
6
7
8
flag = False
for i in (2, 3, 5, 7):
if i == 3:
print(i)
flag = True
break
if not flag:
print('找不到3')

字符串变换

字符串/字节集/字节数组支持批量替换,可以利用这个特性替换base64的码表等:

1
2
3
4
5
6
7
8
9
10
from string import \
ascii_uppercase as upper,\
ascii_lowercase as lower,\
digits as digits
from base64 import b64decode

base64_normal = upper + lower + digits
base64_current = lower + digits + upper
transform = str.maketrans(base64_current, base64_normal)
print(b64decode('0gvI1gY63SZO1gq='.translate(transform)))

另外也可以在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
2
3
4
5
6
7
8
9
10
11
12
13
xs = list(range(10))
print(xs[2:5])
#-> [2, 3, 4]
print(xs[5:2])
#-> []
print(xs[:6])
#-> [0, 1, 2, 3, 4, 5]
print(xs[6:])
#-> [6, 7, 8, 9]
print(xs[::-1])
#-> [9, 8, 7, 6, 5, 4, 3, 2, 1, 0]
print(xs[::-2])
#-> [9, 7, 5, 3, 1]

迭代器切片

在内置库itertools中有一个islice函数类似于列表切片,但它针对的是Iterable对象,而且step只能为正,例如:

1
2
3
4
from itertools import islice
xs = list(range(10))
print(list(islice(xs, None, None, 2)))
#-> [0, 2, 4, 6, 8]

用生成器替代递归

当在Python中使用递归时,可能由于递归过深而引起RecursionError错误,此时我们可能会考虑调用内置库sys中的setrecursionlimit函数强制设置栈深度的最大值。但是这没有解决问题。一方面,它的值不好精确控制,只做一次调用则可能在递归继续深入时仍然报错,另一方面,即使这些处理好了,也可能由于操作系统的限制,底层的Python解释器不具有足够的栈空间,从而引起程序崩溃退出。

在很多地方都指出,程序应该尽量使用循环而不是递归,而把递归改写成循环并不简单,因为需要保存现场而和维护一个自己的栈。好在Python中可以使用yield把递归转换成生成器,从而简化了转换过程。

例如在Python中用递归实现深度优先搜索的代码如下:

1
2
3
4
5
6
7
8
9
10
11
from typing import Generator, Union
Tree = Union[list[int], list['Tree']]

def dfs(root: Tree) -> list[int]:
result = []
for e in root:
if isinstance(e, list):
result.extend(dfs(e))
else:
result.append(e)
return result

我们可以把它转换成对应的循环版本:

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
from typing import Generator, Union
Tree = Union[list[int], list['Tree']]

def dfs(root: Tree) -> list[int]:
def dfs_inner(root: Tree) -> Generator[Tree, list[int], list[int]]:
result = []
for e in root:
if isinstance(e, list):
result.extend((yield e))
else:
result.append(e)
return result

# 循环调用
result, stack = None, [dfs_inner(root)]
while True:
try:
retval = stack[-1].send(result)
stack.append(dfs_inner(retval))
result = None
except StopIteration as e:
result = e.value
stack.pop().close()
if not stack: break
return result

把只调用自身的递归函数f转换成使用循环的函数g的方法如下(其他情况可以稍作修改实现,核心就是Generator.send方法):

  • 创建g函数,参数和返回值与f保持一致
  • f的返回类型修改为Generator[f的参数类型, f的返回值类型, f的返回值类型],并修改函数名称
  • f中的递归调用改为yield 参数列表
  • 加入上面的循环调用代码

新的函数使用列表模拟一个栈,因此不会报递归错误。

list.sort, list.reverse

在列表中这些函数没有返回值,列表的内容就地更改。如果想生成副本可以调用sorted()list(reversed())

1
2
3
4
5
xs = [0, 2, 4, 1, 3]
assert list.sort(list(xs)) == None
assert list.reverse(list(xs)) == None
assert sorted(xs) == [0, 1, 2, 3, 4]
assert list(reversed(xs)) == [3, 1, 4, 2, 0]

nonlocal

在闭包中修改上层代码的变量需要使用nonlocal来修饰此变量:

1
2
3
4
5
6
7
8
9
def foo() -> None:
x = 1
def bar() -> None:
nonlocal x
print(x)
x += 1 # 如果不使用`nonlocal`,此操作会引起`UnboundLocalError`
print(x)
bar()
foo()

闭包

猜猜下面这些代码的输出是什么?

1
2
funcs = [lambda: i for i in range(5)]
print(list(f() for f in funcs))

答案:[4, 4, 4, 4, 4]而不是[0, 1, 2, 3, 4]

因为lambda函数绑定了同一个变量i,而i是动态变化的,在第一个循环结束后i = 4,因此会造成这种现象。

解决方法是使用内置库functools中的partial方法:

1
2
3
from functools import partial
funcs = [partial(lambda i: i, i) for i in range(5)]
print(list(f() for f in funcs))

函数的关键字参数

猜猜下面这些代码的输出是什么?

1
2
3
4
5
6
7
class A:
def __init__(self):
print('A.__init__')
def foo(v=A()):
pass
print('before call foo')
foo()

答案:

1
2
A.__init__
before call foo

可见,关键字参数在函数定义的时候已经被求值,而不是在调用函数前求值。

Python中这样做的原因是在函数被定义后就必须要能够从f.__defaults__或者f.__kwdefaults__中取得默认值,因此求值的时机只能是定义函数时。

一般在函数的关键字参数中只指定不可变的类,例如Nonestrbytes等等,而如果指定像list这类值,则多次调用将会共享这个值。

列表嵌套

猜猜下面这些代码是否引发错误?

1
2
3
4
xss = [[]] * 5
for i in range(5):
xss[i].extend([i] * i)
assert xss == [[0]*0, [1]*1, [2]*2, [3]*3, [4]*4]

答案:AssertionError

因为第一句中的乘法在复制列表时是浅复制,所以实际结果是[[1, 2, 2, 3, 3, 3, 4, 4, 4, 4]]*5

应该修改为:

1
2
3
4
xss = [[] for _ in range(5)]
for i in range(5):
xss[i].extend([i] * i)
assert xss == [[0]*0, [1]*1, [2]*2, [3]*3, [4]*4]