对《流畅的python》的读书笔记以及个人理解
9-20
接着上一篇的内容,上一篇的链接:https://blog.csdn.net/scrence/article/details/100905929
Python唯一支持的参数传递模式是共享传参,这种机制类似于C++中的按引用传参,但是也有一些区别。
共享传参指函数的各个形式参数获得实际参数中各个引用的副本,也就是说,函数内部的形参是实参的别名。这种方案的结果是,函数可能会修改作为参数传入的对象,但是无法修改那些对象的标识(即不能把一个对象替换成另一个对象)。
在函数中,一般将定义函数时设置的参数称为形式参数,在真正调用函数时给这个函数传递的参数称为实际参数,python的参数传递机制规定,作为参数传入的可变对象,函数在运算后,可能会修改原来的对象。
如何理解这句话?首先要注意,什么参数传进函数可能会被改变?是可变对象作为参数传进函数的时候会被改变。可变对象最常见的有列表对象,字典对象等等,如果是int,str,tuple这种不可变对象,那么传进函数后是不会对原来的对象有影响的。其次,可能会修改原来的对象?那什么情况会修改?在函数内部代码涉及操作参数的时候。下面通过代码来说明:
>>> def f(a, b):
... a += b
这里定义了一个简单的函数,它简单地将传递进来的两个参数相加,这是涉及了对参数的修改,现在来看看对于可变对象和不可变对象,这段代码会产生什么样的影响:
>>> x = 10 # 不可变对象
>>> y = 20
>>> f(x, y)
>>> x
10
>>> y
20
可以看到,经过函数的操作后,原来的x和y并没有什么不同,这是正常的,因为它们是不可变对象。
对于可变对象,情况有些不同:
>>> v = [1,2]
>>> w = [3,4]
>>> f(v,w)
>>> v
[1, 2, 3, 4]
对于列表对象而言,函数f修改了原对象的内容。
可选参数可以有默认值,这个是语言的特性。但是,应该尽量避免使用可变的对象作为参数的默认值。下面在代码中说明这个问题。
class HauntedBus:
"""备受幽灵乘客折磨的校车"""
# 以上一个Bus类为基础,在__init__()中修改参数passengers的默认值
# 将None改为一个空列表对象,这样就不用进行判断了
# 这种看似聪明的方法却会带来巨大的麻烦
# 如果没传入参数,passengers会默认为空列表
def __init__(self, passengers = []):
# 这个赋值语句将self.passengers变为passengers的别名,
# 如果没有传入参数,它又是空列表的别名
self.passengers = passengers
# 调用pick和drop方法时,修改的其实是默认列表,它是函数的一个属性
def pick(self, name):
self.passengers.append(name)
def drop(self, name):
self.passengers.remove(name)
接下来演示其诡异行为:
>>> bus1 = HauntedBus(['Alice', 'Bill'])
>>> bus1.pick('Charlie')
>>> bus1.drop('Alice')
>>> bus1.passengers
['Bill', 'Charlie'] # 目前来看bus1是正常的
>>> bus2 = HauntedBus() # 现在创建bus2,它是默认列表
>>> bus2.pick('Carrie') # bus2添加一个学生
>>> bus3 = HauntedBus() # 创建bus3,它也使用默认列表
>>> bus3.passengers # 访问bus3的passengers,问题来了
['Carrie']
bus3明明使用的默认列表,却凭空出现了一个元素,而这个元素,很显然是在bus2中被添加进去的,这意味着,bus2和bu3内部的passengers又共享了,下面的代码再次验证了这一点:
>>> bus3.pick('Dave') # 向bus3添加一个学生
>>> bus2.passengers # 访问bus2的列表
['Carrie', 'Dave']
>>> bus2.passengers is bus3.passengers # 上一篇文章说过,is 运算符会比较对象的地址
True
既然bus2和bus3的列表是共享的,那么,bus1的呢?
>>> bus1.passengers
['Bill', 'Charlie']
很明显,bus1的列表跟其他对象并没有共享。
问题在于,没有指定初始乘客的HauntedBus实例会共享同一个乘客列表。
实例化HauntedBus时,如果传入乘客,会预期运作。但是不为HauntedBus指定乘客时,奇怪的事情就发生了,这是因为
self.passengers
变成了passengers参数默认值的别名。出现这个问题的根源是,默认值在定义函数时计算(通常在加载模块时),因此默认值变成了函数对象的属性。因此,如果默认值是可变对象,而且修改了它的值,那么后续的函数调用都会受到影响。
可变默认值导致的这个问题说明了为什么通常使用None作为接收可变值的参数的默认值。使用None默认值不会变为函数函数内的一个属性,也就不存在共享。
在Keras的源码中有大量函数的参数都使用的None,而不是一个可变对象,防御任何可能出现异常的情况保证了框架代码的正确执行。
9-22
如果定义的函数接收可变对象,应该谨慎考虑调用方是否期望修改传入的参数。
若函数对可变对象进行了修改操作,那么这种操作所带来的影响要不要体现在函数外部?这需要编写者与调用方达成共识。
重新定义一个类来演示这种问题:
class TwilightBus:
"""让乘客销声匿迹的校车"""
def __init__(self, passenegers = None):
if passenegers is None:
self.passengers = [] # 当passengers为空时,创建一个新的列表
else:
# 这个赋值语句将self.passengers变为传入__init__()的参数passengers
# 的别名。
self.passengers = passenegers
# 在self.passenegrs上做修改操作实际上会修改传入的构造方法的实参
def pick(self, name):
self.passengers.append(name)
def drop(self, name):
self.passengers.remove(name)
TwilightBus类的实例与客户共享乘客列表,这会产生意外的结果:
>>> basketball_team = ['Sue', 'Tina', 'Maya', 'Diana', 'Pat']
>>> bus = TwilightBus(basketball_team)
>>> bus.drop('Tina')
>>> bus.drop('Pat')
>>> basketball_team
['Sue', 'Maya', 'Diana']
在这个例子中,basketball_team是一个独立的列表对象,作为参数传入TwilightBus的构造方法之后,其类对象做的修改却影响到了basetball_team的内容,很明显,这并不是我们想要的内容,因为basketball_team与类实例中的passengers共享的是同一个列表对象,要想解决这个问题,就需要修改TwilightBus类的__init__(self, passenegers = None)
:
def __init__(self, passenegers = None):
if passenegers is None:
self.passengers = []
else:
# 这里用list()复制,而不是简单地起了别名
self.passengers = list(passenegers)
在最后一行代码部分,使用了list()复制了passengers参数,使得类实例以参数为模板,创建出了它的副本,这两个列表之间是不一样的对象(使用id()可以知道),他们内容相同,但是地址不同。这样的解决方法可以使类对象不至于修改可变的参数对象,除非这个方法确实需要修改传入的参数对象,否则在类中直接把参数赋值给实例变量之前一定要谨慎,因为这样会为参数对象创建别名。如果不确定方法是否真的需要修改原参数,那就直接创建副本,这样会少一些麻烦。
插个题外话,这可能是原作者不愿使文章篇幅太长而略过了这个问题,我在这里做个补充。
其实使用list(passengers)这种方式还是存在一些问题,因为list(passengers)这种代码实际上只是浅复制,这样做要想真正地不修改原参数还是不够严谨。我们之所以要防御可变参数,根源在于可变参数与类内部自己的属性对象会存在共享,而我们的解决方法就是让他们不共享,真正地创建出一个完全独立的对象。然而,list()这种方法并不是完全地创建出独立对象,因为它是浅复制,在复制对象的时候,对于passengers列表中的可变对象,复制时仍然会把它设为共享的,这样是为了节省内存。假如说,现在原basketball_team对象是这样的:['Sue', ['Tina', 'Maya', 'Diana', 'Pat']]
,Sue是篮球队的教练,指挥一个队伍,这个队伍用列表包装起来,那么这个队伍实际上还是可变参数,在TwilightBus的构造方法内部简单地使用list()复制basketball_team时,对于篮球队,self.passengers与passengers仍然是共享的,这是浅复制所带来的结果,若类方法针对篮球队伍做修改,比如说,上了公交车的篮球队中,成员Tina因公交车位不足而改坐别的车,那么类方法就需要对篮球队中的队伍做修改,将列表['Sue', ['Tina', 'Maya', 'Diana', 'Pat']]
修改为['Sue', ['Maya', 'Diana', 'Pat']]
,那么由于self.passengers与basketball_team共享了 ['Tina', 'Maya', 'Diana', 'Pat']
这个列表,在类方法中针对队员队伍做的修改会影响到原来basketball_team中的队员队伍:
class TwilightBus:
"""让乘客销声匿迹的校车"""
def __init__(self, passenegers = None):
if passenegers is None:
self.passengers = []
else:
self.passengers = list(passenegers)
def pick(self, name):
self.passengers.append(name)
def drop(self, name):
# 这里我针对传入的参数列表的第二个元素做了修改,与上面的代码不同
# 上面的代码是self.passengers.remove(name),其他的代码都是一样的
self.passengers[1].remove(name)
basketball_team = ['Sue', ['Tina', 'Maya', 'Diana', 'Pat']]
bus = TwilightBus(basketball_team)
bus.drop('Tina')
print(basketball_team)
运行结果为['Sue', ['Maya', 'Diana', 'Pat']]
。
可以看到,因为公交车座位不足,队员Tina去了别的公交车,而方法直接将队员Tina开除出了篮球队。当然,drop的代码是我专门针对basketball_team这个列表编写的,目的是为了演示。那么为了不使Tina因为车位不足就被开除出队,我们就要解决问题,问题的根源仍然是对象共享的问题,为了完完全全使basketball_team与self.passengers隔绝开来,就需要使用内置模块copy中的deepcopy()方法来完成这个任务(具体内容参见于上一篇:
第八章《对象引用、可变性和垃圾回收》(上)中的8.3 默认做浅复制):
# 在此之前要导入copy模块 import copy
def __init__(self, passenegers = None):
if passenegers is None:
self.passengers = []
else:
# 这里就使用了深复制,这个操作可以使passengers与self.passengers完全独立
self.passengers = copy.deepcopy(passenegers)
这样一来,上面的代码就不会将Tina开除出队了。
del语句删除名称,而不是对象。仅当删除的变量保留的是对象的最后一个引用,或者无法得到对象时,,del命令可能导致对象被当做垃圾回收。另外,重新绑定也可能会导致对象的引用数量归零,导致对象被销毁。
在Cpython中,垃圾回收使用的主要算法是引用计数。实际上,每个对象都会统计有多少个引用指向自己,当引用计数归零时,对象会被立即销毁:Cpython会在对象上调用__del__
方法,然后释放分配给对象的内存。python的其他实现有更复杂的垃圾回收程序,且不依赖与引用计数,这意味着,对象引用计数归零时可能不会立即调用__del__
方法。
>>> import weakref
>>> s1 = {1,2,3}
>>> s2 = s1 # s1和s2是别名,指向同一个集合:{1,2,3}
>>> def bye(): # 这个函数一定不能是要销毁对象的绑定方法,否则会有一个指向对象的引用
... print("Gone with the wind...")
>>> ender = weakref.finalize(s1, bye) # 在s1引用的对象上注册bye回调
>>> ender.alive # 调用finalize对象之前,.alive属性的值为True
True
>>> del s1
>>> ender.alive # del不删除对象,而是删除对象的引用
True
>>> s2 = 'spam' # 重新绑定最后一个引用s2,让{1,2,3}无法获取。对象被销毁了,调用了bye回调,ender.alive变为False
Gone with the wind...
>>> ender.alive
False
为什么{1,2,3}对象被销毁了?毕竟,代码将s1引用传递给finalize函数了,而为了监控对象和调用回调,必须要有引用,既然有引用,为什么set对象仍然被销毁?这是因为finalize持有的是{1,2,3}的弱引用。
因为有引用的存在,对象才得以在内存中存在。当对象的引用清零后,垃圾回收程序会将对象销毁。
有一类特殊的引用,弱引用,它指向对象时,并不会增加对象的引用计数,因此,弱引用并不会影响垃圾回收程序回收对象,它只是在对象引用清零时,在回收程序即将回收这个对象之前,仍然保留这个对象的引用以做一些用途,但当回收程序开始回收时,弱引用不会影响程序的工作。
弱引用在缓存中很有用,因为我们不想仅仅因为缓存对象被引用着而始终保留缓存对象。
下面的例子是一个控制台会话,python控制台会自动把_变量绑定到结果不为None的表达式结果上。
弱引用是可调用的对象,返回的是被引用的对象;如果所指对象不存在了,返回None
>>> import weakref
>>> a_set = {0, 1}
>>> wref = weakref.ref(a_set) # 创建弱引用对象wref
>>> wref
>>> wref()# 调用wref()返回的是被引用的对象,{0,1}。因为这是控制台会话,所以{0,1}会绑定给 _ 变量
{0, 1}
>>> a_set = {2,3,4} # a_set不再指代{0,1}集合,因此集合的引用数量减少了。但是 _ 变量仍然指代它。
>>> wref() # 调用wref()仍然返回{0,1}
{0, 1}
>>> wref() is None # 计算这个表达式的时候,{0,1}存在,因为_变量指向它,但是随后_绑定到表达式结果False。现在{0,1}没有强引用了。
False
>>> wref() is None 因为{0,1}对象已被清理,所以wref()返回None
True
weakref.ref类是偏底层的接口,多数程序最好使用weakref集合和finalize。也就是说,更推荐使用WeafKeyDictionary、WeakValueDictionary、WeakSet和finalize。
WeakValueDictionary类实现的是一种可变的映射,里面的值是对象的弱引用。被引用的对象在程序中的其他地方被回收后,对应的键会自动从WeakValueDictionary中删除。
class Cheese:
def __init__(self, kind):
self.kind = kind
>>> import weakref
>>> stock = weakref.WeakValueDictionary() # stock是WeakValueDictionary类实例
>>> catalog = [Cheese('Red Leicester'), Cheese('Tilsit'), Cheese('Brie'), Cheese('Parmesan')]
>>> for cheese in catalog:
... stock[cheese.kind] = cheese # stock将kind映射到catalog中Cheese实例的弱引用上。
>>> sorted(stock.keys())
['Brie', 'Parmesan', 'Red Leicester', 'Tilsit'] # stock是完整的
>>> del catalog
>>> sorted(stock.keys())
['Parmesan'] # 删除catalog之后,stock的大部分元素不见了,只剩最后一个元素Parmesan,为什么不是全部呢?
>>> del cheese # 删除全局变量cheese
>>> sorted(stock.keys())
[]
上面的代码之所以剩下Parmesan是因为临时变量cheese引用了这个对象,它使得该变量存在的时间比预期长。通常,这对局部变量来讲不是问题,因为它们会在函数范湖时被销毁,但在上面的代码中,for循环的cheese是全局变量,除非显式删除,否则不会消失。
不是每个python对象都可以作为弱引用的目标。基本的list和dict实例都不能作为弱引用的目标,但它们的子类却可以。
set和用户自定义类型都可以作为弱引用的目标,而对于int和tuple,不仅它们本身不行,它们的子类也不行。
这些局限基本上是CPython的实现细节,其他的python解释器可能不一样,这些局限是内部优化的结果。
对于元组来讲,t[:]和tuple(t)都返回的是同一个元组的引用(对于list来讲则是浅复制,创建一个副本):
>>> t1 = (1,2,3)
>>> t2 = tuple(t1)
>>> t2 is t1 # t2 与t1 绑定在同一个对象
True
>>> t3 = t1[:]
>>> t3 is t1 # t3也是
True
str、bytes和frozenset实例也有这种行为。
>>> s1 = 'ABC'
>>> s2 = 'ABC'
>>> s1 is s2
True
很明显,s1和s2是两个字符串对象,但使用is比较时,却仍然返回True,这其实是一种优化措施,称为驻留,s1和s2是共享的字符串字面量。Cpython还会在比较小的整数上使用这个优化措施,防止重复创建“热门数字”。但是,千万不要依赖于字符串或者整数的驻留,比较字符串或者整数是否相等时,要使用==
运算符而不是is
。
每个Python对象都有标识、类型和值。只有对象的值会不时变化。
如果两个变量指代的不可变对象具有相同的值(a == b 为 True),实际上跟他们指代的是副本还是同一个对象的别名基本没什么关系,因为不可变对象的值不会变,但有一个例外,这里说的例外是不可变的集合,如元组和frozenset:如果不可变集合保存的是可变元素的引用,那么可变元素的值发生变化后,不可变集合也会随之改变,这是不可变集合的相对不变性。但实际上,这种情况并不常见。不可变集合不变的是所含对象的标识。
变量保存的是引用,这一点对编程有很多实际的影响:
1、简单的赋值不创建副本。
2、对+= 或者 *= 所做的增量赋值来说,如果左边的变量绑定的是不可变对象,那么增量赋值会创建新的对象,如果是可变对象,会就地修改。
3、为现有的变量赋予新值,不会修改之前绑定的变量。这叫重新绑定:现在变量绑定了其他对象。如果变量是之前那个对象最后的引用,那么对象会被当做垃圾回收
4、函数的参数以别名的形式传递,这意味着,函数可能会修改通过参数传入的可变对象。这一行为无法避免,除非在本地创建副本,或者使用不可变对象(例如:传入元组,而不是列表)。
5、使用可变类型作为函数参数的默认值有危险,因为如果就地修改了参数,默认值也就变了,这会影响以后使用默认值的调用
在CPython中,对象的引用数量归零后,对象会被立即销毁。如果除了两个循环引用之外没有其他引用,两个对象都会被销毁。在某些情况下,可能需要保存在对象的引用,但不保留对象本身。例如,有一个类想要记录所有实例。这个需求可以使用弱引用实现,这是一种低层机制,是weakref模块中WeakValueDictionary、WeakKeyDictionary和WeakSet等有用的集合类,以及finalize函数的底层支持。
对于weakref类,可以参考官方文档:https://docs.python.org/3/libary/weakref.html