本文目录
上一篇讲 lambda 与函数对象;从这一篇起,数据与行为被 class 包成模板,再按需造出一个个 实例。类不是“带类型的结构体”那么简单——它定义的是命名空间 + 创建协议:类体里写的名字,有的挂在类上,有的挂在每个实例上;方法第一个参数惯例叫 self,指向当前实例。
和其他语言里“先写结构体再 typedef”不同,Python 的 class 在定义时就运行类体代码,并把名字登记到类的命名空间里。之后每次写 Counter(),解释器按创建协议造出新对象,并把你在 __init__ 里写的 self.x = ... 绑到那一份 实例 上。读类定义时,要同时看“类级别共享什么”和“每个对象各自持有什么”。
下面这段代码可以直接跑,用来建立直觉:
class Counter:
step: int = 1 # 类属性:所有实例共享的默认值
def __init__(self, start: int = 0) -> None:
self.value: int = start # 实例属性:每个对象各有一份
def tick(self) -> int:
self.value += Counter.step
return self.value
a = Counter(10)
b = Counter()
print(a.tick(), b.tick()) # 11 1
print(Counter.step) # 1a 和 b 是同一 class 造出的两个 实例;self.value 各管各的,而 Counter.step 是 类属性,改类上的 step 会影响所有还没被实例覆盖的读取路径。
改 类属性 会影响后续所有通过“实例上没有同名属性”这条路径读到的值;在类上执行 Counter.step = 2 后,尚未 shadow 的实例都会看到新步长。若某个实例已经执行过 self.step = 1,它有自己的副本,不再跟随类上的修改——这是“读走 MRO、写只写实例”规则的直接后果。
class 体:定义时执行一次
class Counter: 后面的缩进块在类被定义时执行一遍,不是每次 Counter() 都跑。类体里赋值的简单名字(如 step = 1)默认成为 类属性,存在类的 __dict__ 里:
class Demo:
label: str = "类级别"
count: int = 0
def __init__(self, name: str) -> None:
self.name = name
Demo.count += 1
print(Demo.label) # 类级别
print(Demo.count) # 0
d1 = Demo("Ada")
d2 = Demo("Bob")
print(Demo.count) # 2Demo.count += 1 在 __init__ 里改的是类属性——适合统计“一共造了几个实例”这类共享状态。若写成 self.count = self.count + 1 且之前没在实例上设过 count,会先读到类上的 0,再在实例上新建 count,与类属性脱钩。
类体里的 def 语句也会执行:它创建一个 函数 对象并绑到类名上,而不是绑到某个尚未存在的 实例。因此方法“定义在类上、运行在实例上”是 Python 对象模型的常态。你在 REPL 里 print(Counter.tick) 会看到 function,而 print(Counter(0).tick) 才会变成 method 对象——后面可调用对象一篇会把 method 与 函数 的关系说透。
init:实例化后的初始化
Counter() 这种写法会:先创建实例对象,再自动调用 __init__(self, ...)。__init__ 不负责分配内存(那是 __new__ 的事,下一篇细讲),只负责把刚造出来的 实例 填上初始状态:
class Point:
def __init__(self, x: float, y: float) -> None:
self.x = x
self.y = y
def distance_sq(self) -> float:
return self.x ** 2 + self.y ** 2
p = Point(3.0, 4.0)
print(p.distance_sq()) # 25.0构造器参数(除 self 外)就是你在 Point(3.0, 4.0) 里传的那些;没有类型检查器时,Python 运行时也不强制类型标注,但写上 float 有助于阅读和静态检查。
也可以给 __init__ 设默认值,或只用关键字参数,写法与函数篇相同——__init__ 本质上就是初始化用的普通函数,只是由解释器在实例创建后自动触发:
class Session:
def __init__(
self,
user: str,
*,
readonly: bool = False,
ttl: int = 3600,
) -> None:
self.user = user
self.readonly = readonly
self.ttl = ttl
s = Session("ada", readonly=True)
print(s.user, s.readonly) # ada True实例属性 vs 类属性
| 类属性 | 实例属性 | |
|---|---|---|
| 绑定位置 | ClassName.attr | instance.attr |
| 典型定义 | 类体顶层赋值 | 通常在 __init__ 里 self.x = ... |
| 共享性 | 所有实例共享同一份(除非被实例同名覆盖) | 每个实例独立 |
查找顺序:先实例 __dict__,再沿 MRO 找类。因此 self.step 若实例上没有,会读到 Counter.step。这是 属性查找 与 属性赋值 不对称的另一处体现:读会向上找 类属性,写 self.x = ... 却几乎总在实例上新建绑定(除非用了 __setattr__ 等钩子,后面章节再讲)。
用 hasattr(obj, "name") 和 getattr(obj, "name", default) 也会走同一套查找路径;调试“这个值从类来的还是从实例来的”时,可以临时 print(obj.__dict__, ClassName.__dict__) 对照。
class Config:
debug: bool = False
def __init__(self, name: str) -> None:
self.name = name
cfg = Config("app")
print(cfg.debug) # False,来自类
cfg.debug = True # 在实例上 shadow 类属性
print(Config.debug) # False,类上未变
print(cfg.debug) # True可变类属性(如 items: list = [])是常见坑:所有实例共享同一个 list 对象。可变默认值应放在 __init__ 里创建:
class BadBucket:
items: list[int] = [] # 危险:全实例共享
def add(self, n: int) -> None:
self.items.append(n)
class GoodBucket:
def __init__(self) -> None:
self.items: list[int] = []
def add(self, n: int) -> None:
self.items.append(n)
b1, b2 = BadBucket(), BadBucket()
b1.add(1)
print(b2.items) # [1] —— b2 也被污染
g1, g2 = GoodBucket(), GoodBucket()
g1.add(1)
print(g2.items) # []类体里带类型注解但不赋值的行(如 step: int)在运行时通常不会自动创建类属性,除非配合 dataclass 等工具;手写类时,共享状态用 Demo.count 这种显式赋值,实例状态用 self.x。
方法、self 与绑定
类体里的 def tick(self, ...) 定义的是函数对象;通过实例访问时,Python 会把实例“绑”成第一个参数,所以你写 a.tick() 而不是 tick(a):
class Greeter:
def __init__(self, prefix: str) -> None:
self.prefix = prefix
def greet(self, name: str) -> str:
return f"{self.prefix}, {name}"
g = Greeter("Hello")
print(g.greet("Ada")) # Hello, Ada
print(Greeter.greet(g, "Bob")) # 等价写法,少见self 只是约定俗成的参数名,不是关键字;但团队代码几乎都用 self,读起来一眼知道是实例方法。
类方法与静态方法(知道即可)
有时方法不需要实例,或需要拿到 class 而不是 self:
class Versioned:
schema: int = 1
@classmethod
def from_dict(cls, data: dict[str, str]) -> "Versioned":
obj = cls()
obj.payload = data
return obj
@staticmethod
def is_valid_key(key: str) -> bool:
return key.isidentifier()
v = Versioned.from_dict({"id": "1"})
print(Versioned.is_valid_key("ok")) # True@classmethod 第一个参数是 cls(类本身);@staticmethod 不自动绑实例或类。日常业务类里实例方法占绝大多数,这两种在工厂方法、解析入口里偶尔出现。
继承:轻量扩展
子类在括号里写父类名,会继承父类的属性和方法,并可在子类里覆盖或追加:
class Animal:
def __init__(self, name: str) -> None:
self.name = name
def speak(self) -> str:
return "..."
class Dog(Animal):
def speak(self) -> str:
return f"{self.name}: woof"
d = Dog("Rex")
print(d.speak()) # Rex: woofDog 没有写 __init__ 时,仍用 Animal.__init__。若子类要扩展初始化,应显式调用 super().__init__(name),避免漏掉父类逻辑:
class Employee:
def __init__(self, name: str, dept: str) -> None:
self.name = name
self.dept = dept
class Manager(Employee):
def __init__(self, name: str, dept: str, level: int) -> None:
super().__init__(name, dept)
self.level = level
m = Manager("Ada", "Eng", 3)
print(m.name, m.level) # Ada 3方法查找沿 MRO(方法解析顺序)向上;子类同名方法会覆盖父类实现。isinstance(d, Dog) 与 isinstance(d, Animal) 都为真——类型判断与对象创建顺序留到后面两篇。
从“对象图”理解挂接关系
可以把 class 想成两层挂点:类自己的命名空间(类属性、函数定义)和每个 实例 各自的命名空间(实例属性)。方法定义在类上,调用时通过 self 读到实例上的数据:
class Wallet:
currency: str = "CNY"
def __init__(self, owner: str, balance: float) -> None:
self.owner = owner
self.balance = balance
def deposit(self, amount: float) -> None:
self.balance += amount
w = Wallet("Bob", 100.0)
w.deposit(50.0)
print(w.owner, w.balance, w.currency) # Bob 150.0 CNYdeposit 挂在 Wallet 上,但 self.balance 读的是 w 这份实例状态。多个 Wallet 实例共享 currency 类属性,各自持有不同的 owner / balance。
若把 class 当作“工厂名字”,实例 是产线上的具体产品,self 就是方法内部指向“当前这件产品”的句柄。产品之间不共享实例字段,却可能共享贴在工厂墙上的 类属性 告示——除非某件产品自己贴了一张同名便签(实例 shadow)。
常见误解
- “类 = 实例的模具”只形容一半:类本身也是对象(类型对象),可挂属性、可当值传递。
- 在类体里写
self.x = 1会报错:类体执行时没有当前实例,self不存在。 - 把可变对象当类属性:多个实例会意外共享同一份 list/dict。
- 以为
self.attr = ...会写回类:赋值永远优先在 实例 上新建或更新绑定,除非显式写ClassName.attr = ...。 - 混淆“类型标注”与“运行时属性”:
name: str写在类体顶层不等于自动给每个实例创建name;实例字段仍要在__init__里赋值。
类语法把“有哪些字段、能做什么操作”写在一处;分清 class 上挂什么、每个 实例 上挂什么、self 在方法里指谁,后面读装饰器、描述符、魔术方法时才不会把查找规则搞混。下一篇会从 type(实例) 和 isinstance 回答“这个对象算哪一类”;再下一篇拆 __new__ / __init__ 的先后——本篇的 Counter() 与 __init__ 就挂在那条创建链上。
写业务类时,一个实用习惯是:所有会变的字段进 __init__,真正全局共享且不可变的配置才考虑 类属性;需要 per-instance 默认值但类型可变的(list、dict、set),永远在 __init__ 里 self.items = [] 新建,不要写在 class 体顶层。方法里读配置优先 self.attr,只有明确要读类级常量时才写 ClassName.CONST,避免不小心 shadow 后行为漂移。
命名与模块边界(简)
一个模块里可以定义多个 class;类名通常用 CapWords,实例变量用 snake_case。类定义本身不创建 实例,直到你写下 MyClass()。把“类型(class)”和“值(instance)”在命名上也区分开(例如 User vs user),团队协作时沟通成本会低很多。导入时 from module import Counter 得到的是 class 对象,可以传到别的函数里当参数,说明 class 与 函数 一样是一等值——这与下一篇 type、第十篇 callable 的叙述是同一套对象模型。
最后收束三个关键词:class 是模板与命名空间;实例 是模板造出的具体对象;self 是实例方法里指向“当前对象”的参数。搞清 类属性 与实例属性的读写规则,你就掌握了 Python 面向对象语法的大部分日常用法——更深的属性查找(property、描述符)会在后面章节展开,但都离不开“名字绑在哪个命名空间”这条线。
动手验证时,建议在 REPL 里分别 print(实例.__dict__) 与 print(类.__dict__),并对 类属性 做一次“只改类、不改实例”和“实例 shadow 后再改类”的对照实验——亲眼看到两次读取结果不同,比背规则牢固得多。
继承只点到为止:子类共享父类方法表,覆盖时 self 仍指向子类 实例。多重继承与 MRO 的完整规则超出本篇,但日常单继承写法已够覆盖多数业务 class 定义。遇到 super() 报错时,先确认 __init__ 签名是否与父类一致。