9. Python 类
9.1 名称与对象
- 多个名称可以绑定到同一个对象,称为别名
- 别名的行为类似指针,传递对象代价很小(只传递指针)
- 对不可变类型(数字、字符串、元组)影响不大,但对可变对象(列表、字典等)可能产生意外副作用
- 函数修改了作为参数传入的可变对象,调用者能看到这个变化
9.2 作用域与命名空间
命名空间是名称到对象的映射,底层用字典实现。例子:
- 内置名称集合(
abs、异常名等) - 模块的全局名称
- 函数调用中的局部名称
- 对象的属性集合
属性:点号之后的名称,如 z.real 中的 real。模块属性与模块全局名称共享同一命名空间。
属性可读写:可赋值(modname.x = 42),也可用 del 删除。
命名空间的生命周期:
- 内置命名空间:解释器启动时创建,永不删除
- 模块全局命名空间:读取模块定义时创建,持续到解释器退出
- 函数局部命名空间:函数被调用时创建,函数返回或抛出异常时销毁
- 顶层脚本属于
__main__模块,内置名称在builtins模块中
作用域搜索顺序(LEGB):
- 最内层:当前函数的局部名称
- 外层闭包函数的作用域(从内到外逐层)
- 当前模块的全局名称
- 最外层:内置名称
关键规则:
- 赋值默认进入最内层作用域,不会复制数据,只是绑定名称到对象
global声明:该变量指向模块全局作用域,并在那里重新绑定nonlocal声明:该变量指向外层函数作用域,并在那里重新绑定- 未声明
nonlocal时,外层变量对内层函数只读(写入会在局部作用域创建新变量) del x从局部命名空间中移除x的绑定import语句和函数定义都在局部作用域中绑定名称- 作用域按字面文本静态确定,但名称搜索在运行时动态进行
作用域示例的输出要点:
# do_local() → 不改变外层 spam
# do_nonlocal() → 改变 scope_test 中的 spam
# do_global() → 改变模块级别的 spam9.3 初探类
9.3.1 类定义语法
class ClassName:
<语句>- 类定义必须先执行才能生效(可以放在
if分支或函数内部) - 进入类定义时,创建新的命名空间作为局部作用域
- 类定义正常结束后,创建类对象,并将其绑定到类名
9.3.2 类对象
类对象支持两种操作:
属性引用:MyClass.i、MyClass.f,类属性可被赋值修改。__doc__ 返回文档字符串。
实例化:x = MyClass() 创建实例。若定义了 __init__(),实例化时自动调用并可传参:
class Complex:
def __init__(self, realpart, imagpart):
self.r = realpart
self.i = imagpart
x = Complex(3.0, -4.5)9.3.3 实例对象
实例对象支持的操作只有属性引用,分两种:
- 数据属性:无需声明,首次赋值时产生,类似 C++ 数据成员
- 方法:从属于对象的函数,是函数对象的封装
9.3.4 方法对象
x.f是方法对象,MyClass.f是函数对象,两者不同- 方法对象可以存储后再调用:
xf = x.f; xf() - 调用
x.f()等价于MyClass.f(x),实例对象自动作为第一个参数传入 - 方法调用时,Python 自动将实例和函数对象打包为方法对象,再构造参数列表调用
9.3.5 类变量与实例变量
- 类变量:所有实例共享,定义在类体中(方法外)
- 实例变量:每个实例独有,通常在
__init__中用self.x = ...定义
常见错误:将可变对象(如列表)作为类变量,导致所有实例共享同一个列表:
# 错误:tricks 是类变量,所有 Dog 实例共享
class Dog:
tricks = []
# 正确:tricks 是实例变量
class Dog:
def __init__(self, name):
self.tricks = []9.4 补充说明
实例属性优先于同名类属性(属性查找先找实例,再找类)
Python 没有强制数据隐藏机制,一切基于约定
客户端可以直接操作数据属性,可能破坏方法维护的内部状态
客户端可以向实例添加自定义属性,但要注意命名冲突
方法内引用数据属性必须通过
self,这提升了可读性(局部变量与实例变量一目了然)self只是约定命名,不是关键字,但不遵循此约定会影响可读性类定义之外定义的函数也可以赋值给类属性,成为方法:
pythondef f1(self, x, y): return min(x, x+y) class C: f = f1 h = g # h 与 g 完全等价方法可通过
self调用其他方法方法的全局作用域是定义该方法的模块,不是类
每个值都是对象,其类型存储于
object.__class__
9.5 继承
基本语法:
class DerivedClassName(BaseClassName):
...
# 基类在其他模块时:
class DerivedClassName(modname.BaseClassName):
...- 属性引用:先在派生类中找,找不到则递归向上查找基类
- 派生类可以重写基类方法
- Python 所有方法实际上都是
virtual的 - 重写时可以调用基类方法:
BaseClassName.methodname(self, arguments) - 内置函数:
isinstance(obj, int):检查实例类型,包括子类关系issubclass(bool, int):检查类的继承关系
9.5.1 多重继承
class DerivedClassName(Base1, Base2, Base3):
...- 属性搜索顺序:深度优先、从左到右,但同一个类不会被搜索两次
- 实际使用 C3 线性化算法(MRO),动态调整以支持
super()协同调用 - MRO 保证:保留从左到右顺序、每个父类只调用一次、保持单调性
- 所有类都继承自
object,任何多重继承都存在菱形结构 - 详细规则参见 Python 2.3 MRO 文档
9.6 私有变量
- Python 中不存在真正的私有实例变量
- 约定:单下划线
_spam表示"非公有 API",属于实现细节,可能不经通知修改 - 名称改写(Name Mangling):
__spam(至少两个前缀下划线,至多一个后缀下划线)会被替换为_classname__spam- 改写在类定义内部任何位置发生,不考虑语法位置
- 目的是避免子类意外覆盖父类的私有属性
- 示例:
Mapping中的__update在子类重写update时不会被破坏
- 名称改写主要防止意外冲突,并非强制访问控制,私有变量仍然可以被访问
exec()、eval()中的代码不将唤起类视为当前类,与global语句效果类似- 同样的限制适用于
getattr()、setattr()、delattr()和__dict__的直接引用
9.7 杂项说明
用
dataclasses可以方便地创建类似 C struct 的数据容器:pythonfrom dataclasses import dataclass @dataclass class Employee: name: str dept: str salary: intPython 的鸭子类型:只要类实现了所需的方法,就可以作为该类型的替代(如实现
read()、readline()的类可代替文件对象)实例方法对象的属性:
m.__self__是实例对象,m.__func__是对应的函数对象
9.8 迭代器
for语句在容器上调用iter(),返回迭代器对象迭代器对象需实现
__next__(),元素耗尽时抛出StopIteration可用内置
next()手动调用__next__()自定义迭代器:实现
__iter__()返回带__next__()的对象;若类本身有__next__(),则__iter__()直接返回selfpythonclass Reverse: def __init__(self, data): self.data = data self.index = len(data) def __iter__(self): return self def __next__(self): if self.index == 0: raise StopIteration self.index -= 1 return self.data[self.index]
9.9 生成器
用
yield语句替代return的函数,每次调用next()从上次yield处恢复执行自动保存局部变量和执行状态,无需手动维护
self.index等自动创建
__iter__()和__next__()方法生成器终结时自动抛出
StopIteration写法比等价的类迭代器更紧凑、清晰:
pythondef reverse(data): for index in range(len(data)-1, -1, -1): yield data[index]
9.10 生成器表达式
语法类似列表推导式,但用圆括号:
(x*x for i in range(10))相比完整生成器:更紧凑,但灵活性稍差
相比列表推导式:更省内存(惰性求值,不一次性构建列表)
适用于生成器立即被外层函数消费的场景:
pythonsum(i*i for i in range(10)) # 285 sum(x*y for x,y in zip(xvec, yvec)) # 点乘 set(word for line in page for word in line.split()) list(data[i] for i in range(len(data)-1, -1, -1))