python 中有许多魔法函数(Magic Methods),一般格式为 __func__,常见的有 __init__, __del__, __call__, __str__, __repr__, __len__, __enter__, __exit__, __iter__, __next__, __dict__, __dir__, __all__, __setattr__, __getattr__, __getattribute__, __setitem__, __getitem__, __delitem__, __contains__等,魔法函数能够使类或函数获得特殊的功能。本篇介绍常用的魔法函数,所有代码以 JupyterLab 为平台运行。

__init__ 和 __del__

像 C++ 等面向对象的语言中有类的构造函数(初始化类,实例化类对象)和析构函数(清理内存)。在 python 中也有类似的函数,__init__ 类似于构造函数,__del__类似于析构函数。

1
2
3
4
5
6
7
class Test(object):
def __init__(self, name):
self.name = name
print("构造函数被调用")

def __del__(self):
print("析构函数被调用")
1
t = Test("Zhangsan")
构造函数被调用
1
del t
析构函数被调用

__call__

__call__ 方法类似于”()”运算符。如类实例化”Company()”和函数调用”depart()”。说明类和函数中自带该方法。另外,如果想要类中的实例也能够像函数一样执行,可以在类中显示定义”__call__()”方法。如下面的例子。

1
2
3
class Company(object):
def __init__(self):
pass
1
"类 Company 是否有 __call__ 方法: ", hasattr(Company, "__call__")
('类 Company 是否有 __call__ 方法: ', True)
1
2
3
4
5
6
class Country(object):
def __init__(self):
self.name = None

def __call__(self):
print("yes")
1
2
c = Country()
c()
yes
1
"类 Country 的实例 c 是否有 __call__ 方法: ", hasattr(c, "__call__")
('类 Country 的实例 c 是否有 __call__ 方法: ', True)
1
"类 Country 的属性 name 是否有 __call__ 方法: ", hasattr(c.name, "__call__")
('类 Country 的属性 name 是否有 __call__ 方法: ', False)
1
2
def depart():
print("yes")
1
"函数 depart 是否有 __call__ 方法:", hasattr(depart, "__call__")
('函数 depart 是否有 __call__ 方法:', True)
1
depart()
yes

__str__ 和 __repr__

__str__ 和 __repr__ 方法能够用来方便打印类的实例化对象。当使用 “print()” 函数打印时,应该在类中增加 “__str__()” 方法;当既想 “print()” 函数打印,又想在开发模式下直接输出实例化对象时,应该增加 “__repr__()” 方法。

当在类中没有定义魔法函数时

1
2
3
class Test1(object):
def __init__(self, name):
self.names = name
1
2
t1 = Test1("Zhangsan")
print(t1)
<__main__.Test1 object at 0x7fd380359670>
1
t1
<__main__.Test1 at 0x7fd380359670>

当在类中定义魔法函数: “__str__()” 时

1
2
3
4
5
6
class Test2(object):
def __init__(self, name):
self.name = name

def __str__(self):
return self.name
1
2
t2 = Test2("Zhangsan")
print(t2)
Zhangsan
1
t2
<__main__.Test2 at 0x7fd3802e6ac0>
1
2
3
4
# 通过调用 str() 方法,实现类似效果。
# 一种说法是,__str__方法在对实例化对象是用print()函数输出时调用,其实时Python内部机制调用str()方法,
# 然后str()方法内部继续调用
str(t2)
'Zhangsan'

当在类中定义魔法函数: “__repr__()” 时

1
2
3
4
5
6
class Test3(object):
def __init__(self, name):
self.name = name

def __repr__(self):
return self.name
1
2
t3 = Test3("Zhangsan")
print(t3)
Zhangsan
1
t3
Zhangsan

__len__

Python内置函数中有一个 “len()” 函数,这个函数适用于获取序列类型数据的长度,在对一个实例使用 “len()” 方法时,真实输出的其实是 __len__ 的返回值。所以,只要一个类内部实现了 __len__ 方法,就可以对其实例使用 __len__ 方法。

1
2
3
4
5
6
7
class Company1(object):
def __init__(self, name, employees):
self.name = name
self.employees = employees

def __len__(self):
return len(self.employees)
1
2
c1 = Company1(name="Apple1", employees=["Zhangsan", "Lisi"])
len(c1)
2

__getitem__、__setitem__、__delitem__

在 python 中有内置的字典类型 dict,通过方括号 “[]” 可以对字典进行赋值、取值和删除值。其实在类中定义魔法函数同样可以达到类似字典的上述操作。

1
2
3
4
5
6
7
8
9
10
11
12
class Company2(object):
def __init__(self):
self.company_info = {}

def __setitem__(self, key, value):
self.company_info[key] = value

def __getitem__(self, key):
return self.company_info[key]

def __delitem__(self, key):
del self.company_info[key]
1
2
3
4
c2 = Company2()
c2["name"] = "Apple2"
c2["type"] = "IT"
c2["name"], c2["type"]
('Apple2', 'IT')
1
del c2["type"]
1
c2.company_info
{'name': 'Apple2'}

使用python的反射机制实现

1
2
3
4
5
6
7
8
9
10
11
12
class Company3(object):
def __init__(self):
pass

def __setitem__(self, key, value):
setattr(self, key, value)

def __getitem__(self, key):
return getattr(self, key)

def __delitem__(self, key):
delattr(self, key)
1
2
3
4
c3 = Company3()
c3["name"] = "Apple3"
c3["type"] = "IT"
c3["name"], c3["type"]
('Apple3', 'IT')
1
del c3["type"]

__contains__

在 python 的字典类型中,可以使用关键字 ‘in’ 来判断是否包含有 ‘key’,同样地,在类中定义方法 __contains__ 同样可以获得该种能力

1
2
3
4
5
6
class Company4(object):
def __init__(self):
self.company_info = {}

def __contains__(self, key):
return key in self.company_info
1
2
3
c4 = Company4()
c4.company_info["name"] = "Apple4"
"name" in c4
True
1
"type" in c4
False

使用反射机制实现

1
2
3
4
5
6
7
8
9
class Company5(object):
def __init__(self):
pass

def __setitem__(self, key, value):
setattr(self, key, value)

def __contains__(self, key):
return hasattr(self, key)
1
2
3
c5 = Company5()
c5["name"] = "Apple5"
"name" in c5
True
1
"type" in c5
False

__iter__、__next__

在我的另一篇博文: Python 中的迭代器和生成器 中,我介绍了 python 中的迭代、可迭代对象、迭代器和生成器,核心内容就是 __iter__ 和 __next__ 方法。任何类内部定义有 __iter__ 方法的实例化对象都是可迭代的(Iterable);同时定义有 __iter__ 和 __next__ 的是迭代器(Iterator),生成器(Generator)是一类特殊的迭代器(yield 关键字)。可迭代对象通过函数 “iter()” 可以转化为迭代器。

判断对象是否是可迭代对象、迭代器、生成器

1
from collections.abc import Generator, Iterable, Iterator
1
2
3
class A:
def __iter__(self):
pass
1
2
3
4
5
6
class B:
def __iter__(self):
pass

def __next__(self):
pass
1
2
3
4
5
def FibonacciGenerator():
a, b = 0, 1
while True:
a, b = b, a + b
yield a
1
2
3
a = A()
b = B()
c = FibonacciGenerator()
1
isinstance(a, Iterable), isinstance(a, Iterator), isinstance(a, Generator)
(True, False, False)
1
isinstance(b, Iterable), isinstance(b, Iterator), isinstance(b, Generator)
(True, True, False)
1
isinstance(c, Iterable), isinstance(c, Iterator), isinstance(c, Generator)
(True, True, True)
1
2
d = "abc"
isinstance(d, Iterable), isinstance(d, Iterator), isinstance(d, Generator)
(True, False, False)
1
2
dr = iter(d)
isinstance(dr, Iterable), isinstance(dr, Iterator), isinstance(dr, Generator)
(True, True, False)

__enter__、__exit__

python 中常常使用 with 上下文管理器,如 with open(file_path, 'r') as f 打开文件获取句柄。这是非常有用的,即使是出现异常等,python 也能够关闭句柄,避免内存资源占用。其实,内部是由两个魔法函数来完成的。__enter__ 方法是在 with 语句开始执行的时候调用;__exit__ 方法是在 with 语句结束的时候调用,注意,无论 with 语句中的代码是否正常执行结束,都会执行 __exit__ 方法。除了读写文件外, python 中的数据库操作(特别注意关闭数据库连接)也可以用 with 上下文管理器。

1
2
3
4
5
6
7
8
import os
from pathlib import Path

file = "test.txt"
Path(file).touch()
with open(file, "w") as f:
f.write("test")
os.remove(file)

__getattr__、__setattr__、__getattribute__

python 的类中会有一些属性,属性的获取和设置需要一些魔法函数来支持。__getattribute__ 方法是调用类的属性时首先调用的方法;__getattr__ 方法是当调用类的属性失败时,执行的函数;__setattr__ 方法可以为类设置属性。

1
2
3
4
5
6
class GS(object):
def __init__(self, name):
self.company = name

def __getattr__(self, name):
print("__getattr__ 方法被调用,您找的属性不存在")

找到属性时,不会执行 __getattr__ 方法

1
2
gs = GS("AppleG")
gs.company
'AppleG'

找不到属性时,会执行 __getattr__ 方法

1
gs.name
__getattr__ 方法被调用,您找的属性不存在

当属性找不到时,我们可以进行一些设置,如提醒、设置默认值等。

1
2
3
4
5
6
7
8
9
10
# 自定义一个字典类,能够实现 "字典.键" 获取值
class MyDict(dict):
def __init__(self, *args, **kwargs):
super(MyDict, self).__init__(*args, **kwargs)

def __getattr__(self, key):
try:
return self[key]
except KeyError:
raise AttributeError(r"'MyDict' object has no attribute '%s'" % key)
1
2
d = MyDict({"a": 1, "b": 2})
d.a
1

在类中定义方法 __setattr__,使类的对象能够自定义属性

1
2
3
4
5
6
7
8
9
class GS2(object):
def __init__(self, name):
self.company = name

def __setattr__(self, name, value):
print("__setattr__ 方法被调用")
# self.name = name # 方法 1
# object.__setattr__(self, name, value) # 方法 2
self.__dict__[name] = value # 方法 3
1
2
3
4
gs2 = GS2("AppleGS")
print("*" * 3)
gs2.type = "IT"
gs2.type
__setattr__ 方法被调用
***
__setattr__ 方法被调用
'IT'

从上面的运行结果可以知道,__setattr__ 方法被调用了两次。这是因为在构造函数 __init__ 中也进行了一次属性赋值。所以,在定义 __setattr__ 的时候一定要注意,不能采用方法 1 编写代码,因为本身又会再次调用 __setattr__ 方法,这样会造成无限递归。可以采用方法 2 或方法 3.

1
2
3
4
5
6
7
8
9
10
11
12
13
# 自定义一个字典类,能够实现 "字典.键" 获取值 和 "字典.键=值"
class MyDict2(dict):
def __init__(self, *args, **kwargs):
super(MyDict2, self).__init__(*args, **kwargs)

def __getattr__(self, key):
try:
return self[key]
except KeyError:
raise AttributeError(r"'MyDict' object has no attribute '%s'" % key)

def __setattr__(self, key, name):
self[key] = name
1
2
3
d = MyDict2()
d.group = "Python"
d.group
'Python'

__getattribute__ 方法是获取类的属性时首先调用的方法,因此,必须注意陷入无限递归。在 __getattribute__ 方法的设计中不能执行属性的获取操作,这样会再次除法 __getattribute__ 方法的调用,使程序陷入无限递归。可以通过父类的 __getattribute__ 方法获取对应的属性。

1
2
3
4
5
6
7
class GS3(object):
def __init__(self, name):
self.company = name

def __getattribute__(self, name):
print("__getattribute__ 方法被调用")
return object.__getattribute__(self, name)
1
2
3
gs3 = GS3("AppleGS3")
print("*" * 3)
gs3.company
***
__getattribute__ 方法被调用

'AppleGS3'

__dict__、__dir__、dir()

__dict__ 是对象的一个属性,并不是函数,它的作用是返回对象的所有属性名为 key,属性值为 value 的一个字典,注意,这里所说的所有属性是指数据对象本身的属性,例如类的 __dict__ 只包含类本身的属性和函数,而类实例也只包含类实例的属性。这一点与 dir() 函数不同,dir() 将会返回一个列表,列表中包含对象所有有关的属性名。也就是说,__dict__ 是 dir() 的子集。而 dir() 实际上调用的是 __dir__ 方法。

1
2
3
4
5
6
class DD(object):
def __init__(self, name):
self.company = name

def func(self):
print("func 方法被调用")
1
dd = DD("AppleDD")
1
dd.__dict__
{'company': 'AppleDD'}
1
DD.__dict__
mappingproxy({'__module__': '__main__',
              '__init__': <function __main__.DD.__init__(self, name)>,
              'func': <function __main__.DD.func(self)>,
              '__dict__': <attribute '__dict__' of 'DD' objects>,
              '__weakref__': <attribute '__weakref__' of 'DD' objects>,
              '__doc__': None})
1
dir(dd)
['__class__',
 '__delattr__',
 '__dict__',
 '__dir__',
 '__doc__',
 '__eq__',
 '__format__',
 '__ge__',
 '__getattribute__',
 '__gt__',
 '__hash__',
 '__init__',
 '__init_subclass__',
 '__le__',
 '__lt__',
 '__module__',
 '__ne__',
 '__new__',
 '__reduce__',
 '__reduce_ex__',
 '__repr__',
 '__setattr__',
 '__sizeof__',
 '__str__',
 '__subclasshook__',
 '__weakref__',
 'company',
 'func']
1
c.__dir__()
['__repr__',
 '__getattribute__',
 '__iter__',
 '__next__',
 '__del__',
 'send',
 'throw',
 'close',
 'gi_frame',
 'gi_running',
 'gi_code',
 '__name__',
 '__qualname__',
 'gi_yieldfrom',
 '__doc__',
 '__hash__',
 '__str__',
 '__setattr__',
 '__delattr__',
 '__lt__',
 '__le__',
 '__eq__',
 '__ne__',
 '__gt__',
 '__ge__',
 '__init__',
 '__new__',
 '__reduce_ex__',
 '__reduce__',
 '__subclasshook__',
 '__init_subclass__',
 '__format__',
 '__sizeof__',
 '__dir__',
 '__class__']

魔法函数一览

参考文献

  1. python析构函数
  2. 关于使用 Python 析构函数的正确姿势
  3. Python:实例讲解Python中的魔法函数(高级语法)
  4. python魔法函数