前端 Python 3.12

9. 类

命名空间与作用域、实例/类属性、方法绑定、继承与 MRO、私有化约定、迭代器与生成器——用 JS prototype 做类比但别套用错。

9.1 核心概念

  • :描述行为与初始状态的模板。
  • 实例:每个对象有自己的属性(通常挂在 __dict__)。
  • self:实例方法首参,调用时由解释器绑定。
  • 查找顺序:实例 → 类 → 基类链(MRO)。

9.2 命名空间与作用域

  • LEGB:Local → Enclosing → Global → Builtins。
  • global:绑定模块级名字。
  • nonlocal:写外层函数的名字。

二者都是声明「赋值写到哪」:global → 模块顶层,nonlocal → 外层函数的局部变量。

def scope_test():
    spam = "test spam"

    def do_local():
        spam = "local spam"

    def do_nonlocal():
        nonlocal spam
        spam = "nonlocal spam"

    def do_global():
        global spam
        spam = "global spam"

    do_local()
    print("After do_local:", spam)  # test spam
    do_nonlocal()
    print("After do_nonlocal:", spam)  # nonlocal spam
    do_global()
    # do_global 里的 global 只写模块级 spam;本函数局部 spam 不变
    print("After do_global:", spam)  # nonlocal spam


scope_test()
print("In global scope:", spam)  # global spam

9.3 类基础

class Dog:
    kind = "canine"  # 类属性:所有实例共享同一个 str 对象(别共享可变 list)

    def __init__(self, name: str) -> None:
        self.name = name
        self.tricks: list[str] = []

    def add_trick(self, trick: str) -> None:
        self.tricks.append(trick)


d1 = Dog("Fido")
d2 = Dog("Buddy")
d1.add_trick("roll over")
print(d1.tricks)  # ['roll over']
print(d2.tricks)  # []

反模式tricks: list[str] = [] 写在类体上 → 所有实例共享同一张表。

反模式(anti-pattern)指的是:看起来很常见、甚至被随手写出来的写法,但在该场景下会引出可靠性与维护上的问题;

调用机制

d.add_trick(x) 等价于 Dog.add_trick(d, x)

class Bag:
    def __init__(self) -> None:
        self.data: list[int] = []

    def add(self, x: int) -> None:
        self.data.append(x)

    def add_twice(self, x: int) -> None:
        self.add(x)
        self.add(x)

9.4 @property:把访问器伪装成属性

对齐「getter,但调用方不用写 ()」:

class Circle:
    def __init__(self, r: float) -> None:
        self.r = r

    @property
    def area(self) -> float:
        return 3.1415926 * self.r * self.r


c = Circle(2)
print(c.area)  # 不是 c.area()

9.5 @classmethod@staticmethod

class Point:
    def __init__(self, x: int, y: int) -> None:
        self.x, self.y = x, y

    @classmethod
    def origin(cls) -> "Point":
        return cls(0, 0)

    @staticmethod
    def manhattan(a: "Point", b: "Point") -> int:
        return abs(a.x - b.x) + abs(a.y - b.y)

注解里的 "Point"带引号的类型注解(前向引用):对类型检查器来说仍然表示 Point,不是「字符串类型」。类体里尚未结束定义时就能引用自身,所以常见这种写法。

@classmethod:第一个参数是类本身(约定写 cls)。上面 origincls(0, 0) 创建实例,子类继承时仍会生成子类的实例。常见用途:备用构造函数、按类配置生成对象。

@staticmethod:解释器自动塞 selfcls;只是挂在类名下的普通函数,调用写成 Point.manhattan(a, b)。实例若需要,由参数显式传入(如上例的 ab),而不是靠绑定到「当前对象」。

对照前端:classmethod ≈ 静态方法里仍能拿到「构造函数」(类似工厂里用 this 指向派生类 ctor 的场景);staticmethod挂在类/对象上的纯函数,只为归类命名空间。


9.6 继承与 super()

class Animal:
    def speak(self) -> str:
        return "..."


class Cat(Animal):
    def speak(self) -> str:
        return "Meow"


class Animal2:
    def __init__(self, name: str) -> None:
        self.name = name


class Dog2(Animal2):
    def __init__(self, name: str, breed: str) -> None:
        super().__init__(name)
        self.breed = breed


cat = Cat()
print(isinstance(cat, Cat), isinstance(cat, Animal))  # True True
print(issubclass(Cat, Animal))  # True

isinstance(obj, Cls):判断某个对象是不是 Cls 的实例(沿继承也算)。issubclass(A, B):判断类 A 是不是类 B 的子类(或同一类);两个参数都是类型,不能把实例当作第一个参数。

MRO(C3)

class A:
    tag = "A"


class B(A):
    tag = "B"


class C(A):
    tag = "C"


class D(B, C):
    pass


print(D.__mro__)  # 解释器如何线性化多重基类
d = D()
print(d.tag)  # 跟 MRO:先到 B

多重继承里常见「菱形」:BC 同继承 AD 再继承 BCMRO(方法解析顺序,由 C3 线性化得到)记在 D.__mro__ 里。解析实例属性(如 tag)时沿这条链从左到右找第一个定义,故 d.tag"B"B 排在 C 前)。


9.7 「私有」与名称改写

class Mapping:
    def __init__(self, iterable: list[int]) -> None:
        self.items: list[int] = []
        self.__update(iterable)

    def update(self, iterable: list[int]) -> None:
        self.items.extend(iterable)

    __update = update  # 私有别名:子类不易误触


m = Mapping([1, 2, 3])
# m.__update  # AttributeError —— 被改写为 _Mapping__update

9.8 迭代器与生成器

下面两段都在做同一件事:按索引从尾到头遍历字符串,产出 'f','l','o','g'。上一段是手写迭代器协议__iter__ / __next__);下一段是生成器函数yield),语义等价、代码更短。

class Reverse:
    def __init__(self, data: str) -> None:
        self.data = data
        self.index = len(data)

    def __iter__(self) -> "Reverse":
        return self

    def __next__(self) -> str:
        if self.index == 0:
            raise StopIteration
        self.index -= 1
        return self.data[self.index]


print(list(Reverse("golf")))  # ['f', 'l', 'o', 'g']


def reverse_gen(data: str):
    for i in range(len(data) - 1, -1, -1):
        yield data[i]


print(list(reverse_gen("golf")))  # ['f', 'l', 'o', 'g']

9.9 dataclass(3.7+,工程常用)

存几个字段的类,手写要重复 __init__ / __repr__@dataclass:按 id: int 这类注解自动生成构造和可读 printslots=True 可选:只允许声明过的属性、略省内存;初学可省略。

from dataclasses import dataclass


@dataclass(slots=True)
class User:
    id: int
    name: str


u = User(1, "Ada")
print(u)  # User(id=1, name='Ada')

仅有注解、或手写构造但没写「打印格式」时:

class OnlyAnnot:
    id: int
    name: str


# OnlyAnnot(1, "Ada")  # TypeError:注解不会自动生成 __init__,不能这样构造


class Handwritten:
    def __init__(self, id: int, name: str) -> None:
        self.id = id
        self.name = name


print(Handwritten(1, "Ada"))  # 默认:<__main__.Handwritten object at 0x...>,看不到 id/name

说明:OnlyAnnot 那种写法没有 @dataclass 时,类体里的 id: int 只是标注,不会帮你写出构造函数,也就没法用位置参数直接构造。Handwritten 虽能 print,但用的是对象默认展示,不含字段内容;需要可读字段时要么手写 __repr__,要么用上面的 @dataclass


9.10 速记

JSPython
this显式 self
类字段分清类共享 vs 实例字段,别共享可变默认
extends多继承 + MRO
迭代协议__iter__ / yield

权威延伸9. Classes

On this page