上一篇 类与对象基础 里讲了类的骨架。这一篇要深入类里的”血肉”:属性怎么组织、方法怎么调用、self 背后到底发生了什么。你会看到 Python 的 OOP 比 Java 灵活得多:属性可以动态添加、方法可以借给别的对象、self 只是位置参数……理解这些能让你写出更 Pythonic 的类。
一、属性的三种来源
一个对象的属性可能来自三处:
- 实例属性(
self.x = ...)——每个实例独立; - 类属性(在类体里赋值)——所有实例共享;
- 继承的属性——从父类来的。
访问 obj.x 时,Python 按 实例 → 类 → 父类 → 父父类 … 的顺序查找(MRO 顺序,下一篇讲)。
二、动态添加属性
Python 允许你在任何时候给一个实例加属性:
1 | class Foo: |
灵活是灵活,但不建议在生产代码里滥用——IDE 无法补全,读代码的人一头雾水。规范做法是在 __init__ 里把所有实例属性声明清楚。
三、如何限制属性 —— __slots__
如果一个类会创建大量实例,或你希望”禁止使用者随便加属性”,用 __slots__:
1 | class Point: |
好处:
- 每个实例内存大幅减少(不再需要
__dict__); - 属性名拼错立刻报错;
- 属性访问略快。
代价:
- 不能动态加属性;
- 子类要自己声明
__slots__,否则会退回普通模式。
大规模建模(几百万实例)时非常有价值。日常小类不必强求。
四、__dict__:实例属性的字典
普通类的实例其实是个字典 + 一些别的:
1 | class User: |
这就是为什么 Python 属性这么”动态”——它们真的存在字典里。
五、hasattr / getattr / setattr / delattr
前面 常用内置函数汇总 提过:
1 | u = User("咖飞", 3) |
反射用法适合写”框架/序列化/迁移工具”,日常业务不常用。
六、方法的绑定
看这段代码:
1 | class Dog: |
Dog.bark是普通函数;d.bark是绑定方法——Python 自动把d绑定为self。
绑定发生在属性查找那一刻,用的是描述符协议(进阶话题,可以先不深究)。
七、方法可以互相调用
1 | class Circle: |
一个方法调用另一个用 self.method()——因为 self 就是当前实例,self.method 就是绑定方法。
八、property:把方法伪装成属性
有时候某个”属性”其实是计算出来的,你想让使用者像访问属性一样访问它,用 @property:
1 | class Circle: |
好处:
- 使用者不用关心是”字段”还是”计算”;
- 将来把字段改成计算属性时,调用方代码不用改;
- 可以配合 setter/deleter 加验证。
完整的 property 讲解见 类方法、静态方法与 property。
九、动态改方法
Python 里方法也是属性,一样可以动态改:
1 | class Foo: |
或者只改一个实例:
1 | import types |
工程实践里几乎不用,但你知道能这么玩就好。
十、self 只是约定
前面说过——Python 里 self 不是关键字,你写别的也行:
1 | class Foo: |
但请永远写 self。整个 Python 社区都这么写,标新立异只会让代码难读。
十一、方法的三种类型
Python 类里有三种方法:
- 实例方法:第一个参数是
self,最常见; - 类方法:
@classmethod,第一个参数是cls(类本身); - 静态方法:
@staticmethod,不接受隐式参数,本质就是”放在类里的普通函数”。
1 | class Foo: |
具体用法见 类方法、静态方法与 property。
十二、类属性 vs 实例属性的读写规则
1 | class Foo: |
规则:
- 读属性:先实例,再类;
- 写属性:永远写实例属性(除非用
ClassName.attr = ...)。
用类属性做”实例计数”、”共享配置”时要用类名写:
1 | class Widget: |
十三、封装的实际做法
Java 里”getter/setter”是标配。Python 完全不必:
1 | # ❌ Java 风格 |
Python 提倡”直接访问属性”。如果将来需要加验证或计算,再改成 @property——调用方代码不用改一个字。
十四、__slots__ 与继承
__slots__ 需要从上到下都声明才有效:
1 | class Parent: |
如果任何一层没写 __slots__,就会退回普通 __dict__ 模式。
十五、一个完整例子:银行账户
1 | # bank.py |
十六、常见陷阱
- 忘了
self:def method(x)会导致调用时报错。 - 给类属性
+=变成加实例属性:想改类属性用ClassName.attr = ...。 __slots__与 dataclass 组合:3.10+ 用@dataclass(slots=True)。- 过度封装:写一堆 getter/setter 不 Pythonic,直接暴露属性即可。
- 动态属性拼写错误:
f.nmae = "x"不报错,但读的时候没有。用__slots__或 dataclass 可以避免。 - 修改方法查找规则:一般别改,除非你在写元编程。
十七、小结与延伸阅读
- 实例属性在
__init__里初始化,类属性写在类体; self.x读时查实例→类,写时只创建实例属性;hasattr/getattr/setattr是反射三件套;__slots__减内存、防止乱加属性;- 方法可以互相调用,
self.method()就是绑定方法; - Python 不写 getter/setter,需要计算/验证时用
@property; self只是约定,永远写 self。
延伸阅读:
- 描述符协议:https://docs.python.org/zh-cn/3/howto/descriptor.html
- slots 讨论:https://docs.python.org/zh-cn/3/reference/datamodel.html#slots
下一篇 继承与多态 我们讲 OOP 的核心特性之一——从父类继承。