35 KiB
sources, brief
| sources | brief | ||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
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容器。
- 函数、模块、类和实例。
- 方法、绑定方法、类型对象。
- 异常类和异常实例。
- 迭代器、生成器、文件对象等更高级结构。
对象通常具有三个重要方面:
- 身份 identity:对象在运行时的唯一标识,表示这个对象是谁。可用
id(obj)查看。 - 类型 type:决定对象支持哪些操作,例如列表可原地追加元素,字符串不能原地修改,实例可通过类定义的方法和特殊方法参与 Python 语法。
- 值 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__ 指向其所属类。因此,一个普通实例可以从三个层次理解:
s.__dict__:实例自己的属性。s.__class__:实例所属的类。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 都强调,方法调用并不是一个不可分割的动作,而是由两个步骤组成:
- 查找:使用
.操作符取得属性或方法对象。 - 调用:使用
()操作符执行这个可调用对象。
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 的核心数据结构,再深入对象模型。这种安排体现了一个重要学习路径:
- 先掌握常用数据结构如何使用。
- 再理解它们为什么会有某些行为。
- 最后能够更准确地判断赋值、拷贝、修改、传参、属性访问、方法调用、迭代和容器操作时发生了什么。
例如:
- 列表是可变对象,因此多个变量可能共享同一个列表。
- 元组通常不可变,但如果元组中包含可变对象,内部对象仍可能被修改。
- 字典和集合依赖对象的哈希行为,因此并非所有对象都能作为字典键或集合元素。
- 浅拷贝只复制外层容器,深拷贝递归复制嵌套对象。
- 列表推导式会创建新的列表对象,并填充由表达式生成的对象。
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