Python(三十一) 类的特殊方法(魔术方法)详解
1. 什么是特殊方法
Python 中有一类方法,名字前后都有两个下划线。
例如:
__init__
__str__
__len__
__add__
这种方法通常叫做特殊方法,也常被叫做魔术方法。
英文中常叫:
special method
magic method
dunder method
其中 dunder 是 double underscore 的缩写,意思是“双下划线”。
所以:
__init__
可以读作:
dunder init
通俗地说:
特殊方法是 Python 预留的一些方法名。
当对象遇到某些操作时,Python 会自动调用这些方法。
例如:
print(obj)
可能会自动调用:
obj.__str__()
再比如:
len(obj)
可能会自动调用:
obj.__len__()
2. 为什么叫魔术方法
因为这些方法通常不需要我们手动调用。
它们会在特定语法或内置函数中被 Python 自动调用。
例如:
class Student:
def __init__(self, name):
self.name = name
stu = Student("张三")
当执行:
Student("张三")
Python 会自动调用:
__init__()
所以初学者看起来像“自动发生了什么事情”,因此常叫魔术方法。
但它不是魔法,本质上只是 Python 的固定规则。
3. 特殊方法的核心作用
特殊方法的作用是:
让自定义类的对象也能像 Python 内置类型一样使用。
例如,列表可以这样用:
nums = [1, 2, 3]
print(len(nums))
print(nums[0])
print(nums + [4, 5])
如果我们自己定义一个类,也希望它支持:
len(obj)
obj[0]
obj + other
print(obj)
就可以通过特殊方法实现。
也就是说:
特殊方法让对象支持 Python 的内置语法和内置函数。
4. 特殊方法的命名特点
特殊方法一般长这样:
__方法名__
特点:
- 方法名前面有两个下划线。
- 方法名后面也有两个下划线。
- 名字是 Python 规定好的,不能随便编。
例如:
__init__
__str__
__repr__
__len__
__getitem__
__setitem__
__add__
__eq__
注意:
特殊方法不是自己随便起名的。
必须使用 Python 已经规定好的名字,Python 才会自动调用。
错误示例:
def __show__():
pass
如果 Python 没有规定 __show__ 这个特殊方法,那么它只是一个普通方法名,不会自动触发特殊行为。
5. 特殊方法需要手动调用吗
一般不建议手动调用特殊方法。
例如,不推荐:
stu.__str__()
更推荐:
str(stu)
print(stu)
不推荐:
obj.__len__()
更推荐:
len(obj)
教学记忆:
特殊方法通常由 Python 自动调用。
我们一般使用对应的语法或内置函数触发它。
6. 常见特殊方法分类
常见特殊方法可以分成这些类别:
| 类别 | 常见方法 | 作用 |
|---|---|---|
| 创建和初始化 | __new__、__init__、__del__ |
控制对象创建、初始化、销毁 |
| 字符串表示 | __str__、__repr__ |
控制对象打印和显示 |
| 长度和布尔值 | __len__、__bool__ |
支持 len() 和真假判断 |
| 运算符 | __add__、__sub__、__mul__ |
支持 +、-、* 等 |
| 比较运算 | __eq__、__lt__、__gt__ |
支持 ==、<、> 等 |
| 容器操作 | __getitem__、__setitem__、__contains__ |
支持下标、赋值、in |
| 迭代器 | __iter__、__next__ |
支持 for 循环 |
| 函数调用 | __call__ |
让对象可以像函数一样调用 |
| 上下文管理 | __enter__、__exit__ |
支持 with 语句 |
| 属性访问 | __getattr__、__setattr__ |
控制属性访问和设置 |
下面逐类讲解。
7. init 初始化方法
__init__ 是最常见的特殊方法。
它在对象创建后自动执行,通常用来初始化对象属性。
示例:
class Student:
def __init__(self, name, age):
self.name = name
self.age = age
stu = Student("张三", 18)
print(stu.name)
print(stu.age)
输出:
张三
18
当执行:
Student("张三", 18)
Python 会自动调用:
__init__(self, name, age)
教学理解:
__init__ 负责给新对象准备初始数据。
8. init 的注意事项
8.1 init 不是构造对象本身
很多初学者会说:
__init__ 是构造方法。
在教学初期这样理解问题不大,但更准确地说:
__init__ 是初始化方法。
对象已经创建好了,它负责初始化对象属性。
真正负责创建对象的是 __new__。
不过初学阶段重点掌握 __init__ 即可。
8.2 init 不能返回普通值
错误写法:
class Student:
def __init__(self, name):
self.name = name
return name
__init__ 不能返回非 None 的值。
正确写法:
class Student:
def __init__(self, name):
self.name = name
8.3 init 名字不要写错
正确:
def __init__(self):
pass
错误:
def _init_(self):
pass
错误:
def init(self):
pass
__init__ 前后都是两个下划线。
9. new 创建对象方法
__new__ 是真正负责创建对象的特殊方法。
它在 __init__ 之前执行。
示例:
class Demo:
def __new__(cls):
print("__new__ 被调用")
return super().__new__(cls)
def __init__(self):
print("__init__ 被调用")
obj = Demo()
输出:
__new__ 被调用
__init__ 被调用
执行顺序:
先执行 __new__,创建对象。
再执行 __init__,初始化对象。
教学建议:
初学阶段知道 __new__ 比 __init__ 更早执行即可。
一般业务代码很少需要自己重写 __new__。
10. new 的常见用途
__new__ 通常在这些场景中出现:
控制对象创建过程
实现单例模式
继承不可变类型时定制对象
简单了解即可。
示例:非常简化的单例模式。
class Singleton:
_instance = None
def __new__(cls):
if cls._instance is None:
cls._instance = super().__new__(cls)
return cls._instance
a = Singleton()
b = Singleton()
print(a is b)
输出:
True
说明:
a 和 b 指向同一个对象。
这个例子适合了解,不建议初学阶段重点展开。
11. del 析构方法
__del__ 在对象即将被销毁时可能被调用。
示例:
class Demo:
def __del__(self):
print("对象被销毁")
obj = Demo()
del obj
输出可能是:
对象被销毁
注意:
__del__ 的调用时机不一定总是容易预测。
因此,不建议把重要业务逻辑依赖在 __del__ 中。
例如文件关闭、网络连接关闭,更推荐使用 with 语句和上下文管理器。
12. str 面向用户的字符串
__str__ 用来控制对象转换成字符串时的结果。
常见触发方式:
print(obj)
str(obj)
不定义 __str__ 时:
class Student:
def __init__(self, name, age):
self.name = name
self.age = age
stu = Student("张三", 18)
print(stu)
输出类似:
<__main__.Student object at 0x00000123456789AB>
这对人不太友好。
定义 __str__ 后:
class Student:
def __init__(self, name, age):
self.name = name
self.age = age
def __str__(self):
return f"学生:{self.name},年龄:{self.age}"
stu = Student("张三", 18)
print(stu)
输出:
学生:张三,年龄:18
13. str 的注意事项
13.1 str 必须返回字符串
错误写法:
class Student:
def __str__(self):
return 123
会报错。
正确写法:
class Student:
def __str__(self):
return "这是一个学生对象"
13.2 str 不应该直接 print
错误写法:
class Student:
def __str__(self):
print("学生对象")
问题:
__str__ 应该返回字符串,不是直接打印。
正确写法:
class Student:
def __str__(self):
return "学生对象"
14. repr 面向开发者的字符串
__repr__ 也用于对象的字符串表示。
它更偏向开发者调试使用。
常见触发方式:
repr(obj)
在交互环境中直接输入 obj
列表中显示对象
示例:
class Student:
def __init__(self, name, age):
self.name = name
self.age = age
def __repr__(self):
return f"Student(name={self.name!r}, age={self.age!r})"
stu = Student("张三", 18)
print(repr(stu))
print([stu])
输出:
Student(name='张三', age=18)
[Student(name='张三', age=18)]
这里的 !r 表示使用 repr() 格式化。
15. str 和 repr 的区别
| 方法 | 面向谁 | 常见触发 | 风格 |
|---|---|---|---|
__str__ |
普通用户 | print()、str() |
友好、易读 |
__repr__ |
开发者 | repr()、调试、容器显示 |
准确、明确 |
示例:
class Student:
def __init__(self, name, age):
self.name = name
self.age = age
def __str__(self):
return f"{self.name},{self.age}岁"
def __repr__(self):
return f"Student(name={self.name!r}, age={self.age!r})"
stu = Student("张三", 18)
print(str(stu))
print(repr(stu))
输出:
张三,18岁
Student(name='张三', age=18)
教学建议:
如果只讲一个,先讲 __str__。
如果要讲调试和对象列表显示,再讲 __repr__。
16. len 支持 len()
如果希望自定义对象支持 len(),可以定义 __len__。
示例:
class Team:
def __init__(self, members):
self.members = members
def __len__(self):
return len(self.members)
team = Team(["张三", "李四", "王五"])
print(len(team))
输出:
3
当执行:
len(team)
Python 会自动调用:
team.__len__()
17. len 的注意事项
__len__ 必须返回非负整数。
正确:
def __len__(self):
return 3
错误:
def __len__(self):
return -1
错误:
def __len__(self):
return "3"
__len__ 常用于表示:
容器中元素的数量
队伍人数
购物车商品数
书架图书数
18. bool 支持真假判断
如果希望对象可以参与真假判断,可以定义 __bool__。
示例:
class Cart:
def __init__(self, items):
self.items = items
def __bool__(self):
return len(self.items) > 0
cart1 = Cart(["苹果", "牛奶"])
cart2 = Cart([])
print(bool(cart1))
print(bool(cart2))
if cart1:
print("购物车有商品")
if not cart2:
print("购物车为空")
输出:
True
False
购物车有商品
购物车为空
19. bool 和 len 的关系
如果没有定义 __bool__,但定义了 __len__,Python 会用长度判断真假。
长度为 0 时是假。
长度不为 0 时是真。
示例:
class Cart:
def __init__(self, items):
self.items = items
def __len__(self):
return len(self.items)
cart1 = Cart(["苹果"])
cart2 = Cart([])
print(bool(cart1))
print(bool(cart2))
输出:
True
False
教学记忆:
有 __bool__,优先用 __bool__。
没有 __bool__,可以用 __len__ 判断真假。
20. eq 支持 ==
默认情况下,两个对象用 == 比较,通常比较的是对象身份。
示例:
class Student:
def __init__(self, name, age):
self.name = name
self.age = age
stu1 = Student("张三", 18)
stu2 = Student("张三", 18)
print(stu1 == stu2)
输出:
False
虽然属性一样,但它们是两个不同对象。
如果希望根据属性判断是否相等,可以定义 __eq__。
class Student:
def __init__(self, name, age):
self.name = name
self.age = age
def __eq__(self, other):
return self.name == other.name and self.age == other.age
stu1 = Student("张三", 18)
stu2 = Student("张三", 18)
print(stu1 == stu2)
输出:
True
21. eq 的注意事项
__eq__ 中最好判断 other 的类型。
不推荐:
def __eq__(self, other):
return self.name == other.name
如果 other 不是 Student,可能报错。
更推荐:
class Student:
def __init__(self, name, age):
self.name = name
self.age = age
def __eq__(self, other):
if not isinstance(other, Student):
return False
return self.name == other.name and self.age == other.age
示例:
stu = Student("张三", 18)
print(stu == "张三")
输出:
False
22. 比较运算特殊方法
常见比较运算方法:
| 运算符 | 特殊方法 | 含义 |
|---|---|---|
== |
__eq__ |
等于 |
!= |
__ne__ |
不等于 |
< |
__lt__ |
小于 |
<= |
__le__ |
小于等于 |
> |
__gt__ |
大于 |
>= |
__ge__ |
大于等于 |
示例:按照成绩比较学生。
class Student:
def __init__(self, name, score):
self.name = name
self.score = score
def __lt__(self, other):
return self.score < other.score
stu1 = Student("张三", 90)
stu2 = Student("李四", 85)
print(stu1 < stu2)
print(stu1 > stu2)
输出:
False
True
这里定义了 __lt__,Python 在某些情况下也能配合反向比较完成 >。
教学时可以先讲 __eq__ 和 __lt__。
23. sorted() 和 lt
如果对象定义了 __lt__,就可以用 sorted() 排序。
示例:
class Student:
def __init__(self, name, score):
self.name = name
self.score = score
def __lt__(self, other):
return self.score < other.score
def __repr__(self):
return f"Student({self.name!r}, {self.score})"
students = [
Student("张三", 90),
Student("李四", 85),
Student("王五", 95)
]
print(sorted(students))
输出:
[Student('李四', 85), Student('张三', 90), Student('王五', 95)]
因为 __lt__ 定义了小于比较规则。
24. hash 支持哈希
如果对象想作为字典的键,或者放入集合,通常需要可哈希。
可哈希对象需要有稳定的哈希值。
特殊方法:
__hash__
示例:
class Point:
def __init__(self, x, y):
self.x = x
self.y = y
def __eq__(self, other):
if not isinstance(other, Point):
return False
return self.x == other.x and self.y == other.y
def __hash__(self):
return hash((self.x, self.y))
p1 = Point(1, 2)
p2 = Point(1, 2)
points = {p1, p2}
print(len(points))
输出:
1
因为 p1 和 p2 被认为相等,哈希值也相同。
25. hash 的注意事项
如果定义了 __eq__,但没有定义 __hash__,对象通常会变成不可哈希。
示例:
class Point:
def __init__(self, x, y):
self.x = x
self.y = y
def __eq__(self, other):
return self.x == other.x and self.y == other.y
此时:
p = Point(1, 2)
s = {p}
可能会报错。
注意:
如果对象是可变的,通常不建议让它可哈希。
因为对象内容变了,哈希值也可能变,这会影响集合和字典。
26. 运算符重载是什么
运算符重载指的是:
让自定义对象支持 +、-、*、/ 等运算符。
例如:
1 + 2
会做数字加法。
"a" + "b"
会做字符串拼接。
如果我们希望自己定义的对象也能使用 +,就可以定义 __add__。
27. add 支持 +
示例:两个坐标点相加。
class Point:
def __init__(self, x, y):
self.x = x
self.y = y
def __add__(self, other):
return Point(self.x + other.x, self.y + other.y)
def __str__(self):
return f"({self.x}, {self.y})"
p1 = Point(1, 2)
p2 = Point(3, 4)
p3 = p1 + p2
print(p3)
输出:
(4, 6)
当执行:
p1 + p2
Python 会调用:
p1.__add__(p2)
28. 常见算术运算特殊方法
| 运算符 | 特殊方法 |
|---|---|
+ |
__add__ |
- |
__sub__ |
* |
__mul__ |
/ |
__truediv__ |
// |
__floordiv__ |
% |
__mod__ |
** |
__pow__ |
示例:向量乘以数字。
class Vector:
def __init__(self, x, y):
self.x = x
self.y = y
def __mul__(self, number):
return Vector(self.x * number, self.y * number)
def __str__(self):
return f"Vector({self.x}, {self.y})"
v = Vector(2, 3)
print(v * 4)
输出:
Vector(8, 12)
29. 反向运算 radd 等
看这个例子:
class Number:
def __init__(self, value):
self.value = value
def __add__(self, other):
return self.value + other
n = Number(10)
print(n + 5)
输出:
15
但是:
print(5 + n)
可能会报错。
因为 5 + n 会先尝试让整数处理这个加法。
如果希望支持反向加法,可以定义 __radd__。
class Number:
def __init__(self, value):
self.value = value
def __add__(self, other):
return self.value + other
def __radd__(self, other):
return other + self.value
n = Number(10)
print(n + 5)
print(5 + n)
输出:
15
15
常见反向运算:
| 运算符 | 正向方法 | 反向方法 |
|---|---|---|
+ |
__add__ |
__radd__ |
- |
__sub__ |
__rsub__ |
* |
__mul__ |
__rmul__ |
/ |
__truediv__ |
__rtruediv__ |
30. 原地运算 iadd
+= 对应的方法是 __iadd__。
示例:
class Counter:
def __init__(self, count):
self.count = count
def __iadd__(self, value):
self.count += value
return self
def __str__(self):
return str(self.count)
c = Counter(10)
c += 5
print(c)
输出:
15
注意:
__iadd__ 通常要返回 self。
如果没有定义 __iadd__,Python 可能会退而使用 __add__。
31. getitem 支持下标访问
如果希望对象支持下标访问:
obj[index]
可以定义 __getitem__。
示例:
class MyList:
def __init__(self, data):
self.data = data
def __getitem__(self, index):
return self.data[index]
nums = MyList([10, 20, 30])
print(nums[0])
print(nums[1])
输出:
10
20
当执行:
nums[0]
Python 会调用:
nums.__getitem__(0)
32. setitem 支持下标赋值
如果希望对象支持:
obj[index] = value
可以定义 __setitem__。
示例:
class MyList:
def __init__(self, data):
self.data = data
def __getitem__(self, index):
return self.data[index]
def __setitem__(self, index, value):
self.data[index] = value
nums = MyList([10, 20, 30])
nums[1] = 200
print(nums[1])
输出:
200
33. delitem 支持删除元素
如果希望对象支持:
del obj[index]
可以定义 __delitem__。
示例:
class MyList:
def __init__(self, data):
self.data = data
def __getitem__(self, index):
return self.data[index]
def __delitem__(self, index):
del self.data[index]
def __str__(self):
return str(self.data)
nums = MyList([10, 20, 30])
del nums[1]
print(nums)
输出:
[10, 30]
34. contains 支持 in
如果希望对象支持:
元素 in 对象
可以定义 __contains__。
示例:
class Team:
def __init__(self, members):
self.members = members
def __contains__(self, name):
return name in self.members
team = Team(["张三", "李四", "王五"])
print("张三" in team)
print("赵六" in team)
输出:
True
False
当执行:
"张三" in team
Python 会调用:
team.__contains__("张三")
35. 容器类综合案例
class StudentGroup:
def __init__(self):
self.students = []
def add_student(self, name):
self.students.append(name)
def __len__(self):
return len(self.students)
def __getitem__(self, index):
return self.students[index]
def __contains__(self, name):
return name in self.students
def __str__(self):
return f"学生组:{self.students}"
group = StudentGroup()
group.add_student("张三")
group.add_student("李四")
print(len(group))
print(group[0])
print("李四" in group)
print(group)
输出:
2
张三
True
学生组:['张三', '李四']
这个对象现在像一个简单容器:
可以 len()
可以下标访问
可以用 in 判断
可以 print()
36. iter 支持 for 循环
如果希望对象可以被 for 循环遍历,可以定义 __iter__。
示例:
class Team:
def __init__(self, members):
self.members = members
def __iter__(self):
return iter(self.members)
team = Team(["张三", "李四", "王五"])
for member in team:
print(member)
输出:
张三
李四
王五
这里:
iter(self.members)
返回列表的迭代器。
37. next 支持迭代器
迭代器对象通常需要实现:
__iter__
__next__
示例:自定义一个从 1 数到 n 的计数器。
class CountUp:
def __init__(self, max_num):
self.max_num = max_num
self.current = 0
def __iter__(self):
return self
def __next__(self):
if self.current >= self.max_num:
raise StopIteration
self.current += 1
return self.current
counter = CountUp(3)
for num in counter:
print(num)
输出:
1
2
3
当迭代结束时,__next__ 应该抛出:
StopIteration
这告诉 Python 循环结束。
38. 迭代器的注意事项
上面的 CountUp 对象本身就是迭代器。
它有一个特点:
遍历一次之后,current 已经到末尾。
再次遍历不会重新开始。
示例:
counter = CountUp(3)
for num in counter:
print(num)
for num in counter:
print(num)
第二次循环不会输出内容。
如果希望每次 for 都重新开始,通常让 __iter__ 返回一个新的迭代器。
例如:
class CountUp:
def __init__(self, max_num):
self.max_num = max_num
def __iter__(self):
return iter(range(1, self.max_num + 1))
教学时可以先讲简单版本,再补充这个注意点。
39. call 让对象像函数一样调用
如果一个类定义了 __call__,它的对象就可以像函数一样调用。
示例:
class Greeter:
def __init__(self, name):
self.name = name
def __call__(self):
print(f"你好,{self.name}")
greet = Greeter("张三")
greet()
输出:
你好,张三
当执行:
greet()
Python 会调用:
greet.__call__()
40. call 的使用场景
__call__ 常见于:
需要保存状态的函数对象
装饰器类
回调对象
模型预测对象
简单案例:计数调用次数。
class Counter:
def __init__(self):
self.count = 0
def __call__(self):
self.count += 1
return self.count
counter = Counter()
print(counter())
print(counter())
print(counter())
输出:
1
2
3
这里 counter 像函数一样被调用,同时还能保存自己的状态。
41. enter 和 exit 支持 with
如果希望对象支持 with 语句,需要实现:
__enter__
__exit__
示例:
class DemoContext:
def __enter__(self):
print("进入 with")
return self
def __exit__(self, exc_type, exc_value, traceback):
print("离开 with")
with DemoContext() as demo:
print("执行 with 内部代码")
输出:
进入 with
执行 with 内部代码
离开 with
执行顺序:
进入 with 时调用 __enter__。
with 代码块结束时调用 __exit__。
42. 上下文管理器的用途
上下文管理器常用于:
打开和关闭文件
获取和释放资源
连接和断开数据库
加锁和释放锁
临时切换环境
例如文件操作:
with open("data.txt", "w", encoding="utf-8") as f:
f.write("hello")
with 的好处是:
即使中间发生错误,也能尽量执行清理操作。
自定义文件风格示例:
class SimpleFile:
def __init__(self, filename):
self.filename = filename
self.file = None
def __enter__(self):
self.file = open(self.filename, "w", encoding="utf-8")
return self.file
def __exit__(self, exc_type, exc_value, traceback):
self.file.close()
with SimpleFile("demo.txt") as f:
f.write("你好")
教学时可以强调:
with 适合管理需要开始和结束的资源。
43. exit 的参数
__exit__ 有三个常见参数:
def __exit__(self, exc_type, exc_value, traceback):
pass
含义:
| 参数 | 含义 |
|---|---|
exc_type |
异常类型 |
exc_value |
异常对象 |
traceback |
异常追踪信息 |
如果 with 内没有异常,这三个参数都是 None。
示例:
class DemoContext:
def __enter__(self):
print("进入")
return self
def __exit__(self, exc_type, exc_value, traceback):
print("异常类型:", exc_type)
print("异常信息:", exc_value)
with DemoContext():
print(10 / 0)
会看到异常相关信息,然后程序仍然会抛出异常。
44. exit 返回值
如果 __exit__ 返回 True,表示异常被处理,不再向外抛出。
示例:
class IgnoreError:
def __enter__(self):
return self
def __exit__(self, exc_type, exc_value, traceback):
print("异常被处理:", exc_value)
return True
with IgnoreError():
print(10 / 0)
print("程序继续执行")
输出:
异常被处理: division by zero
程序继续执行
注意:
不要随便返回 True。
否则可能把重要错误隐藏掉。
45. getattr 访问不存在属性时调用
当访问一个不存在的属性时,Python 会尝试调用 __getattr__。
示例:
class Student:
def __init__(self, name):
self.name = name
def __getattr__(self, attr_name):
return f"属性 {attr_name} 不存在"
stu = Student("张三")
print(stu.name)
print(stu.age)
输出:
张三
属性 age 不存在
说明:
stu.name 存在,正常访问。
stu.age 不存在,触发 __getattr__。
46. getattribute 每次访问属性都会调用
__getattribute__ 比 __getattr__ 更底层。
对象每次访问属性时,都会先调用 __getattribute__。
示例:
class Demo:
def __init__(self):
self.name = "张三"
def __getattribute__(self, attr_name):
print("正在访问:", attr_name)
return object.__getattribute__(self, attr_name)
demo = Demo()
print(demo.name)
输出:
正在访问: name
张三
注意:
__getattribute__ 很容易写出递归错误。
初学阶段了解即可,不建议随便重写。
错误示例:
def __getattribute__(self, attr_name):
return self.attr_name
这会再次触发 __getattribute__,可能导致无限递归。
正确访问底层属性通常使用:
object.__getattribute__(self, attr_name)
47. setattr 设置属性时调用
当设置对象属性时,会调用 __setattr__。
示例:
class Demo:
def __setattr__(self, name, value):
print(f"设置属性:{name} = {value}")
object.__setattr__(self, name, value)
demo = Demo()
demo.name = "张三"
demo.age = 18
print(demo.name)
print(demo.age)
输出:
设置属性:name = 张三
设置属性:age = 18
张三
18
注意:
在 __setattr__ 中设置属性时,应该使用 object.__setattr__。
否则可能无限递归。
错误写法:
def __setattr__(self, name, value):
self.name = value
这样会再次触发 __setattr__。
48. delattr 删除属性时调用
当删除属性时,会调用 __delattr__。
示例:
class Demo:
def __delattr__(self, name):
print(f"删除属性:{name}")
object.__delattr__(self, name)
demo = Demo()
demo.name = "张三"
del demo.name
输出:
删除属性:name
这种方法可以控制属性删除行为。
初学阶段了解即可。
49. dict 查看对象属性字典
__dict__ 不是普通方法,而是常见的特殊属性。
它可以查看对象保存的属性。
示例:
class Student:
def __init__(self, name, age):
self.name = name
self.age = age
stu = Student("张三", 18)
print(stu.__dict__)
输出:
{'name': '张三', 'age': 18}
这对理解对象属性很有帮助。
也可以查看类中的内容:
print(Student.__dict__)
输出会包含类中的方法、属性等信息。
50. slots 限制对象属性
__slots__ 可以限制对象能拥有哪些属性。
示例:
class Student:
__slots__ = ("name", "age")
def __init__(self, name, age):
self.name = name
self.age = age
stu = Student("张三", 18)
stu.name = "李四"
stu.age = 19
stu.score = 90
最后一行会报错。
因为 __slots__ 只允许:
name
age
不允许新增:
score
__slots__ 常用于:
限制动态添加属性
节省大量对象的内存
初学阶段了解即可。
51. class 查看对象所属类
对象可以通过 __class__ 查看自己属于哪个类。
示例:
class Student:
pass
stu = Student()
print(stu.__class__)
输出类似:
<class '__main__.Student'>
也可以使用:
type(stu)
通常教学中更推荐先讲:
type(stu)
因为更直观。
52. callable() 和 call
如果对象定义了 __call__,那么 callable() 会返回 True。
示例:
class A:
def __call__(self):
pass
class B:
pass
a = A()
b = B()
print(callable(a))
print(callable(b))
输出:
True
False
说明:
a 可以像函数一样调用。
b 不可以。
53. dir() 查看对象支持的属性和方法
dir() 可以查看对象拥有的属性和方法。
示例:
class Student:
def __init__(self, name):
self.name = name
def study(self):
print("学习")
stu = Student("张三")
print(dir(stu))
输出中会包含很多双下划线方法。
例如:
__class__
__dict__
__str__
__repr__
study
name
教学时可以告诉学生:
很多双下划线方法是 Python 对象默认就有的。
我们可以根据需要重写其中一部分。
54. 特殊方法和内置函数对应关系
| 使用方式 | 可能调用的特殊方法 |
|---|---|
str(obj) |
obj.__str__() |
repr(obj) |
obj.__repr__() |
len(obj) |
obj.__len__() |
bool(obj) |
obj.__bool__() |
obj1 == obj2 |
obj1.__eq__(obj2) |
obj1 < obj2 |
obj1.__lt__(obj2) |
obj1 + obj2 |
obj1.__add__(obj2) |
obj[index] |
obj.__getitem__(index) |
obj[index] = value |
obj.__setitem__(index, value) |
value in obj |
obj.__contains__(value) |
for x in obj |
obj.__iter__() |
obj() |
obj.__call__() |
with obj: |
obj.__enter__()、obj.__exit__() |
这张表适合课堂总结使用。
55. 综合案例:自定义购物车
需求:
购物车可以添加商品。
可以用 len() 查看商品数量。
可以用 in 判断商品是否存在。
可以用下标访问商品。
可以打印购物车。
代码:
class Cart:
def __init__(self):
self.items = []
def add(self, item):
self.items.append(item)
def __len__(self):
return len(self.items)
def __contains__(self, item):
return item in self.items
def __getitem__(self, index):
return self.items[index]
def __str__(self):
return f"购物车:{self.items}"
cart = Cart()
cart.add("苹果")
cart.add("牛奶")
cart.add("面包")
print(len(cart))
print("牛奶" in cart)
print(cart[0])
print(cart)
输出:
3
True
苹果
购物车:['苹果', '牛奶', '面包']
这个案例中用到了:
__len__
__contains__
__getitem__
__str__
56. 综合案例:自定义分数类
需求:
定义分数类 Fraction。
支持打印。
支持两个分数相加。
支持判断两个分数是否相等。
代码:
class Fraction:
def __init__(self, numerator, denominator):
if denominator == 0:
raise ValueError("分母不能为 0")
self.numerator = numerator
self.denominator = denominator
def __str__(self):
return f"{self.numerator}/{self.denominator}"
def __add__(self, other):
new_numerator = self.numerator * other.denominator + other.numerator * self.denominator
new_denominator = self.denominator * other.denominator
return Fraction(new_numerator, new_denominator)
def __eq__(self, other):
if not isinstance(other, Fraction):
return False
return self.numerator * other.denominator == other.numerator * self.denominator
f1 = Fraction(1, 2)
f2 = Fraction(1, 3)
f3 = Fraction(3, 6)
print(f1)
print(f2)
print(f1 + f2)
print(f1 == f3)
输出:
1/2
1/3
5/6
True
这个案例中:
__str__ 控制打印。
__add__ 控制 +。
__eq__ 控制 ==。
57. 综合案例:班级对象
需求:
班级对象可以保存学生。
可以 len() 得到人数。
可以 for 循环遍历学生。
可以用 in 判断学生是否在班级中。
代码:
class Classroom:
def __init__(self, name):
self.name = name
self.students = []
def add_student(self, student):
self.students.append(student)
def __len__(self):
return len(self.students)
def __iter__(self):
return iter(self.students)
def __contains__(self, student):
return student in self.students
def __str__(self):
return f"{self.name}:{len(self)} 人"
room = Classroom("Python 一班")
room.add_student("张三")
room.add_student("李四")
room.add_student("王五")
print(room)
print(len(room))
print("李四" in room)
for student in room:
print(student)
输出:
Python 一班:3 人
3
True
张三
李四
王五
58. 综合案例:计时上下文管理器
示例:使用 with 统计一段代码执行时间。
import time
class Timer:
def __enter__(self):
self.start = time.time()
return self
def __exit__(self, exc_type, exc_value, traceback):
self.end = time.time()
self.cost = self.end - self.start
print(f"耗时:{self.cost:.4f} 秒")
with Timer():
total = 0
for i in range(1000000):
total += i
输出类似:
耗时:0.0523 秒
这个案例中:
__enter__ 记录开始时间。
__exit__ 记录结束时间并打印耗时。
59. 常见错误 1:特殊方法名写错
错误:
def _str_(self):
return "对象"
正确:
def __str__(self):
return "对象"
错误:
def _init_(self):
pass
正确:
def __init__(self):
pass
注意:
特殊方法前后都是两个下划线。
少一个、多一个、位置不对都不行。
60. 常见错误 2:str 返回非字符串
错误:
class Demo:
def __str__(self):
return 123
正确:
class Demo:
def __str__(self):
return "123"
记忆:
__str__ 必须返回字符串。
61. 常见错误 3:len 返回非整数
错误:
def __len__(self):
return "3"
错误:
def __len__(self):
return -1
正确:
def __len__(self):
return 3
__len__ 必须返回非负整数。
62. 常见错误 4:eq 不判断类型
不推荐:
def __eq__(self, other):
return self.name == other.name
如果 other 没有 name 属性,就会报错。
推荐:
def __eq__(self, other):
if not isinstance(other, Student):
return False
return self.name == other.name
63. 常见错误 5:运算符方法返回不合理结果
例如,两个点相加,推荐返回新的点对象。
推荐:
def __add__(self, other):
return Point(self.x + other.x, self.y + other.y)
不推荐返回完全无关的内容:
def __add__(self, other):
return "相加成功"
虽然语法上可能可以,但使用者会困惑。
教学原则:
特殊方法的行为应该符合人们对这个语法的直觉。
64. 常见错误 6:在 setattr 中无限递归
错误:
class Demo:
def __setattr__(self, name, value):
self.name = value
原因:
self.name = value 又会调用 __setattr__。
这样会无限递归。
正确:
class Demo:
def __setattr__(self, name, value):
object.__setattr__(self, name, value)
65. 常见错误 7:在 getattribute 中无限递归
错误:
class Demo:
def __getattribute__(self, name):
return self.name
原因:
self.name 又会触发 __getattribute__。
正确:
class Demo:
def __getattribute__(self, name):
return object.__getattribute__(self, name)
初学阶段不建议重写 __getattribute__。
66. 常见错误 8:滥用魔术方法
特殊方法很强大,但不要为了炫技而滥用。
不推荐:
class Student:
def __add__(self, other):
print("学生开始学习")
+ 通常表示加法或合并。
如果用 + 表示“学习”,会让代码难以理解。
教学建议:
只有当语义自然时,才使用对应的特殊方法。
例如:
购物车 + 商品列表
点 + 点
向量 * 数字
len(班级)
学生 in 班级
这些语义比较自然。
67. 常见错误 9:手动调用特殊方法
不推荐:
obj.__str__()
obj.__len__()
obj.__add__(other)
更推荐:
str(obj)
len(obj)
obj + other
特殊方法是给 Python 语法和内置函数调用的。
教学记忆:
少手动调双下划线方法,多用对应语法。
68. 常见错误 10:del 中写重要逻辑
不推荐把重要逻辑依赖在 __del__ 中。
原因:
__del__ 的执行时机不总是容易预测。
程序退出时对象销毁顺序也可能复杂。
如果需要稳定释放资源,更推荐:
with 语句
显式 close() 方法
try...finally
69. 注意事项 1:特殊方法名称不能随便发明
例如:
def __print__(self):
pass
Python 不会因为你写了 __print__ 就自动在 print(obj) 时调用它。
print(obj) 主要看:
__str__
__repr__
所以要使用 Python 规定好的特殊方法名。
70. 注意事项 2:让行为符合直觉
定义特殊方法时,要让对象行为符合语法本身的含义。
例如:
p1 + p2
适合表示两个点或向量相加。
len(team)
适合表示队伍人数。
student in classroom
适合表示学生是否在班级中。
不要让特殊方法做奇怪的事情。
71. 注意事项 3:返回值类型要合理
不同特殊方法对返回值有要求。
例如:
__str__ 返回字符串
__len__ 返回非负整数
__bool__ 返回布尔值
__iter__ 返回迭代器
__next__ 没有数据时抛 StopIteration
如果返回值不符合要求,可能会报错或导致代码难懂。
72. 注意事项 4:不要一次讲太多
特殊方法数量很多。
教学时建议分阶段讲:
第一阶段:
__init__
__str__
__repr__
第二阶段:
__len__
__eq__
__lt__
__add__
第三阶段:
__getitem__
__setitem__
__contains__
__iter__
第四阶段:
__call__
__enter__
__exit__
属性访问相关方法
这样学生更容易消化。
73. 注意事项 5:先理解普通方法,再理解特殊方法
特殊方法本质上也是方法。
只是方法名特殊,并且会被 Python 自动调用。
所以教学顺序建议:
先讲类和对象
再讲普通方法
再讲 __init__
再讲 __str__
最后逐步讲其他特殊方法
学生如果还不理解 self,直接讲很多特殊方法会比较吃力。
74. 课堂练习 1:自定义 str
要求:
- 定义
Student类。 - 有
name和age属性。 - 定义
__str__,打印对象时显示学生信息。
参考答案:
class Student:
def __init__(self, name, age):
self.name = name
self.age = age
def __str__(self):
return f"学生:{self.name},年龄:{self.age}"
stu = Student("张三", 18)
print(stu)
75. 课堂练习 2:自定义 len
要求:
- 定义
Classroom类。 - 内部保存学生列表。
- 定义
__len__,让len(classroom)返回学生数量。
参考答案:
class Classroom:
def __init__(self, students):
self.students = students
def __len__(self):
return len(self.students)
room = Classroom(["张三", "李四", "王五"])
print(len(room))
76. 课堂练习 3:自定义 eq
要求:
- 定义
Student类。 - 如果两个学生姓名和年龄都相同,就认为相等。
- 使用
==比较两个对象。
参考答案:
class Student:
def __init__(self, name, age):
self.name = name
self.age = age
def __eq__(self, other):
if not isinstance(other, Student):
return False
return self.name == other.name and self.age == other.age
stu1 = Student("张三", 18)
stu2 = Student("张三", 18)
print(stu1 == stu2)
77. 课堂练习 4:自定义 add
要求:
- 定义
Point类。 - 有
x和y属性。 - 支持两个点相加。
- 打印相加后的点。
参考答案:
class Point:
def __init__(self, x, y):
self.x = x
self.y = y
def __add__(self, other):
return Point(self.x + other.x, self.y + other.y)
def __str__(self):
return f"({self.x}, {self.y})"
p1 = Point(1, 2)
p2 = Point(3, 4)
print(p1 + p2)
78. 课堂练习 5:自定义 contains
要求:
- 定义
Team类。 - 内部保存成员列表。
- 支持
"张三" in team判断。
参考答案:
class Team:
def __init__(self, members):
self.members = members
def __contains__(self, name):
return name in self.members
team = Team(["张三", "李四"])
print("张三" in team)
print("王五" in team)
79. 课堂练习 6:自定义 getitem
要求:
- 定义
StudentGroup类。 - 内部保存学生列表。
- 支持通过下标获取学生。
参考答案:
class StudentGroup:
def __init__(self, students):
self.students = students
def __getitem__(self, index):
return self.students[index]
group = StudentGroup(["张三", "李四", "王五"])
print(group[0])
print(group[1])
80. 课堂练习 7:自定义 iter
要求:
- 定义
StudentGroup类。 - 内部保存学生列表。
- 支持
for循环遍历。
参考答案:
class StudentGroup:
def __init__(self, students):
self.students = students
def __iter__(self):
return iter(self.students)
group = StudentGroup(["张三", "李四", "王五"])
for student in group:
print(student)
81. 课堂练习 8:自定义 call
要求:
- 定义
Greeter类。 - 对象可以像函数一样调用。
- 调用时打印问候语。
参考答案:
class Greeter:
def __init__(self, name):
self.name = name
def __call__(self):
print(f"你好,{self.name}")
greet = Greeter("张三")
greet()
82. 课堂练习 9:自定义上下文管理器
要求:
- 定义
DemoContext类。 - 进入
with时打印"进入"。 - 离开
with时打印"离开"。
参考答案:
class DemoContext:
def __enter__(self):
print("进入")
return self
def __exit__(self, exc_type, exc_value, traceback):
print("离开")
with DemoContext():
print("执行代码")
83. 课堂练习 10:判断输出
阅读代码,判断输出:
class Box:
def __init__(self, items):
self.items = items
def __len__(self):
return len(self.items)
def __bool__(self):
return len(self.items) > 0
box1 = Box([1, 2, 3])
box2 = Box([])
print(len(box1))
print(bool(box1))
print(bool(box2))
答案:
3
True
False
84. 教学总结
特殊方法是 Python 类中非常重要的一部分。
一句话总结:
特殊方法是前后带双下划线的方法,
Python 会在特定语法或内置函数中自动调用它们。
核心知识:
- 特殊方法也叫魔术方法、dunder 方法。
- 特殊方法名称由 Python 规定,不能随便发明。
__init__用来初始化对象。__str__控制print()的显示结果。__repr__更适合调试和开发者查看。__len__支持len()。__bool__支持真假判断。__eq__、__lt__等支持比较。__add__等支持运算符重载。__getitem__、__setitem__、__contains__支持容器行为。__iter__、__next__支持迭代。__call__让对象可以像函数一样调用。__enter__、__exit__支持with语句。- 属性访问相关特殊方法很强大,但要谨慎使用。
常用对应关系:
print(obj) # __str__
repr(obj) # __repr__
len(obj) # __len__
bool(obj) # __bool__
obj1 == obj2 # __eq__
obj1 + obj2 # __add__
obj[index] # __getitem__
value in obj # __contains__
for x in obj # __iter__
obj() # __call__
with obj: # __enter__ 和 __exit__
课堂记忆口诀:
双下划线前后包,
Python 自动来调用。
打印对象看 str,
调试看 repr。
长度使用 len,
相等看 eq。
加法找 add,
下标找 item。
for 循环靠 iter,
with 语句 enter exit。
最后记住:
特殊方法不是越多越好。
只有当对象需要支持某种 Python 语法时,
才去实现对应的特殊方法。