美文网首页
Python对象

Python对象

作者: 卫青臣 | 来源:发表于2022-01-15 22:05 被阅读0次

    众所周知,Python是一门面向对象的语言,在Python无论是数值、字符串、函数亦或是类型、类,都是对象。
    对象是在上分配的结构,我们定义的所有变量、函数等,都存储于堆内存,而变量名、函数名则是一个存储于中、指向堆中具体结构的引用。

    • 堆:主要负责存储Python运行时的所有对象实体(也就是Python对象的所有属性数据),例如在执行s='Hello Word'这条语句时,Python会为'Hello Word'这个字符串创建一个PyASCIIObject对象实体,并将其存储到内存中。
    • 栈:又叫数据栈或值栈,它主要负责保存对堆中Python对象的引用,例如在执行s='Hello Word'这条语句时,Python会将'Hello Word'这个字符串对象实体所处的内存地址压入,而不是将'Hello Word'这个字符串值压入栈。
      堆和栈

    要想深入学习Python,首先需要知道Python对象的定义。

    Python对象定义

    Python对象PyObejct

    我们通常说的Python都是指CPython,底层由C语言实现,源码地址:cpython [GitHub]
    Python对象的定义位于Include/object.h,是一个名为PyObject的结构体:

    /* 定义指针以支持所有活动堆对象的双链表  */
    #define _PyObject_HEAD_EXTRA            \
        struct _object *_ob_next;           \
        struct _object *_ob_prev;
    
    /* 实际上没有任何东西被声明为PyObject,但每个指向Python对象的指针都可以转换为PyObject*。这是手动建立的继承。
     * 同样,每个指向可变大小Python对象的指针也可以转换为PyVarObject*。
     */
    typedef struct _object {
        _PyObject_HEAD_EXTRA
        Py_ssize_t ob_refcnt;    // 引用计数
        PyTypeObject *ob_type;  // 类型对象指针,定义Python对象类型
    } PyObject;
    

    Python中的所有对象都继承自PyObejct,PyObject包含一个用于垃圾回收的双向链表,一个引用计数变量ob_refcnt和 一个类型对象指针ob_type

    可变对象PyVarObject

    从PyObejct的注释中,我们可以看到这样一句:每个指向可变大小Python对象的指针也可以转换为PyVarObject*(可变大小的Python对象会在下文中解释)。PyVarObejct就是在PyObject的基础上多了一个ob_size字段,用于存储元素个数:

    typedef struct {
        PyObject ob_base;
        Py_ssize_t ob_size; /* 可变部分中的元素数量 */
    } PyVarObject;
    

    Python类型对象PyTypeObject

    在PyObject结构中,还有一个类型对象指针ob_type,用于表示Python对象是什么类型,定义Python对象类型的是一个PyTypeObject接口体

    typedef struct _typeobject PyTypeObject;
    

    实际定义是位于Include/cpython/object.h_typeobject

    struct _typeobject {
        PyObject_VAR_HEAD
        const char *tp_name; /* 用于打印,格式为"<module>.<name>" */
        Py_ssize_t tp_basicsize, tp_itemsize; /* 用于分配大小 */
    
        /* 实现标准操作的方法 */
        destructor tp_dealloc;
        Py_ssize_t tp_vectorcall_offset;
        getattrfunc tp_getattr;
        setattrfunc tp_setattr;
        PyAsyncMethods *tp_as_async; /* Python 2中的tp_compare 或 Python 3中的tp_reserved */
        reprfunc tp_repr;
    
        /* 标准类的方法集 */
        PyNumberMethods *tp_as_number;  // 数值对象方法
        PySequenceMethods *tp_as_sequence;  // 序列对象方法
        PyMappingMethods *tp_as_mapping;  // 字典对象方法
    
        /* 更多标准操作(此处用于二进制兼容性) */
        hashfunc tp_hash;
        ternaryfunc tp_call;
        reprfunc tp_str;
        getattrofunc tp_getattro;
        setattrofunc tp_setattro;
    
        ...
    };
    

    在这个类型对象中,不仅包含了对象的类型,还包含了如分配内存大小、对象标准操作等信息,主要分为:

    • tp_name:类型名,Python内部使用
    • 创建该类型对象时分配的空间大小信息,即tp_basicsizetp_itemsize
    • 与该类型对象相关的操作信息
    • 一些对象属性

    以Python中的int类型为例,int类型对象的定义如下:

    PyTypeObject PyLong_Type = {
        PyVarObject_HEAD_INIT(&PyType_Type, 0)
        "int",                                      /* tp_name */
        offsetof(PyLongObject, ob_digit),           /* tp_basicsize */
        sizeof(digit),                              /* tp_itemsize */
        ...
    };
    

    Python类型的类型PyType_Type

    从PyObject的定义中我们知道,每个对象的ob_type都要指向一个具体的类型对象,比如一个数值型对象100,它的ob_type会指向int类型对象PyLong_Type

    PyTypeObject结构体第一行是一个PyObject_VAR_HEAD宏,查看宏定义可知PyTypeObject是一个变长对象

    / * PyObject VAR HEAD定义了所有大小可变的容器对象的初始段。
      * 最后声明了一个只有1个元素的数组,但是已经分配了足够的空间,使得数组实际上有足够的空间容纳ob_size的元素。
      * 注意,ob size是一个元素计数,不一定是字节计数。 
    */
    #define PyObject_VAR_HEAD      PyVarObject ob_base;
    

    也就是说,归根结底类型对象也是一个对象,也有ob_type属性,那PyLong_Typeob_type是什么呢?
    回到PyLong_Type的定义,第一行PyVarObject_HEAD_INIT(&PyType_Type, 0),查看对应的宏定义

    // Include/object.h
    #ifdef Py_TRACE_REFS
    ...
    #define _PyObject_EXTRA_INIT 0, 0,
    #else
    ...
    #  define _PyObject_EXTRA_INIT
    #endif
    
    #define PyObject_HEAD_INIT(type)        \
        { _PyObject_EXTRA_INIT              \
        1, type },
    
    #define PyVarObject_HEAD_INIT(type, size)       \
        { PyObject_HEAD_INIT(type) size },
    

    由以上关系可以知道,PyVarObject_HEAD_INIT(&PyType_Type, 0) = { { _PyObject_EXTRA_INIT 1, &PyType_Type } 0},将其代入PyObject_VAR_HEAD,得到一个变长对象:

    / * PyObject_VAR_HEAD,实际是PyVarObject */
    {
        / * ob_base,即PyObject */
        {
            _PyObject_EXTRA_INIT  // _PyObject_HEAD_EXTRA
            1;    // ob_refcnt=1
            &PyType_Type;  // ob_type=&PyType_Type
        } 
        0;  // ob_size=0
    }
    

    这样看就很明确了,PyLong_Type的类型就是PyType_Typ,同理可知,Python类型对象的类型就是PyType_Type,而PyType_Type对象的类型是它本身

    PyTypeObject PyType_Type = {
        PyVarObject_HEAD_INIT(&PyType_Type, 0)
        "type",                                     /* tp_name */
        sizeof(PyHeapTypeObject),                   /* tp_basicsize */
        sizeof(PyMemberDef),                        /* tp_itemsize */
        ...
    };
    
    对象的类型关系

    Python对象分类

    从上述内容中,我们知道了对象和对象类型的定义,那么根据定义,对象可以有以下两种分类

    按大小是否可变分类

    Python对象定义有PyObjectPyVarObject,因此,根据对象大小是否可变的区别,Python对象可以划分为可变对象(变长对象)不可变对象(定长对象)

    • 定长对象:变量发生改变时,对象大小是不变的,改变的是引用
      比如,s = 'hello ',变量s指向一个字符串(对象a),当s发生改变:s += ' world',变量s指向了一个新的字符串(对象b):
    >>> s = 'hello '
    >>> id(s)
    17123904
    >>> s += 'world'
    >>> id(s)
    8213552
    

    原本的对象a大小并没有改变,只是s引用的对象改变了。这里的对象a、对象b就是定长对象

    • 变长对象:变量发生改变时,变量的引用不变,改变的是对象的内容
      比如,l = [1, 2, 3],变量l指向一个数组(对象a),当l发生改变:l.append(4)l += [5]
    >>> l = [1, 2, 3]
    >>> id(l)
    7686744
    >>> l.append(4)
    >>> id(l)
    7686744
    >>> l += [5]
    >>> id(l)
    7686744
    

    可以看到,变量l仍然指向对象a,只是对象a的内容发生了改变,数据量变大了。这里的对象a就是变长对象

    由于存在以上特性,所以使用这两种对象还会带来一种区别:
    声明s2 = s,修改s的值:s = 'new string',s2的值不会一起改变,因为只是s指向了一个新的对象,s2指向的旧对象的值并没有发生改变
    声明l2 = l,修改l的值:l.append(6),此时l2的值会一起改变,因为l和l2指向的是同一个对象,而该对象的内容被l修改了

    此外,对于字符串对象,Python还有一套内存复用机制,如果两个字符串变量值相同,那它们将共用同一个对象:

    >>> s1 = 'Hello'
    >>> s2 = 'Hello'
    >>> id(s1)
    25512608
    >>> id(s2)
    25512608
    >>>
    

    对于数值型对象,Python会默认创建0~28 以内的整数对象,也就是0 ~ 256之间的数值对象是共用的:

    >>> n1=0
    >>> n2=0
    >>> id(n1)
    2038522960
    >>> id(n2)
    2038522960
    >>>
    >>> n1 = 256
    >>> n2 = 256
    >>> id(n1)
    2038527056
    >>> id(n2)
    2038527056
    >>>
    >>> n1 = 257
    >>> n2 = 257
    >>> id(n1)
    19586496
    >>> id(n2)
    25412160
    

    按数据类型分类

    按照Python数据类型,对象可分为以下几类:

    • Fundamental 对象:类型对象
    • Numberic 对象:数值对象
    • Sequence 对象:容纳其他对象的序列集合对象
    • Mapping 对象:键值对关联的字典对象
    • Internal 对象:Python虚拟机内部使用的对象
    对象分类

    对象的创建

    Python创建对象有两种方式,泛型API和和类型相关的API

    泛型API(Abstract Object Layer)

    这类API通常以PyObject_xxx的形式命名,可以应用在任意Python对象上,如:

    • PyObject_New(type, typeobj) 为给定类型的对象分配内存,并初始化该对象的一部分
    • PyObject_NewVar(type, typeobj, n) 为可变大小的对象分配内存并分配n个元素的空间
    • PyObject_Init(op, typeobj) 不分配内存,接受指向新对象(由任意分配器分配)的指针,而不是“类型”形参,并初始化其对象头字段
    • PyObject_InitVar(op, typeobj, n) 同上

    使用PyObjecg_New创建一个数值型对象:

    PyObject* longObj = PyObject_New(PyObject, &PyLong_Type);
    

    与类型相关API(Concrete Object Layer)

    这类API通常只能作用于一种类型的对象上,如:

    • PyObject * PyLong_FromLong(long ival) 通过长整型创建新对象
    • PyObject * PyLong_FromUnsignedLong(unsigned long ival) 通过无符号长整型创建新对象
    • ...

    使用PyLong_FromLong创建一个数值型对象:

    PyObject* longObj = PyLong_FromLong(10)
    

    对象的多态性

    在我们使用Python声明变量的时候,并不需要为变量指派类型,在给变量赋值的时候,可以赋值任意类型数据,如:

    a = 1
    a = 'string'
    a = [1, 2, 3]
    

    从Python对象的定义我们已经可以知晓造成这个特点的原因了,Python创建对象时,会分配内存进行初始化,然后Python内部通过PyObject*变量来维护这个对象,所以在Python内部各函数直接传递的都是一种泛型指针PyObject*,这个指针所指向的对象类型是不固定的,只能通过所指对象的ob_type属性动态进行判断,而Python正是通过ob_type实现了多态机制

    引用计数

    Python在管理维护对象时,通过引用计数来判断内存中的对象是否需要被销毁,Python中所有事物都是对象,所有对象都有引用计数ob_refcnt
    当一个对象的引用计数减少到0之后,Python将会释放该对象所占用的内存和系统资源。
    但这并不意味着最终一定会释放内存空间,因为频繁申请释放内存会大大降低Python的执行效率,因此Python中采用了内存对象池的技术,是的对象释放的空间会还给内存池,而不是直接释放,后续需要申请空间时,优先从内存对象池中获取。

    相关文章

      网友评论

          本文标题:Python对象

          本文链接:https://www.haomeiwen.com/subject/nflocrtx.html