Python 定制类:给类装上"智能感应器"
引言:从"毛坯房"到"智能家居"
想象你买了一套毛坯房(默认的 Python 类):
- 墙是白的,门是木门,灯是灯泡——能用,但不够"智能";
- 你想要:进门自动亮灯、说声"开空调"就制冷、喊"关窗帘"就合上——这就是定制。
Python 的定制类就是给毛坯房装上各种智能感应器:
__str__:回家自动报"欢迎主人回家"(打印时显示友好信息);__iter__:说声"开灯"就逐个亮灯(for 循环遍历);__getitem__:按房间号取东西(下标/切片访问);__getattr__:喊任何需求都有回应(动态属性);__call__:拍拍房子就能对话(实例直接调用)。
这些形如 __xxx__ 的方法叫特殊方法(也叫"魔术方法"),它们让类表现得像 Python 内置类型(list、dict、str)一样自然。
一、__str__ 与 __repr__:类的"自我介绍"
1.1 问题:打印实例像看天书
class Student(object):
def __init__(self, name):
self.name = name
print(Student('Michael'))
# <__main__.Student object at 0x109afb190>这串地址码对用户毫无意义。
1.2 __str__:给用户看的自我介绍
class Student(object):
def __init__(self, name):
self.name = name
def __str__(self):
return 'Student object (name: %s)' % self.name
print(Student('Michael'))
# Student object (name: Michael)生活化理解:__str__ 是名片上的自我介绍——"我是张三,软件工程师"。
1.3 __repr__:给开发者看的调试信息
s = Student('Michael')
s # 直接敲变量,不经过 print
# <__main__.Student object at 0x109afb310>直接敲变量调用的是 __repr__(),不是 __str__()。
区别:
__str__→ 给用户看,美观易读;__repr__→ 给开发者看,准确详细( ideally 能重建对象)。
偷懒写法(通常两者一样):
class Student(object):
def __init__(self, name):
self.name = name
def __str__(self):
return 'Student object (name=%s)' % self.name
__repr__ = __str__ # 直接复用二、__iter__:让类支持 for 循环
2.1 原理:迭代器协议
如果一个类想被 for ... in 循环,必须实现:
__iter__():返回一个迭代对象;__next__():返回下一个值,没有时抛StopIteration。
2.2 斐波那契数列
class Fib(object):
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 > 100000: # 退出条件
raise StopIteration()
return self.a使用:
for n in Fib():
print(n)
# 1, 1, 2, 3, 5, 8, 13, ...生活化理解:Fib 像一个自动售货机——__iter__ 是投币口(确认能买),__next__ 是出货按钮(每按一次出一瓶),StopIteration 是"售罄"提示。
三、__getitem__:让类支持下标和切片
3.1 按下标取元素
class Fib(object):
def __getitem__(self, n):
a, b = 1, 1
for x in range(n):
a, b = b, a + b
return a
f = Fib()
print(f[0]) # 1
print(f[10]) # 893.2 支持切片
__getitem__ 收到的可能是 int(下标),也可能是 slice(切片对象),要分别处理:
class Fib(object):
def __getitem__(self, n):
if isinstance(n, int): # 下标
a, b = 1, 1
for x in range(n):
a, b = b, a + b
return a
if isinstance(n, slice): # 切片
start = n.start or 0
stop = n.stop
a, b = 1, 1
L = []
for x in range(stop):
if x >= start:
L.append(a)
a, b = b, a + b
return L
f = Fib()
print(f[0:5]) # [1, 1, 2, 3, 5]
print(f[:10]) # [1, 1, 2, 3, 5, 8, 13, 21, 34, 55]注意:还没处理步长 f[:10:2] 和负数 f[-3:],完整实现要更多代码。
3.3 配套方法
| 方法 | 作用 | 类比 |
|---|---|---|
__getitem__ | 读取元素 | 从书架取书 |
__setitem__ | 修改元素 | 把书放回书架 |
__delitem__ | 删除元素 | 把书扔掉 |
核心思想:不需要继承 list,只要实现这些方法,类就表现得像 list——这就是 Python 的鸭子类型:"走起来像鸭子,叫起来像鸭子,那就是鸭子"。
四、__getattr__:动态响应不存在的属性
4.1 问题:访问不存在的属性报错
class Student(object):
def __init__(self):
self.name = 'Michael'
s = Student()
print(s.name) # Michael
print(s.score) # AttributeError4.2 __getattr__:兜底响应
class Student(object):
def __init__(self):
self.name = 'Michael'
def __getattr__(self, attr):
if attr == 'score':
return 99
if attr == 'age':
return lambda: 25 # 返回函数
s = Student()
print(s.score) # 99
print(s.age()) # 25规则:只有属性不存在时才调用 __getattr__;存在的属性(如 name)不会走这里。
4.3 严格模式:不认识的属性抛错
class Student(object):
def __getattr__(self, attr):
if attr == 'score':
return 99
raise AttributeError("'Student' object has no attribute '%s'" % attr)4.4 实战:REST API 链式调用
class Chain(object):
def __init__(self, path=''):
self._path = path
def __getattr__(self, path):
return Chain('%s/%s' % (self._path, path))
def __str__(self):
return self._path
__repr__ = __str__
# 使用
print(Chain().status.user.timeline.list)
# /status/user/timeline/list生活化理解:Chain 像快递单号生成器——每报一个站点(属性),就自动拼到路径上,无论 API 怎么变,代码不用改。
五、__call__:把实例当函数用
5.1 基本用法
class Student(object):
def __init__(self, name):
self.name = name
def __call__(self):
print('My name is %s.' % self.name)
s = Student('Michael')
s() # My name is Michael.生活化理解:s 不仅是学生,还是一个"按钮"——按一下(s())就自我介绍。
5.2 带参数
class Adder(object):
def __init__(self, base):
self.base = base
def __call__(self, x):
return self.base + x
add5 = Adder(5)
print(add5(3)) # 8
print(add5(10)) # 15核心思想:对象和函数的界限模糊了——对象可以像函数一样调用,函数也可以像对象一样有属性。
5.3 callable():判断是否可调用
print(callable(Student())) # True
print(callable(max)) # True
print(callable([1, 2, 3])) # False
print(callable('str')) # False六、知识链条:特殊方法如何协同工作
默认类(毛坯房)
↓
__str__ / __repr__ → 打印时友好显示(自我介绍)
↓
__iter__ + __next__ → 支持 for 循环(迭代能力)
↓
__getitem__ / __setitem__ / __delitem__ → 支持下标/切片(容器能力)
↓
__getattr__ → 动态响应任意属性(无限扩展)
↓
__call__ → 实例可直接调用(函数化)
↓
结果:类表现得像内置类型,自然融入 Python 生态七、常见误区与避坑指南
7.1 误区一:__getattr__ 与 __getattribute__ 混淆
class Student(object):
def __getattr__(self, attr):
return 99 # 只有属性不存在时才调用
def __getattribute__(self, attr):
return 99 # 每次访问属性都调用,包括存在的!区别:
__getattr__:属性找不到时兜底;__getattribute__:所有属性访问都经过(容易写崩,慎用)。
7.2 误区二:__getitem__ 忘记处理切片
class MyList(object):
def __getitem__(self, n):
return self.data[n] # 只处理了 int,slice 会报错修正:判断 isinstance(n, int) 和 isinstance(n, slice)。
7.3 误区三:__call__ 里又调用自己
class Bad(object):
def __call__(self):
return self() # ❌ 无限递归!修正:__call__ 里做具体逻辑,别再调 self()。
7.4 误区四:以为特殊方法必须成对出现
不是。按需实现:
- 只想要打印好看 → 只写
__str__; - 只想要 for 循环 → 只写
__iter__和__next__; - 只想要下标访问 → 只写
__getitem__。
八、实际应用案例
案例 1:自定义字典——带默认值和类型检查
class CheckedDict(object):
def __init__(self):
self._data = {}
def __getitem__(self, key):
return self._data.get(key, 'N/A') # 不存在返回 N/A
def __setitem__(self, key, value):
if not isinstance(key, str):
raise TypeError('key 必须是字符串!')
self._data[key] = value
def __delitem__(self, key):
del self._data[key]
def __str__(self):
return str(self._data)
__repr__ = __str__
# 使用
d = CheckedDict()
d['name'] = 'Alice'
d['age'] = 25
print(d['name']) # Alice
print(d['score']) # N/A(不报错)
d[123] = 'x' # TypeError: key 必须是字符串!
print(d) # {'name': 'Alice', 'age': 25}生活化理解:CheckedDict 像快递柜——输错取件码(key 不存在)不会报错,显示"无此包裹";但输手机号(非字符串 key)会提示"格式错误"。
案例 2:矩阵运算——支持下标和循环
class Matrix(object):
def __init__(self, rows):
self._rows = rows # [[1,2], [3,4]]
def __getitem__(self, idx):
return self._rows[idx]
def __setitem__(self, idx, value):
self._rows[idx] = value
def __iter__(self):
return iter(self._rows)
def __str__(self):
return '\n'.join(str(row) for row in self._rows)
def __call__(self, scalar):
"""矩阵数乘"""
return Matrix([[x * scalar for x in row] for row in self._rows])
# 使用
m = Matrix([[1, 2], [3, 4]])
print(m[0]) # [1, 2]
print(m[1][0]) # 3
for row in m:
print(row) # [1, 2] 然后 [3, 4]
print(m)
# [1, 2]
# [3, 4]
m2 = m(2) # 矩阵数乘
print(m2)
# [2, 4]
# [6, 8]生活化理解:Matrix 像 Excel 表格——可以按行号取行(m[0]),可以循环每行,可以整体乘一个数(m(2)),打印出来还是整齐的表格。
九、实战练习
练习 1:实现一个"星期"类
要求:
__str__返回中文星期名(如"星期一");__getitem__支持week[0]返回"星期一",week[1]返回"星期二";__call__支持week(1)返回"星期一"。
class Week(object):
def __init__(self):
self._days = ['星期一', '星期二', '星期三', '星期四', '星期五', '星期六', '星期日']
# 你的代码
# 测试
w = Week()
print(w) # 星期一 ~ 星期日(或类似格式)
print(w[0]) # 星期一
print(w(2)) # 星期三参考答案
class Week(object):
def __init__(self):
self._days = ['星期一', '星期二', '星期三', '星期四', '星期五', '星期六', '星期日']
def __str__(self):
return ' ~ '.join([self._days[0], self._days[-1]])
__repr__ = __str__
def __getitem__(self, n):
return self._days[n]
def __call__(self, n):
return self._days[n - 1] # week(1) 返回星期一练习 2:动态属性访问
实现一个 Config 类,支持链式设置配置:
c = Config()
c.db.host = 'localhost'
c.db.port = 3306
print(c.db.host) # localhost
print(c.db.port) # 3306参考答案
class Config(object):
def __init__(self):
self._data = {}
def __getattr__(self, key):
if key not in self._data:
self._data[key] = Config()
return self._data[key]
def __setattr__(self, key, value):
if key == '_data':
super().__setattr__(key, value)
else:
self._data[key] = value
def __str__(self):
return str(self._data)
# 测试
c = Config()
c.db.host = 'localhost'
c.db.port = 3306
print(c.db.host) # localhost
print(c.db.port) # 3306注意:__setattr__ 需要特殊处理 _data,否则会无限递归。
十、小结
- 特殊方法形如
__xxx__,让类表现得像内置类型; __str__/__repr__:控制打印显示,用户看__str__,开发者看__repr__;__iter__+__next__:让类支持for循环;__getitem__/__setitem__/__delitem__:让类支持下标、切片、像 dict/list 一样操作;__getattr__:动态响应不存在的属性,适合链式调用、API 封装;__call__:让实例可以像函数一样调用,模糊对象与函数的界限;- 核心思想:鸭子类型——不需要继承,只要行为像,就是那一类。
特殊方法是 Python 的"智能感应器"——按需安装,类就能感知各种操作并做出自然反应,代码既简洁又强大。