Python对象模型小记

Python 对象模型小记

本文针对Python3,所有例子在Python2下均无试验,即使使用Python2新类也不能保证其准确性。

1. "讨厌"的self

Python定义class的时候有一特立独行之处,那就是需要显示传递self。这一点看似让人百思不得其解,却是和整个Python对象模型紧密相关的。

class A:
    def foo(self):
        print(self)

a = A()
a.foo() #=> "<__main__.A object..."

可以看到上述代码中,定义的foo方法期待一个self参数,但在调用foo的时候却并不需要传递这个self参数。可以说,Python为我们隐式地传递了这个参数,那就是a,但这一切又是怎么发生的呢?这得从Python对象模型说起。

Python的对象模型是松散的,整个对象模型建筑依靠着self(实例变量)和cls(类变量)这两颗钉子结合在一起,不似Ruby对象模型的精致,却整个向外表现出一种简约的气质。如果仅仅是一般的编程,上述代码的知识量已经足够:传个self,然后在self上操作实例即可。但让我们走得更近一点。

Python的对象模型松散到整个类定义可以被认为是面向过程加命名空间的语法糖!要阐明这一点,首先来看一下实例调用方法的本质:

a.foo() #=> "<__main__.A object..."

# 注:在此后的所有例子中,表明属性查找都会使用"__dict__"而不是"."
# "__dict__"只查找本类的空间;而"."还包括父类等的属性,隐含一条mro。
A.__dict__['foo'].__get__(a)() #=> "<__main__.A object..."

上述两行代码的效果是相同的,第一行代码可以认为是第二行代码的语法糖。从中,我们可以发现一些有意思的事情:

第一,所谓的foo方法,其本质是个函数,在调用时绑定实例以成为方法:

# foo 是个函数
A.__dict__['foo'] #=> "<function A.foo ..."

# 绑定变量后成为方法
A.__dict__['foo'].__get__(a) #=> "<bound method A.foo of <__main__.A object..."

第二,函数有个__get__(instance)方法,可以返回一个绑定了instance为第一参数的函数(实际上是个类函数的可调用对象)。__get__的实现大致可认为如下:

def __get__(self, instance):
    def _new_foo(*args, **kw):
        return self(instance, *args, **kw)

    return _new_foo

所以,我们可以将类定义打散,事实上这也是Python类运行时扩展的一种手段:

class A:
    pass

def foo(self):
    print(self)

A.foo = foo

a = A()

a.foo() #=> "<__main__.A object..."

上述代码还有一个有趣的地方:A.foo = foo。我们像使用一个变量一样使用一个函数。

我们可以说,Pyhton是一种Lisp-1型语言,相对于Lisp-2型语言,Lisp-1型的语言并不区分过程这两个概念,甚至其认为这两个本来就是一回事。以下面的代码为例,i()j有区别吗?当然,他们的使用方式不同!对,但他们也仅仅是使用方式不同而已!任何j所使用的地方都可以用i()代替[当然,不允许在i()上做赋值操作,不予讨论]

def i():
    return 1

j = 1

对应到Python,值就是变量,过程就是函数,Python并不区分变量和函数。贯穿一切皆对象的思想,在Python里所有的东西都是对象,所有的标识符都只是一个对象的引用,所以我们可以把一个值赋给一个函数标识符或一个类标识符,或者反过来把函数或类赋值给一个变量,这都不会有问题。虽然如此,但最好别去修改Python内置的标识符,因为这会把一切搞得乱七八糟。

综上,我们对Python的变量和函数有了一定的了解,再来讨论self的问题。在类定义的时候,我们定义的所谓的函数或者说方法,其本质是一个变量。既然是一个变量,就意味着我们可以操作其本身的值。如果我们要把一个类的方法赋值给另外一个类,就意味着这个方法本身不能和原来的类耦合在一起,否则我们无法抽离方法self便是其中解耦的关键性设计。self的存在,使得方法和实例挂钩而断开了和具体类的联系,Python的灵活性一下子就有了质的飞升,我们可以像处理变量一样处理类的方法了!无论是方法的添加、修改还是删除,都能随心所欲。以下是一个抛砖引玉的例子:

class A:
    def foo(self):
        print(self)
class B:
    pass

# 把A的foo方法赋值给B
B.foo = A.foo

b = B()
b.foo() #=>  '<__main__.B object ...'

至此,我们可以对Python对象模型做个小小的结论:Python的类定义只是为变量提供了一个命名空间,而这个空间里的所有东西都和这个空间本身没有任何关系。

2. 描述符

从函数的__get__方法可以一窥Python对象模型的另一个面:Python通过语法糖的形式,对内实现复杂的逻辑过程,对外保持简单的调用形式,而描述符便是其中的翘楚。

Python描述符的意义,在于向我们提供一种控制变量存取过程的途径。利用描述符,我们不仅能存取变量的值,更能进行一些在变量存取过程中的额外操作,如类型检查等等。Python已经提供了一种使用描述符的简单手段,即property:

class A:
    @property
    def prop(self):
        print("get x")
        return self.x

    @prop.setter
    def prop(self, value):
        print("set x")
        self.x = value


a= A()
a.prop = 1 #=> set x
a.prop 
#=> get x
#=> 1

从上述代码可知,Property的原理有两个重点:

  1. 对字段的存取托管给Property类
  2. Python存取描述符的语法糖

Python的语法糖,让描述符的使用变得异常简单,和普通变量的存取完全一致。具体而言,Python存取描述符经过了如下一系列的转换:

# 对字段的存取托管给Property类
A.__dict__['prop'] #=> "<property object..."

#Python存取描述符的语法糖
A.__dict__['prop'].__get__(a, A) #=> 等同a.prop
A.__dict__['prop'].__set__(a, 1) #=> 等同a.prop = 1

显然,Python对于描述符的存取在内部是通过调用托管类的__get____set__方法实现的,这一点非常重要。既然知道了原理就没什么能阻止我们定义自己的托管类:

class P:
    # self 描述符本身;instance 实例变量;cls 类变量
    def __get__(self, instance, cls):
        print("P.__get__ called")
        return instance.attr

    def __set__(self, instance, value):
        print("P.__set__ called")
        instance.attr = value

class A:
    p = P() # 声明有效

    def foo(self):
        self.p1 = P() # 声明无效,A.__dict__['p1']是找不到的
        self.__class__.p2 = P()    # 声明有效,A.__dict__['p2']找得到

描述符还有个__delete__(self, instance)方法定义删除描述符时的动作。

如果使用过Python的元类,那么Python另一个重要的语法糖就呼之欲出了:()

为了生成类的实例,我们使用如下的代码:a = A(),而就在这个语句中,简简单单的()却在幕后至少调用了三个函数:其元类的__call__方法、类的生成实例的静态方法__new__和类的实例初始化方法__init__和。有关这部分内容将在下一小节叙述。

3. Python元类

Python元类的存在可以说是对Python一切皆对象的完美补充,因为此时Python类也成了对象,也成了实例。这似乎很难理解,因为从C++以来,一直类是类,实例是实例,现在类和实例之间竟然没了区别!事实上,加入元类后,类和实例之间的界限就模糊了,两者的区别只有角度不同而已。类生成实例,那么什么生成类呢?在Python里我们可以说是元类,元类生成了类,类生成了实例,所以元类和类之间的关系就是类和实例的关系,两者没有区别。从这个角度出发,元类的使用就回归到类的使用了。

让我们来对比下元类和类的使用:

# 下面两行代码一个生成实例,一个生成类,可以看到两者没区别
a = A() # 类生成实例
B = type("B", (), {}) # 默认元类生成类,参数:类名、父类和属性

# 继承type类以自定义元类
class Mytype(type):
    # __new__方法用于控制类的生成
    def __new__(type_t, name, bases, dicts):
        print("Mytype.__new__", name, bases, dicts)
        return super().__new__(type_t, name, bases, dicts)

    # __init__方法用于控制类的初始化
    def __init__(cls, name, bases, dicts):
        print("Mytype.__init__", name, bases, dicts)

# 通过metaclass指定元类
class A(metaclass=MyType):
    pass

"""
结果打印出A类生成的过程:
Mytype.__new__ A () {'__module__': '__main__', '__qualname__': 'A'}
Mytype.__init__ A () {'__module__': '__main__', '__qualname__': 'A'}
"""

在上述代码中class A(metaclass=MyType)实际上是A = Mytype("A", (), {})的语法糖,由此可见,用元类生成类的过程和类生成实例的过程完全一致,调用其元类(类)的__new__方法来生成对象,然后调用__init__初始化,两者唯一的不同仅在于语法。

现在让我们继续上一节的话题,即对类的()的讨论。很明显()是个操作符,表示函数调用。在Python中,对象上的操作符调用都会转化为对应类上的方法调用,如+对应__add__()操作符对应__call__,由此可见,A()语句会被转化为type.__call__(A)

在type的__call__方法中,理所当然地有两个重要的方法被调用:一个是A.__new__,用来生成实例;另一个是A.__init__,用来初始化实例。需要注意的一点是__new____call__方法必须返回生成的对象,否则不能正常传递生成的实例。

利用上述知识,我们来写一个简单的单例类:

class Singleton(type):
    def __init__(cls, name, bases, dicts):
        # 初始化单例为None从而使第一次访问时不会报错
        cls.__instance = None

    def __call__(cls, *args, **kw):
        print("Singleton.__call__({}, {})".format(args, kw))

        # 检验实例对象的类型
        if not isinstance(cls.__instance, cls):
            cls.__instance = super().__call__(*args, **kw)

        return cls.__instance


class MySingletonCls(metaclass=Singleton):
    def __new__(cls, a, b):
        print("MySingleCls.__new__({}, {})".format(a, b))

        return super().__new__(cls)

    def __init__(self, a, b):
        print("MySingleton.__init__({}, {})".format(a, b))


my = MySingletonCls(11, b=13)
my2 = MySingletonCls(10, 11)

print(my == my2)

'''
运行结果:
Singleton.__call__((11,), {'b': 13}) // 元类的__call__方法
MySingleCls.__new__(11, 13) // 类的__new__方法
MySingleton.__init__(11, 13) //类的 __init__方法
Singleton.__call__((10, 11), {})
True
'''

4. Python的MRO和类模型结构

Python的对象模型是基于多继承的,但同时其利用mro的原理有效解决了多继承带来的问题。通俗来讲,mro就是将多继承模拟成单继承,在存在多个父类的情况下分出先后次序,排列出一条单一的查找属性方法的路径。当然,多继承对象模型的类关系很复杂,有时候,Python类的mro路径无法排序出来就会报错。

借助mro,我们可以简单地将Python的对象模型展示在一张二维图上:

^ mro              object
|                    |
|        object --  type  
|           |        |
| obja -- ClsA  -- MyType
|           |        |
| objb -- ClsB  ------
|           |        |
| objc -- ClsC  ------
---------------------------->
  实例      类       元类

上图中,每一整条竖线代表一条mro,每一条横线代表左是右的实例。可以很清晰的看到,无论实例还是类,其都是object的实例;每一个类还是type的实例;其中type具有继承性,ClsC也是MyType的实例。当然在这张图的x方向上可以无限延伸,如定义元类的元类,元类的元类的元类......

实例的属性查找可归结为“右拐向上”:先在自己的空间查找,若没找到则"右拐"到对应类空间去查找,并一路向上直至object。如objc查找属性的路径为(objc, ClsC, ClsB, ClsA, object)。

类的属性查找稍稍复杂一点,可归结为"向上再右拐向上":因为类本身处在一条mro上,所以先在自己的mro上查找,若没找到再"右拐"到元类的类空间查找,并一路直至object。如ClsC的查找路径为(ClsC,ClsB,ClsA,object,MyType,type,object)。

但是对于操作符对应的方法,如__add__等,则直接在类空间上查找。也就是说,对于objc,+等操作符调用方法的起点是ClsC,在objc的空间定义__add__是无效的。

注意,任何赋值操作都将在对象自己的空间里生成新的属性,当然描述符除外,因为它已经转化为方法调用了。

posted @ 2017-07-31 14:48  天涯海角路  阅读(118)  评论(0)    收藏  举报