《流畅的Python》笔记本篇是“面向对象惯用方法”的第一篇,一共六篇。本篇主要是一些概念性的讨论,内容有:Python中的变量,对象标识,值,别名,元组的某些特性,深浅复制,引用,函数参数,垃圾回收,del命令,弱引用等,比较枯燥,但却能解决程序中不易察觉的bug。html
先用一个形象的比喻来讲明Python中的变量:变量是标注而不是盒子。也就是说,Python中的变量更像C++中的引用,最能说明这一点的就是多个变量指向同一个列表,但也有例外,在遇到某些内置类型,好比字符串str
时,变量则变成了“盒子”:python
# 代码1 >>> a = [1, 2] >>> b = a # 标注,引用 >>> a.append(3) >>> b [1, 2, 3] >>> c = "c" >>> d = c # “盒子” >>> c = "cc" >>> d 'c'
补充:说到了赋值方式,Python和C++同样,也是等号右边先执行。算法
用一个更学术的词来替换“标注”,那就是“别名”。在C++中,引用就是变量的别名,Python中也是,好比代码1
中的变量b
就是变量a
的别名,但若是是如下形式,变量b
则不是a
的别名:编程
# 代码2 >>> a = [1, 2] >>> b = [1, 2] >>> a == b # a和b的值相等 True >>> a is b # a和b分别绑定了不一样的对象,虽然对象的值相等 False
==
检测对象的值是否相等,is
运算符检测对象的标识(ID)是否相等,id()
返回对象标识的整数表示。通常判断两对象的标识是否相等并不直接使用id()
,更多的是使用is
运算符。缓存
对象ID在不一样的实现中有所不一样:在CPython中,id()
返回对象的内存地址,但在其余Python解释器中多是别的值。但无论怎么,对象的ID必定惟一,且在生命周期中保持不变。微信
一般咱们关心的是值,而不是标识,因此==
出现的频率比is
高。但在变量和单例值之间比较时,应该使用is
。目前,最常使用is
检测变量绑定的值是否是None
,推荐的写法是:数据结构
# 代码3 x is None # 并不是 x == None x is not None # 并不是 x != None
is
运算符比==
速度快,由于它不能重载,因此Python不用寻找并调用特殊方法,而是直接比较两个对象的ID。a == b
实际上是语法糖,实际调用a.__eq__(b)
。虽然继承自object
的__eq__
方法也是比较对象的ID,结果和is
同样,但大多数内置类型覆盖了该方法,处理过程更复杂,这就是为何is
比==
快。app
元组和大多数Python集合同样,保存的是对象的引用。元组的不可变性实际上是指tuple
数据结构的物理内容(即保存的引用)不可变,与引用的对象无关。若是引用的对象可变,即使元组自己不可变,元素依然可变,不变的是元素的标识:框架
# 代码4 >>> t1 = (1, 2, [30, 40]) >>> t2 = (1, 2, [30, 40]) >>> t1 == t2 True >>> id(t1[-1]) 2019589413704 >>> t1[-1].append(99) >>> t1 (1, 2, [30, 40, 99]) >>> id(t1[-1]) # 内容变了,标识没有变 2019589413704 >>> t1 == t2 False
这同时也说明,并非每一个元组都是可散列的!函数
复制对象时,相等性和标识之间的区别有更深刻的影响。副本与源对象相等,但ID不一样。而若是对象内部还有其余对象,这就涉及到了深浅复制的问题:究竟是复制内部对象呢仍是共享内部对象?
对列表和其余可变序列来讲,咱们可使用构造方法或[:]
来建立副本。然而,这两种方法作的都是浅复制,它们只复制了最外层的容器,副本中的元素是源容器中元素的引用。若是全部元素都是不可变的,那这样作没问题,还能节省内存;但若是其中有可变元素,这么作就可能出问题:
# 代码5 l1 = [3, [11, 22], (7, 8)] l2 = list(l1) # <1> l1.append(100) l1[1].remove(22) print("l1:", l1, "\nl2:", l2) l2[1] += [33, 44] # <2> l2[2] += (10, 11) # <3> print("l1:", l1, "\nl2:", l2) # 结果 l1: [3, [11], (7, 8), 100] # 追加元素只影响了l1 l2: [3, [11], (7, 8)] # 但删除l1[1]中的元素影响了两个列表 l1: [3, [11, 33, 44], (7, 8), 100] # +=对可变对象是就地操做,影响了两个列表 l2: [3, [11, 33, 44], (7, 8, 10, 11)] # +=对不可变对象会建立新对象,只影响了l2
以上代码有3点须要解释:
l1[1]
和l2[1]
指向同一列表,l1[2]
和l2[2]
指向同一元组。由于是浅复制,只是复制引用;+=
运算对可变对象来讲是就地运算,不会建立新对象,因此对两个列表都有影响;+=
运算对元组这样的不可变对象来讲,等同于l2[2] = l2[2] + (10, 11)
,此操做隐式地建立了新对象,l2[2]
从新绑定到了新对象,因此只有列表l2[2]
发生了改变,而l1[2]
没有改变。浅复制并不是是一种错误,只是一种选择。而有时咱们须要的是深复制,即副本不共享内部对象的引用。copy模块提供的deepcopy
和copy
函数能为任意对象作深复制和浅复制。
# 代码6 import copy l1 = [3, [11, 22]] l2 = copy.copy(l1) # 浅复制 l3 = copy.deepcopy(l1) # 深复制 l1[1].append(33) # 影响了l2,但没有影响l3 print("l1:", l1, "\nl2:", l2, "\nl3:", l3) # 结果 l1: [3, [11, 22, 33]] l2: [3, [11, 22, 33]] l3: [3, [11, 22]]
在作深复制时,若是对象之间有循环引用,朴素的深复制算法(换句话说就是你本身写的深复制算法)极可能会陷入无限循环,而后报错。deepcopy
会记住已经复制的对象,而不会进入无限循环:
# 代码7 >>> a = [10, 20] >>> b = [a, 30] # 包含a的引用 >>> b [[10, 20], 30] >>> a.append(b) # 相互引用 >>> a [10, 20, [[...], 30]] >>> a[2][0] [10, 20, [[...], 30]] >>> a[2][0][2][0] [10, 20, [[...], 30]] >>> from copy import deepcopy >>> c = deepcopy(a) # 不会报错,能正确处理相互引用的问题 >>> c [10, 20, [[...], 30]]
此外,深复制有时可能太深了。例如,对象可能会引用不应复制的外部资源或单例值,这时,深复制就不该该复制这些值。若是要控制copy
和deepcopy
的行为,咱们能够在对象中重写特殊方法__copy__
和__deepcopy__
,具体内容这里就不展开了,你们能够参考copy模块的官方文档。
经过别名共享对象还能解释Python中传递参数的方式,以及使用可变类型做为参数默认值引发的问题。
Python惟一支持的参数传递模式是共享传参(call by sharing),它指函数的形参得到实参中各个引用的副本,即形参是实参的别名。这种方案的结果就是,函数可能会修改做为参数传入的可变对象,但没法修改这些对象的标识(不能把一个对象替换成另外一个对象):
# 代码8 def f(a, b): a += b return a x, y = 1, 2 print(f(x, y), x, y) a, b = [1, 2], [3, 4] print(f(a, b), a, b) t, u = (10, 20), (30, 40) print(f(t, u), t, u) # 结果 3 1 2 # x, y是不可变对象,没有影响到x, y [1, 2, 3, 4] [1, 2, 3, 4] [3, 4] # x是可变对象,影响到了x (10, 20, 30, 40) (10, 20) (30, 40) # x没有指向新的元组,但形参a指向了新的元组
不要使用可变类型做为参数的默认值!其实这个问题在以前的文章“Python学习之路7-函数”的2.3小节中有所说起。如今咱们来看下面这个例子:
首先定义一个类:
# 代码9 class Bus: def __init__(self, passengers=[]): # 默认值是个可变对象 self.passengers = passengers def pick(self, name): self.passengers.append(name) def drop(self, name): self.passengers.remove(name)
下面是这个类的行为:
# 代码10 >>> bus1 = Bus(["Alice", "Bill"]) # 直到第8行Bus的表现都是正常的 >>> bus1.passengers ['Alice', 'Bill'] >>> bus1.pick("Charlie") >>> bus1.drop("Alice") >>> bus1.passengers ['Bill', 'Charlie'] >>> bus2 = Bus() # 使用默认值 >>> bus2.pick("Carrie") >>> bus2.passengers ['Carrie'] # 到目前为止也是正常的 >>> bus3 = Bus() # 也是用默认值 >>> bus3.passengers ['Carrie'] # 不正常了! >>> bus3.pick("Dave") >>> bus2.passengers ['Carrie', 'Dave'] # bus2的值也被改变了 >>> bus2.passengers is bus3.passengers # 这俩是同一对象的别名 True >>> bus1.passengers # bus1依然正常 ['Bill', 'Charlie']
上述行为的缘由在于,参数的默认值在导入模块时计算,方法或函数的形参指向这个默认值。而在上面这个例子中,类的属性self.passengers
其实是形参passengers
所指向的对象(所指对象,referent)的别名。而bus1
行为正常是由于从一开始它的passengers
就没有指向默认值。
这里有点像单例模式:参数的默认值是惟一的,只要采用默认值,无论建立多少个Bus
的实例,它们的self.passengers
都是同一个空列表[]
对象的别名,不会为每个实例单首创建一个专属的[]
。
运行上述代码以后,能够查看Bus.__init__
对象的__defaults__
属性,它存储了参数的默认值:
# 代码11 >>> Bus.__init__.__defaults__ (['Carrie', 'Dave'],) >>> Bus.__init__.__defaults__[0] is bus2.passengers # self.passengers就是一个别名! True
这也说明了为何要用None
做为接收可变值的参数的默认值:
# 代码12 class Bus: def __init__(self, passengers=None): # 默认值是个可变对象 if passengers is None: # 并不推荐 if passengers == None 这种写法 self.passengers = [] else: self.passengers = list(passengers) # 注意这里! -- snip --
代码12
中的第7行并非直接把形参passengers
赋值给self.passengers
,而是形参的副本(这里是浅复制)。若是直接赋值,即self.passengers = passengers
(self.passengers
变成了用户传入的参数的别名),则用户传入的参数在运行过程当中可能会被修改,而这并不必定是用户想要的,这便违反了"最少惊讶原则"(竟然还真有这么个原则)
对象毫不会自行销毁;然而,没法获得对象时,可能会被当作垃圾回收。——Python语言参考手册
del
语句删除变量(即"引用"),而不是对象。del
命令可能致使对象被当作垃圾回收,但这仅发生在当删除的变量保存的是对象的最后一个引用,或者没法获得对象时(若是两个对象相互引用,如代码7
,当它们的引用只存在两者之间时,垃圾回收程序会断定它们都没法获取,进而把它们都销毁)。从新绑定也可能会致使对象的引用数量归零,进而对象被销毁。
在CPython中,垃圾回收使用的主要算法是引用计数。实际上,每一个对象都会统计有多少个引用指向本身。当引用计数归零时,对象当即被销毁。但在其余Python解释器中则不必定是引用计数算法。
补充:有个__del__
特殊方法,它不是用来销毁实例的,而是在实例被销毁前用来执行一些最后的操做,好比释放外部资源等。咱们不该该在代码中调用它,Python解释器会在销毁实例时先调用它(若是定义了),而后再释放内存。它至关于C++中的析构函数。
咱们可使用weakref.finalize
来演示对象被销毁时的状况:
# 代码13 >>> import weakref >>> s1 = {1, 2, 3} >>> s2 = s1 >>> def bye(): # 它充当一个回调函数 ... print("Gone with the wind...") # 必定不要传入待销毁对象的绑定方法,不然会有一个指向对象的引用 >>> ender = weakref.finalize(s1, bye) # 在s1引用的对象上注册bye回调 >>> ender.alive True >>> del s1 >>> ender.alive True # 说明 del s1并无删除对象 >>> s2 = "spam" Gone with the wind... # 引用计数为零,对象被删除 >>> ender.alive False
不知道你们看到上述代码第15行时会不会产生以下疑惑:第8行代码明明把s1
引用传给了finalize
函数(为了监控对象和调用回调,必需要有引用),那么对象{1, 2, 3}
则应该至少有三个引用,可为何最后它仍是被销毁了呢?这就牵扯到了弱引用这个概念。
弱引用不会妨碍所指对象被当作垃圾回收,即弱引用不会增长对象的引用计数。(弱引用常被用于缓存,但具体用在缓存的哪些地方目前笔者还不清楚.....)
弱引用仍是可调用对象,下面的代码展现了如何使用weakref.ref
实例获取所指对象。
补充在代码以前:Python控制台会自动把结果不为None
的表达式的结果绑定到变量_
(下划线)上。这也说明了一个问题:微观管理内存时,隐式赋值会为对象建立新引用,而这有可能会致使一些意外结果。
# 代码14 >>> import weakref >>> a_set = {1, 2} # 对象{1, 2}的引用数+1 >>> wref = weakref.ref(a_set) # 并无增长所指对象的引用数 >>> wref <weakref at 0x0000013D739E2D18; to 'set' at 0x0000013D739BE588> >>> wref() # 弱引用是个可调用对象 {1, 2} # 发生了隐式赋值,变量 _ 指向了对象{1, 2},引用数+1 >>> a_set = {2, 3} # 引用数 -1 >>> wref() # 所指对象依然存在,尚未被销毁 {1, 2} >>> wref() is None # 此时所指对象依然存在 False # 变量 _ 指向了对象False,对象{1, 2}引用数归零,销毁 >>> wref() is None # 验证所指对象已被销毁 True
weakref.ref
类实际上是底层接口,供高级用途使用,通常程序最好使用werakref
集合和finalize
函数,即最好使用WeakKeyDictionary
、WeakValueDictionary
、WeakSet
和finalize
(它们在内部使用弱引用),不推荐本身动手建立并处理weakref.ref
实例,除非你的工做就是专门和这些东西打交道的。
WeakValueDictionary
类实现的是一种可变映射,里面的值("键值对"中的"值",而不是字典中的"值")是对象的弱引用。被引用的对象在程序中的其余地方被当作垃圾回收后,对应的键会自动从WeakValueDictionary
中删除。所以,它常常用于缓存。(查看缓存中变量是否依然存在?给框架用?)
# 代码15 >>> import weakref >>> class Cheese: ... def __init__(self, kind): ... self.kind = kind ... >>> stock = weakref.WeakValueDictionary() >>> catalog = [Cheese("Red Leicester"), Cheese("Parmesan")] >>> for cheese in catalog: ... stock[cheese.kind] = cheese ... >>> sorted(stock.keys()) ['Red Leicester', 'Parmesan'] # 表现正常 >>> del catalog >>> sorted(stock.keys()) ['Parmesan'] # 这是怎么回事? >>> del cheese # 这是问题所在 >>> sorted(stock.keys()) []
临时变量引用了对象,这可能会致使该变量的存在时间比预期长。一般,这对局部变量来讲不是问题,由于它们在函数返回时会被销毁。但上述代码中,for
循环中的变量cheese
是全局变量,除非显示删除,不然不会消失。
与WeakValueDictionary
对应的是WeakKeyDictionary
,后者的键是弱引用,它的一些可能用途以下:
它的实例能够为应用中其余部分拥有的对象附加数据,这样就无需为对象添加属性。这对属性访问受限的对象尤为有用。
WeakSet
类的用途则很简单:"保存元素弱引用的集合。当某元素没有强引用时,集合会把它删除。"若是一个类须要知道它的全部实例,一种好的方案是建立一个WeakSet
类型的类属性,保存实例的弱引用。
weakref
集合以及通常的弱引用,能处理的对象类型有限:
基本的list
和dict
实例不能做为弱引用的所指对象,但它们的子类则能够;
class MyList(list): """MyList的实例可做为弱引用的所指对象"""
set
的实例可做为所指对象;int
和tuple
的实例不能做为弱引用的所指对象,它们的子类也不行。但这些局限基本上是CPython的实现细节,其余Python解释器的状况可能不一样。
本节内容是Python实现的细节,能够跳过。
这些细节是CPython核心开发者走的捷径和优化措施,利用这些细节写的代码在其余Python解释器中可能没用,在CPython将来的版本中也可能没用。下面是具体内容:
t
来讲,t[:]
和tuple(t)
不建立副本,而是返回同一个对象的引用;str
、bytes
和frozenset
实例也是如此,而且frozenset
的copy
方法返回的也不是副本(注意,frozenset
的实例fs
不能用fs[:]
,由于fs
不是序列);str
的实例还有共享字符串字面量的行为:
>>> s1 = "ABC" >>> s2 = "ABC" >>> s1 is s2 True
这叫作"驻留"(interning),这是一种优化措施。CPython还会在小的整数上使用这种优化,防止重复建立经常使用数字,如0,-1。但CPython不会驻留全部字符串和数字,驻留的条件是实现细节,并且没有文档说明。因此千万不要依赖这个特性!(比较字符串或数字请用==
,而不是is
!)
每一个Python对象都有标识、类型和值,只有对象的值可能变化。
变量保存的是引用,这对Python编程有不少实际的影响:
+=
或*=
等运算符来讲,若是左边的变量绑定了不可变对象,则会建立新对象,而后从新绑定;若是是可变对象,则就地修改;==
用于比较值,is
用于比较引用。某些状况下,可能须要保存对象的引用,但不留存对象自己,好比记录某个类的全部实例,这能够用弱引用解决。
迎你们关注个人微信公众号"代码港" & 我的网站 www.vpointer.net ~