K 的一隅

Python Python 语言核心

实例、属性和方法:Python 类语法怎么挂东西

从 class 语法到 __init__、实例属性与类属性的区别,以及实例方法里 self 的含义;顺带一提继承的基本写法。

10 分钟阅读 更新于 2026-07-28
本文目录

上一篇讲 lambda 与函数对象;从这一篇起,数据与行为被 class 包成模板,再按需造出一个个 实例。类不是“带类型的结构体”那么简单——它定义的是命名空间 + 创建协议:类体里写的名字,有的挂在类上,有的挂在每个实例上;方法第一个参数惯例叫 self,指向当前实例。

和其他语言里“先写结构体再 typedef”不同,Python 的 class 在定义时就运行类体代码,并把名字登记到类的命名空间里。之后每次写 Counter(),解释器按创建协议造出新对象,并把你在 __init__ 里写的 self.x = ... 绑到那一份 实例 上。读类定义时,要同时看“类级别共享什么”和“每个对象各自持有什么”。

下面这段代码可以直接跑,用来建立直觉:

python
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)        # 1

ab 是同一 class 造出的两个 实例self.value 各管各的,而 Counter.step类属性,改类上的 step 会影响所有还没被实例覆盖的读取路径。

类属性 会影响后续所有通过“实例上没有同名属性”这条路径读到的值;在类上执行 Counter.step = 2 后,尚未 shadow 的实例都会看到新步长。若某个实例已经执行过 self.step = 1,它有自己的副本,不再跟随类上的修改——这是“读走 MRO、写只写实例”规则的直接后果。

class 体:定义时执行一次

class Counter: 后面的缩进块在类被定义时执行一遍,不是每次 Counter() 都跑。类体里赋值的简单名字(如 step = 1)默认成为 类属性,存在类的 __dict__ 里:

python
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)   # 2

Demo.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__ 的事,下一篇细讲),只负责把刚造出来的 实例 填上初始状态:

python
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__ 本质上就是初始化用的普通函数,只是由解释器在实例创建后自动触发:

python
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.attrinstance.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__) 对照。

python
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__ 里创建:

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

python
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

python
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 不自动绑实例或类。日常业务类里实例方法占绝大多数,这两种在工厂方法、解析入口里偶尔出现。

继承:轻量扩展

子类在括号里写父类名,会继承父类的属性和方法,并可在子类里覆盖或追加:

python
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: woof

Dog 没有写 __init__ 时,仍用 Animal.__init__。若子类要扩展初始化,应显式调用 super().__init__(name),避免漏掉父类逻辑:

python
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 读到实例上的数据:

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

deposit 挂在 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__ 签名是否与父类一致。