Skip to content

9. Python 类


9.1 名称与对象

  • 多个名称可以绑定到同一个对象,称为别名
  • 别名的行为类似指针,传递对象代价很小(只传递指针)
  • 对不可变类型(数字、字符串、元组)影响不大,但对可变对象(列表、字典等)可能产生意外副作用
  • 函数修改了作为参数传入的可变对象,调用者能看到这个变化

9.2 作用域与命名空间

命名空间是名称到对象的映射,底层用字典实现。例子:

  • 内置名称集合(abs、异常名等)
  • 模块的全局名称
  • 函数调用中的局部名称
  • 对象的属性集合

属性:点号之后的名称,如 z.real 中的 real。模块属性与模块全局名称共享同一命名空间。

属性可读写:可赋值(modname.x = 42),也可用 del 删除。

命名空间的生命周期

  • 内置命名空间:解释器启动时创建,永不删除
  • 模块全局命名空间:读取模块定义时创建,持续到解释器退出
  • 函数局部命名空间:函数被调用时创建,函数返回或抛出异常时销毁
  • 顶层脚本属于 __main__ 模块,内置名称在 builtins 模块中

作用域搜索顺序(LEGB)

  1. 最内层:当前函数的局部名称
  2. 外层闭包函数的作用域(从内到外逐层)
  3. 当前模块的全局名称
  4. 最外层:内置名称

关键规则

  • 赋值默认进入最内层作用域,不会复制数据,只是绑定名称到对象
  • global 声明:该变量指向模块全局作用域,并在那里重新绑定
  • nonlocal 声明:该变量指向外层函数作用域,并在那里重新绑定
  • 未声明 nonlocal 时,外层变量对内层函数只读(写入会在局部作用域创建新变量)
  • del x 从局部命名空间中移除 x 的绑定
  • import 语句和函数定义都在局部作用域中绑定名称
  • 作用域按字面文本静态确定,但名称搜索在运行时动态进行

作用域示例的输出要点

python
# do_local()    → 不改变外层 spam
# do_nonlocal() → 改变 scope_test 中的 spam
# do_global()   → 改变模块级别的 spam

9.3 初探类

9.3.1 类定义语法

python
class ClassName:
    <语句>
  • 类定义必须先执行才能生效(可以放在 if 分支或函数内部)
  • 进入类定义时,创建新的命名空间作为局部作用域
  • 类定义正常结束后,创建类对象,并将其绑定到类名

9.3.2 类对象

类对象支持两种操作:

属性引用MyClass.iMyClass.f,类属性可被赋值修改。__doc__ 返回文档字符串。

实例化x = MyClass() 创建实例。若定义了 __init__(),实例化时自动调用并可传参:

python
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 = ... 定义

常见错误:将可变对象(如列表)作为类变量,导致所有实例共享同一个列表:

python
# 错误:tricks 是类变量,所有 Dog 实例共享
class Dog:
    tricks = []

# 正确:tricks 是实例变量
class Dog:
    def __init__(self, name):
        self.tricks = []

9.4 补充说明

  • 实例属性优先于同名类属性(属性查找先找实例,再找类)

  • Python 没有强制数据隐藏机制,一切基于约定

  • 客户端可以直接操作数据属性,可能破坏方法维护的内部状态

  • 客户端可以向实例添加自定义属性,但要注意命名冲突

  • 方法内引用数据属性必须通过 self,这提升了可读性(局部变量与实例变量一目了然)

  • self 只是约定命名,不是关键字,但不遵循此约定会影响可读性

  • 类定义之外定义的函数也可以赋值给类属性,成为方法:

    python
    def f1(self, x, y): return min(x, x+y)
    class C:
        f = f1
        h = g  # h 与 g 完全等价
  • 方法可通过 self 调用其他方法

  • 方法的全局作用域是定义该方法的模块,不是类

  • 每个值都是对象,其类型存储于 object.__class__


9.5 继承

基本语法

python
class DerivedClassName(BaseClassName):
    ...
# 基类在其他模块时:
class DerivedClassName(modname.BaseClassName):
    ...
  • 属性引用:先在派生类中找,找不到则递归向上查找基类
  • 派生类可以重写基类方法
  • Python 所有方法实际上都是 virtual
  • 重写时可以调用基类方法:BaseClassName.methodname(self, arguments)
  • 内置函数:
    • isinstance(obj, int):检查实例类型,包括子类关系
    • issubclass(bool, int):检查类的继承关系

9.5.1 多重继承

python
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 的数据容器:

    python
    from dataclasses import dataclass
    
    @dataclass
    class Employee:
        name: str
        dept: str
        salary: int
  • Python 的鸭子类型:只要类实现了所需的方法,就可以作为该类型的替代(如实现 read()readline() 的类可代替文件对象)

  • 实例方法对象的属性:m.__self__ 是实例对象,m.__func__ 是对应的函数对象


9.8 迭代器

  • for 语句在容器上调用 iter(),返回迭代器对象

  • 迭代器对象需实现 __next__(),元素耗尽时抛出 StopIteration

  • 可用内置 next() 手动调用 __next__()

  • 自定义迭代器:实现 __iter__() 返回带 __next__() 的对象;若类本身有 __next__(),则 __iter__() 直接返回 self

    python
    class 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

  • 写法比等价的类迭代器更紧凑、清晰:

    python
    def reverse(data):
        for index in range(len(data)-1, -1, -1):
            yield data[index]

9.10 生成器表达式

  • 语法类似列表推导式,但用圆括号:(x*x for i in range(10))

  • 相比完整生成器:更紧凑,但灵活性稍差

  • 相比列表推导式:更省内存(惰性求值,不一次性构建列表)

  • 适用于生成器立即被外层函数消费的场景:

    python
    sum(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))

Released under the MIT License.