Skip to content

Python 定制类:给类装上"智能感应器"

引言:从"毛坯房"到"智能家居"

想象你买了一套毛坯房(默认的 Python 类):

  • 墙是白的,门是木门,灯是灯泡——能用,但不够"智能";
  • 你想要:进门自动亮灯、说声"开空调"就制冷、喊"关窗帘"就合上——这就是定制

Python 的定制类就是给毛坯房装上各种智能感应器

  • __str__:回家自动报"欢迎主人回家"(打印时显示友好信息);
  • __iter__:说声"开灯"就逐个亮灯(for 循环遍历);
  • __getitem__:按房间号取东西(下标/切片访问);
  • __getattr__:喊任何需求都有回应(动态属性);
  • __call__:拍拍房子就能对话(实例直接调用)。

这些形如 __xxx__ 的方法叫特殊方法(也叫"魔术方法"),它们让类表现得像 Python 内置类型(list、dict、str)一样自然。


一、__str____repr__:类的"自我介绍"

1.1 问题:打印实例像看天书

python
class Student(object):
    def __init__(self, name):
        self.name = name

print(Student('Michael'))
# <__main__.Student object at 0x109afb190>

这串地址码对用户毫无意义。

1.2 __str__:给用户看的自我介绍

python
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__:给开发者看的调试信息

python
s = Student('Michael')
s   # 直接敲变量,不经过 print
# <__main__.Student object at 0x109afb310>

直接敲变量调用的是 __repr__(),不是 __str__()

区别

  • __str__ → 给用户看,美观易读;
  • __repr__ → 给开发者看,准确详细( ideally 能重建对象)。

偷懒写法(通常两者一样):

python
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 循环,必须实现:

  1. __iter__():返回一个迭代对象;
  2. __next__():返回下一个值,没有时抛 StopIteration

2.2 斐波那契数列

python
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

使用:

python
for n in Fib():
    print(n)
# 1, 1, 2, 3, 5, 8, 13, ...

生活化理解Fib 像一个自动售货机——__iter__ 是投币口(确认能买),__next__ 是出货按钮(每按一次出一瓶),StopIteration 是"售罄"提示。


三、__getitem__:让类支持下标和切片

3.1 按下标取元素

python
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])   # 89

3.2 支持切片

__getitem__ 收到的可能是 int(下标),也可能是 slice(切片对象),要分别处理:

python
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 问题:访问不存在的属性报错

python
class Student(object):
    def __init__(self):
        self.name = 'Michael'

s = Student()
print(s.name)    # Michael
print(s.score)   # AttributeError

4.2 __getattr__:兜底响应

python
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 严格模式:不认识的属性抛错

python
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 链式调用

python
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 基本用法

python
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 带参数

python
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():判断是否可调用

python
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__ 混淆

python
class Student(object):
    def __getattr__(self, attr):
        return 99   # 只有属性不存在时才调用

    def __getattribute__(self, attr):
        return 99   # 每次访问属性都调用,包括存在的!

区别

  • __getattr__:属性找不到时兜底;
  • __getattribute__所有属性访问都经过(容易写崩,慎用)。

7.2 误区二:__getitem__ 忘记处理切片

python
class MyList(object):
    def __getitem__(self, n):
        return self.data[n]   # 只处理了 int,slice 会报错

修正:判断 isinstance(n, int)isinstance(n, slice)

7.3 误区三:__call__ 里又调用自己

python
class Bad(object):
    def __call__(self):
        return self()   # ❌ 无限递归!

修正__call__ 里做具体逻辑,别再调 self()

7.4 误区四:以为特殊方法必须成对出现

不是。按需实现:

  • 只想要打印好看 → 只写 __str__
  • 只想要 for 循环 → 只写 __iter____next__
  • 只想要下标访问 → 只写 __getitem__

八、实际应用案例

案例 1:自定义字典——带默认值和类型检查

python
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:矩阵运算——支持下标和循环

python
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:实现一个"星期"类

要求:

  1. __str__ 返回中文星期名(如"星期一");
  2. __getitem__ 支持 week[0] 返回"星期一",week[1] 返回"星期二";
  3. __call__ 支持 week(1) 返回"星期一"。
python
class Week(object):
    def __init__(self):
        self._days = ['星期一', '星期二', '星期三', '星期四', '星期五', '星期六', '星期日']

    # 你的代码

# 测试
w = Week()
print(w)        # 星期一 ~ 星期日(或类似格式)
print(w[0])     # 星期一
print(w(2))     # 星期三
参考答案
python
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 类,支持链式设置配置:

python
c = Config()
c.db.host = 'localhost'
c.db.port = 3306
print(c.db.host)   # localhost
print(c.db.port)   # 3306
参考答案
python
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,否则会无限递归。


十、小结

  1. 特殊方法形如 __xxx__,让类表现得像内置类型;
  2. __str__ / __repr__:控制打印显示,用户看 __str__,开发者看 __repr__
  3. __iter__ + __next__:让类支持 for 循环;
  4. __getitem__ / __setitem__ / __delitem__:让类支持下标、切片、像 dict/list 一样操作;
  5. __getattr__:动态响应不存在的属性,适合链式调用、API 封装;
  6. __call__:让实例可以像函数一样调用,模糊对象与函数的界限;
  7. 核心思想鸭子类型——不需要继承,只要行为像,就是那一类。

特殊方法是 Python 的"智能感应器"——按需安装,类就能感知各种操作并做出自然反应,代码既简洁又强大。