情感测试
情感测试

您现在的位置: 情感测试简介_情感测试玩法 > 情感测试玩法 > Python进阶如何正确使用魔法方法

Python进阶如何正确使用魔法方法

发布时间:2021-8-14 13:23:41   点击数:

阅读本文大约需要10分钟。

在做Python开发时,我们经常会遇到以双下划线开头和结尾的方法,例如__init__、__new__、__getattr__、__setitem__等等,这些方法我们通常称之为「魔法方法」,而使用这些「魔法方法」,我们可以非常方便地给类添加特殊的功能。

这篇文章,我们就来分析一下,Python中的魔法方法都有哪些?使用这些魔法方法,我们可以实现哪些实用的功能?

魔法方法概览

首先,我们先对Python中的魔法方法进行归类,常见的魔法方法大致可分为以下几类:

构造与初始化类的表示访问控制比较操作容器类操作可调用对象序列化

由于魔法方法分类较多,这篇文章我们先来看前几个:构造与初始化、类的表示、访问控制。剩下的魔法方法,我们会在下一篇文章进行分析讲解。

构造与初始化

首先,我们来看关于构造与初始化相关的魔法方法,主要包括以下几种:

__init____new____del____init__

关于构造与初始化的魔法方法,我们使用最频繁的一个就是__init__了。

我们在定义类的时候,通常都会去定义构造方法,它的作用就是在初始化一个对象时,定义这个对象的初始值。

#coding:utf8classPerson(object):def__init__(self,name,age):self.name=nameself.age=agep1=Person(张三,25)p2=Person(李四,30)__new__

在初始化一个类的属性时,除了使用__init__之外,还可以使用__new__这个方法。

我们在平时开发中使用的虽然不多,但是经常能够在开源框架中看到它的身影。实际上,这才是「真正的构造方法」。

#coding:utf8classPerson(object):def__new__(cls,*args,**kwargs):print"call__new__"returnobject.__new__(cls,*args,**kwargs)def__init__(self,name,age):print"call__init__"self.name=nameself.age=agep=Person("张三",20)#Output:#call__new__#call__init__

从例子我们可以看到,__new__会在对象实例化时第一个被调用,然后才会调用__init__,它们的区别如下:

__new__的第一个参数是cls,而__init__的第一个参数是self__new__返回值是一个实例对象,而__init__没有任何返回值,只做初始化操作__new__由于返回的是一个实例对象,所以它可以给所有实例进行统一的初始化操作

了解了它们之间的区别,我们来看__new__在什么场景下使用?

由于__new__优先于__init__调用,而且它返回的是一个实例,所以我们可以利用这个特性,在__new__方法中,每次返回同一个实例来实现一个单例类:

#coding:utf8classSingleton(object):"""单例"""_instance=Nonedef__new__(cls,*args,**kwargs):ifnotcls._instance:cls._instance=super(Singleton,cls).__new__(cls,*args,**kwargs)returncls._instanceclassMySingleton(Singleton):passa=MySingleton()b=MySingleton()assertaisb#True

另外一个使用场景是,当我们需要继承内置类时,例如想要继承int、str、tuple,就无法使用__init__来初始化了,只能通过__new__来初始化数据:

#coding:utf8classg(float):"""千克转克"""def__new__(cls,kg):returnfloat.__new__(cls,kg*2)a=g(50)#50千克转为克printa#printa+#由于继承了float,所以可以直接运算,非常方便!

在这个例子中,我们实现了一个类,这个类继承了float,之后,我们就可以对这个类的实例进行计算了,是不是很神奇?

除此之外,__new__比较多的应用场景是配合「元类」使用,关于「元类」的原理,我会在后面的文章中讲到。

__del__

__del__这个方法就是我们经常说的「析构方法」,也就是在对象被垃圾回收时被调用。

但是请注意,当我们执行delobj时,这个方法不一定会执行。

由于Python是通过引用计数来进行垃圾回收的,如果这个实例在执行del时,还被其他对象引用,那么就不会触发执行__del__方法。

我们来看一个例子:

classPerson(object):def__del__(self):print__del__

我们定义了一个带有__del__方法的类,此时我们直接执行:

a=Person()printexit#Output:#exit#__del__

由于我们没有对实例进行任何引用操作时,所以__del__在程序退出时被调用。

如果我们显示执行delobj,如下:

a=Person()dela#手动销毁对象printexit#Output:#__del__#exit

同样地,由于实例没有被其他对象所引用,当我们手动销毁这个实例时,__del__被调用后程序正常退出。

如果这个对象被其他对象所引用:

a=Person()b=a#b引用adela#手动销毁不触发__del__printexit#Output:#exit#__del__

可以看到,如果这个实例有被其他对象引用,尽管我们手动销毁这个实例,但不会触发__del__方法,而是在程序正常退出时被调用执行。

通常来说,__del__这个方法我们很少会使用到,除非需要在显示执行del执行特殊清理逻辑的场景中才会使用到。

但另一方面,也给我们一个提醒,当我们在对文件、Socket进行操作时,如果要想安全地关闭和销毁这些对象,最好是在try异常块后的finally中进行关闭和释放操作,从而避免资源的泄露。

类的表示

接下来,我们来看关于类的表示相关的魔法方法,主要包括以下几种:

__str__/__repr____unicode____hash__/__eq____nozero____str__/__repr__

关于__str__和__repr__这2个魔法方法,非常类似,很多人区分不出它们有什么不同,我们来看几个例子,就能理解这2个方法的效果:

a=hellostr(a)hello%s%a#调用__str__hellorepr(a)#对象a的标准表示也就是a是如何创建的"hello"%r%a#调用__repr__"hello"importdatetimeb=datetime.datetime.now()str(b)-02-:28:40.printb#等同于printstr(b)-02-:28:40.repr(b)#展示对象b的标准创建方式(如何创建的)datetime.datetime(,2,22,12,28,40,)b#等同于printrepr(b)datetime.datetime(,2,22,12,28,40,)c=eval(repr(b))#repr(b)目标针对于机器所以可执行cdatetime.datetime(,2,22,12,28,40,)

从上述例子中我们可以看出这2个方法的区别:

__str__强调可读性,而__repr__强调准确性/标准性__str__的目标人群是用户,而__repr__的目标人群是机器,__repr__返回的结果是可执行的,通过eval(repr(obj))可以正确运行占位符%s调用的是__str__,而%r调用的是__repr__方法

所以,我们在实际中开发中定义类时,一般这样使用:

#coding:utf8classPerson(object):def__init__(self,name,age):self.name=nameself.age=agedef__str__(self):#格式化友好对用户展示returnname:%s,age:%s%(self.name,self.age)def__repr__(self):#标准化展示return"Person(%s,%s)"%(self.name,self.age)person=Person(zhangsan,20)#强调对用户友好printstr(person)#name:zhangsan,age:20print%s%person#name:zhangsan,age:20#强调对机器友好结果eval可执行printrepr(person)#Person(zhangsan,20)print%r%person#Person(zhangsan,20)

明白了它们之间的区别,我们再思考一下,如果只定义了__str__或__repr__其中一个,那会是什么结果?

只定义__str__,但没有定义__repr__:

#coding:utf8classPerson(object):def__init__(self,name,age):self.name=nameself.age=agedef__str__(self):returnname:%s,age:%s%(self.name,self.age)person=Person(zhangsan,20)printstr(person)#name:zhangsan,age:20print%s%person#name:zhangsan,age:20printrepr(person)#__main__.Personobjectat0x10beeprint%r%person#__main__.Personobjectat0x10bee

只定义__repr__,但没有定义__str__:

#coding:utf8classPerson(object):def__init__(self,name,age):self.name=nameself.age=agedef__repr__(self):return"Person(%s,%s)"%(self.name,self.age)person=Person(zhangsan,20)printstr(person)#Person(zhangsan,20)print%s%person#Person(zhangsan,20)printrepr(person)#Person(zhangsan,20)print%r%person#Person(zhangsan,20)

从例子中我们可以看到结果:

如果只定义了_str__,那么repr(person)输出__main__.Personobjectat0x10bee如果只定义了__repr__,那么str(person)与repr(person)结果是相同的

也就是说,__repr__在表示类时,是一级的,如果只定义它,那么__str__=__repr__。

而__str__展示类时是次级的,如果没有定义__repr__,那么repr(person)将会展示缺省的定义。

__unicode__

如果一个类定义了__unicode__方法,那么在调用unicode(obj)时,此方法将被调用,但是其返回值类型是unicode。

#coding:utf8classPerson(object):def__unicode__(self):#这里不是uhelloreturnhelloperson=Person()printunicode(person)#hellloprinttype(unicode(person))#typeunicode

从例子中我们可以看到,虽然我们定义的__unicode__返回值不是unicode类型,但在输出时,程序会自动转换成unicode类型。

这个方法在开发中一般很少使用,通常我们只需要定义__str__即可。

__hash__/__eq__

__hash__方法返回一个整数,用来表示实例对象的唯一标识,配合__eq__方法,可以判断两个对象是否相等:

#coding:utf8classPerson(object):def__init__(self,uid):self.uid=uiddef__repr__(self):returnPerson(%s)%self.uiddef__hash__(self):returnself.uiddef__eq__(self,other):returnself.uid==other.uidp1=Person(1)p2=Person(1)p1==p2#Truep3=Person(2)printset([p1,p2,p3])#根据唯一标识去重输出set([Person(1),Person(2)])

如果我们需要判断两个对象是否相等,只需要我们重写__hash__和__eq__方法就可以了。

此外,当我们使用set时,在set中存放这些对象,也会根据这两个方法进行去重操作。

__nonzero__

当调用bool(obj)时,会调用__nonzero__方法,返回True或False:

#coding:utf8classPerson(object):def__init__(self,uid):self.uid=uiddef__nonzero__(self):returnself.uid10p1=Person(1)p2=Person(15)printbool(p1)#Falseprintbool(p2)#True

在Python3中,__nonzero__被重命名为__bool__。

访问控制

接下来,我们来看关于访问控制的魔法方法,主要包括以下几种:

__setattr__:通过「.」设置属性或setattr(key,value)设置属性时调用__getattr__:访问不存在的属性时调用__delattr__:删除某个属性时调用__getattribute__:访问任意属性或方法时调用

我们来看使用这些方法的完整例子:

#coding:utf8classPerson(object):def__setattr__(self,key,value):"""属性赋值"""ifkeynotin(name,age):returnifkey==ageandvalue0:raiseValueError()super(Person,self).__setattr__(key,value)def__getattr__(self,key):"""访问某个不存在的属性"""returnunknowndef__delattr__(self,key):"""删除某个属性"""ifkey==name:raiseAttributeError()super(Person,self).__delattr__(key)def__getattribute__(self,key):"""所有属性/方法调用都经过这里"""ifkey==money:returnifkey==hello:returnself.sayreturnsuper(Person,self).__getattribute__(key)defsay(self):returnhellop1=Person()p1.name=zhangsan#调用__setattr__p1.age=20#调用__setattr__printp1.name#zhangsanprintp1.age#20setattr(p1,name,lisi)#调用__setattr__setattr(p1,age,30)#调用__setattr__printp1.name#lisiprintp1.age#30p1.gender=male#__setattr__中忽略对gender赋值printp1.gender#gender不存在所以会调用__getattr__返回unknownprintp1.money#money不存在在__getattribute__中返回printp1.say()#helloprintp1.hello()#hello调用__getattribute__间接调用say方法delp1.name#__delattr__中引发AttributeErrorp2=Person()p2.age=-1#__setattr__中引发ValueError

我们仔细看一下这个例子,我已经添加好了详细的注释。

__setattr__

先来说__setattr__,当我们在给一个对象进行属性赋值时,都会经过这个方法,在这个例子中,我们只允许对name和age这2个属性进行赋值,忽略了gender属性,除此之外,我们还对age赋值进行了校验。

通过__setattr__方法,我们可以非常方便地对属性赋值进行控制。

__getattr__

再来看__getattr__,由于我们在__setattr__中忽略了对gender属性的赋值,所以当访问这个不存在的属性时,会调用__getattr__方法,在这个方法中返回了默认值unknown。

很多同学以为这个方法与__setattr__方法对等的,一个是赋值,一个是获取。其实不然,__getattr__只有在访问「不存在的属性」时才会被调用,这里我们需要注意。

__getattribute__

了解了__getattr__后,还有一个和它非常类似的方法:__getattribute__。

很多人经常把这个方法和__getattr__混淆,通过例子我们可以看出,它与前者的区别在于:

__getattr__只有在访问不存在的属性时被调用,而__getattribute__在访问任意属性时都会被调用__getattr__只针对属性访问,而__getattribute__不仅针对所有属性访问,还包括方法调用

在上面的例子,虽然我们没有定义money属性和hello方法,但是在__getattribute__里拦截到了这个属性和方法,就可以对其执行不同的逻辑。

__delattr__

最后,我们来看__delattr__,它比较简单,当删除对象的某个属性时,这个方法会被调用,所以它一般会用在删除属性前的校验场景中使用。

总结

这篇文章,我们主要介绍了Python中常见的魔法方法,主要有构造与初始化、类的表示、访问控制这3个模块。

构造与初始化的魔法方法,常常用在类的初始化过程中,其中__init__一般用于实例初始化,而__new__可以改变初始化实例的行为,通过它我们可以实现一个单例或者继承一个内置类。

关于类的表示的魔法方法,比较常用的,当我们想表示一个类时,可以使用__str__或__repr__方法,当需要判断两个对象是否相等时,可以使用__hash__和__eq__方法。

关于访问控制的魔法方法,它可以控制实例的属性赋值、属性访问、方法访问、属性删除等操作,这对于我们实现一个复杂功能的类有很大帮助。

在下一篇文章,我们会继续分析剩下的魔法方法,主要包括关于比较操作、容器类操作、可调用对象、序列化相关的魔法方法。

Tips:后台回复关键字进阶可查看文章列表。

-EOF-

推荐阅读点击标题可跳转

1、没想到,Python还可以制作Web可视化页面!

2、用Python创作酷炫的几何图形

3、Python爬虫进阶必备:某游戏网站密码加密逻辑分析(webpack的js加密代码怎么扣-思路分析)

觉得本文对你有帮助?请分享给更多人

推荐

转载请注明:http://www.zmax-alibaba.com/qgwf/137956.html

网站简介 | 发布优势 | 服务条款 | 隐私保护 | 广告合作 | 合作伙伴 | 版权申明 | 网站地图

当前时间: