Python03深入
Python深入
特殊方法与多范式
特殊方法,或者说魔术方法, 方法名的前后各有两个下划线, 可以通过dir()查看对象拥有的特殊方法
1 | print(dir(1)) |
当对象中定义了特殊方法的时候,Python也会对它们有“特殊优待”。比如定义了__init__()方法的类,会在创建对象的时候自动执行__init__()方法中的操作。
运算符
Python的运算符是通过调用对象的特殊方法实现的
1 | result = 'abc' + 'xyz' |
所以,在Python中,两个对象是否能进行加法运算,首先就要看相应的对象是否有__add__()方法。一旦相应的对象有__add__()方法,即使这个对象从数学上不可加,我们都可以用加法的形式,来表达obj.__add__()所定义的操作。在Python中,运算符起到简化书写的功能,但它依靠特殊方法实现。
Python不强制用户使用面向对象的编程方法。用户可以选择自己喜欢的使用方式 (比如选择使用+符号,还是使用更加面向对象的__add__()方法)。
内置函数
与运算符类似,许多内置函数也都是调用对象的特殊方法
1 | result = len([1,2,3]) |
列表(list)元素引用
程序运行到li[3]的时候,Python发现并理解[]符号,然后调用__getitem__()方法。
1 | li = [1,2,3,4,5] |
函数
函数也是一种对象。实际上,任何一个有__call__()特殊方法的对象都被当作是函数。比如下面的例子:
1 | class Samples(object): |
add为SampleMore类的一个对象,当被调用时,add执行加5的操作。add还可以作为函数对象,被传递给map()函数。
上下文管理器
上下文管理器(context manager)是Python2.5开始支持的一种语法,用于规定某个对象的使用范围。一旦进入或者离开该使用范围,会有特殊操作被调用 (比如为对象分配或者释放内存)。它的语法形式是with...as...
关闭文件
1 | # without context manager |
1 | 两段程序实际上执行的是相同的操作。 |
自定义
任何定义了__enter__()和__exit__()方法的对象都可以用于上下文管理器。文件对象f是内置对象,所以f自动带有这两个特殊方法,不需要自定义。
1 | class MyVow(object): |
在进入上下文和离开上下文时,对象的text属性发生了改变(最初的text属性是”I’m fine”)。
__enter__()返回一个对象。上下文管理器会使用这一对象作为as所指的变量,也就是myvow。在__enter__()中,我们为myvow.text增加了前缀 (“I say: “)。在__exit__()中,我们为myvow.text增加了后缀(“!”)。
注意:
__exit__()中有四个参数。当程序块中出现异常(exception),__exit__()的参数中exc_type, exc_value, traceback用于描述异常。我们可以根据这三个参数进行相应的处理。如果正常运行结束,这三个参数都是None。
对象的属性
Python一切皆对象(object),每个对象都可能有多个属性(attribute)。Python的属性有一套统一的管理方案。
属性的__dict__系统
对象的属性储存在对象的__dict__属性中。__dict__为一个词典,键为属性名,对应的值为属性本身。
1 | class bird(object): |

Python中的属性是分层定义的,比如这里分为object/bird/chick/summer这四层。当我们需要调用某个属性的时候,Python会一层层向上遍历,直到找到那个属性。(某个属性可能出现在不同的层被重复定义,Python向上的过程中,会选取先遇到的那一个,也就是比较低层的属性定义)。
上面的情况中,我们已经知道了summer对象的类为chick,而chick类的父类为bird。如果只有一个对象,而不知道它的类以及其他信息的时候,我们可以利用__class__属性找到对象的类,然后调用类的__base__属性来查询父类
1 | print(summer.__class__) # chick 查询对象所属的类 <class '__main__.chick'> |
特性property
特性(property)。同一个对象的不同属性之间可能存在依赖关系。当某个属性被修改时,我们希望依赖于该属性的其他属性也同时变化。这时,我们不能通过__dict__的方式来静态的储存属性。Python提供了多种即时生成属性的方法。其中一种称为特性(property)。
特性使用**内置函数property()**来创建。
1 | class bird(object): |
property()最多可以加载四个参数。前三个参数为函数,分别用于处理查询特性、修改特性、删除特性。最后一个参数为特性的文档,可以为一个字符串,起说明作用。
1 | class num(object): |
使用特殊方法__getattr__
我们可以用__getattr__(self, name)来查询即时生成的属性。当我们查询一个属性时,如果通过__dict__方法无法找到该属性,那么Python会调用对象的__getattr__方法,来即时生成该属性。
1 | class bird(object): |
每个特性需要有自己的处理函数,而__getattr__可以将所有的即时生成属性放在同一个函数中处理。__getattr__可以根据函数名区别处理不同的属性。比如上面我们查询属性名weight的时候,raise AttributeError。
Python中还有一个
__getattribute__特殊方法,用于查询任意属性。__getattr__只能用来查询不在__dict__系统中的属性
__setattr__(self, name, value)和__delattr__(self, name)可用于修改和删除属性。它们的应用面更广,可用于任意属性。
闭包
函数对象的作用域
和其他对象一样,函数对象也有其存活的范围,也就是函数对象的作用域。函数对象是使用def语句定义的,函数对象的作用域与def所在的层级相同。
1 | def line_conf(): |
如果使用lambda定义函数,那么函数对象的作用域与lambda所在的层级相同。
闭包
函数是一个对象,所以可以作为某个函数的返回结果。
1 | def line_conf(): |
引用了外部的变量
1 | def line_conf1(): |
一个函数和它的环境变量合在一起,就构成了一个闭包(closure) , 即闭包是一个包含有环境变量取值的函数对象。环境变量取值被保存在函数对象的__closure__属性中。
1 | def line_conf1(): |

闭包与并行运算
闭包有效的减少了函数所需定义的参数数目。这对于并行运算来说有重要的意义。
在并行运算的环境下,我们可以让每台电脑负责一个函数,然后将一台电脑的输出和下一台电脑的输入串联起来。最终,我们像流水线一样工作,从串联的电脑集群一端输入数据,从另一端输出数据。这样的情境最适合只有一个参数输入的函数。闭包就可以实现这一目的。正适合函数式编程。
装饰器 decorator
装饰器可以对一个函数、方法或者类进行加工
装饰函数和方法
使用场景: 在原有功能上新增需求
1 | # 原代码 |
1 | # 使用装饰器实现修改 |
装饰器可以用def的形式定义,如上面代码中的decorator。装饰器接收一个可调用对象作为输入参数,并返回一个新的可调用对象。装饰器新建了一个可调用对象,也就是上面的new_F。new_F中,我们增加了打印的功能,并通过调用F(a, b)来实现原有函数的功能。
如果我们有其他的类似函数,我们可以继续调用decorator来修饰函数,而不用重复修改函数或者增加新的封装。这样,我们就提高了程序的可重复利用性,并增加了程序的可读性。
含参的装饰器
在上面的装饰器调用中,比如@decorator,该装饰器默认它后面的函数是唯一的参数。装饰器的语法允许调用decorator时,提供其它参数,比如@decorator(a)。这样,就为装饰器的编写和使用提供了更大的灵活性。
1 | def pre_str(pre = ''): |
上面的pre_str是允许参数的装饰器。它实际上是对原有装饰器的一个函数封装,并返回一个装饰器。我们可以将它理解为一个含有环境参量的闭包。当我们使用@pre_str('-_-')调用的时候,Python能够发现这一层的封装,并把参数传递到装饰器的环境中。
装饰类
在Python 2.6以后,装饰器被拓展到类。一个装饰器可以接收一个类,并返回一个类,从而起到加工类的效果。
1 | def ClassDecorator(aClass): |
在decorator中,我们返回了一个新类newClass。在新类中,我们记录了原来类生成的对象(self.call_class),并附加了新的属性called_time,用于记录调用display的次数。我们也同时更改了display方法。
内存管理
对象的内存使用
为了探索对象在内存的存储,可以求助于Python的内置函数id()。它用于返回对象的身份(identity)
1 | a = 1 # 整数1为一个对象。而a是一个引用。 |
整数和短小的字符,Python都会缓存这些对象,以便重复使用。
当我们创建多个等于1的引用时,实际上是让所有这些引用指向同一个对象。
1 | b = 1 |
为了检验两个引用指向同一个对象,我们可以用is关键字。is用于判断两个引用所指的对象是否相同。
1 | a = 1 |
在Python中,每个对象都有存有指向该对象的引用总数,即引用计数(reference count)。
我们可以使用sys包中的getrefcount(),来查看某个对象的引用计数
需要注意的是,当使用某个引用作为参数,传递给getrefcount()时,参数实际上创建了一个临时的引用。因此,getrefcount()所得到的结果,会比期望的多1。
1 | from sys import getrefcount |
对象引用对象
Python的一个容器对象(container),比如表、词典等,可以包含多个对象。实际上,容器对象中包含的并不是元素对象本身,是指向各个元素对象的引用。
也可以自定义一个对象,并引用其它对象:
1 | class from_obj(object): |
对象引用对象,是Python最基本的构成方式。
即使是a = 1这一赋值方式,实际上是让词典的一个键值”a”的元素引用整数对象1。
该词典对象用于记录所有的全局引用。该词典引用了整数对象1。
我们可以通过内置函数globals()来查看该词典。
当一个对象A被另一个对象B引用时,A的引用计数将增加1。
1 | a = 1 |
容器对象的引用可能构成很复杂的拓扑结构。我们可以用objgraph包来绘制其引用关系,objgraph是Python的一个第三方包。安装之前需要安装xdot
1 | # pip install xdot && pip install objgraph |
1 | x = [1,2,3] |


两个对象可能相互引用,从而构成所谓的引用环(reference cycle)。引用环会给垃圾回收机制带来很大的麻烦
1 | import objgraph |

即使是一个对象,只需要自己引用自己,也能构成引用环。
1 | import objgraph |

引用减少
某个对象的引用计数可能减少。比如,可以使用del关键字删除某个引用:
1 | a = [1,2,3] |
1 | # 如果某个引用指向对象A,当这个引用被重新定向到某个其他对象B时,对象A的引用计数减少: |
垃圾回收
当Python的某个对象的引用计数降为0时,说明没有任何引用指向该对象,该对象就成为要被回收的垃圾了。
Python只会在特定条件下,自动启动垃圾回收
运行时记录其中分配对象(object allocation)和取消分配对象(object deallocation)的次数。
当两者的差值高于某个阈值时,垃圾回收才会启动。
可以通过gc模块的get_threshold()方法,查看该阈值:
1 | import gc |
分代回收
1 | # 分代(generation)回收的策略。这一策略的基本假设是,存活时间越久的对象,越不可能在后面的程序中变成垃圾。 |
1 | import gc |
孤立的引用环
引用环的存在会给上面的垃圾回收机制带来很大的困难。
这些引用环可能构成无法使用,但引用计数不为0的一些对象。
1 | from sys import getrefcount |
先创建了两个表对象,并引用对方,构成一个引用环。删除了a,b引用之后,这两个对象不可能再从程序中调用,就没有什么用处了。但是由于引用环的存在,这两个对象的引用计数都没有降到0,不会被垃圾回收。

为了回收这样的引用环,Python复制每个对象的引用计数,可以记为gc_ref。假设,每个对象i,该计数为gc_ref_i。Python会遍历所有的对象i。对于每个对象i引用的对象j,将相应的gc_ref_j减1。

在结束遍历后,gc_ref不为0的对象,和这些对象引用的对象,以及继续更下游引用的对象,需要被保留。而其它的对象则被垃圾回收。