目录
- 1、概念
- 2、魔法方法分类
- 3、构造与初始化
- 3.1 `__new__()`
- 3.2 `__init__()`
- 3.3 `__del__()`
- 4、类的表示
- 4.1 `__str__() / __repr__()`
- 4.2 `__bool__()`
- 5、访问控制
- 6、比较操作
- 6.1 `__eq__()`
- 6.2 `__ne__()`
- 6.3 `__lt__()` / `__gt__()`
- 7、容器类操作(重要)
- 8、可调用对象
- 9、序列化
- 参考文献:
1、概念
在Python的类中,以两个下划线开头、两个下划线结尾的方法,如常见的 :__init__、__str__、__del__
等,就被称为「魔术方法」(Magic methods)。魔术方法在类或对象的某些事件出发后会自动执行,如果希望根据自己的程序定制特殊功能的类,那么就需要对这些方法进行重写。使用这些「魔法方法」,我们可以非常方便地给类添加特殊的功能。
本文将系统性的介绍 Python 中的魔法方法都有哪些?另外,我们使用这些魔法方法,可以实现哪些实用的功能?
2、魔法方法分类
python中常见的魔法方法大致可分为以下几类:
- 构造与初始化
- 类的表示
- 访问控制
- 比较操作
- 容器类操作
- 可调用对象
- 序列化
接下来分别对每一类方法进行介绍。
3、构造与初始化
我们都知道一个最基本的魔术方法,
__init__
。通过此方法我们可以定义一个对象的初始操作。但你知道吗,当实例化我们定义的类,如x = SomeClass() 的时候,__init__
并不是第一个被调用的方法。实际上,还有一个叫做__new__
的方法,来实例化这个对象。然后给在开始创建时候的初始化函数 来传递参数。在对象生命周期的另一端,也有一个__del__
方法。接下来看一看这三个方法:
__init__()
__new__()
__del__()
3.1 __new__()
(1)__new__(cls, [...])
是在一个对象实例化的时候所调用的第一个方法,所以它才是真正意义上的构造方法。
(2)它的第一个参数是这个类,其他的参数是用来直接传递给 __init__
方法。
(3)__new__
决定是否要使用该 __init__
方法,因为 __new__
可以调用其他类的构造方法或者直接返回别的实例对象来作为本类的实例,如果 __new__
没有返回实例对象,则 __init__
不会被调用。
(4)__new__
主要是用于继承一个不可变的类型比如一个 tuple 或者 string。
例子:
class Person(object):
def __new__(cls, *args, **kwargs):
print("__new__()方法被调用了")
print('这个是*agrs', *args)
print('这个是kwagrs', **kwargs)
# cls表示这个类,剩余所有的参数传给__init__()方法,
# 若不返回,则__init__()不会被调用
return object.__new__(cls)
def __init__(self, name, age):
print("__init__()方法被调用了")
self.name = name
self.age = age
print(self.name, self.age)
p = Person("张三", 20)
# Output:
# __new__()方法被调用了
# 这个是*agrs 张三 20
# 这个是kwagrs
# __init__()方法被调用了
# 张三 20
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
那__new__()
在什么场景使用呢?
答:当我们需要继承内置类时,例如,想要继承 int、str、tuple
,就无法使用 __init__
来初始化了,只能通过 __new__
来初始化数据:下面这个例子实现了一个类,这个类继承了 float,之后就可以对这个类的实例进行计算了。
class g(float):
"""千克转克"""
def __new__(cls, kg):
return float.__new__(cls, kg * 2)
a = g(50) # 50千克转为克
print(a) # 100
print(a + 100) # 200 由于继承了float,所以可以直接运算,非常方便!
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
3.2 __init__()
__init__()
方法:构造器,当一个实例被创建的时候调用的初始化方法。
例子:
class Person(object):
def __init__(self, name, age):
self.name = name
self.age = age
p1 = Person('张三', 20)
p2 = Person('李四', 22)
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
3.3 __del__()
__del__()
方法:析构器,当一个实例被销毁时自动调用的方法。
class Washer:
def __del__(self):
"""
当删除对象时,解释器会自动调用del方法
"""
print('对象已删除!')
haier = Washer()
# output:
# 对象已删除!
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
4、类的表示
关于类的表示相关的魔法方法,主要包括以下几种:
__str__() / __repr__()
__bool__()
4.1 __str__() / __repr__()
这两个方法都是用来描述类或对象信息的,比如你直接实例化了一个对象,打印出来的是这个对象的地址。而要是重新在类中定义了这两个方法,那打印对象的结果就是方法返回的信息。
例子:
class Washer:
def __int__(self):
pass
def __repr__(self):
return '我是__repr__()魔法方法!'
def __str__(self):
"""
这个str的作用就是:类的说明或对象状态的说明
:return:
"""
return '我是__str__魔法方法!'
haier = Washer()
# 不定义str方法,直接打印,结果是对象的内存地址,定义了str方法,
# 显示的就是str方法返回的内容
print(haier) # 我是__str__魔法方法
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
我发现,要是同时写了这两个方法,只会调用__str__
方法。都是用来描述类或对象的信息,那为啥要定义两个呢?
答:
设计的目的是不一样的: 1.__repr__
的目标是准确性,或者说,__repr__
的结果是让解释器用的。 2.__str__
的目标是可读性,或者说,__str__
的结果是让人看的。更详细的信息参考:link。
4.2 __bool__()
当调用 bool(obj) 时,会调用 __bool__()
方法,返回 True 或 False:
class Person(object):
def __init__(self, uid):
self.uid = uid
def __bool__(self):
return self.uid > 10
p1 = Person(4)
p2 = Person(14)
print(bool(p1)) # False
print(bool(p2)) # True
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
5、访问控制
关于访问控制的魔法方法,主要包括以下几种:
__setattr__
:定义当一个属性被设置时的行为
__getattr__
:定义当用户试图获取一个不存在的属性时的行为
__delattr__
:删除某个属性时调用
__getattribute__
:访问任意属性或方法时调用
例子:
class Person(object):
def __setattr__(self, key, value):
"""属性赋值"""
if key not in ('name', 'age'):
return
if key == 'age' and value < 0:
raise ValueError()
super(Person, self).__setattr__(key, value)
def __getattr__(self, key):
"""访问某个不存在的属性"""
return 'unknown'
def __delattr__(self, key):
"""删除某个属性"""
if key == 'name':
raise AttributeError()
super().__delattr__(key)
def __getattribute__(self, key):
"""所有属性/方法调用都经过这里"""
if key == 'money':
return 100
elif key == 'hello':
return self.say
return super().__getattribute__(key)
p1 = Person()
p1.name = '张三' # 调用__setattr__
p1.age = 20 # 调用__setattr__
print(p1.name, p1.age) # 张三 20
setattr(p1, 'name', '李四') # 调用__setattr__
setattr(p1, 'age', 30) # 调用__setattr__
print(p1.name, p1.age) # 李四 30
print(p1.sex) # 调用__getattr__
# 上面只要是访问属性的地方,都会调用__getattribute__方法
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
6、比较操作
比较操作的魔法方法主要包括以下几种:
__eq__()
__ne__()
__lt__()
__gt__()
6.1 __eq__()
__eq__
方法,可以判断两个对象是否相等:
例子:
class Person(object):
def __init__(self, uid):
self.uid = uid
def __eq__(self, other):
return self.uid == other.uid
p1 = Person(1)
p2 = Person(1)
p3 = Person(2)
print(p1)
print(p1 == p2) # True
print(p2 == p3) # False
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
6.2 __ne__()
判断两个对象是否不相等,这个和__eq__()
方法基本一样,只不过这个是反面:
class Person(object):
def __init__(self, uid):
self.uid = uid
def __ne__(self, other):
"""对象 != 判断"""
return self.uid != other.uid
p1 = Person(1)
p2 = Person(1)
p3 = Person(2)
print(p1 != p2) # False
print(p2 != p3) # True
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
6.3 __lt__()
/ __gt__()
这两个方法比较对象的大小的,__lt__()
为小于,__gt__()
为大于:
class Person(object):
def __init__(self, uid):
self.uid = uid
def __lt__(self, other):
"""对象 < 判断 根据self.uid"""
return self.uid < other
def __gt__(self, other):
"""对象 > 判断 根据self.uid"""
return self.uid > other
p1 = Person(1)
p2 = Person(1)
p3 = Person(2)
print(p1 < p2) # False
print(p2 < p3) # True
print(p1 > p2) # False
print(p2 > p3) # False
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
7、容器类操作(重要)
容器类的魔法方法,主要包括:
-
__setitem__(self, key, value)
:定义设置容器中指定元素的行为,相当于 self[key] = value; -
__getitem__(self, key)
: 定义获取容器中指定元素的行为,相当于 self[key]; -
__delitem__(self, key)
:定义删除容器中指定元素的行为,相当于 del self[key]; -
__len__(self)
:定义当被 len() 调用时的行为(返回容器中元素的个数); -
__iter__(self)
:定义当迭代容器中的元素的行为; -
__contains__(self, item)
:定义当使用成员测试运算符(in 或 not in)时的行为; -
__reversed__(self)
:定义当被 reversed() 调用时的行为。
在介绍容器的魔法方法之前,首先要知道,Python 中的容器类型都有哪些,Python 中常见的容器类型有:
- 字典
- 元组
- 列表
- 字符串
因为它们都是「可迭代」的。可迭代是因为,它们都实现了容器协议,也就是下面要介绍到的魔法方法。
下面通过自己定义类实现列表,来说明这些方法的用法:
class MyList(object):
"""自己实现一个list"""
def __init__(self, values=None):
# 初始化自定义list
self.values = values or []
self._index = 0
def __setitem__(self, key, value):
# 添加元素
self.values[key] = value
def __getitem__(self, key):
# 获取元素
return self.values[key]
def __delitem__(self, key):
# 删除元素
del self.values[key]
def __len__(self):
# 自定义list的元素个数
return len(self.values)
def __iter__(self):
# 可迭代
return self
def __next__(self):
# 迭代的具体细节
# 如果__iter__返回self 则必须实现此方法
if self._index >= len(self.values):
raise StopIteration()
value = self.values[self._index]
self._index += 1
return value
def __contains__(self, key):
# 元素是否在自定义list中
return key in self.values
def __reversed__(self):
# 反转
return list(reversed(self.values))
# 初始化自定义list
my_list = MyList([1, 2, 3, 4, 5])
print(my_list[0]) # __getitem__
my_list[1] = 20 # __setitem__
print(1 in my_list) # __contains__
print(len(my_list)) # __len__
print([i for i in my_list]) # __iter__
del my_list[0] # __del__
reversed_list = reversed(my_list) # __reversed__
print([i for i in reversed_list]) # __iter__
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
- 48
- 49
- 50
- 51
- 52
- 53
- 54
- 55
- 56
- 57
- 58
- 59
说明:
这个例子实现了一个 MyList 类,在这个类中,定义了很多容器类的魔法方法。这样一来,这个 MyList 类就可以像操作普通 list 一样,通过切片的方式添加、获取、删除、迭代元素了。
__setitem__()
:
当执行 my_list[1] = 20 时,就会调用 __setitem__
方法,这个方法主要用于向容器内添加元素。
__getitem__()
:
当执行 my_list[0] 时,就会调用 __getitem__
方法,这个方法主要用于从容器中读取元素。
__delitem__()
:
当执行 del my_list[0] 时,就会调用 __delitem__
方法,这个方法主要用于从容器中删除元素。
__len__()
:
当执行 len(my_list) 时,就会调用 __len__
方法,这个方法主要用于读取容器内元素的数量。
__iter__
这个方法需要重点关注,为什么我们可以执行 [i for i in my_list]
?就是因为定义了 __iter__
。
这个方法的返回值可以有两种:
1)返回 iter(obj)
:代表使用 obj 对象的迭代协议,一般 obj 是内置的容器对象;
2)返回 self
:代表迭代的逻辑由本类来实现,此时需要重写 next 方法,实现自定义的迭代逻辑
在这个例子中,__iter__
返回的是 self,所以需要定义 __next__
方法,实现自己的迭代细节。__next__
方法使用一个索引变量,用于记录当前迭代的位置,这个方法每次被调用时,都会返回一个元素,当所有元素都迭代完成后,此时 for 会停止迭代,若迭代时下标超出边界,这个方法会返回 StopIteration
异常。
8、可调用对象
在Python中,方法也是一种高等的对象。这意味着他们也可以像其他对象一样被传递到方法中,这是一个非常惊人的特性。 Python中有一个特殊的魔术方法可以让类的实例的行为表现的像函数一样,你可以调用他们,将一个函数当做一个参数传到另外一个函数中等等。这个魔法方法就是 __call__(self, [args...])
。
下面通过实际例子说明:
class Circle(object):
def __init__(self, x, y):
self.x = x
self.y = y
def __call__(self, x, y):
self.x = x
self.y = y
a = Circle(10, 20) # __init__
print(a.x, a.y) # 10 20
a(100, 200) # 此时a这个对象可以当做一个方法来执行,这是__call__魔法方法的功劳
print(a.x, a.y) # 100 200
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
这个例子首先初始化一个 Circle 实例 a,此时会调用 __init__
方法,这个很好理解。
但是,我们对于实例 a 又做了调用 a(100, 200)
,注意,此时的 a 是一个实例对象,当我们这样执行时,其实它调用的就是 __call__
。这样一来,我们就可以把实例当做一个方法来执行。
也就是说,Python 中的实例,也是可以被调用的,通过定义 __call__
方法,就可以传入自定义参数实现自己的逻辑。
这个魔法方法通常会用在类实现一个装饰器、元类等场景中,当遇到这个魔法方法时,能理解其中的原理就可以了。
9、序列化
Python 提供了序列化模块 pickle
,当使用这个模块序列化一个实例化对象时,也可以通过魔法方法来实现自己的逻辑,这些魔法方法包括:
-
__getstate__()
-
__setstate__()
例子:
import pickle
class Person(object):
def __init__(self, name, age, birthday):
self.name = name
self.age = age
self.birthday = birthday
def __getstate__(self):
# 执行 pick.dumps 时 忽略 age 属性
return {
'name': self.name,
'birthday': self.birthday
}
def __setstate__(self, state):
# 执行 pick.loads 时 忽略 age 属性
self.name = state['name']
self.birthday = state['birthday']
person = Person('李四', 20, (2017, 2, 23))
pickled_person = pickle.dumps(person) # 自动执行 __getstate__ 方法
p = pickle.loads(pickled_person) # 自动执行 __setstate__ 方法
print(p.name, p.birthday) # 李四 (2017, 2, 23)
# 由于执行 pick.loads 时 忽略 age 属性,所以下面执行回报错
print(p.age) # AttributeError: 'Person' object has no attribute 'age'
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
说明:
__getstate__()
:
这个例子首先初始了 Person 对象,其中包括 3 个属性:name、age、birthday。
当调用 pickle.dumps(person)
时,__getstate__
方法就会被调用,在这里忽略了 Person 对象的 age 属性,那么 person 在序列化时,就只会对其他两个属性进行保存。
__setstate__()
:
同样地,当调用 pickle.loads(pickled_person)
时,__setstate__
会被调用,其中传入的参数就是 __getstate__
返回的结果。
在 __setstate__
方法,我们从入参中取得了被序列化的 dict,然后从 dict 中取出对应的属性,就达到了反序列化的效果。
参考文献:
【1】http://kaito-kidd.com/2017/02/23/python-magic-methods-2/.
【2】https://pycoders-weekly-chinese.readthedocs.io/en/latest/issue6/a-guide-to-pythons-magic-methods.html.
【3】https://www.cnblogs.com/dachenzi/p/8185792.html.