无知的 tonyseek

Yet Another Seeker

Python 对象的实例化过程

我们知道,Python 的构造函数有两个,一个是我们最常用的 __init__ ,另一个是很少用到的 __new__ 。而从它们的函数定义 def __new__(cls, [...])def __init__(self, [...]) 可以知道, __init__ 被调用时实例已经被创建(就是 self 参数所引用的);而 __new__ 被调用的时候,实例不一定已被创建(暂时只能这么说),而 __new__ 的第一个参数为当前类的引用。

所以我们可以通过一些途径,跟踪 Python 对象的实例化过程,来研究 __new____init__ 的工作。这个途径就是上一篇博文 利用 with 语法实现可拆卸的装饰器 中用到的函数调用轨迹装饰器。

先将上篇博文的代码保存为一个 Python 模块 withaop.py ,然后在同目录下再创建一个文件 instantiation.py ,这个 instantiation.py 就是我们的工作区。导入 withaop 模块中的 trace 装饰器,然后就可以开始了。

先从最简单的类开始:

# -*- coding:utf-8 -*-

from withaop import trace

class MyClass(object):
    __new__ = trace(object.__new__) # 装饰 __new__ 方法

    @trace # 装饰 __init__ 方法
    def __init__(self):
        pass

# 实例化
my_instance = MyClass()

可以看到上面代码中, __new__ 方法和 __init__ 方法安装装饰器的方式是不同的。 __new__ 方法可以直接将 object 的 __new__ 方法安装上装饰器,然后赋予 MyClass 。这是因为所有没有自己定义 __new__ 的类(特指 new-style 类,忽视 old-style 类,下同),都会在被创建时继承 object__new__ 引用。严格意义上来说, __new__ 甚至都不是一个方法,而是一个内建函数。这点在 Python Shell 一试便知:

图片-1

__init__ 则不能照搬,因为 object 是没有 __init__ 成员的。但这并不意味着 object 的子类在没有用户定义 __init__ 时会没有 __init__ ,子类在被创建时如果没有用户定义 __init__ 则会被赋予一个空的 __init__ 方法,正如 def __init__(self): pass 。所以,这里采取的方式是自行定义一个 __init__ 方法,并安装装饰器。

结果上述步骤,我们就模拟了一个没有任何定义的类 class MyClass(object): pass 并给 __new____init__ 安装了轨迹跟踪装饰器。

运行,可以看到结果:

图片-2

在 MyClass 实例化生成 MyClass 实例的时候,首先被调用的是 __new__ ,第一个参数 cls 如我们所料是 MyClass 。而 __new__ 的返回值,注意返回值,是 MyClass object ,也就是我们所期望的 MyClass 实例,是所有成员方法的第一个参数 self 所指向的实例对象。至此,我们得知了, __new__ 调用的实质是由类创建对象的过程,而 __init__ 则是在 MyClass 实例被创建之后自动调用的。我们可以理解为 __new__ 的职责是创建一个实例,而 __init__ 则是按照用户定义的内容初始化新创建的实例。

了解了这一点,我们就可以开始下一步,跟踪有具体 __init__ 的类创建的过程。下面定义的是一个 Person 类, __init__ 有两个普通参数用户名、密码和一个关键字参数作为选项。

    __new__ = trace(object.__new__)

    @trace
    def __init__(self, username, password, **kwargs):
        self.usr, self.pwd = username, password
        self.opt = kwargs

tonyseek = Person('tonyseek', 'my_password',
                  has_blog=True, has_notebook=False)

然后运行,观察装饰器跟踪的结果如下图:

图片-3

可以看到,我们为 __init__ 设置的参数,很诡异的先被传给了 __new__ ,然后才被传给了 __init__ 。为什么会这样呢?我提出我的一个猜测:众所周知 Python 和其他很多语言不同之处,是没有 new 操作符。实例化一个对象,和调用一个函数一样,是对类进行 call 操作,如下:

tonyseek = Person('tonyseek', 'my_password',
                  has_blog=True, has_notebook=False)

我的猜测就是,我们 call 一个类的本质是 call 其 __new__ 方法,而 __new__ 方法的本质就是这个类的工厂函数。所以我们 call Person 的时候传入的参数是被 __new__ 接收的。object 等内置基类定义的 __new__ 方法返回实例之前,又把调用自身所用到的参数用来 call 对象的 __init__ 来进行初始化操作。

这个猜测是否属实不得而知,而且比较难从 Python 语言层证实,因为我们很难知道 object.__new__ 到底做了什么。但是我相信这个猜测是属实的,为此单独使用 __new__ 来做一些测试。

把上面的 Person 引用进来,然后换一种实例化方法:

tonyseek = object.__new__(Person, 'tonyseek',  'my_password',
                          has_blog=True, has_notebook=False)

print tonyseek
print tonyseek.usr

运行结果如下图

图片-4

访问属性 tonyseek.usrAttributeError 了,可见 __init__ 根本没被调用。但是 Person 对象确确实实是生产出来了。可见 __new__ 是工厂函数的说法不是个奇迹。 __init__ 被无视是不是 object 没有 __init__ 的缘故呢?我们换种方式测试:

tonyseek = Person.__new__(Person, 'tonyseek', 'my_password',
                          has_blog=True, has_notebook=False)

print tonyseek
print tonyseek.usr

运行结果如图:

图片-5

从轨迹跟踪来看,的确是 Person 的 __new__ 被调用了。但是 __init__ 还是被无视了。据此我得出结论: __new__ 自己并不会隐式接触 __init__ ,而是在使用 Python 的实例化语法 —— call 一个类的时候,Python 做了两件事,第一件事是调用 __new__ 将未初始化的对象(我称为 raw instance)生产出来,第二件事是在 raw instance 上调用 __init__ 初始化。而调用两个构造函数的时候,都会将 call 到类上的参数分发进去。

了解了整个机理,我想应该改口了:按照其他语言的习惯, __init__ 这个“初始化者”才是真正的“构造方法”,而 __new__ 是“工厂函数”。

Python Note

Comments