Python面向对象高级编程:__slots__@property、多重继承、定制类、枚举类与元类

上一篇我们已经系统学习了 Python 面向对象编程的基础部分,包括类、实例、封装、继承、多态,以及实例属性和类属性。

如果说上一篇解决的是“如何写一个类”,那么这一篇要解决的就是:

当类开始变复杂时,Python 还提供了哪些更灵活、更强大的面向对象能力?

Python 是一门动态语言,因此它的面向对象系统也比很多静态语言更灵活。你不仅可以定义类、创建实例、继承父类,还可以:

  • 限制实例可绑定的属性;
  • 把方法伪装成属性;
  • 通过多重继承组合多种能力;
  • 让自定义类看起来像内置类型一样自然;
  • 用枚举类表示一组固定常量;
  • 甚至在“类创建时”动态修改类的定义。

这些能力组合起来,就构成了 Python 中常说的“面向对象高级编程”。

本文会围绕下面几个主题展开:

  • __slots__:限制实例属性;
  • @property:把方法变成属性;
  • 多重继承与 MixIn;
  • 定制类:让对象支持 print()、索引、迭代、调用等行为;
  • 枚举类:更优雅地表示常量集合;
  • 元类:在类创建阶段定制类本身。

理解这一章之后,你会对 Python 的对象模型有更深一层的认识,也会更容易读懂很多框架代码中的“魔法写法”。

一、为什么还需要“面向对象高级编程”?

学完基础 OOP 后,很多人会觉得:

  • 类会写了;
  • 继承也懂了;
  • 多态也知道了;
  • 那为什么还要继续学这些“高级特性”?

原因在于,真实项目中的类,通常不会只停留在“有几个属性、几个方法”这么简单。

例如,你可能会遇到这些需求:

  • 希望对象只能拥有有限的几个属性,避免写错字段名;
  • 希望外部访问 obj.score 时自动做合法性校验;
  • 希望某个对象能像列表一样支持下标访问;
  • 希望某个实例能像函数一样被直接调用;
  • 希望一组状态值具备明确含义,而不是到处写魔法数字;
  • 希望框架在你定义类时自动帮你补充方法或收集字段信息。

这些问题,靠基础类定义虽然也能“勉强完成”,但写法往往不够优雅。Python 提供这些高级能力,就是为了让对象行为更自然、接口更清晰、代码更有表达力。


二、__slots__:限制实例可以绑定哪些属性

2.1 动态绑定属性的灵活性与问题

Python 是动态语言,这意味着类的实例通常可以在运行时随时绑定新属性。

例如:

1
2
3
4
5
6
7
8
class Student:
pass


s = Student()
s.name = 'Alice'
s.age = 20
s.score = 95

这种写法很灵活,但也带来一个问题:

  • 属性结构不固定;
  • 容易因为拼写错误绑定出错误字段;
  • 当对象数量很多时,也会增加一定的内存开销。

如果你希望某个类的实例只允许拥有固定几个属性,就可以使用 __slots__

2.2 __slots__ 的基本写法
1
2
class Student:
__slots__ = ('name', 'age')

这表示 Student 的实例只允许绑定:

  • name
  • age

例如:

1
2
3
s = Student()
s.name = 'Alice'
s.age = 20

这没有问题。

但如果再绑定一个不在 __slots__ 中的属性:

1
s.score = 95

通常就会抛出 AttributeError

2.3 __slots__ 的作用理解

你可以把 __slots__ 理解为:

提前声明这个类的实例允许有哪些属性。

它带来的好处主要有两个:

  1. 约束结构:避免对象被随意塞入额外字段;
  2. 节省内存:在大量实例场景下通常更明显。

不过对初学者来说,更重要的意义通常不是性能,而是“让对象结构更明确”。

2.4 __slots__ 对子类的影响

这里有一个很重要的注意点:

__slots__ 只对当前类本身生效,对子类默认不自动完全继承限制效果。

例如:

1
2
3
4
5
6
class Student:
__slots__ = ('name', 'age')


class GraduateStudent(Student):
pass

此时 GraduateStudent 的实例可能仍然可以绑定新的额外属性,除非子类自己也定义 __slots__

1
2
class GraduateStudent(Student):
__slots__ = ('research_topic',)

这时子类可用属性会在父类基础上继续扩展。

2.5 什么时候适合使用 __slots__

适合场景通常包括:

  • 这个类的实例字段非常明确;
  • 你不希望别人随意往实例上塞属性;
  • 会创建大量同类对象;
  • 你想提高字段拼写错误的暴露速度。

但如果你的对象本身就需要灵活地挂载很多动态属性,那就未必适合使用它。


三、@property:把方法变成“像属性一样用”的接口

3.1 为什么需要 @property

先看一个常见写法:

1
2
3
4
5
6
7
8
9
10
11
12
13
class Student:
def __init__(self):
self._score = 0

def get_score(self):
return self._score

def set_score(self, value):
if not isinstance(value, int):
raise ValueError('score 必须是整数')
if value < 0 or value > 100:
raise ValueError('score 必须在 0 到 100 之间')
self._score = value

使用时:

1
2
3
s = Student()
s.set_score(95)
print(s.get_score())

这种写法没有问题,但在 Python 里稍显啰嗦。因为从使用者角度看,score 更像是一个“属性”,而不是一个必须显式调用的动作。

所以 Python 提供了 @property,让你可以:

  • 内部仍通过方法控制逻辑;
  • 外部却像访问普通属性一样使用它。
3.2 @property 的基本写法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class Student:
def __init__(self):
self._score = 0

@property
def score(self):
return self._score

@score.setter
def score(self, value):
if not isinstance(value, int):
raise ValueError('score 必须是整数')
if value < 0 or value > 100:
raise ValueError('score 必须在 0 到 100 之间')
self._score = value

使用方式会变得更自然:

1
2
3
s = Student()
s.score = 95
print(s.score)

看起来像属性访问,但底层其实仍然执行了 getter 和 setter 方法。

3.3 @property 的核心价值

@property 的好处在于:

  1. 接口更自然:对外表现得像字段;
  2. 内部可控:可以做校验、转换、计算;
  3. 更利于封装:外部不需要知道内部实现细节。

这非常适合那些“逻辑上像属性,但访问时需要控制”的数据。

3.4 定义只读属性

如果只定义 getter,不定义 setter,那么这个属性就是只读属性。

1
2
3
4
5
6
7
8
9
10
11
class Student:
def __init__(self, birth_year):
self._birth_year = birth_year

@property
def birth_year(self):
return self._birth_year

@property
def age(self):
return 2026 - self._birth_year

这里:

  • birth_year 可以读取;
  • age 也是属性,但它不是直接存储的,而是根据出生年份计算出来的。

使用时:

1
2
3
s = Student(2000)
print(s.birth_year)
print(s.age)

如果你尝试:

1
s.age = 30

通常会报错,因为 age 没有 setter。

3.5 @property 的常见使用场景

比较适合使用 @property 的情况有:

  • 需要对赋值做合法性校验;
  • 希望把一个“计算结果”包装成属性;
  • 希望未来修改内部实现,但外部访问方式不变。

例如:

  • 分数范围检查;
  • 身高、体重、年龄等派生值;
  • 配置值的延迟计算或格式转换。

四、多重继承:一个类可以同时继承多个父类

4.1 Python 支持多重继承

很多编程语言只支持单继承,但 Python 支持多重继承。

例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class Runnable:
def run(self):
print('Running...')


class Flyable:
def fly(self):
print('Flying...')


class Animal:
pass


class Dog(Animal, Runnable):
pass


class Bat(Animal, Flyable):
pass

这意味着:

  • Dog 可以继承 Animal 的身份,同时拥有 run()
  • Bat 可以继承 Animal 的身份,同时拥有 fly()
4.2 为什么会需要多重继承?

因为现实中的分类往往不是单维度的。

比如:

  • 按“物种”分类:哺乳类、鸟类;
  • 按“能力”分类:能跑、能飞;
  • 按“饮食习惯”分类:肉食、草食;

如果把所有组合都写成单独类,类的数量会迅速膨胀。多重继承允许你把不同维度拆开,再按需组合。

4.3 MixIn:为类“混入”能力

在 Python 中,多重继承非常常见的一种设计方式叫 MixIn

通常做法是把“附加功能类”命名为 SomethingMixIn

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class RunnableMixIn:
def run(self):
print('Running...')


class CarnivorousMixIn:
def eat_meat(self):
print('Eating meat...')


class Mammal:
pass


class Dog(Mammal, RunnableMixIn, CarnivorousMixIn):
pass

这样一眼就能看出:

  • Mammal 是主线继承;
  • RunnableMixInCarnivorousMixIn 是额外混入的能力。
4.4 多重继承的使用建议

多重继承虽然强大,但也不能滥用。

比较好的使用方式通常是:

  • 主继承链保持清晰;
  • 附加功能用 MixIn 提供;
  • 避免多个父类之间职责严重重叠。

否则类关系会变得很难理解。

在学习阶段,你先掌握它的主要价值就够了:

多重继承不是为了堆砌复杂结构,而是为了组合多个相对独立的能力。


五、定制类:让自定义对象更像 Python 内置对象

Python 的很多内置对象之所以“自然”,是因为它们实现了很多特殊方法。例如:

  • print(obj)
  • len(obj)
  • obj[0]
  • for x in obj
  • obj()

这些看起来像语法,其实背后都和类中的特殊方法有关。

5.1 __str__():让打印结果更友好

默认情况下,打印一个实例,通常会得到类似这样的输出:

1
<__main__.Student object at 0x...>

这对人类并不友好。

可以通过 __str__() 改进:

1
2
3
4
5
6
class Student:
def __init__(self, name):
self.name = name

def __str__(self):
return f'Student(name={self.name})'

现在:

1
print(Student('Alice'))

输出就会更清晰。

很多时候还会顺手把 __repr__ 也指向它:

1
__repr__ = __str__

这样在交互环境里直接查看对象时,也更友好。

5.2 __iter__()__next__():让对象可迭代

如果你希望自己的对象也能被 for ... in ... 遍历,就需要让它成为可迭代对象。

例如定义一个斐波那契数列对象:

1
2
3
4
5
6
7
8
9
10
11
12
class Fib:
def __init__(self):
self.a, self.b = 0, 1

def __iter__(self):
return self

def __next__(self):
self.a, self.b = self.b, self.a + self.b
if self.a > 100:
raise StopIteration()
return self.a

这样就可以:

1
2
for n in Fib():
print(n)
5.3 __getitem__():支持下标和切片访问

如果你希望对象支持 obj[index] 这样的写法,就可以定义 __getitem__()

1
2
3
4
5
6
class Fib:
def __getitem__(self, n):
a, b = 1, 1
for _ in range(n):
a, b = b, a + b
return a

使用:

1
2
3
f = Fib()
print(f[0])
print(f[5])

进一步还可以支持切片,这样自定义对象就会更像列表。

5.4 __getattr__():拦截不存在的属性访问

当访问一个不存在的属性时,Python 默认会抛出异常。

但如果你定义了 __getattr__(),就可以自定义这种行为:

1
2
3
4
5
6
7
8
class Student:
def __init__(self):
self.name = 'Alice'

def __getattr__(self, attr):
if attr == 'score':
return 99
raise AttributeError(f"'Student' object has no attribute '{attr}'")

这时:

1
2
3
s = Student()
print(s.name)
print(s.score)

虽然 score 没有真实定义,但仍能返回结果。

这个能力在链式调用、懒加载对象、代理对象中非常常见。

5.5 __call__():让实例像函数一样可调用

如果一个类实现了 __call__(),那么它的实例就可以像函数一样使用。

1
2
3
4
5
6
class Student:
def __init__(self, name):
self.name = name

def __call__(self):
print(f'My name is {self.name}.')

使用时:

1
2
s = Student('Alice')
s()

这说明:

  • s 是对象;
  • 但它的行为又像函数。

这在回调对象、可调用配置对象、函数对象封装中非常实用。

5.6 定制类的核心理解

定制类最重要的思想不是“背一堆双下划线方法”,而是理解:

Python 很多看起来像语法糖的行为,本质上都可以通过特殊方法接入。

这也是为什么很多框架里的对象用起来非常自然,因为它们在底层定制了这些协议。


六、枚举类:用更清晰的方式表示一组固定常量

6.1 为什么普通常量不够好?

很多初学者一开始会这样表示星期:

1
2
3
MON = 1
TUE = 2
WED = 3

这种写法能用,但存在问题:

  • 可读性一般;
  • 类型上只是整数;
  • 不利于统一管理;
  • 很容易误传错误值。

Python 提供了 Enum 来更优雅地表达这种“固定常量集合”。

6.2 最基本的枚举类定义
1
2
3
4
from enum import Enum


Weekday = Enum('Weekday', ('Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'))

使用时:

1
2
3
print(Weekday.Mon)
print(Weekday.Mon.name)
print(Weekday.Mon.value)

这里:

  • .name 是成员名;
  • .value 是成员值。
6.3 更常见的写法:继承 Enum
1
2
3
4
5
6
7
8
9
10
11
12
from enum import Enum, unique


@unique
class Weekday(Enum):
Sun = 0
Mon = 1
Tue = 2
Wed = 3
Thu = 4
Fri = 5
Sat = 6

这里的 @unique 用来保证枚举值不重复。

使用时:

1
2
3
print(Weekday.Mon)
print(Weekday['Mon'])
print(Weekday.Mon.value)
6.4 枚举类适合哪些场景?

适合使用枚举类的地方非常多,例如:

  • 星期几
  • 订单状态
  • 支付方式
  • 用户角色
  • 请求阶段
  • 任务执行状态

它最大的价值在于:

  • 语义清晰;
  • 限定取值范围;
  • 比裸整数或裸字符串更安全。

七、元类:在“创建类”这一步定制类

7.1 先理解一个关键事实:类本身也是对象

在前面的学习里,我们常常说:

  • 先定义类;
  • 再根据类创建实例。

但在 Python 中,类本身其实也是对象。

例如:

1
2
3
4
5
6
7
8
class Hello:
pass


h = Hello()

print(type(h))
print(type(Hello))

这里:

  • h 的类型是 Hello
  • Hello 的类型通常是 type

也就是说:

普通实例由类创建,而类本身通常由 type 创建。

7.2 type() 不只是查看类型,还能创建类

我们平时常用:

1
print(type(123))

type() 还有另一种用法:直接动态创建类。

1
2
3
4
5
6
7
8
def hello(self, name='world'):
print(f'Hello, {name}.')


Hello = type('Hello', (object,), {'hello': hello})

h = Hello()
h.hello()

这里传给 type() 的三个参数分别可以理解为:

  1. 类名;
  2. 父类元组;
  3. 类属性和方法组成的字典。

这说明 Python 中类的定义,本质上是可以在运行时动态完成的。

7.3 什么是元类?

元类,英文是 metaclass。

可以把它简单理解为:

元类是“创建类的类”。

关系可以这样看:

  • 元类创建类;
  • 类创建实例。

所以如果普通类是“实例的模板”,那么元类就是“类的模板”。

7.4 一个简单的元类示例
1
2
3
4
5
6
7
8
class ListMetaclass(type):
def __new__(cls, name, bases, attrs):
attrs['add'] = lambda self, value: self.append(value)
return type.__new__(cls, name, bases, attrs)


class MyList(list, metaclass=ListMetaclass):
pass

使用:

1
2
3
4
L = MyList()
L.add(1)
L.add(2)
print(L)

这里发生了什么?

  • MyList 在创建时,不是直接按默认方式生成;
  • 而是先经过 ListMetaclass.__new__()
  • 在这个阶段,我们给类额外增加了一个 add() 方法。
7.5 元类真正适合什么场景?

说实话,元类是 Python 里最“魔法”的部分之一。

正常业务开发中,大多数人很少需要手写元类。它真正常见的场景通常是:

  • ORM 框架
  • 序列化框架
  • 类注册机制
  • 声明式 DSL
  • 大型框架底层能力构建

例如 ORM 中常见这种写法:

1
2
3
4
class User(Model):
id = IntegerField('id')
name = StringField('username')
email = StringField('email')

表面上看只是在定义类属性,但框架可以在类创建阶段扫描这些字段,自动构建“类属性 -> 数据库字段”的映射关系。

这就是元类最典型的应用思路:

  • 在“类刚被创建出来的时候”做一些自动处理。
7.6 学习元类的正确心态

对于初学者来说,最重要的不是马上写出复杂元类,而是理解这三点:

  1. 类本身也是对象
  2. 类可以在运行时动态创建
  3. 元类允许你拦截和定制类的创建过程。

只要理解到这里,这一节就已经足够了。

更复杂的元类技巧,通常等你接触框架源码时再深入,会更容易理解。


八、这些高级特性之间有什么联系?

这一章内容看起来比较散,但其实它们都围绕一个共同主题:

Python 允许你更深入地参与“对象行为”与“类行为”的定义。

可以简单这样理解:

  • __slots__:控制实例能拥有什么属性;
  • @property:控制属性访问时执行什么逻辑;
  • 多重继承:控制类如何组合多种能力;
  • 定制类:控制对象如何响应打印、索引、遍历、调用等操作;
  • 枚举类:控制固定值集合如何表达得更清晰;
  • 元类:控制类在创建时如何被加工。

也就是说,面向对象高级编程本质上是在回答两个问题:

  1. 对象应该如何表现得更自然?
  2. 类应该如何被更灵活地定义和组织?

九、学习建议:哪些内容要重点掌握,哪些先理解即可?

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 面向对象高级编程中的几个核心主题:

  1. __slots__:限制实例可绑定的属性,让对象结构更明确;
  2. @property:把方法包装成属性调用,让接口更自然,同时保留校验与控制能力;
  3. 多重继承:允许一个类组合多个父类能力;
  4. MixIn:为类混入附加功能,是多重继承中很常见的设计方式;
  5. 定制类:通过特殊方法让对象支持打印、索引、迭代、调用等行为;
  6. 枚举类:用更清晰、更安全的方式表示固定常量集合;
  7. 元类:在类创建阶段对类本身进行定制,是 Python 最具动态特征的能力之一。

如果说上一篇是在学习“如何写出面向对象代码”,那么这一篇更像是在学习:

如何让你的类和对象更像 Python 世界中的“原生公民”。

掌握这些内容后,你不仅能写出更自然的类,也更容易理解很多框架中的高级写法和底层设计思路。

继续往后学时,比较自然的衔接方向通常有两个:

  • 错误处理、调试与测试:把程序写得更稳、更容易排错;
  • 常用内建模块与标准库实战:进一步提升实际开发能力。

参考资料