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的内容照样执行,所以非常适合用作资源释放。

全部评论

相关推荐

评论
16
4
分享

创作者周榜

更多
牛客网
牛客网在线编程
牛客网题解
牛客企业服务