Python面向对象高级编程:__slots__、@property、多重继承、定制类、枚举类与元类
上一篇我们已经系统学习了 Python 面向对象编程的基础部分,包括类、实例、封装、继承、多态,以及实例属性和类属性。
如果说上一篇解决的是“如何写一个类”,那么这一篇要解决的就是:
当类开始变复杂时,Python 还提供了哪些更灵活、更强大的面向对象能力?
Python 是一门动态语言,因此它的面向对象系统也比很多静态语言更灵活。你不仅可以定义类、创建实例、继承父类,还可以:
- 限制实例可绑定的属性;
- 把方法伪装成属性;
- 通过多重继承组合多种能力;
- 让自定义类看起来像内置类型一样自然;
- 用枚举类表示一组固定常量;
- 甚至在“类创建时”动态修改类的定义。
这些能力组合起来,就构成了 Python 中常说的“面向对象高级编程”。
本文会围绕下面几个主题展开:
__slots__:限制实例属性;@property:把方法变成属性;- 多重继承与 MixIn;
- 定制类:让对象支持
print()、索引、迭代、调用等行为; - 枚举类:更优雅地表示常量集合;
- 元类:在类创建阶段定制类本身。
理解这一章之后,你会对 Python 的对象模型有更深一层的认识,也会更容易读懂很多框架代码中的“魔法写法”。
一、为什么还需要“面向对象高级编程”?
学完基础 OOP 后,很多人会觉得:
- 类会写了;
- 继承也懂了;
- 多态也知道了;
- 那为什么还要继续学这些“高级特性”?
原因在于,真实项目中的类,通常不会只停留在“有几个属性、几个方法”这么简单。
例如,你可能会遇到这些需求:
- 希望对象只能拥有有限的几个属性,避免写错字段名;
- 希望外部访问
obj.score时自动做合法性校验; - 希望某个对象能像列表一样支持下标访问;
- 希望某个实例能像函数一样被直接调用;
- 希望一组状态值具备明确含义,而不是到处写魔法数字;
- 希望框架在你定义类时自动帮你补充方法或收集字段信息。
这些问题,靠基础类定义虽然也能“勉强完成”,但写法往往不够优雅。Python 提供这些高级能力,就是为了让对象行为更自然、接口更清晰、代码更有表达力。
二、__slots__:限制实例可以绑定哪些属性
2.1 动态绑定属性的灵活性与问题
Python 是动态语言,这意味着类的实例通常可以在运行时随时绑定新属性。
例如:
1 | class Student: |
这种写法很灵活,但也带来一个问题:
- 属性结构不固定;
- 容易因为拼写错误绑定出错误字段;
- 当对象数量很多时,也会增加一定的内存开销。
如果你希望某个类的实例只允许拥有固定几个属性,就可以使用 __slots__。
2.2 __slots__ 的基本写法
1 | class Student: |
这表示 Student 的实例只允许绑定:
nameage
例如:
1 | s = Student() |
这没有问题。
但如果再绑定一个不在 __slots__ 中的属性:
1 | s.score = 95 |
通常就会抛出 AttributeError。
2.3 __slots__ 的作用理解
你可以把 __slots__ 理解为:
提前声明这个类的实例允许有哪些属性。
它带来的好处主要有两个:
- 约束结构:避免对象被随意塞入额外字段;
- 节省内存:在大量实例场景下通常更明显。
不过对初学者来说,更重要的意义通常不是性能,而是“让对象结构更明确”。
2.4 __slots__ 对子类的影响
这里有一个很重要的注意点:
__slots__只对当前类本身生效,对子类默认不自动完全继承限制效果。
例如:
1 | class Student: |
此时 GraduateStudent 的实例可能仍然可以绑定新的额外属性,除非子类自己也定义 __slots__。
1 | class GraduateStudent(Student): |
这时子类可用属性会在父类基础上继续扩展。
2.5 什么时候适合使用 __slots__?
适合场景通常包括:
- 这个类的实例字段非常明确;
- 你不希望别人随意往实例上塞属性;
- 会创建大量同类对象;
- 你想提高字段拼写错误的暴露速度。
但如果你的对象本身就需要灵活地挂载很多动态属性,那就未必适合使用它。
三、@property:把方法变成“像属性一样用”的接口
3.1 为什么需要 @property?
先看一个常见写法:
1 | class Student: |
使用时:
1 | s = Student() |
这种写法没有问题,但在 Python 里稍显啰嗦。因为从使用者角度看,score 更像是一个“属性”,而不是一个必须显式调用的动作。
所以 Python 提供了 @property,让你可以:
- 内部仍通过方法控制逻辑;
- 外部却像访问普通属性一样使用它。
3.2 @property 的基本写法
1 | class Student: |
使用方式会变得更自然:
1 | s = Student() |
看起来像属性访问,但底层其实仍然执行了 getter 和 setter 方法。
3.3 @property 的核心价值
@property 的好处在于:
- 接口更自然:对外表现得像字段;
- 内部可控:可以做校验、转换、计算;
- 更利于封装:外部不需要知道内部实现细节。
这非常适合那些“逻辑上像属性,但访问时需要控制”的数据。
3.4 定义只读属性
如果只定义 getter,不定义 setter,那么这个属性就是只读属性。
1 | class Student: |
这里:
birth_year可以读取;age也是属性,但它不是直接存储的,而是根据出生年份计算出来的。
使用时:
1 | s = Student(2000) |
如果你尝试:
1 | s.age = 30 |
通常会报错,因为 age 没有 setter。
3.5 @property 的常见使用场景
比较适合使用 @property 的情况有:
- 需要对赋值做合法性校验;
- 希望把一个“计算结果”包装成属性;
- 希望未来修改内部实现,但外部访问方式不变。
例如:
- 分数范围检查;
- 身高、体重、年龄等派生值;
- 配置值的延迟计算或格式转换。
四、多重继承:一个类可以同时继承多个父类
4.1 Python 支持多重继承
很多编程语言只支持单继承,但 Python 支持多重继承。
例如:
1 | class Runnable: |
这意味着:
Dog可以继承Animal的身份,同时拥有run();Bat可以继承Animal的身份,同时拥有fly()。
4.2 为什么会需要多重继承?
因为现实中的分类往往不是单维度的。
比如:
- 按“物种”分类:哺乳类、鸟类;
- 按“能力”分类:能跑、能飞;
- 按“饮食习惯”分类:肉食、草食;
如果把所有组合都写成单独类,类的数量会迅速膨胀。多重继承允许你把不同维度拆开,再按需组合。
4.3 MixIn:为类“混入”能力
在 Python 中,多重继承非常常见的一种设计方式叫 MixIn。
通常做法是把“附加功能类”命名为 SomethingMixIn:
1 | class RunnableMixIn: |
这样一眼就能看出:
Mammal是主线继承;RunnableMixIn、CarnivorousMixIn是额外混入的能力。
4.4 多重继承的使用建议
多重继承虽然强大,但也不能滥用。
比较好的使用方式通常是:
- 主继承链保持清晰;
- 附加功能用 MixIn 提供;
- 避免多个父类之间职责严重重叠。
否则类关系会变得很难理解。
在学习阶段,你先掌握它的主要价值就够了:
多重继承不是为了堆砌复杂结构,而是为了组合多个相对独立的能力。
五、定制类:让自定义对象更像 Python 内置对象
Python 的很多内置对象之所以“自然”,是因为它们实现了很多特殊方法。例如:
print(obj)len(obj)obj[0]for x in objobj()
这些看起来像语法,其实背后都和类中的特殊方法有关。
5.1 __str__():让打印结果更友好
默认情况下,打印一个实例,通常会得到类似这样的输出:
1 | <__main__.Student object at 0x...> |
这对人类并不友好。
可以通过 __str__() 改进:
1 | class Student: |
现在:
1 | print(Student('Alice')) |
输出就会更清晰。
很多时候还会顺手把 __repr__ 也指向它:
1 | __repr__ = __str__ |
这样在交互环境里直接查看对象时,也更友好。
5.2 __iter__() 和 __next__():让对象可迭代
如果你希望自己的对象也能被 for ... in ... 遍历,就需要让它成为可迭代对象。
例如定义一个斐波那契数列对象:
1 | class Fib: |
这样就可以:
1 | for n in Fib(): |
5.3 __getitem__():支持下标和切片访问
如果你希望对象支持 obj[index] 这样的写法,就可以定义 __getitem__()。
1 | class Fib: |
使用:
1 | f = Fib() |
进一步还可以支持切片,这样自定义对象就会更像列表。
5.4 __getattr__():拦截不存在的属性访问
当访问一个不存在的属性时,Python 默认会抛出异常。
但如果你定义了 __getattr__(),就可以自定义这种行为:
1 | class Student: |
这时:
1 | s = Student() |
虽然 score 没有真实定义,但仍能返回结果。
这个能力在链式调用、懒加载对象、代理对象中非常常见。
5.5 __call__():让实例像函数一样可调用
如果一个类实现了 __call__(),那么它的实例就可以像函数一样使用。
1 | class Student: |
使用时:
1 | s = Student('Alice') |
这说明:
s是对象;- 但它的行为又像函数。
这在回调对象、可调用配置对象、函数对象封装中非常实用。
5.6 定制类的核心理解
定制类最重要的思想不是“背一堆双下划线方法”,而是理解:
Python 很多看起来像语法糖的行为,本质上都可以通过特殊方法接入。
这也是为什么很多框架里的对象用起来非常自然,因为它们在底层定制了这些协议。
六、枚举类:用更清晰的方式表示一组固定常量
6.1 为什么普通常量不够好?
很多初学者一开始会这样表示星期:
1 | MON = 1 |
这种写法能用,但存在问题:
- 可读性一般;
- 类型上只是整数;
- 不利于统一管理;
- 很容易误传错误值。
Python 提供了 Enum 来更优雅地表达这种“固定常量集合”。
6.2 最基本的枚举类定义
1 | from enum import Enum |
使用时:
1 | print(Weekday.Mon) |
这里:
.name是成员名;.value是成员值。
6.3 更常见的写法:继承 Enum
1 | from enum import Enum, unique |
这里的 @unique 用来保证枚举值不重复。
使用时:
1 | print(Weekday.Mon) |
6.4 枚举类适合哪些场景?
适合使用枚举类的地方非常多,例如:
- 星期几
- 订单状态
- 支付方式
- 用户角色
- 请求阶段
- 任务执行状态
它最大的价值在于:
- 语义清晰;
- 限定取值范围;
- 比裸整数或裸字符串更安全。
七、元类:在“创建类”这一步定制类
7.1 先理解一个关键事实:类本身也是对象
在前面的学习里,我们常常说:
- 先定义类;
- 再根据类创建实例。
但在 Python 中,类本身其实也是对象。
例如:
1 | class Hello: |
这里:
h的类型是Hello;Hello的类型通常是type。
也就是说:
普通实例由类创建,而类本身通常由
type创建。
7.2 type() 不只是查看类型,还能创建类
我们平时常用:
1 | print(type(123)) |
但 type() 还有另一种用法:直接动态创建类。
1 | def hello(self, name='world'): |
这里传给 type() 的三个参数分别可以理解为:
- 类名;
- 父类元组;
- 类属性和方法组成的字典。
这说明 Python 中类的定义,本质上是可以在运行时动态完成的。
7.3 什么是元类?
元类,英文是 metaclass。
可以把它简单理解为:
元类是“创建类的类”。
关系可以这样看:
- 元类创建类;
- 类创建实例。
所以如果普通类是“实例的模板”,那么元类就是“类的模板”。
7.4 一个简单的元类示例
1 | class ListMetaclass(type): |
使用:
1 | L = MyList() |
这里发生了什么?
MyList在创建时,不是直接按默认方式生成;- 而是先经过
ListMetaclass.__new__(); - 在这个阶段,我们给类额外增加了一个
add()方法。
7.5 元类真正适合什么场景?
说实话,元类是 Python 里最“魔法”的部分之一。
正常业务开发中,大多数人很少需要手写元类。它真正常见的场景通常是:
- ORM 框架
- 序列化框架
- 类注册机制
- 声明式 DSL
- 大型框架底层能力构建
例如 ORM 中常见这种写法:
1 | class User(Model): |
表面上看只是在定义类属性,但框架可以在类创建阶段扫描这些字段,自动构建“类属性 -> 数据库字段”的映射关系。
这就是元类最典型的应用思路:
- 在“类刚被创建出来的时候”做一些自动处理。
7.6 学习元类的正确心态
对于初学者来说,最重要的不是马上写出复杂元类,而是理解这三点:
- 类本身也是对象;
- 类可以在运行时动态创建;
- 元类允许你拦截和定制类的创建过程。
只要理解到这里,这一节就已经足够了。
更复杂的元类技巧,通常等你接触框架源码时再深入,会更容易理解。
八、这些高级特性之间有什么联系?
这一章内容看起来比较散,但其实它们都围绕一个共同主题:
Python 允许你更深入地参与“对象行为”与“类行为”的定义。
可以简单这样理解:
__slots__:控制实例能拥有什么属性;@property:控制属性访问时执行什么逻辑;- 多重继承:控制类如何组合多种能力;
- 定制类:控制对象如何响应打印、索引、遍历、调用等操作;
- 枚举类:控制固定值集合如何表达得更清晰;
- 元类:控制类在创建时如何被加工。
也就是说,面向对象高级编程本质上是在回答两个问题:
- 对象应该如何表现得更自然?
- 类应该如何被更灵活地定义和组织?
九、学习建议:哪些内容要重点掌握,哪些先理解即可?
9.1 优先真正掌握 @property
在这些高级特性里,最常用、最值得立即掌握的通常是 @property。
因为它在实际开发中非常常见,而且确实能明显改善接口设计。
9.2 __slots__ 要知道用途和限制
你不一定每天都会写 __slots__,但至少要知道:
- 它能限制实例属性;
- 它和动态绑定属性的默认行为不同;
- 它对继承场景有自己的规则。
9.3 多重继承要重点理解 MixIn 思路
比起死记“一个类可以继承多个类”,更重要的是理解:
- 主线继承负责“是什么”;
- MixIn 负责“额外能力”。
这样你以后看到框架中的 XXXMixIn 才不会陌生。
9.4 定制类先掌握几个最常见的特殊方法
优先熟悉下面这些就够用了:
__str__()__repr__()__iter__()__next__()__getitem__()__getattr__()__call__()
你不用一开始就把所有特殊方法背下来,但至少要知道:
- 它们的存在;
- 它们分别影响什么行为。
9.5 元类先理解概念,不必急着重度使用
元类是“理解型知识”非常重要,但“使用型知识”不一定马上高频。
你只要先知道:
- 类是对象;
type()可以创建类;- 元类可以定制类创建过程;
这就已经足够超过很多初学者了。
十、总结
这一篇我们系统梳理了 Python 面向对象高级编程中的几个核心主题:
__slots__:限制实例可绑定的属性,让对象结构更明确;@property:把方法包装成属性调用,让接口更自然,同时保留校验与控制能力;- 多重继承:允许一个类组合多个父类能力;
- MixIn:为类混入附加功能,是多重继承中很常见的设计方式;
- 定制类:通过特殊方法让对象支持打印、索引、迭代、调用等行为;
- 枚举类:用更清晰、更安全的方式表示固定常量集合;
- 元类:在类创建阶段对类本身进行定制,是 Python 最具动态特征的能力之一。
如果说上一篇是在学习“如何写出面向对象代码”,那么这一篇更像是在学习:
如何让你的类和对象更像 Python 世界中的“原生公民”。
掌握这些内容后,你不仅能写出更自然的类,也更容易理解很多框架中的高级写法和底层设计思路。
继续往后学时,比较自然的衔接方向通常有两个:
- 错误处理、调试与测试:把程序写得更稳、更容易排错;
- 常用内建模块与标准库实战:进一步提升实际开发能力。