前言
在python中,有一些很特别的方法,它以”__”双下划线开头和结尾,这类方法我们称之为魔术方法。这些方法有特殊的用途,有的不需要我们自己去定义,有的我们通过简单的定义可以实现一些神奇的功能。
__init__、__new__和__del__
构造器:__init__和__new__
__init__应该是我们最为熟悉和常见的一个魔术方法,很多时候,我们会直接把它叫做构造函数,其实不然,在实例一个对象的时候,它并不是第一个被调用的函数,而是一个__new__的方法,这两者共同作用才是真正的构造函数。
看栗子:
1 | class A: |
结果中我们可以发现有两个问题:
1、当实例对象a
时,会自动先去调用__new__方法,但是为什么 __init__没有被调用呢?
2、为什么这个实例对象没有创建成功,返回的是None
呢?
__init__没有被调用的原因是对象没有创建成功,那为什么对象没有创建成功呢?
原因是,python3之后,所有的类都是默认继承object
类的,我们可以点进object
类查看源码:
1 | class object: |
可以看到,基类中有__new__方法,而在上面的例子中,我们其实是重写了基类的__new__方法!导致它不能真正的去创建对象!(所以__new__要慎用,一般情况最好不要去重写)
我们修改下上面的例子,让类A中的new方法继承基类中的new方法:
1 | class A: |
可以看到,实例对象a
时,自动先调用__new__方法,由它去创建对象并且由*args
, **kwargs
来接收创建对象时的参数,第二步,对象创建好后,自动调用__init__方法,由它来完成实例对象的属性初始化操作。因此,由__new__和__init__组成的构造器,完成了对象的创建和初始化。
前面说到最好不要重写__new__方法,但在做单例模式时,我们会这么做。
单例模式
单例模式(Singleton Pattern)是软件设计中的一种常用设计模式,目的主要是使某个类只存在一个实例(节约内存),比如在我们自动化测试框架中,像日志模块和数据库操作等模块,这两个操作类中,其实我们只需要实例一个日志输出对象和数据库操作对象即可,这种情况,我们即可采用单例。
具体实现:
1 | class Singleton: |
在创建对象会自动调用__new__方法,那么我们则可以通过重写__new__的方式,来限制一个类的对象创建。定义一个类属性__obj
,如果该类没有创建过对象,那么就正常创建这个对象,并把这个对象赋值给类属性__obj
,如果创建过对象,那么直接返回__obj
,即之前创建的对象,所以最后可以看到,这个类创建的对象id都是一样的,即是同一个对象。
**ps:**通过装饰器的方式,也可以实现单例模式:
1 | def singleton(cls): |
还有更多的单例实现方式,可以参考听风大佬的博文。
析构器:__del__
python中通过__del__就是一个析构函数了,当对象被销毁时,会调用他本身的析构函数,另外当对象在某个作用域中调用完毕,在跳出其作用域的同时析构函数也会被调用一次,这样可以用来释放内存空间。
析构函数(destructor) 与构造函数相反,当对象结束其生命周期,如对象所在的函数已调用完毕时,系统自动执行析构函数。析构函数往往用来做“清理善后” 的工作(例如在建立对象时用new开辟了一片内存空间,delete会自动调用析构函数后释放内存)。
简单来说,就是擦屁股的,这个对象删除不用了,ok,我帮你善后。
1 | class A: |
1 | class A: |
__enter__和__exit__
在平常操作文件时,通常需要先打开文件、操作读写、再关闭文件,而当使用with
关键字去操作文件时,却可以自动关闭文件,这是为什么呢?why?
1 | # 方法一: |
这背后的原理其实就是:上下文管理器。(上下文管理器具体原理看这篇《上下文管理器》)
而实现上下文管理器的方法之一,涉及到两个魔术方法:__enter__和__exit__。
1 | class Demo: |
如上所示,Demo
实现了__enter__
和__exit__
这两个上下文管理器协议,当Demo调用/实例化的时候,则创建了上下文管理器Demo
。
当上下文管理器遇到with
关键字,上下文管理器就会被激活,先自动调用__enter__
,然后进入运行时上下文环境,执行with_body
中的语句,执行完成后,自动调用__exit__
。其实这里的__enter__
和__exit__
就类似于我们测试中的前置和后置。
通常,with
会和as
一起使用,当有as
从句时:
1 | class Demo: |
在执行__enter__
方法后,会返回自身或另一个与运行时上下文相关的对象(此例返回了Demo对象),然后赋值给变量x
,在上下文环境中,实例对象x
执行了count
方法。
此外,在上面的例子中,应该能注意到,我们分别打印了__exit__
中的三个参数:exc_type
,exc_val
, exc_tb
,但是打印结果却都为None
,why?
首先,这三个参数代表的含义为:
- exc_type: 异常类型
- exc_val:异常值
- exc_tb:异常的溯源信息
当在执行with_body
中的语句时,若出现了异常,则会自动执行__exit__
方法,并且将异常信息分别对应这三个参数,传递进__exit__
方法中进行处理。
我们用上面的例子,修改一下count
的参数,构造一个异常:
1 | with Demo() as x: |
此时的运行结果为:
1 | 执行前置条件 |
可以看到,with
代码块中的代码出现了异常后,立即触发了__exit__
方法捕捉到这个异常,并分别打印了异常的类型、值、溯源信息,然后抛出了异常。
需要注意的是,如果__exit__
方法中有return True
,那么该方法则不会抛出异常。
__str__
先抛一个问题:为什么str(123)
打印出来会是字符串"123"
呢?
先不忙解释,我们先看如果打印一个类的实例对象,会是什么呢?
1 | class Demo: |
可以看到,打印结果是a
的对象信息和内存地址。其实通过查看str()
源码会发现,str
其实是一个类,所有的字符串都是它的实例对象,那么为什么同样是类,我们自定义类的实例对象的打印结果却不一样呢?原因就在于__str__
。
1 | class str(object): |
__str__
方法可以自定义实例对象的打印结果为指定的字符串,这里str()
的实例对象返回的是Return str(self)
,即返回的对象本身,所以,这就是为什么str(123)
打印出来会是字符串"123"
。
我们也可以来自定义试试:
1 | class Car: |
同理,int()
、bool()
等其实也是类似的原理。
1 | class int(object): |
1 | class bool(int): |
python自2.2以后,对类和类型进行了统一,做法就是将int()、float()、str()、list()、tuple()这些BIF转换为工厂函数,所谓工厂函数,其实就是一个类对象,当你调用他们的时候,事实上就是创建一个相应的实例对象。
__add__
"hello," + "world"
字符串为什么可以拼接成"hello,world"
?
1+1
为什么可以等于整型相加为什么可以等于2
?
[1,2,3] + ["a","b"]
列表相加为什么可以合并为[1,2,3,"a","b"]
?
{"a":1} + {"b":2}
字典又为什么不能直接相加合并呢?
原因是可以相加的类里都有__add__
方法,而字典没有该方法。
1 | def __add__(self, *args, **kwargs): # real signature unknown |
那为什么有这个方法就能让同类型的对象相加呢?
因为__add__
方法定义了对象相加的逻辑和返回值,当使用+
操作时,将会触发__add__()
方法。
直接上例子:
1 | class Person: |
这里Person
类中并没有定义对象相加的方法,此时直接相加就会报错,提示不支持该操作:
1 | Traceback (most recent call last): |
加上__add__
后:
1 | class Person: |
__add__
方法中的参数self
和other
分别指Person
的不同实例对象,这里self
即对象man
,other
即对象woman
。配合上文提到的__str__
,还可以自定义+
的打印结果:
1 | class Person: |
这里只提到了+
的魔术方法,其实其他的运算符也都有对应的魔术方法,原理也差不多,本文就不再赘述。
附上其它运算符对应魔术方法:
__slots__
众所周知,python是一门动态语言,所谓动态就是运行代码时可以根据某些条件改变自身结构,例如创建一个类的实例对象,可以给该实例绑定任意的属性和方法。
绑定属性:
1 | class A: |
绑定方法:
1 | from types import MethodType |
此外,每个类的实例对象都会被分配一个__dict__
属性,它会维护该实例的所有属性。
1 | class A: |
如果我们再实例一个对象:
1 | b = A('古二',19) |
由此可见,类的每次实例化,都会为其对象分配一个__dict__
属性,那么试想,如果需要创建大量实例,这个__dict__
就会有点浪费内存了。
所以python也是提供了一种解决方案,就是在类中定义__slots__
属性。
__slots__
一是可以限制实例能添加的属性,二是阻止类实例化时分配__dict__
属性。
1 | class A: |
输出结果:
1 | Traceback (most recent call last): |
报错提示A
对象没有gender
这个属性,因为__slots__
限制了这个类的所有实例对象只能有name
、age
这两个属性,不能再添加任何属性。
另外,A
的实例对象也不再有__dict__
属性了:
1 | print(a.__dict__) |
1 | Traceback (most recent call last): |
注意:
__slots__
仅对当前类起作用,对继承的子类不起作用- 在子类中定义
__slots__
,子类允许定义的属性就是自身的__slots__
加上父类的__slots__
说了这么多,有啥实际应用呢?
在接口自动化测试中,有个场景是需要验证查询日志的返回结果是否跟查询条件一致,比如返回了一组这样的json数据:
即我需要校验其中每个字典中某些字段的值和查询条件一致,那么我该怎么处理呢?
第一个方法很简单,直接用jsonpath
提取查询字段,然后遍历比较就行,但是日志的数量通常是上万条的,这时候jsonpath
的性能就不太行。
第二个方法,就是自己定义一个逻辑实现。我们可以看到,这样一组json数据中,列表中嵌套了许多字典,每个字典的结构其实都一样,key
都是一样的,只是value
不一定一样,那么,我们可以封装一个校验json的类,把每个字典都当做一个实例对象,里面的key
就是实例属性,value
就是实例属性值,然后我们还可以顶一个is_validated
方法来对做相关的校验,比如这样:
1 | class Validated: |
另外有些key
对我们来说,是不需要的,我们只需要保留查询条件字段即可(比如我们的查询字段为happenTime
、 type
、 appName
,那么我们就只需要保留这三个字段),因此我们可以设置__slots__
来限制只保留我们需要的字段,相当于一个白名单,另外,也可以阻止__dict__
的生成来节约内存。
这个方法的好处是,代码复用性会更强,扩展性也更强,如果下一次又加了几个新字段,那么继承一下,那么第一个版本和第二个版本分别是属性隔离、接口隔离的,也符合面向对象的开闭原则。
但是,目前这个方法的运行速度还不够快,还需要优化,暂时还没想到更好的方法,害。
自定义属性访问
python的自定义属性访问涉及到四个魔术方法,分别是:__getattribute__
、__getattr__
、__setattr__
和__delattr__
。
__getattribute__
1 | class A(object): |
可以看到,当我们在一个类中定义了__getattribute__
方法,在访问对象属性的时候,会去调用这个方法,但是这里返回的结果却是None
,这是因为我们重写了父类的__getattribute__
方法,它无法完成返回对应属性值的操作。
父类object中的__getattribute__
方法:
1 | def __getattribute__(self, *args, **kwargs): # real signature unknown |
注意:虽然看源码我们知道该方法其实就是Return getattr(self, name),但是我们重写的时候绝对不能这么写:
1 | def __getattribute__(self, item): |
这样将会陷入无限递归~
因此我们需要调用父类的该方法,来实现返回属性值的功能。
1 | class A(object): |
a.x
等价于a.__getattribute__(x)
。
__getattr__
接上面的例子,当访问一个存在的属性时:
1 | class A(object): |
此时会直接报错:
1 | 调用__getattribute__ |
如果我们在这个类中定义一个__getattr__
方法:
1 | class A(object): |
输出结果:
1 | 调用__getattribute__ |
可以发现,当访问属性时,会先调用__getattribute__
,如果这个属性存在,就返回属性值,如果这个属性不存在(发生AttributeError错误),将会触发__getattr__
,这个方法会捕获异常。
其实可以总结出,访问属性时,属性的查找过程如下(如print(a.attr)
):
1、首先会在对象的实例属性中寻找,未找到则执行第二步
2、在对象所在的类中查找类属性,未找到则执行第三步
3、到对象的继承链上寻找,未找到则只需第四步
4、最后调用__getattr__
方法,如果该属性还是没有找到且没有定义__getattr__
方法,那么就会抛出AttributeError
,终止查找。
__setattr__
这个很好理解,就是在给对象定义属性时,就会触发__setattr__
方法:
1 | class A(object): |
可以看到三种定义对象属性的方式,不管是哪一种,都会触发__setattr__
。
此外,这里重写__setattr__
方法时,也必须调用父类__setattr__
,否则属性不会真正设置成功。
__delattr__
很明显,最后一个方法就是在删除对象属性时触发的:
1 | class A(object): |
值得注意的是,当重写了__delattr__
时,如果没有调用父类的__delattr__
,其实属性是不会被删除的。要想删除,就必须调用父类方法。
1 | def __delattr__(self, item): |
小例子
上面说了那么多,结合一个小例子,来看看这几个魔术方法的作用。
1 | 定义一个Students类: |
1 | class Students: |
总结
上文中总结了一些常用的魔术方法,所谓魔术方法,我个人理解其实就像LOL中每个英雄的被动技能,当我们给一个类定义了一些魔术方法,就相当于赋予了它一个被动技能,这个被动技能会在特定的情况下被自动触发,从而使对象变得更加灵活强大,实现一些或高级或复杂的功能,是真正的黑魔法。
当然,python中的魔术方法远不止上面所总结的,下面附上python中的魔术方法大全以供查阅。
附录
魔术方法大全
魔法方法 | 含义 |
---|---|
基本的魔法方法 | |
new(cls[, …]) | __new__是在一个对象实例化的时候所调用的第一个方法 |
init(self[, …]) | 构造器,当一个实例被创建的时候调用的初始化方法 |
del(self) | 析构器,当一个实例被销毁的时候调用的方法 |
call(self[, args…]) | 允许一个类的实例像函数一样被调用:x(a, b) 调用 x.call(a, b) |
len(self) | 定义当被 len() 调用时的行为 |
repr(self) | 定义当被 repr() 调用时的行为 |
str(self) | 定义当被 str() 调用时的行为 |
bytes(self) | 定义当被 bytes() 调用时的行为 |
hash(self) | 定义当被 hash() 调用时的行为 |
bool(self) | 定义当被 bool() 调用时的行为,应该返回 True 或 False |
format(self, format_spec) | 定义当被 format() 调用时的行为 |
有关属性 | |
getattr(self, name) | 定义当用户试图获取一个不存在的属性时的行为 |
getattribute(self, name) | 定义当该类的属性被访问时的行为 |
setattr(self, name, value) | 定义当一个属性被设置时的行为 |
delattr(self, name) | 定义当一个属性被删除时的行为 |
dir(self) | 定义当 dir() 被调用时的行为 |
get(self, instance, owner) | 定义当描述符的值被取得时的行为 |
set(self, instance, value) | 定义当描述符的值被改变时的行为 |
delete(self, instance) | 定义当描述符的值被删除时的行为 |
比较操作符 | |
lt(self, other) | 定义小于号的行为:x < y 调用 x.lt(y) |
le(self, other) | 定义小于等于号的行为:x <= y 调用 x.le(y) |
eq(self, other) | 定义等于号的行为:x == y 调用 x.eq(y) |
ne(self, other) | 定义不等号的行为:x != y 调用 x.ne(y) |
gt(self, other) | 定义大于号的行为:x > y 调用 x.gt(y) |
ge(self, other) | 定义大于等于号的行为:x >= y 调用 x.ge(y) |
算数运算符 | |
add(self, other) | 定义加法的行为:+ |
sub(self, other) | 定义减法的行为:- |
mul(self, other) | 定义乘法的行为:* |
truediv(self, other) | 定义真除法的行为:/ |
floordiv(self, other) | 定义整数除法的行为:// |
mod(self, other) | 定义取模算法的行为:% |
divmod(self, other) | 定义当被 divmod() 调用时的行为 |
pow(self, other[, modulo]) | 定义当被 power() 调用或 ** 运算时的行为 |
lshift(self, other) | 定义按位左移位的行为:<< |
rshift(self, other) | 定义按位右移位的行为:>> |
and(self, other) | 定义按位与操作的行为:& |
xor(self, other) | 定义按位异或操作的行为:^ |
or(self, other) | 定义按位或操作的行为:| |
反运算 | |
radd(self, other) | (与上方相同,当左操作数不支持相应的操作时被调用) |
rsub(self, other) | (与上方相同,当左操作数不支持相应的操作时被调用) |
rmul(self, other) | (与上方相同,当左操作数不支持相应的操作时被调用) |
rtruediv(self, other) | (与上方相同,当左操作数不支持相应的操作时被调用) |
rfloordiv(self, other) | (与上方相同,当左操作数不支持相应的操作时被调用) |
rmod(self, other) | (与上方相同,当左操作数不支持相应的操作时被调用) |
rdivmod(self, other) | (与上方相同,当左操作数不支持相应的操作时被调用) |
rpow(self, other) | (与上方相同,当左操作数不支持相应的操作时被调用) |
rlshift(self, other) | (与上方相同,当左操作数不支持相应的操作时被调用) |
rrshift(self, other) | (与上方相同,当左操作数不支持相应的操作时被调用) |
rand(self, other) | (与上方相同,当左操作数不支持相应的操作时被调用) |
rxor(self, other) | (与上方相同,当左操作数不支持相应的操作时被调用) |
ror(self, other) | (与上方相同,当左操作数不支持相应的操作时被调用) |
增量赋值运算 | |
iadd(self, other) | 定义赋值加法的行为:+= |
isub(self, other) | 定义赋值减法的行为:-= |
imul(self, other) | 定义赋值乘法的行为:*= |
itruediv(self, other) | 定义赋值真除法的行为:/= |
ifloordiv(self, other) | 定义赋值整数除法的行为://= |
imod(self, other) | 定义赋值取模算法的行为:%= |
ipow(self, other[, modulo]) | 定义赋值幂运算的行为:**= |
ilshift(self, other) | 定义赋值按位左移位的行为:<<= |
irshift(self, other) | 定义赋值按位右移位的行为:>>= |
iand(self, other) | 定义赋值按位与操作的行为:&= |
ixor(self, other) | 定义赋值按位异或操作的行为:^= |
ior(self, other) | 定义赋值按位或操作的行为:|= |
一元操作符 | |
pos(self) | 定义正号的行为:+x |
neg(self) | 定义负号的行为:-x |
abs(self) | 定义当被 abs() 调用时的行为 |
invert(self) | 定义按位求反的行为:~x |
类型转换 | |
complex(self) | 定义当被 complex() 调用时的行为(需要返回恰当的值) |
int(self) | 定义当被 int() 调用时的行为(需要返回恰当的值) |
float(self) | 定义当被 float() 调用时的行为(需要返回恰当的值) |
round(self[, n]) | 定义当被 round() 调用时的行为(需要返回恰当的值) |
index(self) | 1. 当对象是被应用在切片表达式中时,实现整形强制转换 2. 如果你定义了一个可能在切片时用到的定制的数值型,你应该定义 index 3. 如果 index 被定义,则 int 也需要被定义,且返回相同的值 |
上下文管理(with 语句) | |
enter(self) | 1. 定义当使用 with 语句时的初始化行为 2. enter 的返回值被 with 语句的目标或者 as 后的名字绑定 |
exit(self, exc_type, exc_value, traceback) | 1. 定义当一个代码块被执行或者终止后上下文管理器应该做什么 2. 一般被用来处理异常,清除工作或者做一些代码块执行完毕之后的日常工作 |
容器类型 | |
len(self) | 定义当被 len() 调用时的行为(返回容器中元素的个数) |
getitem(self, key) | 定义获取容器中指定元素的行为,相当于 self[key] |
setitem(self, key, value) | 定义设置容器中指定元素的行为,相当于 self[key] = value |
delitem(self, key) | 定义删除容器中指定元素的行为,相当于 del self[key] |
iter(self) | 定义当迭代容器中的元素的行为 |
reversed(self) | 定义当被 reversed() 调用时的行为 |
contains(self, item) | 定义当使用成员测试运算符(in 或 not in)时的行为 |
参考文章:《python魔术方法大全》