Python类实例理解

Foo这个类开始说起:

class Foo(object):
    def __init__(self, x, y=0):
        self.x = x
        self.y = y

当实例化这个类时候发生了什么?

f  = Foo(1, y=2)

实例化的时候Foo被调用了,在这里面到底有什么方法被依次调用了呢?__init__肯定是被调用了。但是__init__并不会返回一个对象,而且__init__需要一个self参数。调用Foo(1, y=2)的时候也并没有这样给一个参数。所以另有玄机藏于内。

调用的序列

在Python中实例化一个对象要经过几个步骤。Foo是一个类,但是类在Python中也是一个对象,类/方法/实例在Python中都是对象。不管任何时候你将参数通过括号的形式包括起来放在对象后面,都调用了一个__call__方法。所以Foo(1, y=2) 相当于Foo.__call__(1, y=2)。 这个__call__是由Foo的class定义出来。那什么是Foo的类呢?

>>> Foo.__class__
<class 'type'>

所以说,Foo是类type的对象 调用__call__返回一个Foo类的实例。有点绕口( :。接下来让我们看看type__call__ 方法到底长什么样子?这个方法有点复杂,我们试着简化一些。 下面我们从CPython和PyPy的实现来解读一下。

CPython

代码地址

static PyObject *
type_call(PyTypeObject *type, PyObject *args, PyObject *kwds)
{
    PyObject *obj;

    if (type->tp_new == NULL) {
        PyErr_Format(PyExc_TypeError,
                     "cannot create '%.100s' instances",
                     type->tp_name);
        return NULL;
    }

    obj = type->tp_new(type, args, kwds);
    obj = _Py_CheckFunctionResult((PyObject*)type, obj, NULL);
    if (obj == NULL)
        return NULL;

    /* Ugly exception: when the call was type(something),
       don't call tp_init on the result. */
    if (type == &PyType_Type &&
        PyTuple_Check(args) && PyTuple_GET_SIZE(args) == 1 &&
        (kwds == NULL ||
         (PyDict_Check(kwds) && PyDict_Size(kwds) == 0)))
        return obj;

    /* If the returned object is not an instance of type,
       it won't be initialized. */
    if (!PyType_IsSubtype(Py_TYPE(obj), type))
        return obj;

    type = Py_TYPE(obj);
    if (type->tp_init != NULL) {
        int res = type->tp_init(obj, args, kwds);
        if (res < 0) {
            assert(PyErr_Occurred());
            Py_DECREF(obj);
            obj = NULL;
        }
        else {
            assert(!PyErr_Occurred());
        }
    }
    return obj;
}

PyPy

代码地址

def descr_call(self, space, __args__):
    promote(self)
    # invoke the __new__ of the type
    if not we_are_jitted():
        # note that the annotator will figure out that self.w_new_function
        # can only be None if the newshortcut config option is not set
        w_newfunc = self.w_new_function
    else:
        # for the JIT it is better to take the slow path because normal lookup
        # is nicely optimized, but the self.w_new_function attribute is not
        # known to the JIT
        w_newfunc = None
    if w_newfunc is None:
        w_newtype, w_newdescr = self.lookup_where('__new__')
        if w_newdescr is None:    # see test_crash_mro_without_object_1
            raise oefmt(space.w_TypeError, "cannot create '%N' instances",
                        self)
        w_newfunc = space.get(w_newdescr, self)
        if (space.config.objspace.std.newshortcut and
            not we_are_jitted() and
            isinstance(w_newtype, W_TypeObject)):
            self.w_new_function = w_newfunc
    w_newobject = space.call_obj_args(w_newfunc, self, __args__)
    call_init = space.isinstance_w(w_newobject, self)

    # maybe invoke the __init__ of the type
    if (call_init and not (space.is_w(self, space.w_type) and
        not __args__.keywords and len(__args__.arguments_w) == 1)):
        w_descr = space.lookup(w_newobject, '__init__')
        if w_descr is not None:    # see test_crash_mro_without_object_2
            w_result = space.get_and_call_args(w_descr, w_newobject,
                                               __args__)
            if not space.is_w(w_result, space.w_None):
                raise oefmt(space.w_TypeError,
                            "__init__() should return None")
    return w_newobject

代码很长,不过没关系,如果我们不看异常处理的话,实际上执行过程约等于:

def __call__(obj_type, *args, **kwargs):
    obj = obj_type.__new__(*args, **kwargs)
    if obj is not None and issubclass(obj, obj_type):
        obj.__init__(*args, **kwargs)
    return obj

实际上__new__给对象分配了内存, 将对象的结构给搭起来。然后__init__初始化对象结构中的值。

所以总结来说:

  • Foo(*args, **kwargs) 等同于 Foo._call__(*args, **kwargs)
  • 因为Footype的实例,Foo._call__(*args, **kwargs)进一步会执行type.__call__(Foo, *args, **kwargs).
  • type.__call__(Foo, *args, **kwargs)函数内部会执行type.__new__(Foo, *args, **kwargs) 。这个函数会返回obj
  • obj通过调用obj.__init__(*args, **kwargs)进行初始化
  • 返回obj对象,这个对象实例化结束

知道这些能干啥

我们聚焦__new__这个方法。实际上,这个方法才是真正管理实际对象创建的。在本文中我们不深入讨论__new__的实现。该函数主要目的就是为将要创建的对象分配空间。我们可以修改__new__函数,从而对对象的创建过程做一些有意思的控制。比较具有代表性的是通过修改__new__函数实现单例类:

class Singleton(object):
    
    _instance = None
    
    def __new__(cls, *args, **kwargs):
        if cls._instance is None:
            cls._instance = super().__new__(cls, *args, **kwargs)
        return cls._instance

然后:

>>> s1 = Singleton()
... s2 = Singleton()
... s1 is s2
True

共享模式Borg design pattern

class Borg(object):
    _dict = None

    def __new__(cls, *args, **kwargs):
        obj = super().__new__(cls, *args, **kwargs)
        if cls._dict is None:
            cls._dict = obj.__dict__
        else:
            obj.__dict__ = cls._dict
        return obj

然后

>>> b1 = Borg()
... b2 = Borg()
... b1 is b2
False
>>> b1.x = 8
... b2.x
8

谨慎的使用

上面的例子显示出__new__还是挺强大的,你能用它,但不代表你必须用它

__new__ is one of the most easily abused features in Python. It’s obscure, riddled with pitfalls, and almost every use case I’ve found for it has been better served by another of Python’s many tools. However, when you do need __new__, it’s incredibly powerful and invaluable to understand.
– Arion Sprague, Python’s Hidden New

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 201,784评论 5 474
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 84,745评论 2 378
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 148,702评论 0 335
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,229评论 1 272
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,245评论 5 363
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,376评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,798评论 3 393
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,471评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,655评论 1 295
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,485评论 2 318
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,535评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,235评论 3 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,793评论 3 304
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,863评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,096评论 1 258
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,654评论 2 348
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,233评论 2 341

推荐阅读更多精彩内容

  • 了解元类之前,先了解几个魔术方法: __new__、__init__、__call__ __new__: 对象的创...
    大富帅阅读 9,076评论 2 16
  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 134,571评论 18 139
  • Python 面向对象Python从设计之初就已经是一门面向对象的语言,正因为如此,在Python中创建一个类和对...
    顺毛阅读 4,205评论 4 16
  • 看不见的风、把看到的云吹成看不见的样子、 我欲乘风破浪、踏遍黄沙海洋、与其误会一场、也要不负勇往、
    在路上虫儿飞阅读 175评论 0 0
  • 某个人说,你们换换吧。 这是个值得纪念,也是我事业的转折点,我记恨这个人,肯定记恨(虽然我的字典里已经将恨这个词给...
    等妳遇見我阅读 327评论 0 0