默认参数

参考:【Python】我精心设计的默认参数,怎么就出问题了呢?_哔哩哔哩_bilibili

在定义类的时候,会定义一些默认参数,可能会像这样

1
2
3
4
class Player:
def __init__(self, name, items=[]):
self.name = name
self.items = items

然后如果像这样调用的话

1
2
3
4
5
6
7
8
9
if __name__ == "__main__":
p1 = Player("John", ["sword", "shield"])
p2 = Player("Jane")
p3 = Player("Jack")

p2.items.append("bow")
p3.items.append("axe")

print(p3.items)

输出结果为['bow', 'axe']

很明显,这不应该是我们想要的结果,我们期望输出应该是['axe']

Python的Document里有这么一段话8. 复合语句 — Python 3.11.2 文档

默认形参值会在执行函数定义时按从左至右的顺序被求值。 这意味着当函数被定义时将对表达式求值一次,相同的“预计算”值将在每次调用时被使用。 这一点在默认形参为可变对象,例如列表或字典的时候尤其需要重点理解:如果函数修改了该对象(例如向列表添加了一项),则实际上默认值也会被修改。 这通常不是人们所想要的。

简而言之,对于这种可变对象,如果使用默认参数,他们会指向同一个对象

Document中也提到了解决方法

使用 None 作为默认值

因此,我们将类定义修改为如下,即可解决这一问题。

1
2
3
4
5
6
class Player:
def __init__(self, name, items=None):
if items is None:
items = []
self.name = name
self.items = items

使用is None对None进行判断

参考:【python】为什么判断一个值是否为None的时候,一定要用is呢?_哔哩哔哩_bilibili

不要直接作为布尔值判断(if None)

除了None之外,其他传入的空数据结构,如Dictionary,List,Tuple等也会被识别成False。

对于以下这种情况,我们原本期望共享ls1这个数组,最终能够输出[1, 1],可是程序运行最终只输出了[1],原因就是输入的ls1是空数组,使用逻辑判断结果为False。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class Test:
def __init__(self, ls=None):
if ls:
self.ls = ls
else:
self.ls = []

def cal(self):
self.ls.append(1)

def __str__(self):
return str(self.ls)

if __name__ == "__main__":
ls1 = []
t1 = Test(ls1)
t2 = Test(ls1)
t1.cal()
t2.cal()
print(t2)

我们也可以使用__bool__自定义类判断为False的情况。输出结果为output2。

1
2
3
4
5
6
7
8
class Test:
def __bool__(self):
return False

if Test():
print("output1")
else:
print("output2")

因此,由于一些数据结构会对bool进行重载,造成对None的判断出现问题。

也不要使用==

由于可以使用__eq__对运算符==进行重载,也可能导致判断出错。以下代码结果即可输出output1。

1
2
3
4
5
6
7
8
class Test:
def __eq__(self, other):
return True

if Test() == None:
print("output1")
else:
print("output2")

使用is

某些运算符不能重载 —— is、and、or以及not(位运算符&、|以及~可以重载)[ref]流畅的Python 笔记12——运算符重载 - 知乎 (zhihu.com)[/ref]

使用dis这个module对==is进行比较,观察字节码

Python3.11中运行结果

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
>>> import dis
>>> dis.dis('a == None')
0 0 RESUME 0

1 2 LOAD_NAME 0 (a)
4 LOAD_CONST 0 (None)
6 COMPARE_OP 2 (==)
12 RETURN_VALUE
>>> dis.dis('a is None')
0 0 RESUME 0

1 2 LOAD_NAME 0 (a)
4 LOAD_CONST 0 (None)
6 IS_OP 0
8 RETURN_VALUE
>>>

使用Python3.9

1
2
3
4
5
6
7
8
9
10
  1           0 LOAD_NAME                0 (a)
2 LOAD_CONST 0 (None)
4 COMPARE_OP 2 (==)
6 RETURN_VALUE
None
1 0 LOAD_NAME 0 (a)
2 LOAD_CONST 0 (None)
4 IS_OP 0
6 RETURN_VALUE
None

可以看到只有COMPARE_OPIS_OP的差别。

在Python源代码中,is是进行地址的指针比较,在c中是非常快的,而COMPARE_OP则要慢。

generator中的坑

参考:【python】几乎没人做对的题目?聊聊生成器表达式里不为人知的秘密_哔哩哔哩_bilibili

对于以下的代码,运行结果为[1, 2]。

注意:第二行是(),不是生成式的[]。

1
2
3
4
ls = [1, 2, 3, 4, 5]
g = (n for n in ls if n in ls)
ls = [0, 1, 2]
print(list(g))

实际上,这段代码等价于

1
2
3
4
ls = [1, 2, 3, 4, 5]
g = (n for n in ls if n in ls2)
ls2 = [0, 1, 2]
print(list(g))

在PEP 289PEP 289 – Generator Expressions | peps.python.org中提到

Only the outermost for-expression is evaluated immediately, the other expressions are deferred until the generator is run

只有最外层的 for 表达式被立即计算,其他表达式被延迟到生成器运行