Python 魔术方法笔记
前言
来源:B站 码农高天 该UP主将用一系列视频详细讲解Pyhton的魔术方法,我为了不遗失他讲解里面的任何细节,故做此笔记。 在看他的视频之前,我对Python的部分(常用的)魔术有过详细的了解,但为了掌握的更加全面和系统,就必须谦虚起来,认为自己啥也不会,细心地看完这些视频,听讲他说的每一句话。 另外值得一提的是,UP主的讲话比较缓慢,语重心长,利于课程的理解。
P1 基础篇
__new__ 和 __init__
你只需要记住,__new__ 是从一个class建立一个object的过程。而__init__是基于有了这个object,给它初始化的过程。
class A:
def __new__(cls,x) :
print(“__new__ is called”)
return super().__new__(cls) # 有返回值的,返回一个对象
def __init__(self,x) :
self.x = x
print(“__init__”) # 没有返回值
ob = A()
可以粗略理解为,class A作为参数传入cls,执行了__new__,得到了A的对象,然后A的对象作为参数传入self,执行了__init__ 。 如果建立对象的时候传入了一些参数,那么这个参数既会被传入__new__,也会被传入__init__. 使用背景:单例模式。
__del__函数
class A:
def __del__(self):
print("__del__")
ob = A()
该函数指明一个对象被释放的时候,要做的事情。但是我们知道,一个对象在python中何时被回收一个小复杂的事情,可能在字节码执行结束被回收,也或许被gc回收机制回收。使用的时候,需要多加小心。 并且,这里提醒一句,不要想当然的,将这个魔术方法和del关键词联合使用,他们没有强关联。del关键字,只是为了让某个对象少一个引用。这个对象的引用被减少到0,那才会触发回收机制,执行__del__.
__repr__ 和 __str__
class A:
def __repr__(self) :
return "something more specific"
def __str__(self):
return "something easy"
两个方法的功能相似,都是返回该类的对象的字符串表示。他们的区别体现在语义上的不同。
__str__返回的内容,注重可读性。
__repr__返回它更详细的信息。
如果两个方法都定义了,那么对一个对象使用print,会执行__str__。
内置repr()施加一个对象,会调用__repr__.
如果不想两个都定义,你可以只定义__repr__.
其中,弹幕里提到了这样一句话,一般要求eval(ob.__repr()__)的结果等于这个ob本身
,个人简单实验了一下确实如此。
>>> li = []
>>> eval(li.__repr__())
[]
>>> li = [1,2,3]
>>> eval(li.__repr__())
[1, 2, 3]
__format__方法
使用频率更低。你如果想格式化打印一个对象,那么可能就需要自定义这个方法。
class A:
def __format__(self,spec):
if spec == "x" :
return "0xA"
return "<A>"
print(f"{A()}")
print(f"{A():x}")
上面的代码不难理解,spec显然就是冒号后面的指定格式,形式是一个str。用的比较少,不多赘述了。
__bytes__方法
class A:
def __bytes__(self):
print("__bytes__ is called")
return bytes([0,1])
print(bytes(A()))
用来定制化你的对象的bytes表示,否则不必去使用。
P2 比较篇
Rich comparison一个有6个比较。等于,不等于,大于,小于,大于等于,小于等于。
当你没有写一个类的比较逻辑的时候,如果你使用
print(x==y)
,则相当于print(x is y)
,换言之比较两个对象的id是否相等。
但是如果你想定制化==的行为,那么就需要定义__eq__.会改变==的比较的逻辑。
(笔者补充,一般情况下,如果is返回了True,那么多半==也会返回True,因为毕竟id相等,但是不绝对,比如nan非数值)
值得注意的是,__eq__方法没有限定返回值的类型。你可以不返回bool而做一些trick。比如做两个向量的比较,返回一个bool向量。
如果你使用了!=,python如果没有发现你定义__ne__而只定义__eq__的话,那么会把__eq__逻辑取反,就是__ne__的逻辑了。
注意,等于和不等于有默认实现(直接用is来做比较),但是其他的四个,没有默认实现。自己写。
大于或者小于,只实现其中一个,就可以使用>和<了。
关于大于号,方法是__gt__.关于小于号,方法是__lt__.
这里注意,你如果使用父类和其子类进行比较的时候,优先使用其子类定义的比较方法,因为我们一般会认为子类定义的方法会更近一步。又是一个小小的细节~
小总结,做比较的时候eg: x == y
,如果x和y不是同一个类的对象的时候:如果y是x的衍生类,优先使用y的比较函数;否则优先使用x的比较函数。大部分情况下,优先使用运算符左边那个类的比较运算函数,如果左边那个没有相对应的函数,那么去右边那个类找相对应的函数,除非右边那个是左边的子类,此时优先使用右边那个类的函数。(比较绕,细品)
最后,大于等于(__ge__)和小于等于(__le__)。不要想当然认为,小于等于(大于等于也一样)就是小于和等于两者逻辑的或连接。人家python不会给你费劲做这个推测。如果想使用这种比较,请乖乖写代码。
__hash__和hash()
如果你对自定义类定义了一个__eq__函数,那么默认的__hash__就会被删掉。也就是说,你最好连带两个函数一起定义,万无一失。
__hash__的要求如下:必须返回一个整数对象;两个相等的对象,返回相等的hash值。 自定义__hash__方法的技巧是,使用内置的hash方法,把对象的核心属性打包成元组,扔给hash(),然后返回结果即可~
__bool__方法
定义对一个对象转化成bool的逻辑,个人觉得没啥可讲的。
P3 属性篇
__getattr__方法
当你访问的属性不存在的时候,你希望此时做点什么。注意,只有访问不存在的属性的时候,才会执行这个函数。
class A:
def __getattr__(self,name):
print(f"getting{name}")
raise AttributeError
o = A()
print(o.test)
所访问的属性,将以一个string的形式传入方法的name形参中。
__getattribute__方法
这个与上个函数相比容易混淆。但凡你尝试读取对象的任何属性,它都会被调用。不论这个属性是存在的还是不存在的。
class A:
def __init__(self):
self.data = "abc"
self.count = 0
def __getattribute__(self,name):
if name == "data" :
self.count += 1
return super().__getattribute__(name)
注意,方法最后使用super的默认方法实现默认行为。你如果使用getattr内置函数实现这个最后的return,你会发现,getattr会调用这个魔术方法,因此造成了递归调用。
另外,这个函数很容易产生一些不容易察觉到的递归,事实上,代码段中self.count+=1
这里涉及到了该魔术方法的递归调用。
__setattr__方法
比较简单,传入self,name,val三个参数,其中name代表属性的名字,val是属性的值,然后执行super()的默认逻辑,就能实现属性的设置,没什么好讲的。
__delattr__方法
和对象的消亡没啥关系,当我们尝试删除一个object属性才会调用。
__dir__和dir内置函数
__dir__的逻辑直接影响dir内置函数的结果。一般情况下dir之内函数会打印对象可以访问到的属性和方法,包括默认的和自定义的。 __dir__要求必须返回sequence。 比如这样的写法,可以过滤掉默认属性和方法。
def __dir__(self):
lst = super().__dir__() # 先拿到正常的dir
return [el for el in lst if not el.startwith("_")]
描述器相关的魔术方法
class D:
def __get__(self,obj,owner=None):
print(obj,owner)
return 0
class A:
x = D()
o = A()
print(o.x)
像上面的这样,self对应的是描述对象本身,换言之是x;obj对应的是o,也就是class A的对象,owner对应了o的class。
class D:
def __init__(self):
self.val = 0
def __get__(self,obj,owner=None):
return self.val
def __set__(self,obj,value):
self.val = value
def __delete(self,obj)
class A:
x = D()
o = A()
print(o.x)
o.x = 1
print(o.x)
描述对象这个东西让我联想起Java的get,set方法,虽然不太一样,但也差不多吧——对某一个描述器进行访问,会执行其get魔术方法,相应的进行设置会进行set魔术方法。 删除这个描述器的时候,__delete__会被调用。
__slots__特殊名字
不属于特殊方法,这东西就是用来省内存的。如果你的类就那么指定的几个属性,不是那么灵活,就没必要使用dict来存储了,这东西比较消耗内存————而是使用__slots__,在底层使用的是数组的方式存储的属性,内存大大节省。 视频讲到的,这是一个白名单机制,我觉得非常贴切。
__init_subclass__方法 P4
这是定义在基类的方法,只有在建立衍生类的时候才会体现用处。这个方法甚至可以为子类传参!
class Base:
def __init_subclass__(cls,name) :
cls.x = {}
cls.name = name
class A(Base, name = "hi"):
pass
print(A.x)
print(A.name)
'''
{}
hi
'''
__set_name__方法
# 适用在描述器中
class D:
def __set_name__(self,owner,name) :
print(owner,name) # owner是实例化这个描述符的类,name是这个对象的名字
class A:
x = D()
# <class '__main__.A'> x
__class_getitem__方法
和__getitem__区别一下,我们知道对象使用方括号访问数值的时候,会调用__getitem__方法,但是你如果对类进行这样的操作,那么会调用__class_getitem__方法。
class A:
def __class_getitem__(cls,item) :
print(item)
return "abc"
print(A[0]) # 0当做item传入函数中
'''
0
abc
'''
__mro_entries__方法
有些方法你只有和已知的东西做一个对比才能学会他的用法。
class A:
def __mro_entries__(self,bases) : # 如果你继承一个对象的话,那么这个对象的类定义需要定义这个函数
print(bases)
return ()# (A,)
class B(A()) :
pass
print(issubclass(B,A))
用法比较晦涩,简单理解就是B建立对象的时候,向A的对象进行询问哪里去找基类,此时就会调用这个方法给出答案。 返回一个空的tuple,意思就是没有基类;返回一个带A的tuple,意思就是A就是B的基类。
__prepare__方法
涉及到Meta类,使用Meta类建立新类的时候,可以传kw
class Meta(type):
# @classmethod
# def __prepare__(cls,name,bases,**kws) :
# print(name,bases,kws)
# return {} # 这个字典可以给类的传k:v
@classmethod
def __prepare__(cls,name,bases,**kws) :
return {"x" : 10 }
class A(metaclass=Meta):
pass
print(A.x) # 10
__instancecheck__和__subclasscheck__方法
这是两个一般会定义在元类的方法。 分别对应isinstance和issubclass这两个函数。比较好理解,就是可能不太常用。不多讲力。
class Meta(type):
def __instancecheck__(self,instance):
print("instance check")
print(self)
return True
def __subclasscheck__(self,subclass) :
print("Subclass Check")
print(self)
if subclass is int:
return True
return False
class A(metaclass=Meta) :
pass
o = A()
print(isinstance(123,A))
print(issubclass(int,A))
'''
instance check
<class '__main__.A'>
True
Subclass Check
<class '__main__.A'>
True
'''
P6 模拟篇 最后一期
__call__方法
可以像使用函数对象一样使用实例对象
class Multiplier:
def __init__(self,mul) :
self.mul = mul
def __call__(self,arg):
return self.mul * arg
print(Multipier(4)(3))
# 输出12
__len__方法
返回一个container的长度
class Mylist:
def __init__(self,data) :
self._data = data
def __len__(self) :
return len(self._data)
x = Mylist([1,2])
print(len(x))
当你把x当做一个布尔去使用的话,先去查看__bool__魔术方法,如果没有定义就来找__len__魔术方法。长度是0就是false,否则就是true
__length_hint__魔术方法
这个方法很少用到,返回的是一个估计长度。 被operator.length_hint(obj)调用。
__getitem__和__setitem__和__delitem__和__reversed__和__contains__和__iter__魔术方法
class Mylist:
def __init__(self,data) :
self._data = data
def __getitem__(self,key) :
return self._data[int(key)]
def __setitem__(self,key,value) :
self._data[key] = value
'''
下面是忽视IndexError的写法
try:
self_data[key] = value
except IndexError :
pass
'''
def __delitem__(self,key) :
self._data = self._data[0:key] + self._data[key+1:]
def __reversed__(self) : # 翻转,但不替换本身,而是返回一个新建的对象
return Mylist(self._data[::-1])
def __contains__(self,item) : # 当你使用in关键字的时候被调用的
return item in self._data
def __iter__(self):
return iter(self._data) # 返回被包装序列的迭代对象
x = Mylist([1,2,3])
x[0] = 3
print(x[0])
del x[1]
print(2 in x)
print(5 in x)
for i in x :
print(i)
__missing__魔术方法
如果找不到这个key,就会调用missing方法
class MyDict(dict) :
def __missing__(self,key) :
return 0
d = MyDict ()
print(d[0])
context type
with 后面的东西就叫context
import time
class Timer :
def __enter__(self):
self.start = time.time() # 记录时间
return self
def __exit__(self,exc_type,exc_value,traceback) :
print(f"T: {time.time() - self.start}")
with Timer() as t:
_ = 1000 * 100
定义一个上下文,就得定义__enter__和__exit__两个魔术方法。
至于as t
,就是把enter的返回值,赋给了t。
exit函数还有若干参数,都是出现异常的时候使用的。顾名思义,不多说了。即便出现了异常,exit的内容照样执行,所以非常适合用作资源释放。