Python中的魔术方法 1—构建
魔术方法简介
Python中有一些特别的方法是以被双下划线包围的,这些方法通常不需要主动调用,却可以让我们的类实现一些特殊的功能。比如实现 __add__()
方法,即可让我们的类支持 +
运算符操作。今天带来的是3个与构建相关的魔术方法。
__init__()
这个方法对于使用Python的人应该都不陌生吧,这就是类的初始化方法,在里面做一些初始化实例的工作,在实例化一个类的时候被调用,如运行到 a = A()
时,即会调用 A
中的该方法。相信大家都很熟悉了,这里就不过多介绍了。但是,创建一个实例的时候可不仅仅是调用这一个方法,毕竟,我们都知道, __init__(self)
的第一个参数是 self
,即这个实例本身,也就是说调用到它的时候已经有了这个实例, 那么这个实例是从哪里来的呢,那就要说道下一个魔术方法了。
__new__()
__new__()
方法也是在类的实例化的时候被调用,而且在 __init__()
之前就被调用,正是由 __new__()
创建并返回一个实例,接下来传递给 __init__()
方法,由他进行实例的初始化。这个方法可能大家平时应用的机会比较少,下面举例进行说明。
调用时机展示
class NewObject(object):
def __new__(cls):
print('new')
return super().__new__(cls)
def __init__(self):
print('init')
if __name__ == '__main__':
n = NewObject()
# output:
# new
# init
可以从打印的顺序看到先执行的 __new__()
方法,再执行 __init__()
方法。
单例模式
可以使用 __new__()
来实现一个单例模式,如下
class Single(object):
_instance = None
def __new__(cls):
if cls._instance is None:
cls._instance = super().__new__(cls)
return cls._instance
if __name__ == '__main__':
s1 = Single()
s2 = Single()
print(id(s1), id(s2), s1 is s2)
# output
# 3017957719904 3017957719904 True
同理,可以扩展为线程安全的单例,有兴趣的可以自己试一下。
元编程
通过继承type并复写 __new__()
方法可以定制子类的行为,这种做法称作元编程,如
class Meta(type):
def __new__(mcls, name, bases, namespace, **kwargs):
for attr in namespace:
if attr.startswith('test'):
raise Exception(f'不能使用以test开头的方法: {attr}')
return super().__new__(mcls, name, bases, namespace, **kwargs)
class Base(metaclass=Meta):
pass
class Test(Base):
def test_print(self):
pass
# output
# Traceback (most recent call last):
# File "...", line ..., in <module>
# class Test(Base):
# File "...", line ..., in __new__
# raise Exception(f'不能使用以test开头的方法: {attr}')
# Exception: 不能使用以test开头的方法: test_print
#
# 进程已结束,退出代码为 1
例子中子类不能定义以test开头的任何方法名,否则抛出异常。另外,要注意到上述代码只是在进行类型定义,并没有做实例化之类的操作,也会报错,可以认为是在“编译期”进行的检查。这种编程方法一般在框架编写中应用较多,如Django的ORM,便是使用元编程实现的。
注意事项
需要注意的是 __init__()
和 __new__()
这两个方法的参数除了 self
和 cls
有所区别外,其他要保持一致。
另外在 __new__()
中创建实例一般是要显式调用其他类的 __new__()
方法,否则会造成类型错误的问题,如:
class NewObject1(object):
def __new__(cls, *args, **kwargs):
return object().__new__(cls)
class NewObject2(object):
def __new__(cls, *args, **kwargs):
return object()
if __name__ == '__main__':
a = NewObject1()
b = NewObject2()
print(type(a), type(b))
# output:
# <class '__main__.NewObject1'> <class 'object'>
__del__()
与构造方法相对应, __del__()
即是析构方法,是在实例的生命周期结束后,进行垃圾回收时调用的方法,可以将资源释放等的代码写在里面。这里要注意的是执行的时机,并不是说在进行 del
的时候就会被调用,需要该对象的所有引用均被释放才行,另外解释器的垃圾回收也并不一定是在引用释放的那一刻就会生效,所以不要将状态控制代码放在里面。
import time
class TestDel(object):
def __del__(self):
print(time.ctime(), 'del')
a = TestDel()
b = a
del a
time.sleep(1)
del b
time.sleep(1)
print(time.ctime(), 'end')
# output
# Wed Sep 15 14:20:12 2021 del
# Wed Sep 15 14:20:13 2021 end
这里我们可以看到del和end相隔了一秒,即在 del a
的时候并没有触发 __del__()
在 del b
后所有引用均被删除,才触发 __del__()
。另外,即不显式调用 del
,程序结束之后也会触发垃圾回收,如
a = TestDel()
print(time.ctime(), 'end')
# output
# Wed Sep 15 14:27:29 2021 end
# Wed Sep 15 14:27:29 2021 del
从中可以看到程序执行完毕(输出end),然后触发 __del__()
(输出del)。