6.《Python高质量代码的91个建议》内部机制

1 建议 54:理解 built-in objects

  • 在 Python 中一切皆对象,type 也是对象

  • Python2中为了兼容,分为古典类(旧式类)和新式类;Python3中所有类都是新式类

  • object 和古典类没有基类,其他所有类(包括新式类)都继承自 object

  • Python2中古典类是深度优先原则,新式类中广度优先原则,Python3对于多重继承的属性搜索都是广度优先搜索

  • 新式类中执行 type(s)a.__class__ 的结果是一样的,但古典类则不一定

2 建议 55:__init__() 不是构造方法

__init__() 并不是真正意义上的构造方法,__init__() 方法所做的工作是在类的对象创建好之后进行变量的初始化。__new__() 方法才会真正创建实例,是类的构造方法。这两个方法都是 object 类中默认的方法,继承自 object 的新式类,如果不覆盖这两个方法将会默认调用 object 中对应的方法。

来看看 __new__() 方法和 __init__() 方法的定义:

  • object.__new__(cls[, args...]):其中 cls 代表类,args 为参数列表

  • object.__init__(self[, args...]):其中 self 代表实例对象,args 为参数列表

__new__() 方法 VS __init__() 方法

  • __new__() 方法是静态方法,而 __init__() 为实例方法
  • __new__() 方法一般需要返回类的对象,当返回类的对象时将会自动调用 __init__() 方法进行初始化,如果没有对象返回,则 __init__() 方法不会被调用。__init__() 方法不需要显示返回,默认为 None,否则会在运行时抛出 TypeError
  • 控制实例创建时使用 __new__() 方法,控制实例初始化时使用 __init__() 方法
  • 一般情况下不需要覆盖 __new__() 方法,但当子类继承自不可变类型,如 strintunicode 或者 tuple 的时候,往往需要覆盖该方法
  • 当覆盖 __new__()__init__() 方法时,参数必须保持一致,否则将导致异常。

3 建议 56:理解名字查找机制

命名空间(Namespace)是从名称到对象的映射(一对一或一对多)

变量名所在的命名空间直接决定了其能访问到的范围,即变量的作用域

Python 有四种作用域:

  • L(Local):最内层,比如一个函数/方法内部的局部变量。调用 locals() 函数可查看
  • E(Enclosing):包含了非局部(non-local)也非全局(non-global)的变量。比如两个嵌套函数,一个函数(或类) A 里面又包含了一个函数 B ,那么对于 B 中的变量名称来说 A 中的作用域就为 nonlocal。
  • G(Global):当前脚本的最外层,比如当前模块的全局变量。调用 globals() 函数可查看
  • B(Built-in): 包含了内建的变量/关键字等,在 __builtin__ 中可查看

变量查找的规则顺序: L –> E –> G –> B

4 建议 57:为什么需要 self 参数

self 表示的就是实例对象本身,即类的对象在内存中的地址。

self的保留也方便区分静态方法与类方法、局部变量与实例变量

5 建议 58:理解 MRO 与多继承

MRO(Method Resolution Order),对于类中的方法的解析顺序(数据属性也适用)

在古典类中,MRO 搜索采用简单的自左向右的深度优先方法

而新式类采用的而是 C3 MRO 搜索方法,解决原来基于深度优先搜索算法不满足本地优先级,和单调性的问题。

  • 本地优先级:指声明时父类的顺序,比如C(A,B),如果访问C类对象属性时,应该根据声明顺序,优先查找A类,然后再查找B类。

  • 单调性:如果在C的解析顺序中,A排在B的前面,那么在C的所有子类里,也必须满足这个顺序。

具体算法步骤可参考:Python的多重继承问题-MRO和C3算法

6 建议 59:理解描述符机制

每一个类或实例都有一个 __dict__ 属性表,其中包含它的所有属性

通过 "." 操作符访问一个属性时,根据访问实例还是类有以下两种情况:

  • 通过实例访问,比如代码 obj.x,如果 x 是一个描述符,那么 __getattribute__() 会返回 type(obj).__dict__['x'].__get__(obj, type(obj)) 结果,即:type(obj) 获取 obj 的类型;type(obj).__dict__['x'] 返回的是一个描述符,这里有一个试探和判断的过程;最后调用这个描述符的 __get__() 方法。
  • 通过类访问的情况,比如代码 cls.x,则会被 __getattribute__() 转换为 cls.__dict__['x'].__get__(None, cls)

描述符的定义很简单,实现了下列_任意一个方法_的 Python 对象就是一个描述符(descriptor):

  • __get__(self, obj, type=None)
  • __set__(self, obj, value)
  • __delete__(self, obj)

描述符可以用来控制对属性的访问行为,实现计算属性、懒加载属性、属性访问控制等功能

进一步细节可参考理解 Python 中的描述符

7 建议 60:区别 __getattr__()__getattribute__() 方法

__getattr__()__getattribute__() 都可以用作实例属性的获取和拦截

  • 对于所有属性的访问都会调用__getattribute__()方法,对类变量的访问不会涉及 __getattribute__()__getattr__() 方法。

  • __getattribute__()方法调用后要么返回实际的值,要么抛出异常

  • property 也能控制属性的访问,如果一个类中同时定义了 property__getattribute__()__getattr__() 来对属性进行访问控制,则最先搜索的是 __getattribute__() 方法

  • __getattr__() 方法调用发生在以下两种情况:

    1. 当属性不在实例、基类、祖先类的 __dict__
    2. 触发 AttributeError 异常
  • AttributeError 异常一般由__getattribute__() 方法或property 中 get() 方法抛出

  • __getattr__() 调用后,一般返回AttributeError 异常或者显式返回值,否则返回None

8 建议 61:使用更为安全的 property

property 是一种实现了 __get__()__set__() 方法用于管理属性的特殊类

  • 若实现了 __set__()__delete__() 任一方法,该描述符是一个数据描述符
  • 若仅实现 __get__() 方法,该描述符是一个非数据描述符
  • property一种特殊的数据描述符,相比于普通描述符所提供的较为低级的控制属性访问机制,property以标准库的形式提供描述符的高级应用(安全、统一、简洁、可控)

9 建议 62:掌握 metaclass

  • 元类是关于类的类,是类的模版

  • 元类是用来控制如何创建类的,正如类是创建对象的模版一样

  • 元类的实例为类,正如类的实例为对象

10 建议 63:熟悉 Python 对象协议

  • 类型转换协议:__str__() 字符串表示、 __repr__()字符串对象表示、__init__()实例的变量初始化、__long__()长整型表示、__float__()浮点型表示、__nonzero__()布尔型表示
  • 比较大小的协议,依赖于 __cmp__(),当两者相等时,返回 0,当 self < other 时返回负值,反之返回正值。还有__eq__()__ne__()__lt__()__gt__() 等方法来实现相等、不等、小于和大于的判定
  • 数值类型相关的协议,加减乘除之类的,不再赘叙
  • 容器类型协议,内置函数 len(),通过 __len__() 来完成。而 __getitem__()__setitem__()__delitem__() 则对应读、写和删除。__iter__() 实现了迭代器协议, __reversed__()提供对内置函数 reversed() 的支持,而__contains__()则提供对判断符 in 和 not in 的支持。
  • 可调用对象协议,__call__
class Functor(object):
    def __init__(self, context):
        self._context = context
    def __call__(self):
        print("do something with {}".format(self._context))
lai_functor = Functor("lai")
lai_functor()
  • 可哈希对象协议,__hash__()(只有支持可哈希协议的类型才能作为 dict 的键)
  • 描述符协议和属性交互协议,__getattr__()__setattr__()__delattr__(),还有上下文管理器协议,也就是对 with 语句的支持,这个协议通过 __enter__()__exit__() 两个方法来实现对资源的清理,确保资源无论在什么情况下都会正常清理。

11 建议 64:使用操作符重载实现中缀语法

pipe 模块实现了类似于unix的管道符操作方式(个人之前没怎么用过,不赘述)

示例:找出小于 1000000 的斐波那契数,并计算其中的偶数的平方之和

fib() | take_while(lambda x: x < 1000000) \
      | where(lambda x: x % 2) \
      | select(lambda x: x * x) \
      | sum()

具体可参考Pipe的Github地址

12 建议 65:熟悉 Python 的迭代器协议

迭代器协议简单归纳如下:

  • 实现 __iter__() 方法,返回一个迭代器

  • 实现 next() 方法,返回当前的元素,并指向下一个元素的位置,如果当前位置已无元素,则抛出 StopIteration 异常。

以斐波那契数列为例,定义一个迭代器:

class Fib(object):
    def __init__(self, max=0):
        super(Fib, self).__init__()
        self.prev = 0
        self.curr = 1
        self.max = max

    def __iter__(self):
        return self

    def __next__(self):
        if self.max > 0:
            self.max -= 1
            # 当前要返回的元素的值
            value = self.curr
            # 下一个要返回的元素的值
            self.curr += self.prev
            # 设置下一个元素的上一个元素的值
            self.prev = value
            return value
        else:
            raise StopIteration

if __name__ == '__main__':
    fib = Fib(10)
    # 调用next()的过程
    for n in fib:
        print(n)
    # raise StopIteration
    print(next(fib))

针对迭代器,itertools 模块提供了一系列计算快速、内存高效的函数,比如说:

迭代器 实参 说明 示例
count() start, [step] 算术递增数列 count(10) --> 10 11 12 ...
cycle() p 无限重复的序列 cycle('ABC') --> A B C A B C ...
repeat() elem [,n] 同一个值重复的序列 repeat(10, 3) --> 10 10 10
accumulate() p [,func] 值累积相加的序列 accumulate([1,2,3,4]) --> 1 3 6 10
compress() data, selectors 筛选后的序列 compress('ABCD', [1,0,0,1]) --> A D
product() p, q, ... [repeat=1] 笛卡尔积
permutations() p[, r] 全排列
combinations() p, r 无放回抽样
combinations_with_replacement() p[, r] 有放回抽样

具体可参考itertools--为高效循环而创建迭代器的函数

13 建议 66:熟悉 Python 的生成器

生成器函数用关键字 yield 代替 return 来返回值,是一种更加优雅的迭代器

以斐波那契数列为例,定义一个生成器:

def fib(max):
    prev, curr = 0, 1
    while max > 0:
        max -= 1
        yield curr
        prev, curr = curr, prev + curr

if __name__ == '__main__':
    fib = fib(6)
    # 调用next()的过程
    for n in fib:
        print(n)
    # raise StopIteration
    print(next(fib))

参考如何更好地理解Python迭代器和生成器? - 刘志军的回答 - 知乎

14 建议 67:基于生成器的协程及 greenlet

1_study/ComputerScience/programming/进程、线程与协程#协程

15 建议 68:理解 GIL 的局限性

16 建议 69:对象的管理与垃圾回收

Python 中内存管理的方式:Python 使用引用计数器(Reference counting)的方法来管理内存中的对象,即针对每一个对象维护一个引用计数值来表示该对象当前有多少个引用。当引用计数的值为 0 时的时候该对象是个不可达对象,会被垃圾收集器回收。

引用计数算法最明显的缺点是无法解决循环引用的问题,即两个对象相互引用。不过Python 自带的一个 gc 模块能找出复杂数据结构之间的循环引用,同时回收内存垃圾。

触发垃圾回收的两种情况

  • 显式地调用 gc.collect() 进行垃圾回收
  • 当对象的引用数量超过 threshold 时自动进行垃圾回收

具体可参考gc — Garbage Collector interface

往年同期文章