前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >《Python 源码剖析》一些理解以及勘误笔记(2)

《Python 源码剖析》一些理解以及勘误笔记(2)

作者头像
s1mba
发布2017-12-28 11:53:53
1.1K0
发布2017-12-28 11:53:53
举报
文章被收录于专栏:开发与安全开发与安全

以下是本人阅读此书时理解的一些笔记,包含一些影响文义的笔误修正,当然不一定正确,贴出来一起讨论。

注:此书剖析的源码是2.5版本,在python.org 可以找到源码。纸质书阅读,pdf 贴图。

文章篇幅太长,故切分成3部分,这是第二部分。

p248: 嵌套函数、闭包和 decorator 

co_cellvars: 通常是一个tuple,保存嵌套的作用域内使用的变量名集合;

co_freevars: 通常是一个tuple,保存使用了的外层作用域中的变量名集合。

如下的一段Python 代码:

代码语言:python
复制
def get_func():
    value = "inner"
    def inner_func():
        print value
    return inner_func

show_value = get_func()
show_value()

则py 文件编译出来的PyCodeObject 有3个,那么与get_func 对应的对象中的 co_cellvars 就应该包含字符串 "value",而与 inner_func 

对应的PyCodeObject 对象的co_freevars 也应该有字符串"value"。

闭包从创建、传递到使用的全过程可以用以下三幅图演示:

inner_func 可以认为是 get_func 的局部变量,如图2 中 inner_func 对应的 PyFunctionObject 对象的 func_closure 指向 tuple。在inner_func 调用过

程中,tuple 中包含的一个个cell  对象就被放到 f_localplus 中相应的位置,当引用外层作用域符号时,一定是先到 f_localsplus 中的 free 变量区域获

取符号对应的值。实际上 value 的值可以通过 show_value.__closure__[0].cell_contents 访问到。使用闭包的时候需要注意返回的函数不要引用任何循环变量,或者后续会发生变化的变量,否则出现的情况可能与你预期不同。

在closure 技术的基础上,Python 实现了 decorator,decorator 可以认为是 "func = should_say(func)" 的一种包装形式。

代码语言:python
复制
# decorator 实现
def should_say(fn):
    def say(*args):
        print 'say something...'
        fn(*args)
    return say

@should_say
def func():
    print 'in func'

func()
# 输出结果为
# say something...
# in func

# 不用decorator 的实现
...
def func():
    print 'in func'

func = should_say(func)
func()

注意还有含参的装饰器(再封装一层),以及装饰类(接收一个类,并返回一个新类)。

p264: Python 中的可调用性(callable)。只要一个对象对应的class 对象中实现了"__call__" 操作(更确切地说,在 Python 内部的 

PyTypeObject 中,tp_call 不为空),那么这个对象就是一个可调用的对象,比如:

class A(object):

def __call__(self): print  'Hello Python'

那么 a= A()   a() 会输出'Hello Python' ;可以认为 PyA_Type 对象的 tp_call 不为空。在 c++ 看来也就是函数对象的实现。

所谓 “调用”,就是执行对象的 type 所对应的 class 对象的 tp_call 操作。

p268: 内置类型对应的PyTypeObject 的tp_dict 填充、descriptor

在Python 内部,存在多种 descriptor,PyType_Ready 在通过add_operators 添加了 PyTypeObject 对象中定义的一些 operator 后,

还会通过 add_methods、add_members、add_getsets 添加在PyType_Object 中定义的 tp_methods、tp_members、tp_getset 函数

集。这些 add_*** 的过程与 add_operator 类似,不过最后添加到 tp_dict 中的descriptor 就不再是PyWrapperDescrObject,而分别是

PyMethodDescrObject、PyMemberDescrObject、PyGetSetDescrObject。

注:PyWrapperDescrObject 的 ob_type 是 PyWrapperDescr_Type,PyWrapperDescr_Type 对象中的 tp_call 是wrapperdescr_call,当

Python 虚拟机”调用“一个 descriptor 时,也就会调用 wrapperdescr_call 。

一般而言,对于一个 Python 中的对象obj,如果 obj.__class__ 对应的 class 对象中存在 __get__ 、__set__、__delete__ 三种操作,那么 obj 可以称

为Python 的一个 descriptor。像 PyWrapperDescr_Type 的 tp_descr_get 设置了 wrapperdescr_get,故称  PyWrapperDescrObject 为 descriptor。

如上图来说,实际上 mp_subscript 和 d_wrapped  都是函数指针变量,它们的值相等,都是 list_subscript 。

如下的例子重写了list 的 '__repr__ ' 方法,则初始化完成后的 A 如下图所示:

class A(list):

def __repr__(self): return ‘Python'

即如果没有重写则 A.tp_repr 没有定义,A.tp_dict 里面也没有定义 '__repr__',当 a = A();  a.__repr__()  找到是在mro 列表中某个基类定义的 

'__repr__' ,比如 PyList_Type 的 d_wrapped 和 tp_repr 一样,都是 list_repr。

如果重写了则创建时A.tp_repr 被赋值为 slot_to_repr。在 slot_to_repr 中,会寻找 '__repr__' 方法对应的 PyFunctionObject 对象,正好就找到在 A 定

义中重写的函数。比如 A.__dict__['__repr__'] 显示是<function  __repr__  at  ...>,而

list.__dict__['__repr__'] 显示的是 <slot wrapper  '__repr__'  of  'list' object>。

所谓的MRO 即 Method Resolve Order,也是一个class 对象的属性解析顺序(继承情况下),class A(list)  class B(list)  class C(A)  class D(C, B)

则 D 的 mro 列表 是(D, C, A, B, list),保存在 PyTypeObject.tp_mro 中,可以访问 type.__mro__ 获取。

基类中的子类列表保存在 PyTypeObject.tp_subclasses 中,比如访问 int.__subclasses__() 是 <type 'bool'>。

Python 虚拟机在 PyType_Ready 中对各种内置类型对应的 PyTypeObject 对象进行了复杂的改造动作,包括如下:

1). 设置 ob_type 信息,tp_base 信息(指向一个 PyTupleObject 对象,即基类列表);

2). 填充 tp_dict(自定义类型填充的是自定义函数和class 变量);

3). 确定 mro 列表在 tp_mro; 

4). 基于mro 列表从基类继承属性操作(自定义类型不会继承基类属性操作到tp_dict);

5). 设置基类的子类列表在 tp_subclasses 。

p286: 在创建类对应的 PyFrameObject 对象时,f_locals 被创建并指向一个PyDictObject 对象,包含class 变量和成员函数对象,在 Frame 回退

时 f_locals 先会被压入到运行时栈后被弹出给前一个 Frame 的运行时栈,并作为创建 class 对象的其中一个属性表参数,这个参数一般还包含

{'__module__' : '__main__',  '__doc__' : None}。

而在函数机制中,f_locals 被设置为NULL,函数机制中的局部变量是以一种位置参数的形式放在了运行时栈前面的那段内存中。

p292: slotoffset = base->basicsize;   basicsize 修改为 tp_basicsize 。

p283: 创建 class 对象、创建 instance 对象

PyIntObject、PyDictObject 等对象是Python 静态提供的,它们都具有相同的接口集合,当然有的对象可能不支持某个接口,但这并

不影响它们的所有元信息全存储在其类型对象 PyType_Type 中;而用户自定义的class 对象A,其接口是动态的,不可能在 

metaclass  中静态地指定,故在利用PyType_Type 对象创建 用户自定义 class 对象A 时还需要传递 (classname,  bases 基类列表,  

methods 属性表[class 变量、成员函数])。

因为PyType_Type 实现了 tp_call,故我们说 '调用'  PyType_Type 创建一个自定义class 对象,流程是 

call_function -->  do_call --> PyObject_Call --> tp_call(type_call)  --> tp_new(type_new) --> tp_alloc, 

tp_alloc 继承自<type 'object'> 的 tp_alloc 即 PyType_GenericAlloc,最终申请的内存大小是

 metatype->tp_basicsize + metatype->tp_itemsize,从 typeobject.c 中的定义可以看到实际上就是 

sizeof(PyHeapTypeObject) + sizeof(PyMemberDef)。

而 Atype->tp_basicsize = base->tp_basicsize + 8;  Atype->tp_itemsize = base->itemsize; 其中 8 是 2*sizeof(PyObject*)

    Atype->tp_dictoffset = base->tp_basicsize;  Atype->tp_weaklistoffset = base->tp_basicsize + 4;  

如果自定义 class 重写了 __new__, 将__new__ 对应的函数改造为 static method; Atype->tp_dict 设置为 methods 属性dict ; 调用 PyType_Ready 

对 class 对象进行初始化。

当通过 a=A() 这样的表达式创建instance 对象时,即 ‘调用’ class 对象将创建 instance 对象,同样沿用上面的调用路径,但 

PyType_Type.tp_call 中调用的是A.tp_new,A.tp_new 继承自object.tp_new,在PyBaseObject_Type 中定义为object_new。

在object_new 中,调用了A.tp_alloc,这个操作也是继承自object,即PyType_GenericAlloc。由上面分析可知,申请的内存大小为

A.tp_basicsize + A.tp_itemsize(0) ,申请完内存空间回到 type_call,由于创建的不是 class 对象而是 instance 对象,会尝试调用 

AType->tp_init 进行 instance 初始化。一般情况下我们会重写  class A 的 '__init__' 方法,故 tp_init 不再是在 PyType_Ready 中继承

的PyBaseObject_Type 的 object_init 操作,而是 slot_tp_init。

p296: 访问和设置 instance 对象中的属性

代码语言:python
复制
class A(object):
    name = "Python"
    def __init__(self):
        print "A::__init__"
    
    def f(self):
        print "A::f"
    
    def g(self, aValue):
        self.value = aValue
        print self.Value

a =  A()
a.f()
a.g(10)

在 Python 中,形如 x.y or x.y() 形式的表达式称为“属性引用”,属性可能是简单的数据或者成员函数。PyObject_GetAttr 中调用  Atype->tp_getattro

操作,是继承自 PyBaseObject_Type 的 PyObject_GenericGetAttr、对应的还有 PyObject_GenericSetAttr,下面是简略版函数实现。

代码语言:cpp
复制
// a = A()  a.f()                                                      // object.c 
// obj=&a  name = "f"
PyObject *
PyObject_GenericGetAttr(PyObject *obj, PyObject *name)
{
    PyTypeObject *tp = obj->ob_type;  // A
    PyObject *descr = NULL;
    PyObject *res = NULL;
    descrgetfunc f;
    Py_ssize_t dictoffset;
    PyObject **dictptr;

    ...

    if (tp->tp_dict == NULL) {
        if (PyType_Ready(tp) < 0)
            goto done;
    }

// 在 A 的 mro 列表中的基类对象的 tp_dict 中查找 'f' 对应的 descriptor 
    /* Inline _PyType_Lookup */
    {
        Py_ssize_t i, n;
        PyObject *mro, *base, *dict;

        /* Look in tp_dict of types in MRO */
        mro = tp->tp_mro; //tuple
        n = PyTuple_GET_SIZE(mro);
        for (i = 0; i < n; i++) {
            base = PyTuple_GET_ITEM(mro, i);
            dict = ((PyTypeObject *)base)->tp_dict;
            descr = PyDict_GetItem(dict, name);
            if (descr != NULL)
                break;
        }
    }

    f = NULL;
    if (descr != NULL) {
        f = descr->ob_type->tp_descr_get;  // type = descriptor.__class__,  __get__ 
        if (f != NULL && descr->ob_type->tp_descr_set != NULL) { // __set__
            res = f(descr, obj, (PyObject *)obj->ob_type);
            goto done;  // PyObject_GenericGetAttr 返回 type.__get__(descriptor, a, A) 的执行结果
        }
    }

    // 如果不是 data descriptor (type 同时定义了__set__ 和 __get__)
    // 在 a.__dict 中查找
    /* Inline _PyObject_GetDictPtr */
    dictoffset = tp->tp_dictoffset;
    if (dictoffset != 0) {
        PyObject *dict;
        if (dictoffset < 0) {
            // 说明A 继承自 str 这样的变长对象,对 dictoffset 做些调整
        }
        dictptr = (PyObject **) ((char *)obj + dictoffset);
        dict = *dictptr;
        if (dict != NULL) {
            res = PyDict_GetItem(dict, name); // PyObject_GenericGetAttr 返回 a.__dict__['f']
            if (res != NULL) {
                goto done;
            }
        }
    }

// 在 a.__dict__ 中没有找到
    if (f != NULL) {  // __get__
        res = f(descr, obj, (PyObject *)obj->ob_type);
        goto done; // PyObject_GenericGetAttr 返回 type.__get__(descriptor, a, A) 的执行结果
    }

    if (descr != NULL) {
        res = descr;
        /* descr was already increfed above */
        goto done; // PyObject_GenericGetAttr 返回 descriptor
    }

    PyErr_Format(PyExc_AttributeError,
             "'%.50s' object has no attribute '%.400s'",
             tp->tp_name, PyString_AS_STRING(name));
  done:
    return res;
}

// a.time = 2
// obj=&a, name="time", value=2
int
PyObject_GenericSetAttr(PyObject *obj, PyObject *name, PyObject *value)
{
    PyTypeObject *tp = obj->ob_type;
    PyObject *descr;
    descrsetfunc f;
    PyObject **dictptr;
    int res = -1;
    ...

    if (tp->tp_dict == NULL) {
        if (PyType_Ready(tp) < 0)
            goto done;
    }

    descr = _PyType_Lookup(tp, name); // inline function
    f = NULL;
    if (descr != NULL) {
        f = descr->ob_type->tp_descr_set;
        if (f != NULL) {
            res = f(descr, obj, value); //PyObject_GenericSetAttr 返回 type.__set__(descriptor, a, 2) 的结果
            goto done;
        }
    }

    dictptr = _PyObject_GetDictPtr(obj); // inline function 
    if (dictptr != NULL) {
        PyObject *dict = *dictptr;
        if (dict == NULL && value != NULL) {  // a.__dict__ 还没创建
            dict = PyDict_New(); // 
            if (dict == NULL)
                goto done;
            *dictptr = dict;
        }
        if (dict != NULL) {
            if (value == NULL)
                res = PyDict_DelItem(dict, name); // 删除
            else
                res = PyDict_SetItem(dict, name, value); // 设置 a.__dict__['time'] = 2
            goto done;
        }
    }

    if (f != NULL) {
        res = f(descr, obj, value); //PyObject_GenericSetAttr 返回 type.__set__(descriptor, a, 2) 的结果
        goto done;
    }

    if (descr == NULL) {
        PyErr_Format(PyExc_AttributeError,
                 "'%.100s' object has no attribute '%.200s'",
                 tp->tp_name, PyString_AS_STRING(name));
        goto done;
    }

    PyErr_Format(PyExc_AttributeError,
             "'%.50s' object attribute '%.400s' is read-only",
             tp->tp_name, PyString_AS_STRING(name));
  done:
    return res;
}

首先我们在  _PyObject_GetDictPtr 看到前面提过的 tp_dictoffset 的作用,如下图所示:

注意:获取 Atype.__dict__ 访问的是 Atype->tp_dict;  获取 a.__dict__ 访问的是 上图中的__dict__。

对于 descriptor 可以细分为如下两种:

1). data descriptor: type 中定义了 __get__  和 __set__ 的 descriptor;

2). non data descriptor: type 中只定义了 __get__ 的 descriptor;

在 Python 虚拟机访问 instance 对象时,descriptor 的一个作用是影响 Python 虚拟机对属性的选择,虽然 PyObject_GenericGetAttr 

中对属性的选择线路比较复杂,但最终效果上,可以总结如下两条规则:

1). Python 虚拟机按照 instance 属性、class 属性的顺序选择属性,即 instance 属性 优先于 class 属性;

2). 如果在 class 属性中发现同名的 data descriptor,那么该 descriptor 会优先于 instance 属性;

这两条原则在对属性进行设置时仍然会被严格遵守。

参照函数实现,看下面图示代码的演示结果,注意:2.5 源码被我加了很多调试输出而影响观察,故在 Python 3.3.2 下演示,Python 

3.x 输出稍有不同,如 <type 'str'> 变成 <class 'str'> 。

descriptor 改变返回值  ============    instance 属性优先于 non data descriptor =======     data descriptor 优先于 instance 属性

如果我们在上面 py 文件中加入 'print A.name' 这样的访问 class 对象的属性的表达式时,在 PyObject_GetAttr 中的 Typetype->tp_getattro 调用的是

type_getattro,而非 PyObject_GenericGetAttr,对应的还有 type_setattro,简略代码如下:

代码语言:cpp
复制
// B.value                        // typeobject.c
/* This is similar to PyObject_GenericGetAttr(),
   but uses _PyType_Lookup() instead of just looking in type->tp_dict. */
static PyObject *
type_getattro(PyTypeObject *type, PyObject *name)
{
    PyTypeObject *metatype = type->ob_type;  // PyType_Type
    PyObject *meta_attribute, *attribute;
    descrgetfunc meta_get;

    /* Initialize this type (we'll assume the metatype is initialized) */
    if (type->tp_dict == NULL) {
        if (PyType_Ready(type) < 0)
            return NULL;
    }

    /* No readable descriptor found yet */
    meta_get = NULL;

    /* Look for the attribute in the metatype */
    meta_attribute = _PyType_Lookup(metatype, name);  // PyType_Type

    if (meta_attribute != NULL) {
        meta_get = meta_attribute->ob_type->tp_descr_get;

        if (meta_get != NULL && PyDescr_IsData(meta_attribute)) {
            /* Data descriptors implement tp_descr_set to intercept
             * writes. Assume the attribute is not overridden in
             * type's tp_dict (and bases): call the descriptor now.
             */
            return meta_get(meta_attribute, (PyObject *)type,
                    (PyObject *)metatype);
        }
    }

    /* No data descriptor found on metatype. Look in tp_dict of this
     * type and its bases */
    attribute = _PyType_Lookup(type, name); // BType
    if (attribute != NULL) {
        /* Implement descriptor functionality, if any */
        descrgetfunc local_get = attribute->ob_type->tp_descr_get;

        if (local_get != NULL) {
            /* NULL 2nd argument indicates the descriptor was
             * found on the target object itself (or a base)  */
            return local_get(attribute, (PyObject *)NULL,
                     (PyObject *)type);
        }

        return attribute;
    }

    /* No attribute found in local __dict__ (or bases): use the
     * descriptor from the metatype, if any */
    if (meta_get != NULL) {
        PyObject *res;
        res = meta_get(meta_attribute, (PyObject *)type,
                   (PyObject *)metatype);
        return res;
    }

    /* If an ordinary attribute was found on the metatype, return it now */
    if (meta_attribute != NULL) {
        return meta_attribute;
    }

    /* Give up */
    PyErr_Format(PyExc_AttributeError,
             "type object '%.50s' has no attribute '%.400s'",
             type->tp_name, PyString_AS_STRING(name));
    return NULL;
}

// B.value = 1
static int
type_setattro(PyTypeObject *type, PyObject *name, PyObject *value)
{
    if (!(type->tp_flags & Py_TPFLAGS_HEAPTYPE)) {
        PyErr_Format(
            PyExc_TypeError,
            "can't set attributes of built-in/extension type '%s'",
            type->tp_name);
        return -1;
    }
    /* XXX Example of how I expect this to be used...
    if (update_subclasses(type, name, invalidate_cache, NULL) < 0)
        return -1;
    */
    if (PyObject_GenericSetAttr((PyObject *)type, name, value) < 0)
        return -1;
    return update_slot(type, name);
}

对比PyObject_GenericGetAttr 和 type_getattro 的 函数实现可以总结出:如果最终获得的属性是一个存在于 class 对象的 tp_dict 中 的 descriptor 

时,返回的是 descriptor.__get__ 的调用结果;若 descriptor 存在于 instance 对象的 __dict__ 中,则不会调用其 __get__ 方法,如下图所示。

到这里,我们可以看到 descriptor 对 属性的访问影响主要在两个方面:其一是对访问顺序的影响;其二是对访问结果的影响,第二

种影响正是类的成员函数调用的关键。

回到前面的 py 文件,在 class 对象 A 创建完成后,在 A.__dict__ 中保存了一个与符号'f' 对应的 PyFunctionObject 对象。表达式 a.f() 

需要先从 instance 对象 a 找到属性 f,那么 PyObject_GenericGetAttr 返回的是什么对象呢?走一遍流程,回到上面代码注释可以看

到返回的是 descriptor.__class__.__get__(descriptor, a, A) 的执行结果,其实 PyFunctionObject 对象的 class 对象是 

PyFunction_Type,与 '__get__' 对应的 tp_descr_get 在 PyType_Ready 中被设置为了 func_descr_get,这意味着 A.f 实际上是一个 

non data descriptor,在 fun_descr_get 中将 A.f 对应的 PyFunctionObject 进行了一番包装,生成一个新的 PyMethodObject 对象。

代码语言:cpp
复制
typedef struct {
    PyObject_HEAD
    PyObject *im_func;   /* The callable object implementing the method */
    PyObject *im_self;   /* The instance it is bound to, or NULL */
    PyObject *im_class;  /* The class that asked for the method */
    PyObject *im_weakreflist; /* List of weak references */
} PyMethodObject;

其中 im_func = descriptor;  im_self = a; im_class = A; 这样将一个 PyFunctionObject 与 一个 instance 对象通过 PyMethodObject 对象

结合在一起的过程称为成员函数的绑定,如下图所示。

现在我们知道PyObject_GenericGetAttr 返回一个PyMethodObject 对象(ob_type 为 PyMethod_Type),a.f() 接下去的操作就是将其压入运行时栈,

实际上 a.f 仅仅是一个带一个位置参数的函数,故需要把 self 参数解析到运行时栈,接下去的函数调用操作就跟 笔记(1)的 p226 条目类似了。

类似地,a.g(10) 可以看作带2个位置参数的函数调用,函数中 self.value = value; 会设置 a.__dict__ 。

这里再补充说明下上面提到过的PyWrapperDescrObject。 PyWrapperDescr_Type 的 tp_descr_get 设置了 wrapperdescr_get,故称  

PyWrapperDescrObject 为 descriptor。举个例子 class A(list): pass   a = A()    a.__repr__() ,走一遍上述代码流程可知PyObject_GenericGetAttr 返

回的是 PyWrapperDescr_Type.wrapperdescr_get(descriptor, a, A) 的结果,而在 wrapperdescr_get 里面会调用 PyWrapper_New 创建一

个 wrapperobject(ob_type 为 wrappertype),定义如下所示,类比PyMethodObject 可以理解其作用,即 desc =descriptor; self = a,完成了一个

PyWrapperDescrObject 对象与 一个instance 对象的绑定,a.__repr__()  最终调用到 list_repr。

代码语言:cpp
复制
/* --- Wrapper object for "slot" methods --- */
typedef struct {
    PyObject_HEAD
    PyWrapperDescrObject *descr;
    PyObject *self;
} wrapperobject;

p308: Bound Method 和 Unbound Method

当调用Bound Method 时,Python 虚拟机帮我们完成了 PyFunctionObject 对象 与 instance 对象的绑定,instance 对象 自动成为 self 

参数;而调用 Unbound Method 时,没有这个绑定导致 im_self 为 NULL, 我们需要自己传入 self 参数。

故我们可以这样调用 Unbound Method:a = A()   A.f(a, 10);  最终更改的当然是 instance 对象 a。

p311: PyObject_GenericAlloc 修改为 PyType_GenericAlloc

p310: staticmethod

首先,下图2种方式都可以实现 staticmethod:

实际上 staticmethod 是 <type 'staticmethod'>,staticmethod(g) 的过程就是从一个 class 对象(PyStaticMethod_Type) 创建 instance 对象的过程。

代码语言:cpp
复制
typedef struct {
    PyObject_HEAD
    PyObject *sm_callable;
} staticmethod;

在申请完 staticmethod 结构体决定大小的内存之后,还会调用 __init__ 进行初始化,PyStaticMethod_Type 的 tp_init 设置为 sm_init,在函数内

原来'g' 对应的那个 PyFunctionObject 被赋给了 staticmethod 对象的 sm_callable,并在 A.__dict__ 中关联起来。

因为在 PyStaticMethod_Type 中 tp_descr_get 指向了 sm_descr_get,故实际上 staticmethod 对象也是一个 descriptor,sm_descr_get 直接返回

sm_callable。所以当我们无论通过 a.g 还是 A.g 访问,由于'g' 是位于 class 对象A 的 tp_dict 中的 descriptor,所有会调用其 __get__操作,直接返回

当时最开始与'g' 对应的那个 PyFunctionObject 对象,而不是一般成员函数返回的 PyMethodObject 对象,也就没有了绑定self 参数的过程,所以 

'g' 访问不到 a = A(); a.__dict__ 里面的内容,但可以访问到 A.__dict__ 里面的内容,如上图所示。

Python 中的 static method 只是 descriptor 应用的一个例子,还有很多其他特性,如 class method, property 等都是应用 descriptor 的例子。

本文参与 腾讯云自媒体分享计划,分享自作者个人站点/博客。
原始发表:2015-04-28 ,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档