前言
嵌套类(nested class)指在另一个类的类体里再定义一个class。它常被用来做命名空间分组、定义只服务于外层的辅助类型(helper type),或者给namedtuple之类的东西找个归属。
需要先纠正一个普遍误解:Python 的嵌套类不是 Java 的"内部类"。Java 的非静态内部类实例会隐式持有外部类实例的引用,Python 完全没有这个机制——嵌套类的实例就是一个普通实例,它不知道自己属于哪个外层对象。另一个误解是"嵌套类体里能直接用到外层类的变量",这一点在 Python 里同样是错的,因为类体不构成闭包作用域。
本文讲清嵌套类的定义、访问路径、作用域规则和常见用法,并给出全部可以复制运行的示例。
一、定义与访问路径
嵌套类就是放在类体中的class语句。类体执行时,这个class语句会创建一个新的类对象,并把名字绑定到外层类的命名空间里。所以它本质上就是外层类的一个类属性。
# 适用于 Python 3.8+
class Robot:
class Motor: # 嵌套类
def __init__(self, power):
self.power = power
def run(self):
return f"power={self.power}"
def __init__(self):
self.motor = Robot.Motor(10) # 在外层方法里使用
def start(self):
return self.motor.run()
r = Robot()
print(r.start()) # power=10
print(Robot.Motor) # <class '__main__.Robot.Motor'>
print(Robot.Motor(3).run()) # power=3访问路径有两条:Outer.Inner(推荐,明确)以及self.Inner(通过实例查类属性,也能找到,因为属性查找会沿 MRO 上溯到类型)。注意Inner这个名字不会出现在模块全局命名空间里,所以在模块级别直接写Inner会得到NameError。
二、作用域规则:类体不是闭包
这是嵌套类最容易踩的地方。Python 有一个明确的规则:类体不参与嵌套作用域的查找。也就是说,在嵌套类的类体里,看不到外层类体的名字。
# 适用于 Python 3.8+
class Outer:
limit = 10
class Inner:
value = limit # NameError: name 'limit' is not defined原因:Inner的类体在求值时,名字查找顺序是"自身类体局部 → 全局 → 内置",而外层类体Outer的命名空间不属于其中任何一环。要引用外层类属性,必须带上完整路径,并且要保证Outer这个名字在当前作用域可见:
# 适用于 Python 3.8+
class Outer:
limit = 10
class Inner:
value = None # 先占位
Inner.value = limit # 这行在外层类体里,limit 可见
print(Outer.Inner.value) # 10或者让嵌套类的方法在运行时去拿:
# 适用于 Python 3.8+
class Outer:
limit = 10
class Inner:
def show(self):
return Outer.limit # Outer 是模块级名字,运行时可见
print(Outer.Inner().show()) # 10注意第二种写法依赖Outer是模块级的全局名字;如果外层类本身也是嵌套的,就要写更长的路径。
三、嵌套类与外层实例没有隐式关系
在 Java 里,内部类实例自动持有一个外部类实例引用。Python 里没有这回事。如果你希望嵌套类实例访问外层对象的状态,必须显式把外层对象传进去。
# 适用于 Python 3.8+
class Player:
class Stats:
def __init__(self, owner):
self.owner = owner # 显式持有
def describe(self):
return f"{self.owner.name}: {self.owner.hp}"
def __init__(self, name, hp=100):
self.name = name
self.hp = hp
self.stats = Player.Stats(self) # 把 self 传进去
def show(self):
return self.stats.describe()
p = Player("Ada", 80)
print(p.show()) # Ada: 80这种"显式传引用"的写法有一个副作用值得注意:它会让外层对象与嵌套类实例互相引用,形成引用环。这本身没错,但会推迟回收时机,细节见相关内存主题的分析。
四、什么时候值得用嵌套类
| 场景 | 是否推荐嵌套类 | 说明 |
|---|
| 纯粹为外层服务的辅助类型 | 推荐 | 避免污染模块命名空间 |
| 给一组常量/配置分组 | 推荐 | Config.Timeout.SHORT可读性好 |
| 需要在外部大量独立使用 | 不推荐 | 直接定义成模块级类更清晰 |
| 想共享外层实例状态 | 不推荐 | Python 无隐式引用,需手动传self |
| 需要被序列化传输 | 谨慎 | 局部定义的类无法被 pickle |
# 适用于 Python 3.8+
class Http:
class Timeout:
SHORT = 2
LONG = 30
class Status:
OK = 200
NOT_FOUND = 404
print(Http.Timeout.SHORT) # 2
print(Http.Status.OK) # 200
print(Http.Timeout.__qualname__) # Http.Timeout
print(Http.Timeout.__name__) # Timeout__qualname__(限定名)包含外层路径,__name__只有短名。做日志或调试输出时用__qualname__能定位得更准。
五、嵌套类与namedtuple式的轻量类型
嵌套类常用来放置轻量的数据容器,尤其当这个容器只在本模块内流转时:
# 适用于 Python 3.8+
from collections import namedtuple
class Shape:
Point = namedtuple("Point", ["x", "y"])
def __init__(self, x, y):
self.origin = Shape.Point(x, y)
def __repr__(self):
return f"Shape({self.origin.x}, {self.origin.y})"
print(Shape(1, 2)) # Shape(1, 2)
print(Shape.Point(3, 4).y) # 4namedtuple的类名参数与绑定变量名可以不同,但保持一致最不容易迷糊。
常见坑点
- 以为嵌套类实例自带外层引用
❌
``python # 适用于 Python 3.8+ class Outer: class Inner: def show(self): return self.owner # AttributeError``
✅
``python # 适用于 Python 3.8+ class Outer: class Inner: def __init__(self, owner): self.owner = owner``
- 在嵌套类体里直接引用外层类变量
❌
```python # 适用于 Python 3.8+ class Outer: n = 3
class Inner: m = n # NameError ```
✅
```python # 适用于 Python 3.8+ class Outer: n = 3
class Inner: pass
Outer.Inner.m = Outer.n ```
- 在模块级用短名访问嵌套类
❌
```python # 适用于 Python 3.8+ class Outer: class Inner: pass
x = Inner() # NameError ```
✅
``python # 适用于 Python 3.8+ x = Outer.Inner()``
- 想 pickle 局部定义的类
❌
```python # 适用于 Python 3.8+ import pickle
def make(): class Local: pass
return pickle.dumps(Local()) # PicklingError ```
✅
```python # 适用于 Python 3.8+ class Top: pass
pickle.dumps(Top()) # 模块级、可回查的类型才能 pickle ```
- 用实例赋值意外遮蔽嵌套类
❌
```python # 适用于 Python 3.8+ class Outer: class Inner: pass
o = Outer() o.Inner = "not a class" # 实例字典里多了一项,类没变 print(Outer.Inner) # 仍是原类,容易让人困惑 ```
✅
``python # 适用于 Python 3.8+ o = Outer() print(o.Inner) # 读就沿类找,别往实例上写同名属性``
- 混淆
__name__与__qualname__
❌
``python # 适用于 Python 3.8+ print(Outer.Inner.__name__) # 'Inner',日志里看不出层级``
✅
``python # 适用于 Python 3.8+ print(Outer.Inner.__qualname__) # 'Outer.Inner'``
- 以为嵌套类会自动实例化
❌
``python # 适用于 Python 3.8+ class Outer: class Inner: def __init__(self): self.v = 1 # 以为 Outer() 就有了 Inner 实例``
✅
```python # 适用于 Python 3.8+ class Outer: def __init__(self): self.inner = Outer.Inner()
class Inner: def __init__(self): self.v = 1 ```
总结
| 问题 | 结论 |
|---|
| 嵌套类是什么 | 外层类命名空间里的一个类属性 |
| 访问方式 | Outer.Inner或self.Inner |
| 作用域 | 类体不是闭包,看不到外层类名 |
| 与外层实例的关系 | 无隐式引用,需手动传 |
__name__与__qualname__ | 后者含外层路径 |
| 序列化 | 局部定义的类无法 pickle |
嵌套类适合做分组与局部辅助类型的封装,它不提供任何"内层对象知道外层对象"的魔法。写的时候牢记两条:类体不是闭包作用域,引用外层属性要写全路径;需要外层状态就显式把引用传进去。把这两条守住,嵌套类就是干净好用的工具。