一、魔术方法的定义
定义:以双下划线__开头和结尾的特殊方法,如:__init__、__str__等。作用:魔术方法就是重写 Python 内置运算符、内置函数的底层逻辑,让你的自定义对象像列表、数字、字符串一样正常使用 +、len ()、print、with、()、[] 等语法。__new__方法:在__init__之前调用,用于创建对象实例。必须返回一个类的实例。即使没有__init__,__new__也必须存在。__init__方法:在__new__之后调用,用于初始化对象。不返回任何值,只负责设置初始状态。如果__new__返回的是子类实例,则__init__会调用子类的__init__方法。class A: def __new__(cls): print("__new__ 创建空对象") obj = super().__new__(cls) return obj def __init__(self,x): print("__init__ 初始化属性") self.x = xo = A(1)
__new__ 创建空对象__init__ 初始化属性
obj = A.__new__(A) # 只造对象A.__init__(obj,1) # 手动传对象+参数,等价自动调用逻辑
__del__ 析构方法:对象销毁时自动触发,不靠del关键字触发,靠引用计数归0来触发。也就是对象引用计数变为 0 时,Python 自动调用,销毁对象。只是删除变量名和对象的绑定(引用计数 - 1),不是直接销毁对象,只要还有别的变量指向这个对象,引用计数>0,对象存活,__del__不执行。
class A: def __del__(self): print("__del__")o = A() # 实例化对象,引用计数=1x = o # x也指向同一个对象,引用计数+1 → 引用计数=2del o # 删除变量o,引用计数-1,引用计数=1(对象还在,不会执行__del__)# 代码运行结束、程序退出前,x自动销毁,引用计数变成0 → 触发__del__print("finish")
输出:
三、类型表示与转换
3.1 __repr__方法
控制直接打印对象时显示什么文字。
举例子:
情况1:不写__repr__方法时
class Dog: def __init__(self,name): self.name = named = Dog("旺财")print(d) #没写__repr__,输出:<__main__.Dog object at 0xxxxx>
加上 repr 之后
class Dog: def __init__(self,name): self.name = name # __repr__:自定义打印长啥样 def __repr__(self): return f"小狗:{self.name}"d = Dog("旺财")print(d) # 小狗:旺财
该方法的触发方式:
print(d) # 调用__repr__print(repr(d)) # 强制调用__repr__
3.2 __str__方法
__str__:给普通人看,print (对象) 优先调用,返回字符串。
__repr__:给程序员看,控制台直接输入对象、列表里打印用,返回字符串。
class Cat: def __init__(self,name): self.name = name def __str__(self): # 普通人易懂的文字 return f"猫咪名字:{self.name}"c = Cat("小白")print(c) # print → 自动调用 __str__,输出:猫咪名字:小白
同时写 __str__ 和 __repr__:
class Cat: def __init__(self,name): self.name = name def __str__(self): return f"猫咪:{self.name}" def __repr__(self): return f"Cat('{self.name}')"c = Cat("小黑")print(c) # 走__str__ → 猫咪:小黑print(repr(c)) # 走__repr__ → Cat('小黑')print([c]) # 列表里打印,走__repr__
也就是:有__str__就优先用__str__做 print。没有__str__,print 会自动去找__repr__。
3.3 __bytes__方法
返回对象的字节串表示。用于将对象序列化为字节流。
执行 bytes(对象) 的时候,自动调用 __bytes__,就必须返回 b'xxx' 字节类型。
不写__bytes__,直接 bytes () 报错。
class User: def __init__(self,name): self.name = nameu = User("小明")# bytes(u) 直接报错!没有__bytes__方法
加上 bytes
class User: def __init__(self,name): self.name = name # bytes(对象)自动调用这个方法 def __bytes__(self): # 字符串转字节:utf-8编码 return self.name.encode("utf-8")u = User("小明")b = bytes(u) # 触发__bytes__print(b) # b'\xe5\xb0\x8f\xe6\x98\x8e'# 字节变回文字s = b.decode("utf-8")print(s) # 小明
3.4 __format__方法
用 f"{对象:自定义内容}" / format(对象, '自定义内容') 时,自动跑 __format__。:后面写啥,就把啥传给 __format__。
class Fruit: def __init__(self,name): self.name = name # format_spec 就是冒号后面的文字 def __format__(self, format_spec): # 如果冒号后写big,名字变大写 if format_spec == "big": return self.name.upper() # 没写格式就原样返回 return self.nameapple = Fruit("apple")# 不写:xxxprint(f"{apple}") # apple# 写:big → format_spec="big"print(f"{apple:big}") # APPLE
解读:f"{apple:big}"
①、apple → 对象
②、:big → big 传给 __format__ 的第二个参数
③、代码判断是 big → 转大写返回
3.5 __hash__方法
返回对象的哈希值,用于快速比较对象。如果__eq__方法被重写,通常也
需要重写__hash__方法。只写__eq__,不写__hash__,对象不能放进集合、不能当字典键__hash__等于对象的身份证号。
hash(对象) → 自动运行 __hash__,返回一个整数。集合set、字典key靠这个编号存数据。
class Book: def __init__(self,name): self.name = name def __hash__(self): # 给对象一个编号 return 123b = Book("红楼梦")# 调用hash(b)就去找__hash__拿数字。print(hash(b)) # 输出123
为什么需要 hash?
集合{}不能存重复元素,靠两点:
①、先比哈希值(__hash__),数字不一样 = 不是同一个东西;
②、哈希一样,再用__eq__判断内容是否相等。
class Book: def __init__(self,name): self.name = name # 同名书哈希一样 def __hash__(self): return hash(self.name) # 名字相同就是同一本书 def __eq__(self,other): return self.name == other.nameb1 = Book("西游记")b2 = Book("西游记")s = {b1,b2}print(len(s)) # 结果1,自动去重
记住:
①、相等的对象,哈希必须相同;
②、只写__eq__不写__hash__,对象不能放进集合、不能当字典键。
3.6 __bool__方法
if 判断 /while/bool (对象),自动执行 __bool__,必须返回 True 或 False。
没写__bool__默认规则:没写这个方法,Python 看对象里面有没有数据:有内容 = True,空 = False。
class Person: def __init__(self, age): self.age = age def __bool__(self): # 年龄大于18返回True,否则False return self.age >= 18p1 = Person(20)p2 = Person(15)print(bool(p1)) # Trueprint(bool(p2)) # False# if自动调用__bool__if p1: print("成年")if p2: print("未成年")#输出:TrueFalse成年
四、运算符重载
4.1 加法
+:__add__
正常:3+5 数字相加。
自己造的类默认不能 对象+数字,写__add__就能让加号生效。加号左边是你的对象,就执行__add__。
class Money: def __init__(self,money): self.m = money # +左边是当前对象,右边是other def __add__(self,other): return self.m + otherm = Money(100)res = m + 200 # 自动调用 m.__add__(200)print(res) # 300
下面的运算魔术方法规律同上。
| | |
|---|
+ | __add__ | |
- | __sub__ | |
* | __mul__ | |
/ | __truediv__ | |
// | __floordiv__ | |
% | __mod__ | |
** | __pow__ | |
4.2 比较运算符
| | |
|---|
< | __lt__ | |
<= | __le__ | |
> | __gt__ | |
>= | __ge__ | |
== | __eq__ | |
!= | __ne__ | |
以__lt__方法为例:
对象1 < 对象2 → 对象1.__lt__(对象2)
class Student: def __init__(self,age): self.age = age def __lt__(self,other): # < 比较:比年龄 return self.age < other.ages1 = Student(15)s2 = Student(18)print(s1 < s2) # True
4.3 位运算符
| | |
|---|
& | __and__ | |
| | __or__ | |
| __invert__ | 按位非 |
以__invert__方法为例:
class Num: def __init__(self, x): self.x = x def __invert__(self): # ~对象 自动执行这里 return ~self.xn = Num(10)res = ~nprint(res) # -11
4.4 成员检查运算符
class MyList: def __init__(self): # 内部用列表存数据 self.data = [11,22,33,44] # 1.__len__ → len(实例) def __len__(self): return len(self.data) # 2.__getitem__ → 对象[index] 取值 def __getitem__(self,index): return self.data[index] # 3.__setitem__ → 对象[index]=值 修改 def __setitem__(self,index,value): self.data[index] = value # 4.__delitem__ → del 对象[index] 删除 def __delitem__(self,index): del self.data[index] # 5.__iter__ + __next__ 实现for遍历 def __iter__(self): # 返回迭代器 return iter(self.data)# 创建对象lst = MyList()# 1、len(对象)print(len(lst)) # 调用__len__ → 输出4# 2、getitem:lst[下标]print(lst[1]) # 调用__getitem__ → 22print(lst[1:3]) # 还支持切片 [22,33]# 3、setitem:lst[下标]=新值lst[0] = 99 # 调用__setitem__print(lst[0]) # 99# 4、delitem:del lst[下标]del lst[2] # 删除下标2,调用__delitem__print(len(lst)) # 现在只剩3个元素
只要执行 iter(对象) 或者 for x in 对象,自动执行 __iter__,并且必须返回迭代器。案例1:__iter__返回列表迭代器,不用写__next__class A: def __iter__(self): # 返回列表迭代器 return iter([1,2,3])a = A()for i in a: print(i)#输出:# 1# 2# 3
class B: def __init__(self): self.x = 1 def __iter__(self): return self def __next__(self): if self.x > 3: raise StopIteration res = self.x self.x += 1 return resb = B()for i in b: print(i) # 1,2,3
小结:
(1)__iter__返回别人的迭代器,不用写__next__,案例中别人的迭代器指列表[1,2,3];
(2)__iter__返回自己,必须手写__next__控制什么时候停止;
(3)__iter__必须返回迭代器,有它才能被 for 循环。
五、容器协议
5.1 __len__(self)
class Shop: def __init__(self,goods): self.goods = goods def __len__(self): # len()自动调用,必须返回整数 return len(self.goods)s = Shop(["苹果","香蕉","橘子"])print(len(s)) # 3
解读:
(1)len(实例) → 自动执行 __len__;
(2)必须 return 整数;
(3)没写__bool__时,if 判断会借用__len__:长度 0→False,否则 True。
六、属性访问与描述符
6.1 四种普通属性方法
| | | |
|---|
obj.属性名 | __getattribute__(self, name) | | 只要通过 obj.xxx 读取属性,无论属性是否存在,必先触发 |
obj.属性名 | __getattr__(self, name) | | 当 __getattribute__ 没找到对应属性时,自动触发 |
obj.属性名 = 新值 | __setattr__(self, name, value) | | 只要通过 obj.xxx = xxx 给属性赋值,无论属性是否存在,都会触发 |
del obj.属性名 | __delattr__(self, name) | | 只要通过 del obj.xxx 删除属性,就会触发 |
class Person: def __getattr__(self,name): print(f"{name}这个属性不存在")p=Person()print(p.aaa) # aaa这个属性不存在
setattr:只要对象.属性=值 就执行
class Person: def __setattr__(self,name,val): print(f"正在给{name}赋值{val}") self.__dict__[name] = valp = Person()p.name = "小明"
delattr:del 对象.属性 删除时触发
class Person: def __delattr__(self,name): print(f"删除属性:{name}")p=Person()p.age=18del p.age
| | | |
|---|
obj.目标属性 | __get__(self, instance, owner) | | |
obj.目标属性 = 新值 | __set__(self, instance, value) | | |
del obj.目标属性 | __delete__(self, instance) | | |
# 1.描述符类:专门看管 priceclass PriceDesc: # 取值 g.price def __get__(self, instance, owner): # instance = g,实际取值从 g._money 拿 return instance._money # 赋值 g.price=100 def __set__(self, instance, value): if value < 0: raise ValueError("价格不能负数") # 真实数据存在实例的 _money 里 instance._money = value # 删除 del g.price def __delete__(self, instance): print("删除价格") del instance._money# 2.宿主类class Goods: # price交给描述符看管 price = PriceDesc()# 创建对象g = Goods()# 赋值:触发__set__,数据存到 g._moneyg.price = 88# 取值:触发__get__,从 g._money 取出print(g.price)# 删除:触发__delete__,删掉g._moneydel g.price
解读:写 g.price,并不是直接往对象存 price,会自动跳进描述符的__set__方法。定义 __call__ 后:实例对象()就能直接调用,把对象变函数。对象()就能自动执行__call__方法。class Counter: def __init__(self): self.count = 0 def __call__(self): self.count += 1 print("次数:", self.count)c = Counter()c() #1c() #2c() #3
小结:不用每次调用 c.add(),直接 c() 更简洁。场景 2:类实现装饰器(高级用法)
class MyLog: def __call__(self, fn): def inner(): print('执行前') fn() print('执行后') return inner# 创建对象log = MyLog()# 普通函数def say(): print('hello')# 手动装饰:把函数传给对象say = log(say)# 调用say()
log(say)能运行,全靠类里写了__call__。
换成 @语法(和上面一模一样)
class MyLog: def __call__(self, fn): def inner(): print('执行前') fn() print('执行后') return innerlog = MyLog()# @log 等价于 say = log(say)@logdef say(): print('hello')say()
解读:
(1)@log等价于say = log(say);
(2)log(函数),触发__call__方法;
(3)__call__包装函数,返回新 inner;
(4)say()实际跑 inner 里的代码。
场景3:带参数工具类
class Add: def __call__(self,x,y): return x+yadd = Add()print(add(3,7)) #10
7.2 __enter__和__exit__
进入with自动执行__enter__,离开with(不管报错与否)自动执行__exit__。
用途:自动打开资源、自动关闭资源(文件、数据库、网络连接)。
class FileDemo: # with开头,先执行 def __enter__(self): print("打开资源") return "我是as后面的变量" # with代码结束/报错 都会执行 def __exit__(self, exc_type, exc_val, exc_tb): print("关闭资源")with FileDemo() as f: print("中间干活") print(f)
输出:
解读:
①、FileDemo() 创建对象;
②、进入with → 调用__enter__,返回值赋值给 f;
③、运行 with 里面的代码;
④、代码走完 / 代码报错 → 必定执行__exit__,用来释放资源。