记录一下在读《流程的python》时对自己有用的知识点。

1. 变量不是盒子

进行赋值操作的时候,并不是将一个对象放在变量构成的盒子中。实际上,在进行赋值操作的时候,等号右边的对象是在它被创建之后才把等号左边的变量分配给它,相当于给这个对象贴上了一个标签🏷️。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
In [1]: class Gizmo:
...: def __init__(self):
...: print(id(self))
...:

In [2]: x = Gizmo()
4418370088

In [3]: y = Gizmo()[0]
4418271496
---------------------------------------------------------------------------
AttributeError Traceback (most recent call last)
<ipython-input-3-81d29164ac5d> in <module>()
----> 1 y = Gizmo()[0]

AttributeError: Gizmo instance has no attribute '__getitem__'

可以看到,在将变量x分配给类Gizmo的一个实例之前,Gizmo的实例已经被创建。

2. 标识、相等性和别名

正如上边所说,变量相当于是一个对象的标签,当然,一个对象可以被贴上许多标签,即多个变量绑定同一个对象。

1
2
3
4
5
6
7
8
9
10
11
In [4]: list1 = [0,1,2,3,4,5]

In [5]: list2 = list1

In [6]: list2
Out[6]: [0, 1, 2, 3, 4, 5]

In [7]: list2.append(6)

In [8]: list1
Out[8]: [0, 1, 2, 3, 4, 5, 6]

可以看到list1和list2是同一个对象的不同标签。
但是,如何才能判断两个变量是否绑定的是同一个对象?

1
2
3
4
In [9]: list3 = [0,1,2,3,4,5,6]

In [10]: list3 == list1 == list2
Out[10]: True

接着上边的例子,我们现在有一个叫做list3的变量,它和list1、list2是相等的,但是它们三个绑定的是同一个对象吗?python官方文档中有这样的描述:

每个变量都有标识、类型和值。对象一旦创建,它的标识绝对不会变化;你可以把标识当作该对象在内存中的地址。is运算符用以比较两个对象的标识,内建函数id()返回对象标识的整数表示。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
In [12]: list1 is list2
Out[12]: True

In [13]: list3 is list1
Out[13]: False

In [14]: id(list1)
Out[14]: 4418368144

In [15]: id(list2)
Out[15]: 4418368144

In [16]: id(list3)
Out[16]: 4418128064
可以看到`list1 is list2`并且`id(list1) == id(list2)`,这说明list1和list2绑定的是同一个对象,list3则和list1或者list2绑定的是不同的对象,即使是`list3 == list1 == list2`

3. 运算符is==

通过上边的例子,有个直观的印象就是

is用于比较对象的标识,==用于比较对象的值

通常情况下,我们会更多的使用`==`进行对象间的值的比较,但是在<strong>变量和单例值之间的比较,更应该使用`is`</strong>。这是因为: <strong>`is`运算符比`==`更快,因为它不能重载,这样python不用寻找并调用特殊方法,而是直接比较两个整数ID。`a == b`是语法糖,等同于执行`a.__eq__(b)`。继承自object的`__eq__`方法比较的是两个对象的ID,结果和`is`一样,但是多数内置类型都定义了更有意义的方式,覆盖了`__eq__`,这样就可能会给相等性测试带来更多复杂的处理工作。</strong>

4. 元组的相对不可变性

元组与多数python的集合类型一样(这里的一样指的是:列表、字典、集等而不包括像是str,bytes和array.array这样的单一类型序列,它们保存的不是引用,而是在连续内存中保存的数据本身),保存的是对象的引用。所以,如果引用的元素是可变的,即使元组本身不变,它其中的元素也是可变的。元组的不可变性,指的是它其中保存的引用不变(数据结构和物理内容),与引用的对象无关

5. copy和deepcopy

由上边的例子可以看到,像列表、字典等集合类型,它们内部保存的是对象的引用,所以就会出现这样的情况:

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
In [33]: l1 = [3,[4,5,6],(7,8,9)]

In [34]: l2 = list(l1)

In [35]: l2
Out[35]: [3, [4, 5, 6], (7, 8, 9)]

In [36]: l1.append(0)

In [37]: l1[1].append('x')

In [38]: print(l1)
[3, [4, 5, 6, 'x'], (7, 8, 9), 0]

In [39]: print(l2)
[3, [4, 5, 6, 'x'], (7, 8, 9)]

In [40]: l2[1] += ['y','z']

In [41]: l2[2] += ('x','y','z')

In [42]: l1
Out[42]: [3, [4, 5, 6, 'x', 'y', 'z'], (7, 8, 9), 0]

In [43]: l2
Out[43]: [3, [4, 5, 6, 'x', 'y', 'z'], (7, 8, 9, 'x', 'y', 'z')]

In [33]和In [34]进行了简单的列表复制,通过内置的构造函数来完成赋值。这个时候l1 == l2并且l1 is not l2它们绑定了不同的对象。所以In [36]中l1.append(0)并不会对l2造成影响。
但是由于列表内部保存的是对象的引用,所以l1[1] == [4,5,6],如果对l1[1]进行操作,l2自然会收到影响。
In [40]和In [41]分别对l2[1]和l2[2]进行操作,列表在进行+=操作之后,会就地修改列表,但对于元组来说+=操作会创建一个新的元组然后重新绑定给变量l2[2],这样l1[2]l2[2]就不是同一个对象。(个人觉得,上边的这个过程可以当作一道优秀的面试题目😊)

+=或者*=所做的增量复制操作来说,如果操作符左侧绑定的是不可变对象,会创建一个新的对象,如果是可变对象,会就地修改

默认情况下,python进行的复制都是浅复制(副本共享内部对象的引用),通过copy模块提供的deepcopy和copy可以为任何对象做深复制或者浅复制。
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
In [55]: class Bus:
...: def __init__(self,passengers=None):
...: if passengers is None:
...: self.passengers = []
...: else:
...: self.passengers = passengers
...: def pick(self,name):
...: self.passengers.append(name)
...: def drop(self,name):
...: self.passengers.remove(name)
...:

In [56]: import copy

In [57]: bus1 = Bus(['wm','ws','ls','gjy'])

In [58]: bus2 = copy.copy(bus1)

In [59]: bus3 = copy.deepcopy(bus1)

In [60]: map(id,[bus1,bus2,bus3])
Out[60]: [4418132448, 4416805992, 4416805920]

In [61]: bus1.drop('wm')

In [62]: bus2.passengers
Out[62]: ['ws', 'ls', 'gjy']

In [63]: bus3.passengers
Out[63]: ['wm', 'ws', 'ls', 'gjy']

In [64]: map(id,[bus1.passengers,bus2.passengers,bus3.passengers])
Out[64]: [4419499288, 4419499288, 4418048440]
可以看到,通过deepcopy,bus3和bus1之间并没有共享内部对象的引用,而通过copy生成的bus2的`passengers`和bus1是相同的对象。

6. 不要使用可变类型作为参数的默认值

通过下边的例子可以证明上边的这条忠告:

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
In [67]: class HauntedBus:
...: def __init__(self,passengers=[]):
...: self.passengers = passengers
...: def pick(self,name):
...: self.passengers.append(name)
...: def drop(self,name):
...: self.passengers.remove(name)
...:

In [68]: bus1 = HauntedBus(['wm','ws','ls'])

In [69]: bus1.passengers
Out[69]: ['wm', 'ws', 'ls']

In [70]: bus1.pick('gjy')

In [71]: bus1.drop('ws')

In [72]: bus1.passengers
Out[72]: ['wm', 'ls', 'gjy']

In [73]: bus2 = HauntedBus()

In [74]: bus2.pick('wm')

In [76]: bus2.passengers
Out[76]: ['wm']

In [77]: bus3 = HauntedBus()

In [78]: bus3.pick('ws')

In [79]: bus3.passengers
Out[79]: ['wm', 'ws']

可以看到,在没有定义初始乘客的HauntedBus实例会共享同一个乘客列表,这是因为self.passengers变成了passengers参数默认值的别名,默认值在定义函数时计算,因此默认值变成了函数对象的属性,因此,如果默认值是可变对象,并且修改了它的值,那么后续的函数调用都会收到影响。

7. 防御可变参数

如果一个函数接受的是可变参数,那么应该谨慎的考虑是否想要修改传入的参数,是否想要将对这个可变对象的修改作用到函数体之外?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
In [81]: overwatch_team = ['wm','ws','ls','gjy','ll']

In [82]: class Twilightbus:
...: def __init__(self,passengers=None):
...: if passengers is None:
...: self.passengers = []
...: else:
...: self.passengers = passengers
...: def pick(self,name):
...: self.passengers.append(name)
...: def drop(self,name):
...: self.passengers.remove(name)
...:

In [83]: bus = Twilightbus(overwatch_team)

In [84]: bus.drop('wm')

In [85]: bus.drop('ws')

In [86]: overwatch_team
Out[86]: ['ls', 'gjy', 'll']

可以看到Twilightbus可以让乘客莫名其妙的销声匿迹😨。这是因为,在将参数passenger传给Twilightbus的时候,实际上是将self.passengers变成了passengers的别名,所以每当乘客下车执行drop()的时候,直接将乘客从列表中抹去。
如果想要避免这种状况,可以在初始化函数中做一些修改:

1
2
3
4
5
def __init__(self,passengers):
if passengers is None:
self.passengers = []
else:
self.passengers = list(passengers)

经过上边这样的内部处理,就不会发生上边的幽灵巴士案件。所以,在类中直接把参数复制给实例变量以前一定要考虑清楚。