改善python的91个建议(三):内部机制

建议 50:利用模块实现单例模式

满足单例模式的 3 个需求:

  • 只能有一个实例

  • 必须自行创建这个实例

  • 必须自行向整个系统提供这个实例

下面我们使用 Python 实现一个带锁的单例:

class Singleton(object):

    objs = {}
    objs_locker = threading.Lock()

    def __new__(cls, *args, **kw):
        if cls in cls.objs:
            return cls.objs(cls)
        cls.objs_locker.acquire()
        try:
            if cls in cls.objs:
                return cls.objs(cls)
            cls.objs[cls] = object.__new__(cls)
        finally:
            cls.objs_locker.release()

当然这种方案也存在问题:

  • 如果 Singleton 的子类重载了__new__(),会覆盖或干扰 Singleton 类中__new__()的执行

  • 如果子类有__init__(),那么每次实例化该 Singleton 的时候,__init__()都会被调用,这显然是不应该的

虽然以上问题都有解决方案,但让单例的实现不够 Pythonic。我们可以重新审视 Python 的语法元素,发现模块采用的其实是天然的单例的实现方式:

  • 所有的变量都会绑定到模块

  • 模块只初始化一次

  • import 机制是线程安全的,保证了在并发状态下模块也只是一个实例

    # World.py
    import Sun
    
    def run():
        while True:
            Sun.rise()
            Sun.set()
    
    # main.py
    import World
    World.run()

     

建议 51:用 mixin 模式让程序更加灵活

模板方法模式就是在一个方法中定义一个算法的骨架,并将一些实现步骤延迟到子类中。模板方法可以使子类在不改变算法结构的情况下,重新定义算法中的某些步骤。

来看一个例子:

class People(object):
    def make_tea(self):
        teapot = self.get_teapot()
        teapot.put_in_tea()
        teapot.put_in_water()
        return teapot

显然get_teapot()方法并不需要预先定义,也就是说我们的基类不需要预先申明抽象方法,子类只需要继承 People 类并实现get_teapot(),这给调试代码带来了便利。但我们又想到如果一个子类 StreetPeople 描述的是正走在街上的人,那这个类将不会实现get_teapot(),一调用make_tea()就会产生找不到get_teapot()的 AttributeError,所以此时程序员应该立马想到,随着需求的增多,越来越多的 People 子类会选择不喝茶而喝咖啡,或者是抽雪茄之类的,按照以上的思路,我们的代码只会变得越发难以维护。

所以我们希望能够动态生成不同的实例:

class UseSimpleTeapot(object):
    def get_teapot(self):
        return SimpleTeapot()

class UseKungfuTeapot(object):
    def get_teapot(self):
        return KungfuTeapot()

class OfficePeople(People, UseSimpleTeapot): pass

class HomePeople(People, UseSimpleTeapot): pass

class Boss(People, UseKungfuTeapot): pass

def simple_tea_people():
    people = People()
    people.__base__ += (UseSimpleTeapot,)
    return people

def coffee_people():
    people = People()
    people.__base__ += (UseCoffeepot,)

def tea_and_coffee_people():
    people = People()
    people.__base__ += (UseSimpleTeapot, UserCoffeepot,)
    return people

def boss():
    people = People()
    people.__base__ += (KungfuTeapot, UseCoffeepot, )
    return people

 

以上代码的原理在于每个类都有一个__bases__属性,它是一个元组,用来存放所有的基类,作为动态语言,Python 中的基类可以在运行中可以动态改变。所以当我们向其中增加新的基类时,这个类就拥有了新的方法,这就是混入mixin。

利用这个技术我们可以在不修改代码的情况下就可以完成需求:

import mixins   # 把员工需求定义在 Mixin 中放在 mixins 模块

def staff():
    people = People()
    bases = []
    for i in config.checked():
        bases.append(getattr(maxins, i))
    people.__base__ += tuple(bases)
    return people

建议 52:用发布订阅模式实现松耦合

发布订阅模式是一种编程模式,消息的发送者不会发送其消息给特定的接收者,而是将发布的消息分为不同的类别直接发布,并不关注订阅者是谁。而订阅者可以对一个或多个类别感兴趣,且只接收感兴趣的消息,并且不关注是哪个发布者发布的消息。要实现这个模式,就需要一个中间代理人 Broker,它维护着发布者和订阅者的关系,订阅者把感兴趣的主题告诉它,而发布者的信息也通过它路由到各个订阅者处。

from collections import defaultdict
route_table = defaultdict(list)
def sub(topic, callback):
    if callback in route_table[topic]:
        return
    route_table[topic].append(callback)

def pub(topic, *args, **kw):
    for func in route_table[topic]:
        func(*args, **kw)

将以上代码放在 Broker.py 的模块,省去了各种参数检测、优先处理、取消订阅的需求,只向我们展示发布订阅模式的基础实现:

import Broker
def greeting(name):
    print('Hello, {}'.format(name))
Broker.sub('greet', greeting)
Broker.pub('greet', 'LaiYonghao')

注意学习 blinker 和 python-message 两个模块

建议 53:用状态模式美化代码

所谓状态模式,就是当一个对象的内在状态改变时允许改变其行为,但这个对象看起来像是改变了其类。

def workday():
    print('work hard')

def weekend():
    print('play harder')

class People(object): pass
people = People()
while True:
    for i in range(1, 8):
        if i == 6:
            people.day = weekend
        if i == 1:
            people.day = workday
        people.day()

但上述例子还有缺陷:

  • 查询对象的当前状态很麻烦

  • 状态切换时需要对原状态做一些清扫工作,而对新状态做初始化工作,因每个状态需要做的事情不同,全部写在切换状态的代码中必然重复

这时候我们可以使用 Python-state 来解决。

改写之前的例子:

from state import curr, switch, stateful, State, behavior
@stateful
class People(object):
    class Workday(State):
        default = True
        @behavior   # 相当于staticmethod
        def day(self):  # 这里的self并不是Python的关键字,而是有助于我们理解状态类的宿主是People的实例
            print('work hard')
    class Weekend(State):
        @behavior
        def day(self):
            print('play harder')
people = People()
while True:
    for i in range(1, 8):
        if i == 6:
            switch(people, People.Weekend)
        if i == 1:
            switch(people, People.Workday)
        people.day()

@statefule装饰器重载了被修饰的类的__getattr__()从而使得 People 的实例能够调用当前状态类的方法,同时被修饰的类的实例是带有状态的,能够使用curr()查询当前状态,也可以使用switch()进行状态切换,默认的状态是通过类定义的 default 属性标识,default = True的类成为默认状态。

状态类 Workday 和 Weekend 继承自 State 类,从其派生的子类可以使用__begin__和__end___状态转换协议,自定义进入和离开当前状态时对宿主的初始化和清理工作。

下面是一个真实业务的例子:

@stateful
class User(object):
    class NeedSignin(State):
        default = True
        @behavior
        def signin(self, user, pwd):
            ...
            switch(self, Player.Signin)
    class Signin(State):
        @behavior
        def move(self, dst): ...
        @behavior
        def atk(self, other): ...

 

 

建议 54:理解 built-in objects

Python 中一切皆对象,在新式类中,object 是所有内建类型的基类,用户自定义的类可以继承自 object 也可继承自内建类型。

In [1]: class TestNewClass:
   ...:     __metaclass__ = type
   ...:     

In [2]: type(TestNewClass)
Out[2]: type

In [3]: TestNewClass.__bases__
Out[3]: (object,)

In [4]: a = TestNewClass()

In [5]: type(a)
Out[5]: __main__.TestNewClass

In [6]: a.__class__
Out[6]: __main__.TestNewClass

 

新式类支持 property 和描述符特性,作为新式类的祖先,Object 类还定义了一些特殊方法:__new__()、__init__()、__delattr__()、__getattribute__()、__setattr__()、__hash__()、__repr__()、__str__()等。

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

class A(object):
    def __new__(cls, *args, **kw):
        print(cls)
        print(args)
        print(kw)
        print('----------')
        instance = object.__new__(cls, *args, **kw)
        print(instance)
    def __init__(self, a, b):
        print('init gets called')
        print('self is {}'.format(self))
        self.a, self.b = a, b
a1 = A(1, 2)
print(a1.a)
print(a1.b)

 

 

运行结果:

<class '__main__.A'>
(1, 2)
{}
----------
Traceback (most recent call last):
  File "test.py", line 19, in <module>
    a1 = A(1, 2)
  File "test.py", line 13, in __new__
    instance = object.__new__(cls, *args, **kw)
TypeError: object() takes no parameters

从结果中我们可以看出,程序输出了__new__()调用所产生的输出,并抛出了异常。于是我们知道,原来__new__()才是真正创建实例,是类的构造方法,而__init__()是在类的对象创建好之后进行变量的初始化。上面程序抛出异常是因为在__new__()中没有显式返回对象,a1此时为None,当去访问实例属性时就抛出了异常。

根据官方文档,我们可以总结以下几点:

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

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

  • 控制实例创建的时候可使用 __new__() ,而控制实例初始化的时候使用 __init__()

  • __new__()需要返回类的对象,当返回类的对象时将会自动调用__init__()进行初始化,没有对象返回,则__init__()不会被调用。__init__() 方法不需要显示返回,默认为 None,否则会在运行时抛出 TypeError

  • 但当子类继承自不可变类型,如 str、int、unicode 或者 tuple 的时候,往往需要覆盖__new__()

  • 覆盖 __new__() 和 __init__() 的时候这两个方法的参数必须保持一致,如果不一致将导致异常

下面我们来总结需要覆盖__new__()的几种特殊情况:

  • 当类继承不可变类型且默认的 __new__() 方法不能满足需求的时候

  • 用来实现工厂模式或者单例模式或者进行元类编程,使用__new__()来控制对象创建

  • 作为用来初始化的 __init__() 方法在多继承的情况下,子类的 __init__()方法如果不显式调用父类的 __init__() 方法,则父类的 __init__() 方法不会被调用;通过super(子类, self).__init__()显式调用父类的初始化方法;对于多继承的情况,我们可以通过迭代子类的 __bases__ 属性中的内容来逐一调用父类的初始化方法

分别来看例子加深理解:

# 创建一个集合能够将任何以空格隔开的字符串变为集合中的元素
class UserSet(frozenset):
    def __new__(cls, *args):
        if args and isinstance(args[0], str):
            args = (args[0].split(), ) + args[1:]
        return super(UserSet, cls).__new__(cls, *args)

# 一个工厂类根据传入的参量决定创建出哪一种产品类的实例
class Shape(object):
    def __init__(object):
        pass
    def draw(self):
        pass

class Triangle(Shape):
    def __init__(self):
        print("I am a triangle")
    def draw(self):
        print("I am drawing triangle")

class Rectangle(Shape):
    def __init__(self):
        print("I am a rectangle")
    def draw(self):
        print("I am drawing triangle")

class Trapezoid(Shape):
    def __init__(self):
        print("I am a trapezoid")
    def draw(self):
        print("I am drawing triangle")

class Diamond(Shape):
    def __init__(self):
        print("I am a diamond")
    def draw(self):
        print("I am drawing triangle")

class ShapeFactory(object):
    shapes = {'triangle': Triangle, 'rectangle': Rectangle, 'trapzoid': Trapezoid, 'diamond': Diamond}
    def __new__(cls, name):
        if name in ShapeFactory.shapes.keys():
            print('creating a new shape {}'.format(name))
            return ShapeFactory.shapes[name]()
        else:
            print('creating a new shape {}'.format(name))
            return Shape()

 

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

在 Python 中所谓的变量其实都是名字,这些名字指向一个或多个 Python 对象。这些名字都存在于一个表中(命名空间),我们称之为局部变量,调用locals()可以查看:

>>> locals()
{'__package__': None, '__spec__': None, '__loader__': <class '_frozen_importlib.BuiltinImporter'>, '__doc__': None, '__name__': '__main__', '__builtins__': <module 'builtins' (built-in)>}
>>> globals()
{'__loader__': <class '_frozen_importlib.BuiltinImporter'>, '__builtins__': <module 'builtins' (built-in)>, '__package__': None, '__doc__': None, '__spec__': None, '__name__': '__main__'}

 

 

Python 中的作用域分为:

  • 局部作用域: 一般来说函数的每次调用都会创建一个新的本地作用域, 拥有新的命名空间

  • 全局作用域: 定义在 Python 模块文件中的变量名拥有全局作用域, 即在一个文件的顶层的变量名仅在这个文件内可见

  • 嵌套作用域: 多重函数嵌套时才会考虑, 即使使用 global 进行申明也不能达到目的, 其结果最终是在嵌套的函数所在的命名空间中创建了一个新的变量

  • 内置作用域: 通过标准库中的__builtin__实现的

当访问一个变量的时候,其查找顺序遵循变量解析机制 LEGB 法则,即依次搜索 4 个作用域:局部作用域、嵌套作用域、全局作用域以及内置作用域,并在第一个找到的地方停止搜寻,如果没有搜到,则会抛出异常。

Python 3 中引入了 nonlocal 关键字:

def foo(x):
    a = x
    def bar():
        nonlocal a
        b = a * 2
        a = b + 1
        print(a)
    return bar

 

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

在类中当定义实例方法的时候需要将第一个参数显式声明为self, 而调用时不需要传入该参数, 我们通过self.x访问实例变量, self.m()访问实例方法:
class SelfTest(object):
    def __init__(self.name):
        self.name = name
    def showself(self):
        print('self here is {}'.format(self))
    def display(self):
        self.showself()
        print('The name is: {}'.format(self.name))
st = SelfTest('instance self')
st.display()
print('{}'.format(st))

运行结果:

self here is <__main__.SelfTest object at 0x7f440c53ba58>
The name is: instance self
<__main__.SelfTest object at 0x7f440c53ba58>

从中可以发现, self 表示实例对象本身, 即 SelfTest 类的对象在内存中的地址. self 是对对象 st 本身的引用, 我们在调用实例方法时也可以直接传入实例对象: SelfTest.display(st). 同时 self 或 cls 并不是 Python 的关键字, 可以替换成其它的名称.

Python 中为什么需要 self 呢:

  1. 借鉴了其他语言的特征

  2. Python 语言本身的动态性决定了使用 self 能够带来一定便利

  3. 在存在同名的局部变量以及实例变量的情况下使用 self 使得实例变量更容易被区分

Python 属于一级对象语言, 我们有好几种方法可以引用类方法:

A.__dict__["m"]
A.m.__func__

Python 的哲学是:显示优于隐式(Explicit is better than implicit).

建议 58: 理解 MRO 与多继承

古典类与新式类所采取的 MRO (Method Resolution Order, 方法解析顺序) 的实现方式存在差异.

古典类是按照多继承申明的顺序形成继承树结构, 自顶向下采用深度优先的搜索顺序. 而新式类采用的是 C3 MRO 搜索方法, 在新式类通过__mro__得到 MRO 的搜索顺序, C3 MRO 的算法描述如下:

假定,C1C2...CN 表示类 C1 到 CN 的序列,其中序列头部元素(head)=C1,序列尾部(tail)定义 = C2...CN;

C 继承的基类自左向右分别表示为 B1,B2...BN

L[C] 表示 C 的线性继承关系,其中 L[object] = object。

算法具体过程如下:

L[C(B1...BN)] = C + merge(L[B1] ... L[BN], B1 ... BN)

其中 merge 方法的计算规则如下:在 L[B1]...L[BN],B1...BN 中,取 L[B1] 的 head,如果该元素不在 L[B2]...L[BN],B1...BN 的尾部序列中,则添加该元素到 C 的线性继承序列中,同时将该元素从所有列表中删除(该头元素也叫 good head),否则取 L[B2] 的 head。继续相同的判断,直到整个列表为空或者没有办法找到任何符合要求的头元素(此时,将引发一个异常)。

菱形继承是我们在多继承设计的时候需要尽量避免的一个问题.

建议 59: 理解描述符机制

In [1]: class MyClass(object):
   ...:     class_attr = 1
   ...:     
# 每一个类都有一个__dict__属性, 包含它的所有属性
In [2]: MyClass.__dict__
Out[2]:
mappingproxy({'__dict__': <attribute '__dict__' of 'MyClass' objects>,
              '__doc__': None,
              '__module__': '__main__',
              '__weakref__': <attribute '__weakref__' of 'MyClass' objects>,
              'class_attr': 1})

In [3]: my_instance = MyClass()
# 每一个实例也相应有一个实例属性, 我们通过实例访问一个属性时,
# 它首先会尝试在实例属性中查找, 找不到会到类属性中查找
In [4]: my_instance.__dict__
Out[4]: {}
# 实例访问类属性
In [5]: my_instance.class_attr
Out[5]: 1
# 如果通过实例增加一个属性,只能改变此实例的属性
In [6]: my_instance.inst_attr = 'china'

In [7]: my_instance.__dict__
Out[7]: {'inst_attr': 'china'}
# 对于类属性而言并没有丝毫变化
In [8]: MyClass.__dict__
Out[8]:
mappingproxy({'__dict__': <attribute '__dict__' of 'MyClass' objects>,
              '__doc__': None,
              '__module__': '__main__',
              '__weakref__': <attribute '__weakref__' of 'MyClass' objects>,
              'class_attr': 1})
# 我们可以动态地给类增加一个属性
In [9]: MyClass.class_attr2 = 100

In [10]: my_instance.class_attr2
Out[10]: 100
# 但Python的内置类型并不能随意地为它增加属性或方法

操作符封装了对实例属性和类属性两种不同属性进行查找的细节。

但是如果是访问方法呢:

In [1]: class MyClass(object):
   ...:     def my_method(self):
   ...:         print('my_method')
   ...:         

In [2]: MyClass.__dict__['my_method']
Out[2]: <function __main__.MyClass.my_method>

In [3]: MyClass.my_method
Out[3]: <function __main__.MyClass.my_method>

In [4]: type(MyClass.my_method)
Out[4]: function

In [5]: type(MyClass.__dict__['my_method'])
Out[5]: function

根据通过实例访问属性和根据类访问属性的不同,有以下两种情况:

  • 一种是通过实例访问,比如代码 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)。

    描述符协议是一个 Duck Typing 的协议,而每一个函数都有 __get__ 方法,也就是说其他每一个函数都是描述符。所有对属性, 方法进行修饰的方案往往都用到了描述符, 如classmethod, staticmethod, property等, 以下是property的参考实现:

    class Property(object):
        "Emulate PyProperty_Type() in Objects/descrobject.c"
        def __init__(self, fget=None, fset=None, fdel=None, doc=None):
            self.fget = fget
            self.fset = fset
            self.fdel = fdel
            self.__doc__ = doc
        def __get__(self, obj, objtype=None):
            if obj is None:
                return self
            if self.fget is None:
                raise AttributeError, "unreadable attribute"
            return self.fget(obj)
        def __set__(self, obj, value):
            if self.fset is None:
                raise AttributeError, "can't set attribute"
            self.fset(obj, value)
        def __delete__(self, obj):
            if self.fdel is None:
                raise AttributeError, "can't delete attribute"
            self.fdel(obj)

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

    以上两种方法可以对实例属性进行获取和拦截:

    • __getattr__(self, name):适用于属性在实例中以及对应的类的基类以及祖先类中都不存在;

    • __getattribute__(self, name):对于所有属性的访问都会调用该方法

    但访问不存在的实例属性时,会由内部方法__getattribute__()抛出一个 AttributeError 异常,也就是说只要涉及实例属性的访问就会调用该方法,它要么返回实际的值,要么抛出异常。详情请参考

    那么__getattr__()在什么时候调用呢:

    • 属性不在实例的__dict__中;

    • 属性不在其基类以及祖先类的__dict__中;

    • 触发AttributeError异常时(注意,不仅仅是__getattribute__()方法的AttributeError异常,property 中定义的get()方法抛出异常的时候也会调用该方法)。

    当这两个方法同时被定义的时候,要么在__getattribute__()中显式调用,要么触发AttributeError异常,否则__getattr__()永远不会被调用。

    我们知道 property 也能控制属性的访问,如果一个类中如果定义了 property、__getattribute__()以及__getattr__()来对属性进行访问控制,会最先搜索__getattribute__()方法,由于 property 对象并不存在于 dict 中,因此并不能返回该方法,此时会搜索 property 中的get()方法;当 property 中的set()方法对属性进行修改并再次访问 property 的get()方法会抛出异常,这时会触发__getattr__()的调用。

    __getattribute__()总会被调用,而__getattr__()只有在__getattribute__()中引发异常的情况下调用。

建议 61:使用更加安全的 property

property 实际上是一种实现了 __get__() 、 __set__() 方法的类,用户也可以根据自己的需要定义个性化的 property,其实质是一种特殊的数据描述符(数据描述符:如果一个对象同时定义了 __get__() 和 __set__() 方法,则称为数据描述符,如果仅定义了__get__() 方法,则称为非数据描述符)。它和普通描述符的区别在于:普通描述符提供的是一种较为低级的控制属性访问的机制,而 property 是它的高级应用,它以标准库的形式提供描述符的实现,其签名形式为:

property(fget=None, fset=None, fdel=None, doc=None) -> property attribute

property 有两种常用的形式:

1、第一种形式

class Some_Class(object):
    def __init__(self):
        self._somevalue = 0
    def get_value(self):
        print('calling get method to return value')
        return self._somevalue
    def set_value(self, value):
        print('calling set method to set value')
        self._somevalue = value
    def def_attr(self):
        print('calling delete method to delete value')
        def self._somevalue
    x = property(get_value, set_value, del_attr, "I'm the 'x' property.")
obj = Some_Class()
obj.x = 10
print(obj.x + 2)
del obj.x
obj.x

 

2、第二种形式

class Some_Class(self):
    _x = None
    def __init__(self):
        self._x = None
    @property
    def x(self):
        print('calling get method to return value')
        return self._x
    @x.setter
    def x(self, value):
        print('calling set method to set value')
        self._x = value
    @x.deleter
    def x(self):
        print('calling delete method to delete value')
        del self._x

 

 

以上我们可以总结出 property 的优势:

1、代码更简洁,可读性更强

2、更好的管理属性的访问。property 将对属性的访问直接转换为对对应的 get、set 等相关函数的调用,属性能够更好地被控制和管理,常见的应用场景如设置校验(如检查电子邮件地址是否合法)、检查赋值的范围(某个变量的赋值范围必须在 0 到 10 之间)以及对某个属性进行二次计算之后再返回给用户(将 RGB 形式表示的颜色转换为#******)或者计算某个依赖于其他属性的属性。

class Date(object):
    def __init__(self, year, month, day):
        self.year = year
        self.month = month
        self.day = day
    def get_date(self):
        return self.year + '-' + self.month + '-' + self.day
    def set_date(self, date_as_string):
        year, month, day = date_as_string.split('-')
        if not (2000 <= year <= 2017 and 0 <= month <= 12 and 0 <= day <= 31):
            print('year should be in [2000:2017]')
            print('month should be in [0:12]')
            print('day should be in [0, 31]')
            raise AssertionError
        self.year = year
        self.month = month
        self.day = day
    date = property(get_date, set_date)

创建一个 property 实际上就是将其属性的访问与特定的函数关联起来,相对于标准属性的访问,property 的作用相当于一个分发器,对某个属性的访问并不直接操作具体的对象,而对标准属性的访问没有中间这一层,直接访问存储属性的对象:

3、代码可维护性更好。property 对属性进行再包装,以类似于接口的形式呈现给用户,以统一的语法来访问属性,当具体实现需要改变的时候,访问的方式仍然可以保持一致。

4、控制属性访问权限,提高数据安全性。如果用户想设置某个属性为只读,来看看 property 是如何实现的。

class PropertyTest(object):
    def __init__(self):
        self.__var1 = 20
    @property
    def x(self):
        return self.__var1
pt = PropertyTest()
print(pt.x)
pt.x = 12

既然 property 本质是特殊类,那么就可以被继承,我们就可以自定义 property:

def update_meta(self, other):
    self.__name__ = other.__name__
    self.__doc__ = other.__doc__
    self.__dict__.update(other.__dict__)
    return self
class UserProperty(property):
    def __new__(cls, fget=None, fset=None, fdel=None, doc=None):
        if fget is not None:
            def __get__(obj, objtype=None, name=fget.__name__):
                fegt = getattr(obj, name)
                return fget()
            fget = update_meta(__get__, fget)
        if fset is not None:
            def __set__(obj, value, name=fset.__name__):
                fset = getattr(obj, name)
                return fset(value)
            fset = update_meta(__set__, fset)
        if fdel is not None:
            def __delete__(obj, name=fdel.__name__):
                fdel = getattr(obj, name)
                return fdel()
            fdel = update_meta(__delete__, fdel)
        return property(fget, fset, fdel, doc)
class C(object):
    def get(self):
        return self._x
    def set(self, x):
        self._x = x
    def delete(self):
        del self._x
    x = UserProperty(get, set, delete)
c = C()
c.x = 1
print(c.x)
def c.x

UserProperty 继承自 property,其构造函数 __new__(cls, fget=None, fset=None, fdel=None, doc=None) 中重新定义了 fget() 、 fset() 以及 fdel() 方法以满足用户特定的需要,最后返回的对象实际还是 property 的实例,因此用户能够像使用 property 一样使用 UserProperty。

使用 property 并不能真正完全达到属性只读的目的,用户仍然可以绕过阻碍来修改变量。我们来看看一个可行的实现:

def ro_property(obj, name, value):
    setattr(obj.__class__, name, property(lambda obj: obj.__dict__["__" + name]))
    setattr(obj, "__" + name, value)

class ROClass(object):
    def __init__(self, name, available):
        ro_property(self, "name", name)
        self.available = available

a = ROClass("read only", True)
print(a.name)
a._Article__name = "modify"
print(a.__dict__)
print(ROClass.__dict__)
print(a.name)

 

建议 62:掌握 metaclass

关于元类这知识点,推荐stackoverflow上Jerub回答

这里有中文翻译: 深刻理解Python中的元类(metaclass)

自我总结:当你需要动态修改类时,99%的时间里,可能并不希望通过使用元类来对类做修改。你可以通过其他两种技术来修改类:

1) Monkey patching  :猴子补丁

2)   class decorators:装饰器

当确实需要修改元类时:元类的主要用途是创建API。

一个典型的例子是Django ORM。它允许你像这样定义:

class Person(models.Model):
    name = models.CharField(max_length=30)
    age = models.IntegerField()

但是如果你像这样做的话:

guy  = Person(name='bob', age='35')
print guy.age

这并不会返回一个IntegerField对象,而是会返回一个int,甚至可以直接从数据库中取出数据。这是有可能的,因为models.Model定义了__metaclass__, 并且使用了一些魔法能够将你刚刚定义的简单的Person类转变成对数据库的一个复杂hook。Django框架将这些看起来很复杂的东西通过暴露出一个简单的使用元类的API将其化简,通过这个API重新创建代码,在背后完成真正的工作。

 

建议 63:熟悉 Python 对象协议

因为 Python 是一门动态语言,Duck Typing 的概念遍布其中,所以其中的 Concept 并不以类型的约束为载体,而另外使用称为协议的概念。

In [1]: class Object(object):
   ...:     def __str__(self):
   ...:         print('calling __str__')
   ...:         return super(Object, self).__str__()
   ...:     

In [2]: o = Object()

In [3]: print('%s' % o)
calling __str__
<__main__.Object object at 0x7f133ff20160>

比如在字符串格式化中,如果有占位符 %s,那么按照字符串转换的协议,Python 会自动地调用相应对象的 __str__() 方法。

总结一下 Python 中的协议:

1、类型转换协议:__str__() 、__repr__()、__init__()、__long__()、__float__()、__nonzero__() 等。

2、比较大小的协议:__cmp__(),当两者相等时,返回 0,当 self < other 时返回负值,反之返回正值。同时 Python 又有 __eq__()、__ne__()、__lt__()、__gt__() 等方法来实现相等、不等、小于和大于的判定。这也就是 Python 对 ==、!=、< 和 > 等操作符的进行重载的支撑机制。

3、

数值相关的协议:

其中有个 Python 中特有的概念:反运算。以something + other为例,调用的是something的__add__(),若没有定义__add__(),这时候 Python 有一个反运算的协议,查看other有没有__radd__(),如果有,则以something为参数调用。

4、容器类型协议:容器的协议是非常浅显的,既然为容器,那么必然要有协议查询内含多少对象,在 Python 中,就是要支持内置函数 len(),通过 __len__() 来完成,一目了然。而 __getitem__()、__setitem__()、__delitem__() 则对应读、写和删除,也很好理解。__iter__() 实现了迭代器协议,而 __reversed__() 则提供对内置函数 reversed() 的支持。容器类型中最有特色的是对成员关系的判断符 in 和 not in 的支持,这个方法叫 __contains__(),只要支持这个函数就能够使用 in 和 not in 运算符了。

5、可调用对象协议:所谓可调用对象,即类似函数对象,能够让类实例表现得像函数一样,这样就可以让每一个函数调用都有所不同。

In [1]: class Functor(object):
   ...:     def __init__(self, context):
   ...:         self._context = context
   ...:     def __call__(self):
   ...:         print('do something with %s' % self._context)
   ...:         

In [2]: lai_functor = Functor('lai')

In [3]: yong_functor = Functor('yong')

In [4]: lai_functor()
do something with lai

In [5]: yong_functor()
do something with yong

6、还有一个可哈希对象,它是通过 __hash__() 方法来支持 hash() 这个内置函数的,这在创建自己的类型时非常有用,因为只有支持可哈希协议的类型才能作为 dict 的键类型(不过只要继承自 object 的新式类就默认支持了)。

7、上下文管理器协议:也就是对with语句的支持,该协议通过__enter__()和__exit__()两个方法来实现对资源的清理,确保资源无论在什么情况下都会正常清理:

class Closer:
    def __init__(self):
        self.obj = obj
    def __enter__(self):
        return self.obj
    def __exit__(self, exception_type, exception_val, trace):
        try:
            self.obj.close()
        except AttributeError:
            print('Not closeable.')
            return True

这里 Closer 类似的类已经在标准库中存在,就是 contextlib 里的 closing。

以上就是常用的对象协议,灵活地用这些协议,我们可以写出更为 Pythonic 的代码,它更像是声明,没有语言上的约束,需要大家共同遵守。

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

熟悉 Shell 脚本编程应该熟悉|管道符号,用以连接两个程序的输入输出。如按字母表反序遍历当前目录的文件与子目录:

$ ls | sort -r
Videos/
Templates/
Public/
Pictures/
Music/
examples.desktop
Dropbox/
Downloads/
Documents/
Desktop/

管道的处理非常清晰,因为它是中缀语法。而我们常用的 Python 是前缀语法,比如类似的 Python 代码应该是 sort(ls(), reverse=True)。

Julien Palard 开发了一个 pipe 库,利用|来简化代码,也就是重载了 __ror__() 方法:

class Pipe:
    def __init__(self, function):
        self.function = function
    def __ror__(self, other):
        return self.function(other)
    def __call__(self, *args, **kwargs):
        return Pipe(lambda x: self.function(x, *args, **kwargs))

这个 Pipe 类可以当成函数的 decorator 来使用。比如在列表中筛选数据:

@Pipe
def where(iterable, predicate):
    return (x for x in iterable if (predicate(x)))

pipe 库内置了一堆这样的处理函数,比如 sum、select、where 等函数尽在其中,请看以下代码:

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

这样写的代码,意义是不是一目了然呢?就是找出小于 1000000 的斐波那契数,并计算其中的偶数的平方之和。

我们可以使用pip3 install pipe安装,安装完后测试:

In [1]: from pipe import *

In [2]: [1, 2, 3, 4, 5] | where(lambda x: x % 2) | tail(2) | select(lambda x: x * x) | add
Out[2]: 34

此外,pipe 是惰性求值的,所以我们完全可以弄一个无穷生成器而不用担心内存被用完:

In [3]: def fib():
   ...:     a, b = 0, 1
   ...:     while True:
   ...:         yield a
   ...:         a, b = b, a + b
   ...:         

In [4]: euler2 = fib() | where(lambda x: x % 2 ==0) | take_while(lambda x: x < 400000) | add

In [5]: euler2
Out[5]: 257114

读取文件,统计文件中每个单词出现的次数,然后按照次数从高到低对单词排序:

from __future__ import print_function
from re import split
from pipe import *
with open("test_descriptor.py") as f:
    print(f.read()
          | Pipe(lambda x: split("/W+", x))
          | Pipe(lambda x:(i for i in x if i.strip()))
          | groupby(lambda x:x)
          | select(lambda x:(x[0], (x[1] | count)))
          | sort(key=lambda x: x[1], reverse=True)
          )

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

首先介绍一下 iter() 函数,iter() 可以输入两个实参,为了简化,第二个可选参数可以忽略。iter() 函数返回一个迭代器对象,接受的参数是一个实现了 __iter__() 方法的容器或迭代器(精确来说,还支持仅有 __getitem__() 方法的容器)。对于容器而言,__iter__() 方法返回一个迭代器对象,而对迭代器而言,它的 __iter__() 方法返回其自身。

所谓协议,是一种松散的约定,并没有相应的接口定义,所以把协议简单归纳如下:

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

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

没错,其实 for 语句就是对获取容器的迭代器、调用迭代器的 next() 方法以及对 StopIteration 进行处理等流程进行封装的语法糖(类似的语法糖还有 in/not in 语句)。

迭代器最大的好处是定义了统一的访问容器(或集合)的统一接口,所以程序员可以随时定义自己的迭代器,只要实现了迭代器协议即可。除此之外,迭代器还有惰性求值的特性,它仅可以在迭代至当前元素时才计算(或读取)该元素的值,在此之前可以不存在,在此之后也可以销毁,也就是说不需要在遍历之前实现准备好整个迭代过程中的所有元素,所以非常适合遍历无穷个元素的集合或或巨大的事物(斐波那契数列、文件):

class Fib(object):
    def __init__(self):
        self._a, self._b = 0, 1
    def __iter__(self):
        return self
    def next(self):
        self._a, self._b = self._b, self._a + self._b
        return self._a
for i, f in enumerate(Fib()):
    print(f)
    if i > 10:
        break

下面来看看与迭代有关的标准库 itertools。

itertools 的目标是提供一系列计算快速、内存高效的函数,这些函数可以单独使用,也可以进行组合,这个模块受到了 Haskell 等函数式编程语言的启发,所以大量使用 itertools 模块中的函数的代码,看起来有点像函数式编程语言。比如 sum(imap(operator.mul, vector1, vector2)) 能够用来运行两个向量的对应元素乘积之和。

itertools 提供了以下几个有用的函数:chain() 用以同时连续地迭代多个序列;compress()、dropwhile() 和 takewhile() 能用遴选序列元素;tee() 就像同名的 UNIX 应用程序,对序列作 n 次迭代;而 groupby 的效果类似 SQL 中相同拼写的关键字所带的效果。

[k for k, g in groupby("AAAABBBCCDAABB")] --> A B C D A B
[list(g) for k, g in groupby("AAAABBBCCD")] --> AAAA BBB CC D
除了这些针对有限元素的迭代帮助函数之外,还有 count()、cycle()、repeat() 等函数产生无穷序列,这 3 个函数就分别可以产生算术递增数列、无限重复实参的序列和重复产生同一个值的序列。

组合函数意义product()计算 m 个序列的 n 次笛卡尔积permutations()产生全排列combinations()产生无重复元素的组合combinations_with_replacement()产生有重复元素的组合

 

In [1]: from itertools import *

In [2]: list(product('ABCD', repeat=2))
Out[2]: 
[('A', 'A'),
 ('A', 'B'),
 ('A', 'C'),
 ('A', 'D'),
 ('B', 'A'),
 ('B', 'B'),
 ('B', 'C'),
 ('B', 'D'),
 ('C', 'A'),
 ('C', 'B'),
 ('C', 'C'),
 ('C', 'D'),
 ('D', 'A'),
 ('D', 'B'),
 ('D', 'C'),
 ('D', 'D')]
# 其中 product() 可以接受多个序列
In [5]: for i in product('ABC', '123', repeat=2):
   ...:     print(''.join(i))
   ...:     
A1A1
A1A2
A1A3
A1B1
A1B2
A1B3
A1C1
A1C2
...

 

建议 66:熟悉 Python 的生成器

生成器,顾名思义,就是按一定的算法生成一个序列。

迭代器虽然在某些场景表现得像生成器,但它绝非生成器;反而是生成器实现了迭代器协议的,可以在一定程度上看作迭代器。

如果一个函数,使用了 yield 关键字,那么它就是一个生成器函数。当调用生成器函数时,它返回一个迭代器,不过这个迭代器是以生成器对象的形式出现的:

In [1]: def fib(n):
   ...:     a, b = 0, 1
   ...:     while a < n:
   ...:         yield a
   ...:         a, b = b, a + b
   ...: for i, f in enumerate(fib(10)):
   ...:     print(f)
   ...:     
0
1
1
2
3
5
8

In [2]: f = fib(10)

In [3]: type(f)
Out[3]: generator

In [4]: dir(f)
Out[4]: 
['__class__',
 '__del__',
 '__delattr__',
 '__dir__',
 '__doc__',
 '__eq__',
 '__format__',
 '__ge__',
 '__getattribute__',
 '__gt__',
 '__hash__',
 '__init__',
 '__iter__',
 '__le__',
 '__lt__',
 '__name__',
 '__ne__',
 '__new__',
 '__next__',
 '__qualname__',
 '__reduce__',
 '__reduce_ex__',
 '__repr__',
 '__setattr__',
 '__sizeof__',
 '__str__',
 '__subclasshook__',
 'close',
 'gi_code',
 'gi_frame',
 'gi_running',
 'gi_yieldfrom',
 'send',
 'throw']

可以看到它返回的是一个 generator 类型的对象,这个对象带有__iter__()和__next__()方法,可见确实是一个迭代器。

分析:

  1. 每一个生成器函数调用之后,它的函数并不执行,而是到第一次调用 next() 的时候才开始执行;

  2. yield 表达式的默认返回值为 None,当第一次调用 next() 方法时,生成器函数开始执行,执行到 yield 表达式为止;

  3. 再次调用next()方法,函数将在上次停止的地方继续执行。

send() 是全功能版本的 next(),或者说 next() 是 send()的快捷方式,相当于 send(None)。还记得 yield 表达式有一个返回值吗?send() 方法的作用就是控制这个返回值,使得 yield 表达式的返回值是它的实参。

除了能 yield 表达式的“返回值”之外,也可以让它抛出异常,这就是 throw() 方法的能力。

对于常规业务逻辑的代码来说,对特定的异常有很好的处理(比如将异常信息写入日志后优雅的返回),从而实现从外部影响生成器内部的控制流。

当调用 close() 方法时,yield 表达式就抛出 GeneratorExit 异常,生成器对象会自行处理这个异常。当调用 close() 方法,再次调用 next()、send() 会使生成器对象抛出 StopIteration 异常。换言之,这个生成器对象已经不再可用。当生成器对象被 GC 回收时,会自动调用 close()。

生成器还有两个很棒的用处:

  • 实现 with 语句的上下文管理协议,利用的是调用生成器函数时函数体并不执行,当第一次调用 next() 方法时才开始执行,并执行到 yield 表达式后中止,直到下一次调用 next() 方法这个特性;

  • 实现协程,利用的是 send()、throw()、close() 等特性。

第二个用处在下一个小节讲解,先看第一个:

In [1]: with open('/tmp/test.txt', 'w') as f:
   ...:     f.write('Hello, context manager.')
   ...:     

In [2]: from contextlib import contextmanager

In [3]: @contextmanager
   ...: def tag(name):
   ...:     print('<%s>' % name)
   ...:     yield
   ...:     print('<%s>' % name)
   ...:     

In [4]: with tag('h1'):
   ...:     print('foo')
   ...:     
<h1>
foo
<h1>

这是 Python 文档中的例子。通过 contextmanager 对 next()、throw()、close() 的封装,yield 大大简化了上下文管理器的编程复杂度,对提高代码可维护性有着极大的意义。除此之外,yield 和 contextmanager 也可以用以“池”模式中对资源的管理和回收,具体的实现留给大家去思考。

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

先介绍一下协程的概念:

协程,又称微线程和纤程等,据说源于 Simula 和 Modula-2 语言,现代编程语言基本上都支持这个特性,比如 Lua 和 ruby 都有类似的概念。

协程往往实现在语言的运行时库或虚拟机中,操作系统对其存在一无所知,所以又被称为用户空间线程或绿色线程。又因为大部分协程的实现是协作式而非抢占式的,需要用户自己去调度,所以通常无法利用多核,但用来执行协作式多任务非常合适。用协程来做的东西,用线程或进程通常也是一样可以做的,但往往多了许多加锁和通信的操作。

基于生产着消费者模型,比较抢占式多线程编程实现和协程编程实现。线程实现至少有两点硬伤:

  • 对队列的操作需要有显式/隐式(使用线程安全的队列)的加锁操作。

  • 消费者线程还要通过 sleep 把 CPU 资源适时地“谦让”给生产者线程使用,其中的适时是多久,基本上只能静态地使用经验,效果往往不尽如人意。

下面来看看协程的解决方案

def consumer():
    r = ''
    while True:
        n = yield r
        if not n:
            return
        print('[CONSUMER] Consuming %s...' % n)
        r = '200 OK'

def produce(c):
    c.send(None)
    n = 0
    while n < 5:
        n = n + 1
        print('[PRODUCER] Producing %s...' % n)
        r = c.send(n)
        print('[PRODUCER] Consumer return: %s' % r)
    c.close()

c = consumer()
produce(c)

执行结果:

[PRODUCER] Producing 1...
[CONSUMER] Consuming 1...
[PRODUCER] Consumer return: 200 OK
[PRODUCER] Producing 2...
[CONSUMER] Consuming 2...
[PRODUCER] Consumer return: 200 OK
[PRODUCER] Producing 3...
[CONSUMER] Consuming 3...
[PRODUCER] Consumer return: 200 OK
[PRODUCER] Producing 4...
[CONSUMER] Consuming 4...
[PRODUCER] Consumer return: 200 OK
[PRODUCER] Producing 5...
[CONSUMER] Consuming 5...
[PRODUCER] Consumer return: 200 OK

注意到consumer函数是一个generator,把一个consumer传入produce后:

  1. 首先调用c.send(None)启动生成器;

  2. 然后,一旦生产了东西,通过c.send(n)切换到consumer执行;

  3. consumer通过yield拿到消息,处理,又通过yield把结果传回;

  4. produce拿到consumer处理的结果,继续生产下一条消息;

  5. produce决定不生产了,通过c.close()关闭consumer,整个过程结束。

整个流程无锁,由一个线程执行,produce和consumer协作完成任务,所以称为“协程”,而非线程的抢占式多任务。

最后套用Donald Knuth的一句话总结协程的特点:

“子程序就是协程的一种特例。”

greenlet 是一个 C 语言编写的程序库,它与 yield 关键字没有密切的关系。greenlet 这个库里最为关键的一个类型就是 PyGreenlet 对象,它是一个 C 结构体,每一个 PyGreenlet 都可以看到一个调用栈,从它的入口函数开始,所有的代码都在这个调用栈上运行。它能够随时记录代码运行现场,并随时中止,以及恢复。它跟 yield 所能够做到的相似,但更好的是它提供从一个 PyGreenlet 切换到另一个 PyGreenlet 的机制。

from greenlet import greenlet
def test1():
    print(12)
    gr2.switch()
    print(34)
def test2():
    print(56)
    gr1.switch()
    print(78)
gr1 = greenlet(test1)
gr2 = greenlet(test2)
gr1.switch()

协程虽然不能充分利用多核,但它跟异步 I/O 结合起来以后编写 I/O 密集型应用非常容易,能够在同步的代码表面下实现异步的执行,其中的代表当属将 greenlet 与 libevent/libev 结合起来的 gevent 程序库,它是 Python 网络编程库。最后,以 gevent 并发查询 DNS 的例子为例,使用它进行并发查询 n 个域名,能够获得几乎 n 倍的性能提升:

In [1]: import gevent

In [2]: from gevent import socket

In [3]: urls = ['www.baidu.com', 'www.python.org', 'www.qq.com']

In [4]: jobs = [gevent.spawn(socket.gethostbyname, url) for url in urls]

In [5]: gevent.joinall(jobs, timeout=2)
Out[5]: 
[<Greenlet at 0x7f37e439c508>,
 <Greenlet at 0x7f37e439c5a0>,
 <Greenlet at 0x7f37e439c340>]

In [6]: [job.value for job in jobs]
Out[6]: ['115.239.211.112', '151.101.24.223', '182.254.34.74']

 

 

建议 68:理解 GIL 的局限性

多线程 Python 程序运行的速度比只有一个线程的时候还要慢,除了程序本身的并行性之外,很大程度上与 GIL 有关。由于 GIL 的存在,多线程编程在 Python 中并不理想。GIL 被称为全局解释器锁(Global Interpreter Lock),是 Python 虚拟机上用作互斥线程的一种机制,它的作用是保证任何情况下虚拟机中只会有一个线程被运行,而其他线程都处于等待 GIL 锁被释放的状态。不管是在单核系统还是多核系统中,始终只有一个获得了 GIL 锁的线程在运行,每次遇到 I/O 操作便会进行 GIL 锁的释放。

但如果是纯计算的程序,没有I/O操作,解释器则会根据sys.setcheckinterval的设置来自动进行线程间的切换,默认是每隔100个内部时钟就会释放GIL锁从而轮换到其他线程:

在单核 CPU 中,GIL 对多线程的执行并没有太大影响,因为单核上的多线程本质上就是顺序执行的。但对于多核 CPU,多线程并不能真正发挥优势带来效率上明显的提升,甚至在频繁 I/O 操作的情况下由于存在需要多次释放和申请 GIL 的情形,效率反而会下降。

那么 Python 解释器为什么要引入 GIL 呢?

我们知道 Python 中对象的管理与引用计数器密切相关,当计数器变为 0 的时候,该对象便会被垃圾回收器回收。当撤销一个对象的引用时,Python 解释器对对象以及其计数器的管理分为以下两步:

  1. 使引用计数值减1

  2. 判断该计数值是否为 0,如果为0,则销毁该对象

鉴于此,Python 引入了 GIL,以保证对虚拟机内部共享资源访问的互斥性。

GIL 的引入确实使得多线程不能再多核系统中发挥优势,但它也带来了一些好处:大大简化了 Python 线程中共享资源的管理,在单核 CPU 上,由于其本质是顺序执行的,一般情况下多线程能够获得较好的性能。此外,对于扩展的 C 程序的外部调用,即使其不是线程安全的,但由于 GIL 的存在,线程会阻塞直到外部调用函数返回,线程安全不再是一个问题。

在 Python3.2 中重新实现了 GIL,其实现机制主要集中在两个方面:一方面是使用固定的时间而不是固定数量的操作指令来进行线程的强制切换;另一个方面是在线程释放 GIL 后,开始等待,直到某个其他线程获取 GIL 后,再开始尝试去获取 GIL,这样虽然可以避免此前获得 GIL 的线程,不会立即再次获取 GIL,但仍然无法保证优先级高的线程优先获取 GIL。这种方式只能解决部分问题,并未改变 GIL 的本质。

Python 提供了其他方式可以绕过 GIL 的局限,比如使用多进程 multiprocess 模块或者采用 C 语言扩展的方式,以及通过 ctypes 和 C 动态库来充分利用物理内核的计算能力。

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

class Leak(object):
    def __init__(self):
        print('object with id %d was born' % id(self))
while(True):
    A = Leak()
    B = Leak()
    A.b = B
    B.a = A
    A = None
    B = None

运行上述程序,我们会发现 Python 占用的内存消耗一直在持续增长,直到最后内存耗光。

先简单谈谈 Python 中的内存管理的方式:

Python 使用引用计数器(Reference counting)的方法来管理内存中的对象,即针对每一个对象维护一个引用计数值来表示该对象当前有多少个引用。

当其他对象引用该对象时,其引用计数会增加 1,而删除一个队当前对象的引用,其引用计数会减 1。只有当引用计数的值为 0 时的时候该对象才会被垃圾收集器回收,因为它表示这个对象不再被其他对象引用,是个不可达对象。引用计数算法最明显的缺点是无法解决循环引用的问题,即两个对象相互引用。如同上述代码中A、B对象之间相互循环引用造成了内存泄露,因为两个对象的引用计数都不为 0,该对象也不会被垃圾回收器回收,而无限循环导致一直在申请内存而没有释放。

循环引用常常会在列表、元组、字典、实例以及函数使用时出现。对于由循环引用而导致的内存泄漏的情况,可以使用 Python 自带的一个 gc 模块,它可以用来跟踪对象的“入引用(incoming reference)“和”出引用(outgoing reference)”,并找出复杂数据结构之间的循环引用,同时回收内存垃圾。有两种方式可以触发垃圾回收:一种是通过显式地调用 gc.collect() 进行垃圾回收;还有一种是在创建新的对象为其分配内存的时候,检查 threshold 阈值,当对象的数量超过 threshold 的时候便自动进行垃圾回收。默认情况下阈值设为(700,10,10),并且 gc 的自动回收功能是开启的,这些可以通过 gc.isenabled() 查看:

In [1]: import gc

In [2]: print(gc.isenabled())
True

In [3]: gc.isenabled()
Out[3]: True

In [4]: gc.get_threshold()
Out[4]: (700, 10, 10)

所以修改之前的代码:

def main():
    collected = gc.collect()
    print("Garbage collector before running: collected {} objects.".format(collected))
    print("Creating reference cycles...")
    A = Leak()
    B = Leak()
    A.b = B
    B.a = A
    A = None
    B = None
    collected = gc.collect()
    print(gc.garbage)
    print("Garbage collector after running: collected {} objects".format(collected))

if __name__ == "__main__":
    ret = main()
    sys.exit(ret)

gc.garbage 返回的是由于循环引用而产生的不可达的垃圾对象的列表,输出为空表示内存中此时不存在垃圾对象。gc.collect() 显示所有收集和销毁的对象的数目,此处为 4(2 个对象 A、B,以及其实例属性 dict)。

我们再来考虑一个问题:如果在类 Leak 中添加析构方法 __del__(),会发现 gc.garbage 的输出不再为空,而是对象 A、B 的内存地址,也就是说这两个对象在内存中仍然以“垃圾”的形式存在。

这是什么原因呢?实际上当存在循环引用并且当这个环中存在多个析构方法时,垃圾回收器不能确定对象析构的顺序,所以为了安全起见仍然保持这些对象不被销毁。而当环被打破时,gc 在回收对象的时候便会再次自动调用 __del__() 方法。

gc 模块同时支持 DEBUG 模式,当设置 DEBUG 模式之后,对于循环引用造成的内存泄漏,gc 并不释放内存,而是输出更为详细的诊断信息为发现内存泄漏提供便利,从而方便程序员进行修复。更多 gc 模块可以参考文档 。

 

posted on 2014-03-31 19:42  myworldworld  阅读(835)  评论(0)    收藏  举报

导航