Files

35 KiB
Raw Permalink Blame History

sources, brief
sources brief
summaries/07_Objects.md
summaries/05_Object_model__00_Overview.md
summaries/04_Classes_objects__00_Overview.md
summaries/02_Working_with_data__00_Overview.md
summaries/Contents.md
summaries/05_Decorated_methods.md
summaries/01_Iteration_protocol.md
summaries/02_Classes_encapsulation.md
summaries/01_Dicts_revisited.md
summaries/03_Special_methods.md
summaries/01_Class.md
summaries/02_More_functions.md
summaries/01_Datatypes.md
summaries/00_Overview.md
Python 对象模型解释名称、引用、类型、属性、方法和协议如何共同构成运行时行为。

Python 对象模型

本页边界

本页是运行时机制总览,重点解释对象、名称、属性、方法、类型和协议如何协作。若只想理解赋值与变量名,先读 concepts/变量绑定;若关注函数调用,读 concepts/Python-参数传递;若关注对象能否原地修改,读 concepts/Python-可变对象 和 concepts/Python-不可变对象。

Python 对象模型是理解 Python 如何表示、引用、访问和操作数据的底层基础。它说明了 Python 程序中的值并不是孤立的原始数据,而是以对象形式存在;变量不是装值的盒子,而是绑定到对象的名称;容器保存对象引用;函数、类型、模块、异常和类也都是可以传递和存储的一等对象。

这一概念贯穿多个主题:summaries/00_Overview 给出对象、数据和类系统的总体路线;summaries/01_Datatypes、summaries/02_More_functions、summaries/01_Class 和 summaries/03_Special_methods 分别从数据类型、函数、类和特殊方法角度展开;summaries/07_Objects 系统说明赋值、引用、身份、拷贝、类型检查和一等对象;summaries/04_Classes_objects__00_Overview 引入类与对象的基本用法;summaries/05_Object_model__00_Overview 解释 Python 对象内部工作方式;summaries/01_Dicts_revisited 揭示模块、实例、类、属性查找、继承和方法调用在很大程度上都建立在字典式命名空间之上;summaries/02_Classes_encapsulation 说明 Python 的对象虽然开放,但可以通过命名约定、property、受管理属性和 __slots__ 组织封装边界;summaries/01_Iteration_protocol 展示对象如何通过 __iter__()、__next__()、StopIteration 等协议参与 for 循环、next()、容器操作和 Pythonic 接口设计。

理解 Python 对象模型,可以把 concepts/变量与数据类型、concepts/Python-容器、concepts/变量绑定、concepts/Python-参数传递、Python类、类与实例、实例属性、属性查找、concepts/绑定方法、Python特殊方法、Python封装、继承与MRO、Python迭代协议、Python容器协议、concepts/动态属性访问 和 Pythonic设计 统一到同一个视角下。

核心含义

在 Python 中,几乎一切都是对象,包括:

  • 数字、字符串、布尔值等基础数据。
  • 列表、元组、集合、字典等 Python容器。
  • 函数、模块、类和实例。
  • 方法、绑定方法、类型对象。
  • 异常类和异常实例。
  • 迭代器、生成器、文件对象等更高级结构。

对象通常具有三个重要方面:

  1. 身份 identity:对象在运行时的唯一标识,表示这个对象是谁。可用 id(obj) 查看。
  2. 类型 type:决定对象支持哪些操作,例如列表可原地追加元素,字符串不能原地修改,实例可通过类定义的方法和特殊方法参与 Python 语法。
  3. 值 value:对象表示的数据内容。

因此,Python 程序运行时的许多现象都可以理解为:名称绑定到对象,对象根据自身类型支持某些操作,操作可能创建新对象,也可能修改已有对象。对于类实例来说,对象还拥有属性,类则定义对象可用的方法、共享数据和行为协议。

变量不是盒子,而是名称绑定

summaries/07_Objects 最重要的提醒是:赋值操作永远不会自动复制被赋的对象。赋值只是复制引用,也就是让另一个名称指向同一个对象。

a = [1, 2, 3]
b = a
b.append(4)
print(a)  # [1, 2, 3, 4]

这里 a 和 b 都引用同一个列表对象。通过 b 修改列表时,a 看到的内容也会改变。类似地,列表元素赋值、append()、字典键赋值等操作也都是把对象引用存入容器,而不是自动复制对象。

关键原则是:变量是名字,不是内存位置。

重新赋值不会覆盖旧对象

重新赋值不会把旧对象所在内存改写成新对象,而是让名称绑定到另一个对象。

a = [1, 2, 3]
b = a
a = [4, 5, 6]

print(a)  # [4, 5, 6]
print(b)  # [1, 2, 3]

第二次赋值只改变名称 a 的绑定,不会修改 b 仍然引用的原列表。如果原对象没有其他引用,之后才可能被垃圾回收。

这一点对理解修改对象和重新绑定名称的区别非常重要,也直接影响函数参数传递、容器共享和拷贝语义。

对象身份、is 与 ==

is 用于判断两个名称是否引用同一个对象。

a = [1, 2, 3]
b = a
print(a is b)  # True
print(id(a) == id(b))  # True

但 is 比较的是对象身份,不是内容相等。多数情况下,检查两个对象是否相等应使用 ==。

a = [1, 2, 3]
b = a
c = [1, 2, 3]

print(a is b)  # True
print(a is c)  # False
print(a == c)  # True

这里 a 和 c 是两个不同列表对象,但内容相等。理解 is 与 == 的区别,是理解对象身份和值相等的基础。

可变对象与不可变对象

Python 对象模型中的一个关键区别是对象是否可变。

可变对象创建后,其内部内容可以被修改。常见例子包括 list、dict 和 set。

items = [1, 2]
items.append(3)

这里列表对象本身被修改,名称 items 仍然绑定到同一个列表对象。

不可变对象创建后,其值不能原地改变。常见例子包括 int、float、str 和通常意义上的 tuple。

s = 'hello'
s = s.upper()

这里不是原字符串被修改,而是 s.upper() 创建了一个新字符串对象,并让 s 重新绑定到新对象。不可变基础类型也减少了共享引用带来的风险:多个名称共享同一个整数或字符串时,不会因为某处原地修改而污染其他代码。

相关主题包括 可变对象、变量绑定、Python数据类型 和 可变性与引用。

修改对象 vs 重新绑定名称

对象模型中最容易混淆的一点,是修改对象和重新绑定名称看起来都可能导致变量对应的结果变化,但机制完全不同。

def foo(items):
    items.append(42)

a = [1, 2, 3]
foo(a)
print(a)  # [1, 2, 3, 42]

这里 items 和 a 在函数调用期间引用同一个列表对象。append() 修改的是共享列表对象本身,所以函数外部的 a 也能看到变化。

def bar(items):
    items = [4, 5, 6]

b = [1, 2, 3]
bar(b)
print(b)  # [1, 2, 3]

这里 items = [4, 5, 6] 只是让函数内部局部变量 items 绑定到一个新列表对象,并没有修改 b 原本引用的列表。

这一区别是 Python参数传递、可变对象、变量绑定和函数副作用设计的核心。

函数参数传递与对象引用

Python 调用函数时,参数变量是绑定到传入对象的新名称。传入对象不会被自动复制。

因此:

  • 如果传入的是可变对象,函数可能原地修改它,从而影响调用者。
  • 如果函数只是给参数名重新赋值,则只改变局部名称绑定,不会影响调用者变量。
  • 如果需要避免副作用,调用者或函数内部需要显式复制对象。

这也是为什么在设计函数接口时,需要清楚说明函数是否会修改传入对象。相关内容可参见 Python函数设计。

浅拷贝与深拷贝

由于赋值不会复制对象,在需要隔离数据时必须显式拷贝。summaries/07_Objects 区分了浅拷贝和深拷贝。

浅拷贝会创建新的外层容器,但其中的元素引用仍然共享。

a = [2, 3, [100, 101], 4]
b = list(a)

print(a is b)  # False
print(a[2] is b[2])  # True

a[2].append(102)
print(b[2])  # [100, 101, 102]

这里 a 和 b 是不同的外层列表,但内部列表是同一个对象。因此修改内部列表会同时反映在两个外层容器中。

深拷贝会递归复制对象及其包含的对象:

import copy

a = [2, 3, [100, 101], 4]
b = copy.deepcopy(a)

a[2].append(102)
print(b[2])  # [100, 101]
print(a[2] is b[2])  # False

深拷贝适合需要完全隔离嵌套可变结构的场景,但也可能带来额外开销和复杂性。相关主题包括 拷贝语义、可变对象 和 Python容器。

名字、值与类型

变量名本身没有类型,类型属于对象值。

a = 42
b = 'Hello World'

print(type(a))  # int
print(type(b))  # str

type() 可以查看对象类型。类型名通常也可以作为构造或转换函数使用,例如 str(x)、int(x)、float(x)。

如果需要判断对象是否属于某种类型,可使用 isinstance():

if isinstance(a, list):
    print('a is a list')

if isinstance(a, (list, tuple)):
    print('a is a list or tuple')

不过不应过度使用类型检查。过多类型判断会增加代码复杂度,并削弱 Python 的协议式设计风格。通常只有在防止常见误用、给出清晰错误信息或保护对象有效状态时,才值得显式检查类型。

一切皆对象与一等对象

Python 中函数、模块、异常、类和类型都可以像普通数据一样被命名、传递、放入容器、作为参数使用或从函数返回。这就是一等对象特性。

import math

items = [abs, math, ValueError]

print(items[0](-45))      # abs(-45)
print(items[1].sqrt(2))   # math.sqrt(2)

try:
    int('not a number')
except items[2]:
    print('Failed!')

这里列表中同时保存了函数、模块和异常类。能这样做是因为它们都是对象。

这种能力很强大,但也需要节制。对象模型允许把函数和类型当作数据使用,并不意味着任何时候都应该写出难以理解的动态代码。相关概念包括 一等对象、高阶函数 和 可调用对象。

一等对象在数据转换中的应用

summaries/07_Objects 的练习展示了一个实用模式:把类型转换函数放入列表,再与 CSV 字段配对。

types = [str, int, float]
row = ['AA', '100', '32.20']

converted = [func(val) for func, val in zip(types, row)]
print(converted)  # ['AA', 100, 32.2]

这里 str、int、float 本身就是对象,也是可调用对象。通过 zip(types, row),每个转换函数都与对应字段配对,func(val) 则执行实际转换。

进一步结合表头可以构造字典记录:

headers = ['name', 'shares', 'price']
record = dict(zip(headers, converted))

也可以一步完成转换和建表:

record = {name: func(val) for name, func, val in zip(headers, types, row)}

这说明对象模型不仅是理论概念,也能直接支持通用数据清洗、CSV解析、类型转换、concepts/列表推导式 和字典推导式。

容器保存的是对象引用

Python容器 的行为也建立在对象模型之上。列表、元组、字典、集合等容器保存的是对对象的引用,而不是自动深度复制所有值。

这解释了多个常见现象:

  • 一个列表可以同时保存数字、字符串、函数、模块、异常类等不同对象。
  • 一个嵌套列表被浅拷贝后,内部列表仍可能共享。
  • 字典的键和值都是对象引用。
  • dict(zip(headers, row)) 会创建一个新字典对象,把表头对象和值对象关联起来。
  • 列表推导式会创建新的列表对象,并把表达式结果逐个放入其中。

相关概念包括 Python容器、序列、concepts/列表推导式、CSV解析、类型转换 和 一等对象。

局部变量、全局变量与模块字典

Python 对象模型不仅影响数据结构,也影响函数中的变量作用域。

在函数外部赋值的名称通常是全局变量;在函数内部赋值的名称通常是局部变量。模块中的全局名称保存在模块对象的字典中。可以通过 globals() 或模块对象的 __dict__ 查看这些名称。

函数可以读取同一文件中的全局名称,但如果在函数内部给某个名称赋值,Python 默认会把它视为局部变量。如果必须修改全局变量,需要使用 global 声明。不过,summaries/02_More_functions 强调应尽量避免 global。如果函数需要修改外部状态,通常更好的方式是使用类或其他明确的状态管理结构。

相关概念包括 Python作用域、Python命名空间 和 状态管理。

Python 类为何显得自由

summaries/05_Object_model__00_Overview 强调,来自其他面向对象语言的程序员常会觉得 Python 的类系统过于开放,常见原因包括:

  • 没有内建的强制 private、protected 访问控制。
  • 实例方法必须显式写出 self 参数。
  • 对象属性通常可以被外部代码直接读取、添加或修改。
  • 方法、函数、类和模块都可以作为普通对象检查和传递。
  • 对象是否支持某种语法,经常取决于是否实现相应协议,而不是是否继承某个指定基类。

这些现象不是语法缺陷,而是 Python 对象模型的一部分。Python 更强调对象、命名空间、属性查找、协议和约定式设计,而不是通过语言级访问控制严格封锁对象内部。

相关主题包括 面向对象编程惯用法、Python类、类与实例、Python封装 和 Python协议。

字典式命名空间:对象模型的重要实现线索

summaries/01_Dicts_revisited 的核心观点是:Python 对象系统很大程度上可以理解为字典之上的一层协议。字典不仅是普通数据结构,也是解释器实现模块、对象、类和继承机制的重要基础。

模块、实例和类通常都包含某种名称到对象的映射:

  • 模块的全局变量和函数保存在模块字典中。
  • 实例的属性通常保存在实例自己的 __dict__ 中。
  • 类的方法、类变量和特殊方法保存在类的 __dict__ 中。
  • 属性访问会在这些字典式命名空间中按规则查找。

这使 Python 的对象模型显得非常动态:名称可以被创建、重新绑定、删除,属性可以被读取、添加或修改,方法也只是类字典中的函数对象经由绑定机制形成的可调用对象。

相关主题包括 字典与属性存储、Python命名空间、对象属性存储、属性访问、属性查找 和 concepts/动态属性访问。

类、实例与属性

在面向对象代码中,类定义对象的结构和行为,实例则是由类创建出来的具体对象。实例通常通过属性保存自己的状态。

class Stock:
    def __init__(self, name, shares, price):
        self.name = name
        self.shares = shares
        self.price = price

创建实例后,实例属性通常保存在实例自己的 __dict__ 中。给 self.name、self.shares、self.price 赋值,实质上就是向实例属性命名空间写入名称和值。

每个实例都有自己的实例字典;类中的方法和类变量则保存在类字典中,由所有实例共享。这解释了 实例属性、类变量 和 字典与属性存储 的基本机制。

动态实例属性

Python 默认不要求实例属性必须提前声明在 __init__() 中。只要类没有额外限制,就可以在运行时新增属性:

s.date = '6/7/2007'

这会向实例字典中加入新名称。也可以直接操作实例字典,例如 s.__dict__['time'] = '9:45am',随后通常可以通过 s.time 访问。不过,直接修改 __dict__ 并不是常规写法,正常代码应优先使用点号语法。

对象的这种开放性连接了 concepts/动态属性访问、反射 和 Python封装。

类字典、方法与类变量

类本身也有字典。类定义中的函数、类变量和特殊方法保存在类的 __dict__ 中。

class Stock:
    def cost(self):
        return self.shares * self.price

    def sell(self, nshares):
        self.shares -= nshares

实例数据保存在实例字典中,而方法保存在类字典中。所有实例通过类共享这些方法。

在类体中直接赋值的变量是类变量。类变量保存在类字典中,并由实例共享。如果修改类变量,所有未在实例层覆盖该属性的对象都会看到新值。

实例与类的连接:__class__

每个实例都通过 __class__ 指向其所属类。因此,一个普通实例可以从三个层次理解:

  1. s.__dict__:实例自己的属性。
  2. s.__class__:实例所属的类。
  3. s.__class__.__dict__:类中定义的方法、类变量和特殊方法。

实例字典保存实例特有数据,类字典保存所有实例共享的数据和行为。

属性访问:点号背后的查找规则

对象属性访问使用点号操作。设置属性通常会修改实例的 __dict__;删除属性通常会从实例字典中移除对应名称。读取属性时,属性可能出现在多个位置。简化地说,Python 会先查找实例自己的命名空间,再查找类的命名空间;如果涉及继承,则继续沿继承结构查找。

s.name    # 通常在实例字典中找到
s.cost()  # 通常在类字典中找到函数,再绑定为方法

这就是为什么 name、shares、price 是每个实例自己的数据,而 cost()、sell() 是所有实例共享的方法。

相关主题包括 属性查找、属性访问、字典与属性存储 和 Python命名空间。

self 是显式的实例绑定

Python 方法定义中的 self 是对象模型透明化的体现。

class Stock:
    def cost(self):
        return self.shares * self.price

self 表示当前实例对象。调用 s.cost() 可以理解为:先在 s 上查找 cost,从类字典中找到函数对象,并把它绑定到实例 s,然后调用这个绑定方法。self 并不是完全隐藏的魔法,而是方法操作哪个实例的显式名称。

相关概念包括 concepts/绑定方法、实例属性 和 Python类。

方法也是对象:查找与调用是两步

summaries/03_Special_methods 和 summaries/01_Dicts_revisited 都强调,方法调用并不是一个不可分割的动作,而是由两个步骤组成:

  1. 查找:使用 . 操作符取得属性或方法对象。
  2. 调用:使用 () 操作符执行这个可调用对象。
s = Stock('GOOG', 100, 490.10)
c = s.cost      # 查找方法,得到绑定方法对象
c()             # 调用方法

c = s.cost 得到的不是计算结果,而是一个绑定方法对象。它已经绑定到实例 s,因此之后调用 c() 时会以 s 作为操作对象。

绑定方法包含调用一个方法所需的关键部分:

  • __func__:真正实现方法的函数对象,通常来自类字典。
  • __self__:绑定到该方法的实例,也就是调用时的 self。

这说明方法本身也符合 Python 对象模型:它可以被赋值给变量、传递给函数、稍后再调用。相关概念包括 concepts/绑定方法、一等对象 和 可调用对象。

特殊方法:对象如何参与语言语法

Python 对象模型不仅规定对象如何存储和引用,还规定对象如何参与 Python 的内置语法。类可以定义以双下划线开头和结尾的特殊方法,也常称为魔术方法,例如 __init__()、__repr__()、__len__()、__iter__()、__next__()。

这些方法由 Python 解释器在特定语法或内置函数中自动调用。很多语言特性并不是硬编码只能用于内置类型,而是通过对象上的方法协议实现的。相关主题包括 Python特殊方法、Python数据模型 和 Python协议。

迭代协议:for 循环背后的对象模型

summaries/01_Iteration_protocol 展示了 Python 对象模型中非常重要的一类协议:Python迭代协议。字符串、字典、列表、元组、文件对象以及许多自定义对象都能用于 for 循环,并不是因为 for 对它们分别写了特殊逻辑,而是因为它们实现了共同的迭代接口。

一个普通循环:

for x in obj:
    pass

底层大致等价于:

_iter = obj.__iter__()
while True:
    try:
        x = _iter.__next__()
    except StopIteration:
        break

这说明:

  • 被迭代对象需要提供 __iter__()。
  • __iter__() 返回迭代器对象。
  • 迭代器通过 __next__() 逐个产生值。
  • 当没有更多值时,__next__() 抛出 StopIteration。
  • for 循环自动捕获 StopIteration 并结束。

内置函数 next(it) 是调用 it.__next__() 的快捷方式。相关主题包括 Python迭代协议、迭代器、Python特殊方法 和 Python协议。

容器协议:让对象像内置容器一样工作

一个完整的容器类通常可以支持长度查询、索引、切片、成员测试和迭代。

class Portfolio:
    def __init__(self, holdings):
        self._holdings = holdings

    def __iter__(self):
        return self._holdings.__iter__()

    def __len__(self):
        return len(self._holdings)

    def __getitem__(self, index):
        return self._holdings[index]

    def __contains__(self, name):
        return any([s.name == name for s in self._holdings])

这些方法分别支持:

  • for s in portfolio:通过 __iter__()。
  • len(portfolio):通过 __len__()。
  • portfolio[0] 和 portfolio[0:3]:通过 __getitem__()。
  • 'IBM' in portfolio:通过 __contains__()。

这就是 Python容器协议 的典型体现。自定义类不必继承某个特定内置类型,只要实现约定的特殊方法,就能参与对应语法。

字符串表示、运算符与容器语法

对象通常有两种字符串表示:

  • str(obj):面向用户的、适合打印的友好表示。
  • repr(obj):面向程序员的、更精确或更可复现的表示。

类可以通过 __str__() 和 __repr__() 控制这两种表示。

数学运算符也会映射到特殊方法调用,例如 a + b 会触发类似 a.__add__(b) 的协议。因此,自定义类可以通过实现相应特殊方法来定义加法、减法、取绝对值等行为。这就是 运算符重载 的基础。

容器相关语法同样由特殊方法支持,例如 len(x)、x[a]、x[a] = v、del x[a]、x in obj 和 for x in obj。

动态属性访问与反射

除了点号语法,Python 还提供一组内置函数,用于根据字符串动态访问和管理属性:

getattr(obj, 'name')
setattr(obj, 'name', value)
delattr(obj, 'name')
hasattr(obj, 'name')

getattr() 还可以提供默认值。动态属性访问让程序可以根据运行时数据决定读取哪个属性,例如表格打印函数可以接收任意对象列表和字段名列表,然后通过 getattr() 读取字段并交给格式化器输出。

这部分将对象模型扩展到 concepts/动态属性访问、反射、通用编程 和 表格格式化。

封装:约定多于强制

summaries/05_Object_model__00_Overview 和 summaries/02_Classes_encapsulation 都强调,Python 的类和对象几乎都是开放的:可以检查对象内部,可以动态修改属性,也没有像某些语言那样强制性的 private 或 protected 访问控制。

这并不意味着 Python 没有封装。Python 的封装更多依赖命名约定、公共接口设计和对象协议,而不是语言强制限制。一个类通常同时包含两层:

  • 公共接口:外部代码应该使用的属性、方法和行为。
  • 内部实现细节:类为了实现功能而使用的辅助属性、方法和数据结构。

以下划线 _ 开头的名称通常表示内部实现细节。这种私有性只是约定,不会阻止外部代码访问,但良好代码应尊重这一约定。

相关主题包括 Python封装、对象封装、Python命名约定 和 面向对象编程惯用法。

受管理属性与 property

Python 对象默认允许直接设置属性。如果属性需要验证,property 可以保持普通属性访问语法,同时在读取或写入时触发方法逻辑。

class Stock:
    def __init__(self, name, shares, price):
        self.name = name
        self.shares = shares
        self.price = price

    @property
    def shares(self):
        return self._shares

    @shares.setter
    def shares(self, value):
        if not isinstance(value, int):
            raise TypeError('expected an integer')
        self._shares = value

这里公共接口仍然是 s.shares,而实际存储细节是 _shares。读取 s.shares 会调用 getter,赋值 s.shares = 75 会调用 setter。

property 也常用于计算属性,例如:

class Stock:
    @property
    def cost(self):
        return self.shares * self.price

这样调用者可以写 s.cost,而不是 s.cost()。这隐藏了数据是存储的还是即时计算的这一实现细节,使对象接口更加统一。

相关概念包括 Python封装、属性查找、concepts/动态属性访问、Python装饰器 和 受管理属性。

装饰器语法与对象模型

@property 使用的是 Python 装饰器语法。@ 表示把紧随其后的函数定义交给某个装饰器处理。这里 property 会把方法转换成特殊的属性对象,使点号访问触发相应逻辑。

因此,装饰器不是单纯的语法糖,而是对象模型的一部分:函数本身是对象,可以被另一个对象包装、替换或改造成具有特殊协议的对象。相关主题包括 Python装饰器、一等对象 和 Python数据模型。

__slots__:限制属性集合与优化对象内存

默认情况下,实例通常有 __dict__,因此可以动态添加任意新属性。这种开放性很灵活,但也意味着每个实例都需要一个字典来保存属性。

__slots__ 可以限制实例允许拥有的属性名:

class Stock:
    __slots__ = ('name', '_shares', 'price')

如果尝试设置未声明的属性,会抛出 AttributeError。在常见情况下,使用 __slots__ 的实例也不再拥有普通的 __dict__。

__slots__ 有两个重要含义:

  • 从接口角度看,它限制了对象可添加的属性,能帮助发现拼写错误或非法状态。
  • 从实现角度看,它主要是内存和性能优化工具,适合大量数据结构对象。

不过,summaries/02_Classes_encapsulation 提醒不要滥用 __slots__。它不是日常封装的首选手段,多数普通类不需要它。

继承、MRO 与 super()

类可以继承其他类。子类实例可以访问自己类中定义的方法,也可以访问父类中定义的方法。继承本质上扩展了属性查找路径,而不是把父类方法复制到子类实例里。

类的直接父类保存在 __bases__ 中,完整属性查找顺序保存在 __mro__ 中。查找某个属性时,Python 会沿实例所属类的 MRO 中的类依次检查各自命名空间,直到找到目标名称。

在覆盖方法时,应优先使用 super()。super() 并不只是调用父类;更准确地说,它会把调用委托给 MRO 中的下一个类。这在多重继承中特别重要,因为硬编码某个父类方法会破坏协作式多重继承。

多重继承的一个重要用途是 mixin。Mixin 是一个提供局部行为片段的类,通常不单独使用,而是和其他类组合。相关主题包括 继承与MRO、super函数、mixin模式 和 面向对象编程惯用法。

与数据结构的关系

summaries/02_Working_with_data__00_Overview 所概述的 Working With Data 章节先介绍 Python 的核心数据结构,再深入对象模型。这种安排体现了一个重要学习路径:

  1. 先掌握常用数据结构如何使用。
  2. 再理解它们为什么会有某些行为。
  3. 最后能够更准确地判断赋值、拷贝、修改、传参、属性访问、方法调用、迭代和容器操作时发生了什么。

例如:

  • 列表是可变对象,因此多个变量可能共享同一个列表。
  • 元组通常不可变,但如果元组中包含可变对象,内部对象仍可能被修改。
  • 字典和集合依赖对象的哈希行为,因此并非所有对象都能作为字典键或集合元素。
  • 浅拷贝只复制外层容器,深拷贝递归复制嵌套对象。
  • 列表推导式会创建新的列表对象,并填充由表达式生成的对象。
  • dict(zip(headers, row)) 会创建一个新的字典对象,把表头对象和值对象关联起来。
  • for x in obj 依赖对象的 __iter__() 和迭代器的 __next__()。
  • len(x)、x[i]、x[i] = value、x in obj 等容器操作本质上会调用对象实现的特殊方法。
  • 实例属性通常可以理解为实例命名空间中的名称到对象的映射。
  • 类方法通常是类字典中的函数,经由实例访问时形成绑定方法。
  • property 可以让方法表现为属性,在访问路径中加入计算或验证逻辑。
  • __slots__ 可以改变实例属性存储方式,限制动态属性并减少内存开销。
  • 继承不会复制父类内容,而是扩展属性查找路径。

这些内容都与 Python数据类型、Python容器、序列、concepts/列表推导式、Python特殊方法、Python迭代协议、Python容器协议 和 继承与MRO 密切相关。

为什么对象模型重要

理解 Python 对象模型可以帮助解释许多常见现象:

  • 为什么 a = b 通常不会复制对象?
  • 为什么修改列表会影响多个变量?
  • 为什么重新赋值不会覆盖旧对象?
  • 为什么 is 和 == 的结果可能不同?
  • 为什么浅拷贝后嵌套列表仍然共享?
  • 为什么字符串看起来修改后其实产生了新对象?
  • 为什么函数参数传入可变对象时可能产生副作用?
  • 为什么函数内部给参数名重新赋值不会改变调用者的变量?
  • 为什么变量名没有类型,而对象值有类型?
  • 为什么类型名和函数可以放入列表并被调用?
  • 为什么对象属性可以动态添加、修改和删除?
  • 为什么实例属性通常保存在实例 __dict__ 中?
  • 为什么方法不在每个实例中重复保存,而是由类共享?
  • 为什么 s.cost 和 s.cost() 的含义不同?
  • 为什么绑定方法中既有函数对象又有实例对象?
  • 为什么 self 需要在方法定义中显式出现?
  • 为什么类变量会被多个实例共享?
  • 为什么 Python 没有强制性的访问控制却仍然可以组织良好封装?
  • 为什么 property 可以在不改变访问语法的情况下加入验证或计算?
  • 为什么 __slots__ 会阻止随意添加新属性,并可能让实例没有普通 __dict__?
  • 为什么继承中的方法查找要依赖 __mro__?
  • 为什么 for x in obj 能统一遍历字符串、列表、字典、文件和自定义对象?
  • 为什么实现 __len__()、__getitem__()、__contains__() 会让自定义类更像内置容器?

这些问题都不是单纯语法问题,而是 Python 数据模型、对象引用机制、名称绑定、作用域规则、字典式命名空间、属性存储、属性查找、绑定方法、特殊方法协议、迭代协议、容器协议、封装约定、受管理属性、继承 MRO 和协作式多重继承共同作用的结果。

与其他概念的联系

  • Python数据类型:对象模型解释每种数据类型在 Python 中都是对象。
  • Python容器:容器保存对象引用,而不是简单复制所有值。
  • 可变性与引用:解释共享可变对象导致的副作用。
  • 拷贝语义:区分赋值、浅拷贝和深拷贝。
  • 一等对象:函数、类型、模块、异常和类都可作为数据使用。
  • 序列:序列操作如索引、切片、迭代和长度查询都建立在对象行为之上。
  • concepts/列表推导式:列表推导式会创建新的列表对象,并填充由表达式生成的对象。
  • Python函数设计:函数接口设计需要考虑参数是否会被修改。
  • Python参数传递:函数参数是名称到对象的绑定,不是对象复制。
  • Python作用域:局部变量、全局变量和模块变量本质上是不同命名空间中的名称绑定。
  • Python命名空间:模块、类、实例和函数作用域都可以从名称映射角度理解。
  • Python类:类定义实例对象的行为、属性访问方式和方法协议。
  • 类与实例:类是行为和共享属性的定义,实例是具体状态对象。
  • 字典与属性存储:字典式映射是理解对象属性存储的重要线索。
  • 属性查找:点号访问会按实例、类、描述符和继承 MRO 的规则查找名称。
  • concepts/绑定方法:方法查找会产生绑定到实例的方法对象,调用需要额外的 ()。
  • Python封装:Python 封装依赖约定、接口、property 和设计纪律,而不是强制访问控制。
  • Python特殊方法:特殊方法让对象参与内置函数、运算符、迭代和语言语法。
  • Python迭代协议:__iter__()、__next__() 和 StopIteration 定义对象如何被循环消费。
  • Python容器协议:__len__()、__getitem__()、__contains__()、__iter__() 等方法让对象像容器一样工作。
  • Pythonic设计:对象应尽量使用 Python 通用语法和协议,而不是发明孤立接口。
  • concepts/动态属性访问:getattr() 等函数让程序可以根据字符串操作对象属性。
  • CSV解析、类型转换、高阶函数:数据转换流程体现函数、类型和数据同为对象。

小结

Python 对象模型将变量、数据类型、容器、可变性、赋值、身份、相等性、拷贝、函数参数、作用域、模块、属性、方法、类、实例、self、封装、property、__slots__、继承、MRO、super()、特殊方法、迭代协议和容器协议统一到同一个视角下:程序中的名称绑定到对象,容器保存对象引用,函数参数也是对象引用的局部名称,模块和类拥有字典式命名空间,实例属性通常保存在实例 __dict__ 中,属性访问是在实例、类、描述符和继承链上的查找,方法调用分为查找和调用两步,而内置语法通过特殊方法与对象交互。

summaries/07_Objects 补充了这一主题的基础层面:赋值不是复制,重新赋值不是覆盖旧内存,is 比较身份而 == 比较值,浅拷贝和深拷贝有本质差异,变量名没有类型而对象有类型,并且函数、类型、模块、异常和类都可以作为一等对象参与数据处理。

summaries/05_Object_model__00_Overview 则补充了学习动机:虽然不掌握所有内部细节也能写 Python,但多数 Python 程序员都会具备对象模型的基本意识。正是这种意识解释了 Python 类为何开放、为何没有强制访问控制、为何 self 显式出现,以及为何封装更多依赖公共接口、命名约定和惯用法。

See also: summaries/07_Objects

See also: summaries/05_Object_model__00_Overview

See also: summaries/05_Decorated_methods

See also: summaries/Contents

See also: summaries/02_Working_with_data__00_Overview

See also: summaries/04_Classes_objects__00_Overview