在 Python 的里,一切皆为对象。整数、字符串、列表,甚至函数,都是类的实例。类是创建对象的蓝图,它定义了一组属性(Attributes)和方法(Methods),封装了数据和操作数据的行为。
类与实例剖析#
类的定义与实例化#
使用 class 关键字定义一个类。类名通常采用驼峰命名法 (CamelCase)。
class Dog:
# 这是一个类属性,它被所有 Dog 类的实例共享
species = "Canis familiaris"
# __init__ 是一个特殊的初始化方法(构造器)
# 当一个实例被创建时,__init__ 会被自动调用
def __init__(self, name, age):
# self 代表实例对象本身,必须作为方法的第一个参数
# 下面是实例属性,它们属于每个独立的实例
self.name = name
self.age = age
# 这是一个实例方法
def bark(self):
return f"{self.name} says Woof!"
实例化是根据蓝图创建具体对象的过程。
d1 = Dog("Buddy", 5)
print(f"{d1.name} is {d1.age} years old.") # -> Buddy is 5 years old.
print(d1.bark()) # -> Buddy says Woof!
self 的本质#
self 并非关键字,而是一个约定俗成的名称,指向实例对象本身。当你调用 d1.bark() 时,Python 解释器在内部会自动将其转换为 Dog.bark(d1)。self 使得方法能够访问和操作实例自身的属性和方法。
类属性与实例属性#
- 类属性 (Class Attribute): 在
class代码块内、所有方法之外定义。被该类的所有实例共享。通常用于定义该类所有对象共有的、不变的特性。 - 实例属性 (Instance Attribute): 通常在
__init__方法中通过self.attribute = value定义。每个实例独有一份,用于描述每个对象特有的状态。
d2 = Dog("Lucy", 3)
print(d1.species) # -> Canis familiaris
print(d2.species) # -> Canis familiaris
# 修改类属性会影响所有实例
Dog.species = "A friendly dog"
print(d1.species) # -> A friendly dog
类方法:超越实例的范畴#
并非所有与类相关的功能都必须绑定到具体的实例上。
实例方法、类方法与静态方法#
| 类型 | 装饰器 | 第一个参数 | 用途 |
|---|---|---|---|
| 实例方法 | (无) | self (实例) | 操作或访问实例的状态(实例属性)。这是最常见的方法类型。 |
| 类方法 | @classmethod | cls (类) | 操作或访问类的状态(类属性)。最常用于工厂方法,即创建类的实例的替代构造器。 |
| 静态方法 | @staticmethod | (无特定参数) | 作为一个与类相关的工具函数,它不依赖于类或实例的状态。逻辑上属于这个类,但功能上是独立的。 |
实践范例:工厂模式#
class Date:
def __init__(self, year, month, day):
self.year = year
self.month = month
self.day = day
@classmethod
def from_string(cls, date_string):
"""工厂方法:通过 'YYYY-MM-DD' 格式的字符串创建 Date 实例。"""
year, month, day = map(int, date_string.split('-'))
return cls(year, month, day) # cls 就是 Date 类本身
@staticmethod
def is_valid_date(date_string):
"""静态方法:一个独立的工具函数,用于检查日期字符串的格式。"""
try:
year, month, day = map(int, date_string.split('-'))
return True
except (ValueError, TypeError):
return False
# 使用
d1 = Date(2025, 11, 4)
d2 = Date.from_string("2025-12-25") # 使用类方法作为替代构造器,代码更具语义化
print(Date.is_valid_date("2025-02-30")) # True
print(d2.year) # -> 2025
继承:类的层次结构#
继承允许一个类(子类)获取另一个类(父类)的属性和方法,是实现代码重用的核心机制。
基本继承与 super()#
class Animal:
def __init__(self, name):
self.name = name
def speak(self):
raise NotImplementedError("Subclass must implement this abstract method")
class Bulldog(Dog): # Bulldog 继承自 Dog
def __init__(self, name, age, wrinkle_level):
# super() 返回一个代理对象,让你能调用父类的方法。
# 这是实现方法扩展的标准做法,能正确处理复杂的多重继承。
super().__init__(name, age)
self.wrinkle_level = wrinkle_level
# 覆盖 (Override) 父类的 bark 方法
def bark(self):
return f"{self.name} says Grrr!"
多重继承与方法解析顺序 (MRO)#
Python 支持一个类从多个父类继承。当不同父类有同名方法时,Python 使用 C3 线性化算法来确定一个明确的方法解析顺序 (MRO)。
class A: pass
class B(A): pass
class C(A): pass
class D(B, C): pass
# 可以通过 __mro__ 属性或 mro() 方法查看
print(D.__mro__)
# -> (<class '__main__.D'>, <class '__main__.B'>, <class '__main__.C'>, <class '__main__.A'>, <class 'object'>)
MRO 保证了属性查找顺序的确定性和一致性,即使在复杂的菱形继承结构中也是如此。
说明:C3 线性化算法
C3 线性化算法(C3 Linearization Algorithm)是 Python(从 2.3 版本开始)用来确定多重继承中方法解析顺序(Method Resolution Order, MRO)的算法。它的核心任务是,当一个类继承自多个父类时,创建一个明确、一致且无歧义的方法和属性查找顺序列表。
1. 为什么需要一个复杂的算法?—— “菱形问题”
如果没有一个好的算法,多重继承会很快变得混乱。最经典的问题就是 “菱形问题” (The Diamond Problem):
graph TD
A[class A] --> B[class B]
A --> C[class C]
B --> D[class D]
C --> D[class D]
class A:
def who_am_i(self):
print("I am an A")
class B(A):
def who_am_i(self):
print("I am a B")
class C(A):
def who_am_i(self):
print("I am a C")
class D(B, C):
pass
d = D()
d.who_am_i() # ???
当调用 d.who_am_i() 时,Python 应该调用哪个版本的方法?B 的还是 C 的?如果 B 和 C 都没有这个方法,应该只调用一次 A 的方法,还是两次?
一个糟糕的 MRO 算法可能会导致:
- 歧义:不知道该调用哪个方法。
- 冗余调用:多次调用同一个基类的方法。
- 不一致性:查找顺序违背了程序员的直觉。
C3 算法就是为了优雅地解决这些问题而生的。
2. C3 算法的三个指导原则
C3 算法之所以优秀,是因为它始终遵循三个关键原则:
- 子类优先于父类 (Children First):在 MRO 列表中,子类永远出现在其父类之前。
- 尊重父类顺序 (Parent Order Matters):在定义类时,父类的顺序 (
class D(B, C):) 会被保留。在 MRO 列表中,B会出现在C之前。 - 单调性 (Monotonicity):一个类的 MRO 是其所有父类 MRO 的延伸和扩展,而不会颠覆父类自身的继承顺序。这保证了继承链的一致性和可预测性。
3. C3 算法的工作原理
C3 算法的核心思想是合并(merge)父类的 MRO 列表。其基本公式可以表示为:
MRO(C) = [C] + merge(MRO(Parent1), MRO(Parent2), ..., [Parent1, Parent2, ...])
这里的 merge 操作是关键,它的规则如下:
- 从第一个父类的 MRO 列表的头部(第一个元素)开始。
- 检查这个头部元素是否出现在任何其他父类 MRO 列表的尾部(除头部外的所有元素)中。
- 如果“否”(它是一个“好头”),则将其从所有列表中取出,并添加到最终的 MRO 列表中。然后回到步骤 1。
- 如果“是”(它是一个“坏头”,意味着有其他类需要优先于它被解析),则跳过这个列表,去检查下一个父类 MRO 列表的头部。
- 重复以上过程,直到所有列表为空。如果无法找到一个“好头”但列表仍不为空,则意味着继承层次结构存在冲突,Python 会在定义类时就抛出
TypeError。
4. 实例演练:破解菱形问题
让我们用 C3 算法手动计算上面菱形问题中 D 的 MRO。
- 已知 MROs: (为简化,我们假设它们都继承自
object)MRO(object) = [object]MRO(A) = [A, object]MRO(B) = [B, A, object]MRO(C) = [C, A, object]
- 计算
MRO(D):MRO(D) = [D] + merge(MRO(B), MRO(C), [B, C])- 代入已知 MROs:
merge([B, A, object], [C, A, object], [B, C])
- 开始合并:
- 检查
B(第一个列表的头):B没有出现在[C, A, object]的尾部,也没有出现在[B, C]的尾部。它是个好头。- 取出
B。 - 当前 MRO:
[D, B] - 待合并列表:
[A, object],[C, A, object],[C]
- 取出
- 检查
A(第一个列表的头):A出现在了[C, A, object]的尾部。它是个坏头。跳过。 - 检查
C(第二个列表的头):C没有出现在[A, object]的尾部。它是个好头。- 取出
C。 - 当前 MRO:
[D, B, C] - 待合并列表:
[A, object],[A, object]
- 取出
- 检查
A(第一个列表的头):A没有出现在[A, object]的尾部。它是个好头。- 取出
A。 - 当前 MRO:
[D, B, C, A] - 待合并列表:
[object],[object]
- 取出
- 检查
object(第一个列表的头):object是个好头。- 取出
object。 - 当前 MRO:
[D, B, C, A, object] - 待合并列表:
[],[](全部为空)
- 取出
- 检查
- 最终结果:
MRO(D) = [D, B, C, A, object]
现在,让我们用 Python 验证一下:
>>> D.__mro__
(<class '__main__.D'>, <class '__main__.B'>, <class '__main__.C'>, <class '__main__.A'>, <class 'object'>)
结果完全一致!这意味着当调用 d.who_am_i() 时,Python 会:
- 在
D中查找。 - 在
B中查找(找到并执行B的版本)。 - 如果
B中没有,则在C中查找。 - 如果
C中也没有,则在A中查找。
5. C3 的健壮性:检测不一致的继承
C3 算法还能在类定义时就阻止不合逻辑的继承。
class X: pass
class Y: pass
class A(X, Y): pass
class B(Y, X): pass
class C(A, B): pass
# TypeError: Cannot create a consistent method resolution
# order (MRO) for bases Y, X
这里,A 的 MRO 要求 X 在 Y 之前,而 B 的 MRO 要求 Y 在 X 之前。C3 算法在合并时会发现无法同时满足这两个相互矛盾的条件,因此直接拒绝创建类 C,从而在源头上避免了混乱。
总结:
C3 线性化算法是 Python 多重继承系统的智能核心。它不仅仅是一个技术细节,更是保证了 Python 的 OOP 模型既强大灵活,又保持了确定性、可预测性和安全性的关键所在。它优雅地解决了困扰许多其他语言的菱形继承问题,让开发者可以更有信心地使用多重继承。
对象模型控制与自动化#
@property:受控属性伪装#
@property 装饰器可以将一个方法转换为一个“只读属性”,并能为其添加验证逻辑的“设置器”和“删除器”,从而在不破坏简洁访问方式的前提下实现封装。
class Circle:
def __init__(self, radius):
self._radius = radius # 使用 _ 前缀约定为内部属性
@property
def radius(self):
"""Getter: 返回半径。"""
return self._radius
@radius.setter
def radius(self, value):
"""Setter: 在设置值之前进行验证。"""
if value <= 0:
raise ValueError("Radius must be positive")
self._radius = value
@property
def area(self):
"""一个通过计算得出的只读属性。"""
return 3.14159 * (self._radius ** 2)
c = Circle(10)
print(c.radius) # 像访问属性一样调用 getter
print(c.area) # 访问计算属性
c.radius = 12 # 调用 setter,自动进行验证
数据类 (dataclasses)#
对于主要用于存储数据的类,dataclasses 模块可以自动生成 __init__, __repr__, __eq__ 等大量模板代码,从而让开发者“告别”模板代码 (Python 3.7+)。
from dataclasses import dataclass
@dataclass(frozen=True) # frozen=True 使实例创建后不可变
class InventoryItem:
name: str
unit_price: float
quantity_on_hand: int = 0
def total_cost(self) -> float:
return self.unit_price * self.quantity_on_hand
item1 = InventoryItem("Pen", 1.5, 100)
item2 = InventoryItem("Pen", 1.5, 100)
print(item1) # 自动生成了友好的 __repr__
print(item1 == item2) # True, 自动生成了 __eq__
dataclasses 极大提高了开发效率,是现代 Python 中构建数据模型类的首选方式。
性能与内存优化:__slots__#
默认情况下,Python 实例使用一个字典 (__dict__) 来存储属性,这很灵活但内存开销大。当需要大规模创建实例时,__slots__ 可以显著优化性能和内存。
__slots__ 通过声明一组固定的属性,让 Python 使用更紧凑的内部结构替代 __dict__。
class Point:
__slots__ = ('x', 'y') # 声明实例只有 x 和 y 两个属性
def __init__(self, x, y):
self.x = x
self.y = y
p = Point(1, 2)
# p.z = 3 # -> AttributeError,因为实例不能再动态添加新属性
属性存储__dict__:灵活、“昂贵”#
工作原理
当你定义一个普通 Python 类(没有 __slots__)时,每个实例都会在内部维护一个名为 __dict__ 的字典。这个字典负责存储该实例的所有属性(包括方法的引用,尽管方法通常是类属性)。
class MyClass:
def __init__(self, a, b):
self.a = a
self.b = b
self.c = 0 # 运行时动态添加的属性
obj = MyClass(1, 2)
# obj.__dict__ 内部存储了这些信息:
# {'a': 1, 'b': 2, 'c': 0}
为什么“内存开销大”?
- 字典本身的开销:Python 的字典是一种非常高效的数据结构,但它为了实现 O(1) 的平均查找时间,需要额外的空间来存储哈希表、键、值,以及处理哈希冲突等。即使实例只有一个属性,
__dict__本身就有一个固定的最小开销。 - 每个实例一个字典:这意味着如果你创建了 10,000 个
MyClass的实例,你就创建了 10,000 个__dict__对象。即使每个实例只存储几个简单的属性,这些字典加起来的内存占用也会相当可观。 - 动态灵活性:
__dict__允许你在运行时随意添加、修改、删除属性(如obj.d = 4)。这种灵活性是以内存为代价的。
为什么“性能”有影响?
- 查找的间接性:访问
obj.a时,Python 实际上是先查找obj对象,找到它的__dict__,然后在__dict__中查找键'a'。这个额外的查找步骤(虽然平均是 O(1))仍然比直接访问一个预先知道位置的内存空间要慢。 - 字典操作的开销:添加、删除属性时,字典需要进行哈希计算、可能扩容等操作,这些都会消耗 CPU 时间。
在一个非常大的仓库(__dict__)里找东西。即使知道大概的位置,还是需要在这个巨大的仓库里搜索。如果有很多这样的仓库(每个实例一个),管理和搜索它们会变得很慢。
__slots__方案#
工作原理
当定义 __slots__ 时,实际上是在告诉 Python:“这个类的实例只会拥有这些指定的属性,并且不需要为它们准备一个 __dict__。”
class Point:
__slots__ = ('x', 'y') # 声明实例只有 x 和 y 两个属性
def __init__(self, x, y):
self.x = x
self.y = y
p = Point(1, 2)
"""
>>> p.__dict__
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
AttributeError: 'Point' object has no attribute '__dict__'. Did you mean: '__dir__'?
"""
在这种情况下,Point 的实例 p 不会拥有 __dict__。取而代之的是,Python 会为 x 和 y 在实例内部预留固定的内存空间(通常是以 C 语言结构体的方式实现),并提供直接访问这些内存位置的方法。
如何“优化性能和内存”?
- 内存优化:
- 无
__dict__开销:最直接的节省就是省去了每个实例持有一个字典的开销。 - 固定内存占用:每个实例的内存占用就等于其声明的属性所占用的空间,加上少量对象管理的开销。这比一个动态字典的总开销要小得多,尤其是在属性数量不多但实例数量庞大的情况下。
- 无
- 性能优化:
- 直接访问:访问
p.x时,Python 可以直接根据预定义的结构(相当于 C 语言中的结构体成员访问)定位并读取内存,速度更快。 - 避免动态操作开销:因为属性是固定的,Python 不需要执行字典的哈希、查找、扩容等动态操作,从而提高了属性访问和修改的速度。
- 直接访问:访问
使用 __slots__ 就像是为实例设计了一个固定大小的、带有明确标签的工具箱。每个工具(属性)都有其固定位置。查找和取用工具时,直接知道在哪里找,非常快。而且,这个工具箱比前面那个大仓库(__dict__)小很多,也更容易携带。
实际节省效果#
通过一个简单的实验来量化内存节省:
# %%
# 导入所需库
import timeit
import gc
import psutil
import os
import sys
# --- 定义测试用的类 ---
class PointWithDict:
"""使用默认 __dict__ 的类"""
def __init__(self, x, y):
self.x = x
self.y = y
class PointWithSlots:
"""使用 __slots__ 优化内存的类"""
__slots__ = ("x", "y")
def __init__(self, x, y):
self.x = x
self.y = y
# --- 测试函数定义 ---
def measure_memory(cls_to_test, num_instances):
"""
测量创建指定数量的实例所导致的进程内存增量。
返回值为字节数。
"""
# 在测量前运行垃圾回收,确保一个干净的初始状态
gc.collect()
# 获取当前进程对象
process = psutil.Process(os.getpid())
# 记录创建对象前的内存占用
mem_before = process.memory_info().rss
# 创建对象列表
objects = [cls_to_test(i, i * 2) for i in range(num_instances)]
# 记录创建对象后的内存占用
mem_after = process.memory_info().rss
# 清理创建的对象,避免影响后续测试
del objects
gc.collect()
# 返回内存增量
return mem_after - mem_before
def measure_performance(cls_to_test, num_instances):
"""
测量对象的创建、访问和修改性能。
返回一个包含三个时间值的元组。
"""
# 1. 测量创建时间
# timeit 的 setup 参数用于准备环境,stmt 是要重复执行的语句
creation_stmt = f"[{cls_to_test.__name__}(i, i*2) for i in range({num_instances})]"
creation_setup = f"from __main__ import {cls_to_test.__name__}"
# 我们只执行一次创建操作,因为创建大量对象本身就很耗时
creation_time = timeit.timeit(stmt=creation_stmt, setup=creation_setup, number=1)
# 准备用于访问和修改测试的对象列表
objects = [cls_to_test(i, i * 2) for i in range(num_instances)]
# 2. 测量访问时间
def access_objects():
total = 0
for p in objects:
total += p.x
total += p.y
return total
# number 设置为 100,以获得更稳定的平均时间
access_time = timeit.timeit(access_objects, number=100)
# 3. 测量修改时间
def modify_objects():
for p in objects:
p.x += 1
p.y -= 1
modify_time = timeit.timeit(modify_objects, number=100)
# 清理对象
del objects
gc.collect()
return creation_time, access_time, modify_time
def run_comparison(cls_to_test, num_instances):
"""
运行并打印指定类的完整内存和性能测试结果。
"""
print(f"--- 正在测试: {cls_to_test.__name__} ---")
# 内存测试
mem_used = measure_memory(cls_to_test, num_instances)
print(f"内存占用增量: {mem_used / 1024**2:.2f} MB ({mem_used:,.0f} 字节)")
# 性能测试
creation, access, modification = measure_performance(cls_to_test, num_instances)
print(f" - 对象创建时间: {creation:.6f} 秒")
print(f" - 属性访问时间 (100次): {access:.6f} 秒")
print(f" - 属性修改时间 (100次): {modification:.6f} 秒")
print("-" * 30 + "\n")
#%%
# 定义测试规模
NUM_INSTANCES = 500_000 # 使用下划线提高可读性
print(f"Python 版本: {sys.version.split()[0]}")
print(f"测试实例数量: {NUM_INSTANCES:,}")
print("=" * 40 + "\n")
# 对两个类分别进行测试
run_comparison(PointWithDict, NUM_INSTANCES)
run_comparison(PointWithSlots, NUM_INSTANCES)
# %%
实验结果:
PointWithSlots实例的内存占用会比PointWithDict实例小很多。- 但是在实际测试中发现与官方文档中的描述存在差异——访问
PointWithSlots实例属性的性能并不会明显优于PointWithDict。
# - Mac本地(3.11.8)
"""
Python 版本: 3.11.8
测试实例数量: 500,000
========================================
--- 正在测试: PointWithDict ---
内存占用增量: 80.04 MB (83,927,040 字节)
- 对象创建时间: 0.188822 秒
- 属性访问时间 (100次): 5.118392 秒
- 属性修改时间 (100次): 7.855338 秒
------------------------------
--- 正在测试: PointWithSlots ---
内存占用增量: 48.02 MB (50,352,128 字节)
- 对象创建时间: 0.203976 秒
- 属性访问时间 (100次): 5.476134 秒
- 属性修改时间 (100次): 6.850384 秒
------------------------------
"""
# - colab(3.11.13)
"""
Python 版本: 3.11.13
测试实例数量: 500,000
========================================
--- 正在测试: PointWithDict ---
内存占用增量: 82.04 MB (86,024,192 字节)
- 对象创建时间: 0.293373 秒
- 属性访问时间 (100次): 4.721711 秒
- 属性修改时间 (100次): 4.083924 秒
------------------------------
--- 正在测试: PointWithSlots ---
内存占用增量: 48.43 MB (50,782,208 字节)
- 对象创建时间: 0.162264 秒
- 属性访问时间 (100次): 5.220282 秒
- 属性修改时间 (100次): 4.003415 秒
------------------------------
"""
总结 __slots__ 的权衡#
- 优势:
- 显著的内存节省:特别适合于需要创建数万甚至数百万个对象的场景。
更快的属性访问和修改速度。与官方文档存在差异的主要原因可能在于 Python 3.6+ 版本对字典进行了重大优化,使得小字典的访问速度大幅提升 Stack Overflow- 紧凑字典(Compact Dict):Python 3.6+ 采用新的字典实现,内存布局更紧凑
- 小字典优化:对于少量属性(如你的测试中只有 x, y 两个属性),字典查找已经非常快
- 键共享字典:同一类的多个实例可以共享键,减少内存和提升速度
- 阻止动态添加意外属性:有助于提高代码的健壮性,避免无意中引入新的状态。
- 劣势:
- 丧失灵活性:不能动态添加新属性。
- 不支持
__dict__:你无法通过instance.__dict__来查看或修改属性,也无法使用vars(instance)。 - 继承时的注意事项:如果父类定义了
__slots__,子类也必须定义自己的__slots__(或者在其__slots__中包含父类的 slot 名称),否则子类将无法使用__slots__,会重新获得__dict__,导致一些潜在的混乱。 - 对某些内省工具不友好:一些依赖
__dict__的第三方库或工具可能无法与__slots__类完美工作。
何时使用 __slots__?
当且仅当:
- 正在处理大量同类型的对象(数千到数百万)。
- 这些对象的属性集合是固定的,不会在运行时动态增减。
- 不关心实例的
__dict__,或者知道如何处理没有__dict__的类。 - 内存占用或属性访问速度是性能瓶颈,且
__slots__能提供可观的改善。
在其他情况下,__dict__ 的灵活性通常更受欢迎。
高级设计模式与底层机制#
“私有”变量与名称改写#
Python 没有真正的 private。但 __ (双下划线) 前缀会触发名称改写 (Name Mangling),解释器会将 __variable 改为 _ClassName__variable。这主要为了避免子类意外覆盖父类的内部同名属性,而不是为了实现数据隐藏。
class MyClass:
def __init__(self):
self.__super_private = "secret"
obj = MyClass()
# print(obj.__super_private) # -> AttributeError
print(obj._MyClass__super_private) # -> secret,仍可访问
抽象基类 (ABCs) - 定义接口规范#
abc 模块允许你定义抽象基类,以强制其子类必须实现特定的方法。这在构建框架或定义插件接口时至关重要。
from abc import ABC, abstractmethod
class MediaLoader(ABC):
@abstractmethod
def load(self, source):
"""定义一个接口,子类必须实现它。"""
pass
class ImageLoader(MediaLoader):
def load(self, source): # 若不实现 load 方法,则在实例化时会报错
print(f"Loading image from {source}")
# loader = MediaLoader() # -> TypeError: Can't instantiate abstract class...
img_loader = ImageLoader()
元类 (Metaclasses) - 创建类的类#
官方文档:元类
这是 Python 对象模型最深邃的部分。元类是创建类的类。默认的元类是 type。当你写下 class MyClass: ... 时,Python 解释器在背后调用元类来创建 MyClass 这个类对象。
元类允许你在类被创建时自动地修改或增强类。这是 Django ORM、SQLAlchemy 等框架实现其“魔法”的核心技术,它们通过元类读取类定义,并自动生成与数据库交互所需的方法和属性。
极少开发场景需要编写元类,但理解其概念至关重要。它解释了 Python 框架中许多强大功能的来源,并展示了 Python 对象模型的极致灵活性——不仅能控制对象的行为,还能控制类本身的行为。