最近学习Python,整理出来一部分重难点与大家分享。
一、变量不是盒子
Lynn Stein教授曾指出,人们常将变量视为存放数据的盒子,但这一概念不利于理解面向对象语言中的变量。事实上,Python中的变量类似于Java中非基元类型的变量,默认传递引用而不是拷贝。这也就意味着,可变对象的使用要特别小心。
Lynn Stein建议将变量理解为对内存中数据的标签,也就是引用。
下面这个例子说明了原因。
a= [1, 2, 3]
b = a
a.append(4)
print(f"a:{a}")
print(f"b:{b}")
"""
输出:
a:[1, 2, 3, 4]
b:[1, 2, 3, 4]
"""通过变量b就可以看出这个效果。如果你将b视作盒子,存储a盒子中[1, 2, 3]的拷贝,那这个行为是说不通的。因此b = a不是将盒子a中的数据复制一份再放在了新盒子b中,而是为内存中的[1, 2, 3]对象又贴上了一张标签b。
二、相等性、统一性和别名
来看这个例子:
a = {"a": 1, "b": 2}
b = a
c = {"a" : 1, "b": 2}
print(f"a == b : {a == b}")
print(f"a is b : {a is b}")
print(f"a == c : {a == c}")
print(f"a is c : {a is c}")
"""
输出:
a == b : True
a is b : True
a == c : True
a is c : False
"""从这个例子中,我们发现is和==具有不同的行为。a is c与a == c的值并不相同。自此,我们引入别名的概念。
在本例中,a与b持有同一个对象的引用,所以可以说,b是a的别名。而c不是a的别名,因为两者指向的对象持有的数据等价,但并没有指向相同的对象。
回到is和==上来,is比较对象的id,在CPython中,对象的id就是对象的地址,不同的对象拥有不同的地址,但不可变对象具有一些特殊行为。而==默认比较id,但它可以被特殊方法__eq__重载。a == b其实是a.__eq__(b)的语法糖。对于dict而言,dict实现了__eq__方法,只比较对象持有的数据。is适合比较一个对象与一个单例,例如x is None而不写作x == None。在一般情况下,我们只关注值,所以==更常用。
三、不可变对象的奇特行为
元组常被认作“不可变列表”,但这是错误的理解。元组只保证具有相对不可变性,因为它存储的仍然是对象的引用,元组只能保证引用不变,而非对象不变。
所以,如果元组中的元素是可变对象,元素仍然可以被修改。例如:
t = (1, 2, [3, 4])
t[2][1] = 5
print(t)这样的操作是被允许的,因为元组内的列表元素仍然可以被修改。但是,如果尝试修改元组中的元素本身,会得到一个错误提示:
t[0] = 0 # TypeError: 'tuple' object does not support item assignment还需要强调的是,tuple(tuple_name)构造器会返回tuple_name的别名。如下:
tuple_a = (1, 2, 3)
tuple_b = tuple(tuple_a)
print(f"tuple_a is tuple_b : {tuple_a is tuple_b}")
list_a = [1, 2, 3]
list_b = list(list_a)
print(f"list_a is list_b : {list_a is list_b}")正如官方文档所说:
If the argument is a tuple, the return value is the same object.
如果实参是一个元组,返回值是同一个对象。
此外,set和frozenset也有此类行为。
CPython还存在称为驻留的机制(interning),会共享一些小整数和字符串字面量。
这是一个例子:
>>> a = "a"
>>> b = "a"
>>> a is b
True
>>> a == b
True变量a和b本应该不是同一个字符串对象的引用,但CPython悄悄做了手脚。
最后,驻留作为内部优化,不应被依赖或过多提起。
四、不要用可变对象充当默认参数
很多教程都谈到,不要使用可变对象作为默认参数,这里我们解释原因。
看这个例子:
class Bus:
"""
传说中的幽灵巴士
"""
def __init__(self, passengers = None):
self.passengers = passengers
def add(self, name):
self.passengers.append(name)
def drop(self, name):
self.passengers.pop(name)
bus1 = Bus()
bus1.add("jack")
bus1.add("nick")
bus2 = Bus()
bus2.add("helen")
print(bus2.passengers)一切的原因就在于,默认参数仅在函数声明时初始化(求值)一次。默认参数始终都是同一个对象,这将导致不经意间的共享数据。
在确实需要传递可变对象时,请将参数默认值设为一哨符对象,例如None。此外不要忘记进行一次浅拷贝。
这是正确的例子:
class Bus:
"""
从来没有什么幽灵!
"""
def __init__(self, passengers = None):
self.passengers = list(passengers) if passengers is not None else []
def add(self, name):
self.passengers.append(name)
def drop(self, name):
self.passengers.pop(name)
bus1 = Bus()
bus1.add("jack")
bus1.add("nick")
bus2 = Bus()
bus2.add("helen")
print(bus2.passengers)打字不易,多多支持!!!
笔者能力有限,错漏难免,请读者指出!








暂无评论内容