对象模型

2025-04-25
7240字

Python是一个支持多范式的编程语言,你可以使用Python编写面向过程的代码,也可以编写面向对象的代码,甚至函数式编程也不在话下。但是在内部机制上,Python是彻彻底底基于面向对象的思想实现的,无论是列表、元组、字典、函数,还是你写的代码,都以对象的形式存在。

在学习Python对象模型的时候,你可以暂时忘记所有关于面向对象的宏观概念,只需要记住一句话:对象就是对内存数据的抽象。换句话说,Python中的一个对象一定对应若干个内存块。

内存布局

Python对象对应的内存块是靠指针连结在一起的。例如一个保存了1, 2, 3三个整数的列表对象,其内存布局如下图。

这个例子很好理解。结构体变量用于保存列表对象的基本信息,然后开辟一个动态数组,动态数组内的元素都是指针,用于指向其它的对象。这样,列表就可以“保存”任意多的任意对象了。

列表对象就是对黄色框线内的两个内存块的抽象。对于动态数组的指针指向的内存块,逻辑上不应该属于列表对象。图中三个浅蓝色的123内存块,是3个独立的整数对象。

在利用sys模块的getsizeof()函数查看对象占用的内存大小时,也只计量结构体变量和动态数组占用的空间。

同时由于列表是变长的,为了避免每次长度改变都要重新分配内存,在为动态数组分配内存时通常会多分配一些余量,删除元素时也不是立马缩容,因此动态数组尾部通常都有一些空指针,它们对列表对象的行为没有影响,但是会被getsizeof()函数计量。

参考下面的示例代码:

In [1]: import sys

In [2]: sys.getsizeof([1, 2, 3])
Out[2]: 88

In [3]: sys.getsizeof([1, 2, 3, 4])
Out[3]: 88

In [4]: sys.getsizeof(list(range(8)))
Out[4]: 120

In [5]: sum(sys.getsizeof(x) for x in range(8))
Out[5]: 224

为什么内存块会有结构体变量动态数组两种形式呢?

以列表对象举例来说,一个列表需要保存的基本信息是固定的,也就是所需的内存大小以及每个字段的含义都是确定的,所以事先定义一个结构体即可。但是对于“保存”元素的数组,没有办法事先确定这个列表会保存多少个元素,自然也只能动态地进行分配。

[!TIP]

理解了列表的内存布局之后,关于列表和元组的区别就很好理解了。元组和列表的内存布局基本一致,但是动态数组的内容不可以修改,也就是这些指针不能再指向其它对象了,那么从Python层面来看就是元组的内容不可修改。

但是正如前面提到的,这些指针指向的对象是独立的,元组没有办法干涉它们的行为,如果某个指针指向的是一个列表,那么这个列表是可以随意修改的。

总结一下:内存块内容可变的对象是可变对象,内存块内容不可变的对象是不可变对象,但是需要注意这些对象对应的内存块的范围,不属于它的内存块它无法约束其可变性。

使用一个对象

使用一个对象就是使用这若干个内存块,那么首要任务就是在茫茫内存中定位这些内存块。Python有两种方式记录一个对象的地址(即核心内存块的地址,例如列表的结构体变量的地址,有了这个结构体变量,找到动态数组也就是顺藤摸瓜的事了)。

一种是前面提到的指针,另一种也是指针。

第一种应该十分好理解:

In [1]: ls = [1, 2, 3]

In [2]: ls[0] + ls[1]
Out[2]: 3

在使用ls[0]的时候,Python在背后默默做了这些:找到ls动态数组中的第0个指针→找到这个指针指向的目标对象→使用目标对象。

第二种的官方名称叫做名字绑定。原理是这样的:

图中两个指针是绑定在一起的,其中一个指针指向一个字符串对象'a',另外一个指针指向了一个整数对象1,那么字符串'a'和整数1就绑定在一起了。

这个a通常还有一个形象但不确切的名字——变量。

而且这种绑定关系会让我们想起一种熟悉的数据结构——字典。没错,大多数时候名字绑定都是用字典实现的。

In [1]: import sys

In [2]: a = 1

In [3]: d = sys.modules['__main__'].__dict__

In [4]: type(d)
Out[4]: dict

In [5]: 'a' in d
Out[5]: True

In [6]: d['a']
Out[6]: 1

如果你对上面的代码不甚理解,可以暂时略过。这个代码块只是为了佐证上述内容。

常用的名字绑定操作有以下几个:

# 赋值
a = 1

# 模块导入
import xxx
from xx import xxx

# as关键字
with xxx as xx
import xxx as xx
except xxx as xx

# 类定义和函数定义
class xx  # xx会和一个类对象绑定
def xx  # xx会和一个函数对象绑定

# for循环、调用传参
for x in ls  # x会依次和列表元素进行绑定
func(x)  # 在函数内部,被传入的实参和名字'x'绑定

[!IMPORTANT]

Python这种机制存在一些潜在的问题,不同的名字绑定到同一个对象,或者不同的指针指向同一个对象时,如果这个对象是可变的,那么改变这个对象时,这些名字就会“一荣俱荣,一损俱损”(因为它们本来就是同一个)。

In [1]: a = [1, 2, 3]

In [2]: b = a

In [3]: ls = [a, b]

In [4]: tp = (a, b)

In [5]: a[1] = 0x3f

In [6]: print(a, b, ls, tp)
[1, 63, 3] [1, 63, 3] [[1, 63, 3], [1, 63, 3]] ([1, 63, 3], [1, 63, 3])

In [7]: a is b and b is ls[0] and ls[1] is tp[0]
Out[7]: True

对象的特殊方法

Python的一大特点是其运算符(+ - * /等)和内置函数(abs() len()等)是通过调用对象的特殊方法实现的,这种特性可以方便的实现运算符重载。

In [1]: from __future__ import annotations
   ...: import math

In [2]: class Vector:
   ...:
   ...:     def __init__(self, x: int, y: int) -> None:
   ...:         self._x = x
   ...:         self._y = y
   ...:
   ...:     def __repr__(self) -> str:
   ...:         return f"Vector({self._x}, {self._y})"
   ...:
   ...:     def __add__(self, other: Vector) -> Vector:
   ...:         if not isinstance(other, Vector):
   ...:             raise TypeError
   ...:         return Vector(self._x + other._x, self._y + other._y)
   ...:
   ...:     def __abs__(self) -> float:
   ...:         return math.sqrt(self._x * self._x + self._y * self._y)
   ...:

In [3]: a = Vector(1, 2)

In [4]: b = Vector(3, 4)

In [5]: a + b
Out[5]: Vector(4, 6)

In [6]: print(abs(a), abs(b), abs(a + b), sep='\n')
2.23606797749979
5.0
7.211102550927978

In [7]: a + 1
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
Cell In[7], line 1
----> 1 a + 1

Cell In[2], line 12, in Vector.__add__(self, other)
     10 def __add__(self, other: Vector) -> Vector:
     11     if not isinstance(other, Vector):
---> 12         raise TypeError
     13     return Vector(self._x + other._x, self._y + other._y)

TypeError:

Python支持的特殊方法非常多,足够再开一个章节了,这里先简单提一下特殊方法的概念和作用,然后思考另外一个重要的问题:我可能创建无数个Vector对象,每个对象都支持调用.__add__()方法,那每个对象都要复制一份这个方法吗(方法的本质是函数,函数也是一种对象)?

显然是不需要的。不同Vector对象调用.__add__()方法时的行为模式都是一致的,只是传入的参数不同,只需要把这个方法保存一份,每个对象都去调用这个方法就好了。

那么存在哪呢?

存在它的类型对象里。

类型对象

我们换一个简单的类做示例:

In [1]: class Dog:
   ...:
   ...:     def __init__(self, name):
   ...:         self.name = name
   ...:
   ...:     def hello(self):
   ...:         print(f"Hello, I'm {self.name}")

这段代码执行时不会有任何输出,好像什么都没发生,但实际上Python已经在背后创建了一个类型对象,然后把这个类型对象和名字Dog绑定在了一起。

In [2]: id(Dog)
Out[2]: 1879669847104

In [3]: import sys

In [4]: 'Dog' in sys.modules['__main__'].__dict__
Out[4]: True

In [5]: id(sys.modules['__main__'].__dict__['Dog'])
Out[5]: 1879669847104

上面这段代码证明了两件事:确实有一个对象真实存在于内存当中,它的地址是1879669847104;这个对象和名字Dog绑定在了一起,绑定关系记录在一个字典中。

[!TIP]

Python标准中规定,每一个Python对象都有一个标识符(identifier),用来唯一的标识一个对象。内置函数id()就是返回一个对象的唯一标识符。语言标准没有规定这个标识符到底是什么,但是在具体实现中,一般使用对象的内存地址作为标识符,因为这是天然唯一的。

也可以用以下代码佐证构造类型对象的代码确实是被执行了:

In [1]: class Dog:
   ...:
   ...:     print('Start building Dog-type')
   ...:
   ...:     def __init__(self, name):
   ...:         self.name = name
   ...:
   ...:     def hello(self):
   ...:         print(f"Hello, I'm {self.name}")
   ...:
   ...:     print('Dog-type has been built')
   ...:
Start building Dog-type
Dog-type has been built

也可以更直接一点,把这段代码编译成字节码:

In [1]: from dis import dis

In [2]: dis("""\
   ...: class Dog:
   ...:     def __init__(self, name):
   ...:         self.name = name
   ...:
   ...:     def hello(self):
   ...:         print(f"Hello, I'm {self.name}")
   ...: """)
  0           RESUME                   0

  1           LOAD_BUILD_CLASS
              PUSH_NULL
              LOAD_CONST               0 (<code object Dog at 0x000001850BD357A0, file "<dis>", line 1>)
              MAKE_FUNCTION
              LOAD_CONST               1 ('Dog')
              CALL                     2
              STORE_NAME               0 (Dog)
              RETURN_CONST             2 (None)

这里只截取了第一段字节码,这些字节码的含义大概如下:

  • RESUME 0:用于程序控制,可以忽略。

  • LOAD_BUILD_CLASS :把类型构造函数(__build_class__)加载到栈中。

  • PUSH_NULL :与RESUME配合使用。

  • LOAD_CONST 0:加载第0个常量,这个例子中这个常量是一个代码对象

  • MAKE_FUNCTIONLOAD_CONSTCALL:构造一个类型对象的具体操作。

  • STORE_NAME 0:把这个类型对象和名字0(这个例子中是Dog)绑定。

  • RETURN_CONST 2:将第2个常量(这个例子中是None)作为返回值返回。

在明确有一个实实在在的类型对象之后,就可以着手研究它和实例对象(实例对象就是用某个类型对象实例化出来的对象,例如下面的peter)的关系了:

In [1]: class Dog:
   ...:
   ...:     def __init__(self, name):
   ...:         self.name = name
   ...:
   ...:     def hello(self):
   ...:         print(f"Hello, I'm {self.name}")

In [2]: peter = Dog('peter')

In [3]: type(peter)
Out[3]: __main__.Dog

In [4]: type(peter) is Dog
Out[4]: True

In [5]: peter.__class__ is Dog  # 绝大部分情况下,peter.__class__和type(peter)是一致的,都是获取peter的类型对象
Out[5]: True

嗯,它们的关系就是:Dogpeter的类型,peterDog一个实例

[!IMPORTANT]

我们可以这么理解类型对象和实例对象:类型对象是一种特殊的对象,这种对象可以用来创造新的实例对象,这些实例对象可以有自己的属性,但是它们的行为模式(方法)是固定的。在逻辑上,类型对象类似“人”这个抽象概念,实例对象类似“一个具体的人”。但是计算机中不存在“概念”,只有实实在在的内存数据,所以类型对象也是实实在在存在于内存当中的。

[!TIP]

我们使用Dog('peter')新建一个实例时,是像函数一样调用了Dog,这种可以直接调用的对象被称为可调用对象。最常见的可调用对象就是类型对象和函数对象。

下面是“对象的方法保存在它的类型对象中”的证据:

In [6]: 'hello' in Dog.__dict__
Out[6]: True

In [7]: hello_method = Dog.__dict__['hello']

In [8]: type(hello_method)
Out[8]: function

In [9]: hello_method is Dog.hello
Out[9]: True

In [10]: ? hello_method
Signature:  hello_method(self)
Docstring: <no docstring>
File:      
Type:      function

可以看到.hello本质上就是一个函数,保存在Dog这个内存对象中。我们可以直接调用这个函数,达到和peter.hello()同样的效果:

In [11]: Dog.hello(peter)
Hello, I'm peter

In [12]: hello_method(peter)
Hello, I'm peter

In [13]: peter.hello()
Hello, I'm peter

这个方法(函数)是需要接收一个参数的,参数需要是一个实例对象。此举不难理解,因为方法保存在类型对象中,必须要明确是哪个实例对象调用的这个方法,从而针对不同的实例对象输出不同的结果。

那为什么peter.hello()就可以直接调用而不用传递self参数呢?这是因为Python将这个方法和具体实例进行了绑定,生成了一个新的bound method,隐式地帮你传递了self参数。

In [14]: peter.hello
Out[14]: <bound method Dog.hello of <__main__.Dog object at 0x000001852C1C0D70>>

In [15]: peter.hello is hello_method
Out[15]: False

peter.helloDog.hello不是同一个东西,所以才会表现的一个不需要传参一个需要传参。

当然了,peter.helloDog.hello经过处理得到的,所以“实例对象的方法保存在它的类型对象中”这个说法是成立的。

属性查找

从前面peter.helloDog.hello得到不同的结果可以看出来,Python的属性查找(也就是点号运算符)背后别有洞天。

[!TIP]

除了点号运算符之外,getattr(<instance>, <attr_name>)也可以获取对象的属性,两者区别不大。

在使用<instance>.<attr>获取属性时,Python首先获取<instance>的类型对象<type>,然后执行<type>.__getattribute__(<instance>, <attr_name>)进行属性查找(<attr_name><attr>的名字,也就是一个字符串),这个函数的执行过程大概如下:

  1. 检查<instance>是否存在属性字典(也就是Python层面的<instance>.__dict__)。

    • 不存在属性字典存在,执行2;

    • 存在属性字典:在属性字典中查找<attr_name>

      • <attr_name>在字典中,直接返回<attr_name>绑定的对象。
      • <attr_name>不在字典中,执行2。
  2. 获取其类型对象<type>的MRO(Method Resolution Order)<type>.__mro__<type>.__mro__是一个元组,元组第一个元素是<type>自身,后面的元素是<type>的父类(包括直接父类和间接父类)。然后对这个元组进行迭代,对元组中的每个类型对象,都判断<attr_name>是否在其属性字典中(类型对象一定有属性字典):

    • <attr_name>在字典中:

      判断<attr_name>绑定的对象<attr_obj>是不是属性描述符

      • 是属性描述符,返回<attr_obj>.__get__(<instance>, <type>)的结果。
      • 不是属性描述符,直接返回<attr_obj>
    • <attr_name>不在字典中,继续迭代。

    如果迭代完成之后依然没有查找到属性,执行3。

  3. 尝试调用<type>.__getattr__(<instance>, <attr_name>)(注意这里是直接用类型的方法调用的,如果用<instance>.__getattr__,就会再次触发1-2从而陷入死循环):

    • 有这个方法,则返回调用的结果。

    • 没有这个方法,抛出AttributeError

Dog.hello带入上述过程,Dog的类型对象会检查Dog的属性字典,而字符串'hello'就在Dog的属性字典里,所以直接返回这个方法。而Dog的类型对象,就是Python对象系统的两大基石之一——type。后面再说。

peter.hello就多了一个步骤,'hello'不在peter的属性字典中,所以查找其类型对象Dog的属性字典,'hello'在这个字典中,但是它绑定的对象是一个属性描述符,所以返回Dog.__dict__['hello'].__get__(peter, Dog)的结果,也就是前面提到的bound method。

[!TIP]

简单来讲,属性描述符就是定义了__get__的对象。也就是只要这个对象有__get__,那它就是一个属性描述符,如果它存在于类型对象的属性字典中,就会对该类型的实例对象的属性查找产生影响。

如果一个属性描述符只定义了__get__,那么它就是一个非数据描述符;如果同时定义了__set__,那么它就是一个数据描述符。不管是数据描述符还是非数据描述符,都可以选择性地定义__del__

通过定义属性描述符,你可以灵活地控制实例对象的属性查找、属性设置和属性删除行为。例如@property装饰器就是通过属性描述符的特性实现的。

MRO是为了解决多继承引起的继承优先级问题。Python使用C3算法确定父类的查找顺序,顺序在前的会先被查找,也就拥有较高的优先级。

以常见的Enum为例,它的MRO是这样的:

In [1]: from enum import Enum

In [2]: class ColorEnum(str, Enum):
   ...:     RED = '#f00'
   ...:     GREEN = '#0f0'
   ...:     BLUE = '#00f'
   ...:

In [3]: ColorEnum.__mro__
Out[3]: (<enum 'ColorEnum'>, str, <enum 'Enum'>, object)

对于同一级的父类,写在前面的父类优先级更高。对于不同级的父类,优先级类似于反向的嫡长子继承制。

如果需要了解MRO的细节,可以参考源码或者C3算法的介绍。

在MRO的结果中,出现了Python对象系统的另一个基石——objectobject是一个内建的类型对象,所有的类型对象都直接或间接地继承自它(除了它自己),当你自定义的类没有指定父类时,那么它就隐式地继承了objctobject提供了许多基础方法,以便于用户在编写自定义类时可以通过继承直接使用这些方法而不用再手动实现,例如前面提到的__getattribute__

In [1]: class Dog:
   ...:     def __init__(self, name):
   ...:         self.name = name
   ...:
   ...:     def hello(self):
   ...:         print(f"Hello, I'm {self.name}")
   ...:

In [2]: Dog.__bases__  # 返回Dog的直接父类,由于允许多继承,所以是一个元组
Out[2]: (object,)

In [3]: Dog.__getattribute__ is object.__getattribute__
Out[3]: True

[!NOTE]

由于继承实质上是依赖__getattribute__实现的,所以类型对象的__getattribute__不能是继承的,在实际实现时,是直接将Dog这个类型对象的tp_getattro指针指向了objecttp_getattro指针指向的函数,所以Dog.__getattribute__object.__getattribute__是一样的。但是从逻辑上来说,Dog继承了object__getattribute__是成立的。

前面我们获取的MRO是类型对象的__mro__,那实例对象的__mro__是什么呢?由于MRO是确定父类的优先级的,实例对象不是类型,也就不存在父类,那么实例对象不应该有__mro__

In [1]: ls = [1, 2, 3]

In [2]: ls.__mro__
---------------------------------------------------------------------------
AttributeError                            Traceback (most recent call last)
Cell In[2], line 1
----> 1 ls.__mro__

AttributeError: 'list' object has no attribute '__mro__'

In [3]: list.__mro__
Out[3]: (list, object)

这个结果可以用属性查找的过程解释:

ls.__mro__的查找过程是:ls的属性字典(其实它没有属性字典)→list的属性字典→object的属性字典。listobject的属性字典都没有__mro__,而且ls也没有__getattr__方法,所以就抛出了AttributeError

list.__mro__的查找过程是:list的属性字典→type的属性字典(list的类型对象也是type)。type的属性字典中有__mro__,所以就返回了结果。

In [1]: ls = [1, 2, 3]

In [2]: ls.__dict__
---------------------------------------------------------------------------
AttributeError                            Traceback (most recent call last)
Cell In[2], line 1
----> 1 ls.__dict__

AttributeError: 'list' object has no attribute '__dict__'

In [3]: '__mro__' in list.__dict__ or '__mro__' in object.__dict__
Out[3]: False

In [4]: '__mro__' in type.__dict__
Out[4]: True

In [5]: hasattr(type.__dict__['__mro__'], '__get__')
Out[5]: True

Cell 5显示type__mro__是一个属性描述符,这也是意料中的事,因为listDog的类型对象都是type,所以list.__mro__Dog.__mro__的查找结果都是type属性字典中的这个__mro__,但是它们的最终结果是不同的,唯一的解释就是__mro__是一个属性描述符,它的__get__方法做了一些额外的事情。

Cell不能用hasattr(type.__mro__, '__get__')代替,因为type.__mro__会触发属性查找,最终返回一个元组。按照前述属性查找机制,查找type的属性应该先获取type的类型对象,那type的类型对象是谁呢?嗯,是它自己:

In [1]: type.__class__ is type
Out[1]: True

有必要理一下objecttype在对象系统中的作用了。

object与type

继承,描述的是类与类之间的关系,object是Python中除了它自己之外所有类的父类(直接或间接的),是继承关系的顶层,它自己没有父类:

In [1]: list.__bases__
Out[1]: (object,)

In [2]: type.__bases__
Out[2]: (object,)

In [3]: object.__bases__
Out[3]: ()

实例化,描述的是类型与实例的关系,type是实例化关系的底层,并且type自己也是自己的实例:

In [1]: ls = [1, 2, 3]

In [2]: ls.__class__ is list
Out[2]: True

In [3]: list.__class__ is type
Out[3]: True

In [4]: object.__class__ is type
Out[4]: True

In [5]: type.__class__ is type
Out[5]: True

捋一下:

listDog实例化的结果是一个实例对象,type是一个特殊的类型对象,它可以实例化出其它类型对象