在python类中,有很多已经定义好、具有特殊功能的隐式方法(魔法函数),如常用的__init__
、__call__
等,这些方法可以帮助我们实现一些特殊的功能。
python类中的隐式方法名都以__(双下划线)开头,__(双下划线)结尾,并且都是内置定义好的,注意和自定义的私有方法区分。
1. init
__init__
是python中的构造方法,可在__init__
中定义类的成员变量,初始化行为等。
python
class People:
def __init__(self, name, age):
self.name = name
self.age = age
2. del
__del__
是python中的析构方法,可在__del__
中定义对象被回收的行为动作。例如,为了防止开发人员忘记释放数据库连接,或者因为异常释放连接的行为没有被执行,则可以在__del__
中定义关闭连接的操作。
python
class DBConnect:
def __init__(self):
self.connect = ...
def __del__(self):
# 如果数据库连接没有被关闭,则自动关闭
if self.connect.ping():
self.connect.close()
3. str/repr
__str__
是python中的字符串序列化函数,调用str函数时的输出内容,类似于java中的toString。__repr__
和__str__
功能很像,但是__repr__
更多是面向开发人员使用,当__repr__
和__str__
都没有定义时,print§是对象p的内存地址,如果同时定义__repr__
和__str__
,不管是print§还是print(str§),结果都是__str__
返回的结果(实际上print会隐式调用str函数),如果只定义了__repr__
,则才会打印__repr__
的结果。但是当进入debug模式时,即使同时定义了__repr__
和__str__
,在终端中直接p回车,会发现结果就是__repr__
的结果。大多数情况下,都是使用__str__
。
python
class People:
def __init__(self, name, age):
self.name = name
self.age = age
def __str__(self):
return 'name: ' + self.name + ', age: ' + str(self.age)
p = People('Tom', 18)
print(p)
4. len/abs/int/float/hash
__len__
是len函数的内置支持函数;__abs__
是abs函数的内置支持函数;__int__
是int函数的内置支持函数;__float__
是float函数的内置支持函数;__hash__
是hash函数的内置支持函数;
python
class People:
def __init__(self, name, age):
self.name = name
self.age = age
def __len__(self):
return len(self.name)
p = People('Tom', 18)
print(len(p))
5. iter/next
一个类实例如果想通过 for...in... 遍历,则实例本身必须是一个迭代器(iterator)。python中类成为迭代器需要实现两个方法:
__iter__
:返回一个迭代器对象,这个对象必须包含__next__方法
。
__next__
:该方法用于返回下一个迭代元素,且当迭代结束时要抛出StopIteration异常。
其实,当实现了__next__
方法时,类实例就已经可以通过next方法迭代了:
python
class A:
def __next__(self):
return 3
a = A()
print(next(a))
print(next(a))
上面程序的输出结果都是3,且可以一直调用next,因为我们实现的__next__
方法返回值就是3,且没有定义StopIteration异常逻辑(终止条件),所以可以一直调用。
但是我们发现只有__next__
方法只能通过next函数调用,使用for...in...语法会出现:TypeError: 'A' object is not iterable。什么是iterable(可迭代对象)呢?这就涉及到__iter__
方法,如果一个类实现了__iter__
方法,那么这个类就是可迭代对象。例如下面的程序:
python
class A:
def __iter__(self):
return self
a = A()
for i in a:
print(i)
你会发现在ide中上述程序是没有任何警告的,但是一执行,就会出现:TypeError: iter() returneed non-iterator of type 'A',意思就是__iter__
返回的不是iterator类型。前面我们已经说过python中类成为iterator需要实现两个方法__iter__
和__next__
。我们把上述程序整合一下:
python
class A:
def __init__(self, n):
self.i = 0
self.n = n
def __iter__(self):
return self
def __next__(self):
if self.a < self.n:
self.a += 1
return self.a
else:
raise StopIteration()
a = A(10)
for k in a:
print(k)
可以发现,程序正常执行了。现在我们再来总结一下for i in a循环遍历的原理和两个概念:
- 首先调用
__iter__
方法返回一个iterator(iterator必须有__next__
方法) - 对这个iterator循环调用
__next__
方法(相当于手动通过next函数调用) - 一直到触发
__next__
方法的StopIteration逻辑,循环结束
iterable和iterator的不同:
- iterable:实现了
__iter__
方法 - iterator:同时实现了
__iter__
和__next__
方法
至此,我们理解了for i in a的原理,但是我们发现如果再次执行for i in a循环遍历,什么都没有输出。因为此时的迭代器游标已经指向结尾了,所以无法再次遍历(参考多次执行next函数后的异常现象)。
6. getitem/setitem/delitem
__getitem__
也可使类实例成为可迭代对象,并通过for循环遍历,且支持多次遍历。
python
class People:
def __init__(self, name, age):
self.name = name
self.age = age
def __getitem__(self, item):
return self.name[item]
p = People('Tom', 18)
for i in p:
print(i)
print(p[1])
for...in...在遍历p时,实际是调用了__getitem__
方法,item隐式传参0、1、2、...。
相对于for...in...遍历,__getitem__
方法更多是用于通过显式key索引查询:
python
class A:
def __init__(self):
self.data = {'a': 1, 'b': 2, 'c': 3}
def __getitem__(self, item):
return self.data[item]
def __setitem__(self, key, value):
self.data[key] = value
def __delitem__(self, key):
del self.data[key]
a = A()
print(a['b'])
a['f'] = 9
print(a['f'])
del a['c']
※注意:如果同时定义了iter/next和getitem,则for...in...会执行iter/next的逻辑,而通过key显式索引会执行getitem对应逻辑。
根据名称可见__setitem__
是和__getitem__
逻辑相反的方法,即可通过[]对类实例直接赋值,__delitem__
同样表示del操作,案例见6. getitem。
※附 :使用__getitem__
方法和__dict__
内置属性可以实现通过[]索引访问类成员变量。
7. contains
实现__contains__
隐式方法,可支持in语句。
python
class A:
def __init__(self):
self.data = [1, 2, 3]
def __contains__(self, item):
if item in self.data:
return True
else:
return False
class B:
def __init__(self):
self.lower = 1
self.upper = 10
def __contains__(self, item):
if self.lower < item < self.upper:
return True
else:
return False
a = A()
b = B()
print(7 in a)
print(7 in b)
8. call
__call__
方法相当于重载了()运算符,可通过 实例名() 的形式直接执行__call__
方法,即把实例变成了可调用对象。可调用对象的思想在众多python编程框架中应用非常广泛,如fastapi等。
※附录 :可调用对象,如函数,都有__call__
属性,可通过hasattr(函数名/实例名, 'call')或者callable(函数名/实例名)判断。注意是实例名不是类名,因为类可定义实例,一定是可调用的。
python
class A:
def __init__(self):
self.name = 'haha'
def __call__(self, *args, **kwargs):
return self.name
a = A()
print(a())
通过上例可以发现,实现__call__
方法后,可直接使用a()调用__call__
方法。一般在开发中不建议直接使用__call__
方法定义业务逻辑,因为根据见名知意、逻辑清晰的原则,我们会尽量把对应的逻辑定义在一个合适的方法中。但是在需要传递一个可调用对象参数的场景中,__call__
方法就很好用了,这样这个参数就可以同时支持函数、类实例等不同类型参数。
9. new
__new__
是python类实例初始化过程中非常重要的隐式方法,而且在初始化类实例时,__new__
是在__init__
之前被调用的。
在面向对象编程语言中,初始化类实例主要包含两步:(1)分配内存空间,在内存中创建对象;(2)初始化实例,如给成员变量赋初值等。在python中,(1)由__new__
完成,(2)由__init__
完成。
python
class People:
def __init__(self, name, age):
self.name = name
self.age = age
def __new__(cls, *args, **kwargs):
return super().__new__(cls)
p = Person('haha', 16)
__new__
必须返回一个自身类实例(返回分配的内存空间地址引用),这里的cls说明__new__
是一个类方法,cls表示People,return的实例会作为参数传给__init__
,即__init__
的self参数。如果__new__
不返回类本身实例,或者返回的是其他类实例(cls换成其他类,如B),__init__
方法都不会被调用。另外,super().new(cls)的参数只有cls,*args和**kwargs会被自动传递给__init__
。类成员变量及业务逻辑初始化多放在__init__
中,所以大部分情况下我们都不会用到__new__
方法,那么什么情况下会用到呢?
针对__new__
返回内存引用的特性,可用来开发单例模式:
python
class A:
instance = None
# value_flag = False
#
# def __init(self, name):
# if not self.value_flag:
# self.name = name
# self.value_flag = True
def __init(self, name):
self.name = name
def __new__(cls, *args, **kwargs):
if cls.instance is None:
cls.instance = super().__new__(cls)
return cls.instance
else:
return cls.instance
a = A('aaa')
b = A('bbb')
print(id(a), id(b))
print(a.name, b.name)
上述程序即实现了一个单例模式程序,但是需要注意如果类是含参构造,则新的实例参数会自动更新旧的实例属性(b.name和a.name都会变成'bbb'),如果不想新的引用实例修改旧值,则可以定义一个静态属性标识,具体实现参考注释部分程序。
除了用于单例模式以外,还可以利用__new__
对一些内置类型进行封装,从而实现一些特殊逻辑,如定义一个非负整数类型:
python
class NonNeg(int):
def __init__(self, value):
super().__init__()
def __new__(cls, value):
return super().__new__(cls, value) if value >= 0 else None
a = NonNeg(10)
b = NonNeg(7)
print(a-b) # 3
c = NonNeg(-3) # None
上述程序的缺点是无法控制运算结果,如 b-a 仍然是一个负数。当然也可以做一次运算类型封装解决这个问题:NonNeg(b-a),但是如果计算过程比较复杂,运算表达式就会显得臃肿。
此外,在某些场景下,利用__new__
方法还可以实现拷贝构造:b = A(a)。
10. enter/exit
__exit__
和__enter__
方法是用于上下文语义管理的,实现这两个方法,可用于with语句。
在管理文件句柄(读写文件)或者管理数据库事务(写数据库)时,经常会用到with语句,因为不需要手动去关闭文件句柄,而且即使异常也可保证安全性。那么这个机制是怎么实现的呢?
python类提供了__exit__
和__enter__
隐式方法用于支持上下文语义管理,with语句在执行时会首先执行__enter__
方法内容,在运行结束后会执行__exit__
方法内容。
python
class File:
def __init__(self, path, mode='r'):
self.f = open(path, mode)
def read(self, n):
return self.f.read(n)
def write(self, s):
self.f.write(s)
def __enter__(self):
print('--enter--')
return self
def __exit__(self, exec_type, exc_val, exc_tb):
print('--exit--')
self.f.close()
with File('./test.txt', 'a') as f:
print('--test--')
f.write('hello')
# 运行结果
--enter--
--test--
--exit--
__enter__
的返回结果就是with as 的结果,通常会返回类实例自身,__exit__
会在with语句内容全部执行结束后自动运行,__exit__
的几个参数表示异常信息,如果with语句内容出现异常,则exec_type表示异常类型,exc_val表示具体的异常信息,exc_tb表示异常跟踪信息(地址信息),没有异常的情况下,这三个参数都是None。如果__exit__
的返回值是True,即使with语句内容出现异常,程序也不会异常终止,但是with语句中异常后面的程序不会再执行,with代码块后面的程序依然会执行(类似try/except),而且__exit__
中的参数依然可以拿到异常信息。
※附录:enter/exit机制只适用于类的上下文语义管理,如果是函数可使用contextlib库,并且也可以使用contextlib对类进行上下文语义封装。
11. setattr/getattr/getattribute
在介绍setattr/getattr/getattribute之前先说明一下python类实例是怎么保存类成员变量的。在python类实例中,有一个字典类型的__dict__
属性值用于保存所有的成员变量和成员方法,key是成员变量/方法名,value就是对应的值。我们设置的成员变量实际上都保存在了__dict__
中,当通过 类名.属性 名访问数据的时候实际上也是从__dict__
中读取数据。那这个过程是怎么实现的呢?
python类内置了__setattr__
和__getattr__
两个隐式方法来实现上述过程。
python
class A:
def __init__(self, name, age):
print('--name--')
self.name = name
print('--age--')
self.age = age
def __setattr__(self, key, value):
print('---', key)
self.__dict__[key] = value
# super().__setattr__(key, value)
def __getattr__(self, item):
print('not found')
return f'没有属性值{item}'
def __getattribute__(self, item):
print('!!!')
return super().__getattribute__(item)
# 下面的写法是错误的
# return self.__dict__[item]
d = D('haha', 18)
print('==============')
print(d.name)
print('==============')
print(d.address)
print('==============')
# 输出结果
--name--
--- name
!!!
--age--
--- age
!!!
==============
!!!
haha
==============
!!!
not found
没有属性值address
==============
在类中对属性进行赋值操作时,python会自动调用__setattr__
方法来实现对属性的赋值。如果需要重写__setattr__
方法则有两种更新属性值方法:(1)直接更新实例自身的__dict__
属性;(2)调用父类的__setattr__
方法。从上述程序结果也可以看出,在__init__
方法中每初始化一个成员变量都会调用一次__setattr__
方法。如果没有在__setattr__
方法中把属性值保存到__dict__
中,__init__
的初始化也是无效的。
__getattribute__
是获取属性值的方法(属性访问拦截器),当访问某个属性(成员变量/成员方法)时,会自动调用__getattribute__
方法。这里有一个极易遇到的坑,为什么上面程序 __getattribute__
方法中return self.dict[item]的写法不对呢?因为程序在执行到self.__dict__时,发现是在访问__dict__这个属性,所以又去调用__getattribute__
方法,导致无限递归循环,所以在__getattribute__
方法中注意尽量不要直接访问类属性。
由上面程序的执行结果可知,__getattr__
方法是在访问不存在的属性时才会被触发的,python类会先执行__getattribute__
方法,如果找不到这个属性,则会调用__getattr__
方法。
大部分情况下,我们都不会去重写这三个隐式方法,尤其是重写__getattribute__
方法有很大的风险。但是合理利用这几个方法可以实现一些特殊的功能。例如:
(1)在__getattribute__
方法中通过判断item的值可以实现真正的属性私有化(禁止外部通过 类名.属性名 直接访问成员变量)。
(2)通过重写__setattr__
方法把某些属性变成const常量,只要key已经在__dict__中存在,就不允许更新。这也是常见的python const常量解决方案。
※注意:尽量不要重写这三个方法。
12. dir
__dir__
方法可支持dir函数,通过dir函数可查看某个对象的所有属性名和方法名(包括从父类继承)。所以除了使用dir函数外,我们也可以直接调用实例的__dir__
方法。
13. 数学运算
- 比较运算:
__lt__
(<)、__le__
(<=)、__eq__
(==)、__ne__
(!=)、__gt__
(>)、__ge__
(>=) 。 - 单目运算:
__neg__
(-,负数操作,单目运算符)、__pos__
(+,单目运算符) 、__invert__
(~,取反,单目运算符)。 - 算术运算:
__add__
(+,加法,双目运算符)、__sub__
(-,减法,双目运算符)、__mul__
(*)、__truediv__
(/)、__floordiv__
(//)、__mod__
(%)、__divmod__
或divmod()、__pow__
或pow()、__round__
或round()。 - 反向运算:
__radd__
、__rsub__
、__rmul__
、__rtruediv__
、__rfloordiv__
、__rmod__
、__rdivmod__
、__rpow__
。 - 增量赋值运算:
__iadd__
、__isub__
、__imul__
、__ifloordiv__
、__ipow__
。 - 位运算:
__lshift__
(<<)、__rshift__
(>>) - 逻辑运算:
__and__
(&)、__or__
(|)、__xor__
(^)