最近在看Luke Sneeringer的 "Professional Python" 这本书,做一些关于这本书的笔记。
首先是魔术方法,本书的第二部分,第四章。为什么不按顺序来呢,我也不知道。
0. 什么是魔术方法
简单地说,就是在面向对象编程时,编写的一些对于特定操作(例如特定函数、操作符)做出特定响应的函数(作者称之为“钩子”)。
对于魔术方法,遵循统一的格式:双下划线在函数名两端,例如常见的__init__,可以读作“dunder init”。
1. 常用的魔术方法
(1)__init__
在类的实例被创建时立即执行,必须要有参数self,没有也不能有返回值(否则会报错TypeError)。
例如:
class MyClass:
def __init__(self):
print("You create an object.")
cls = MyClass()
输出:
You create an object.
其实__init__更多被运用于为初始化对象赋值,但该方法并不创建对象(由__new__方法创建对象)。其作用很像C++中的构造函数。
(2)__new__
__new__的应用场景相对来说少一些,简单地说,其主要运用于想要继承一些不可变的自带类(如tuple、str、int等等)时,需要改写父类的__new__方法。
两个例子:
编写一个类,永远返回一个非负整数:
class PositiveInt(int):
def __new__(cls, val):
return super(PositiveInt, cls).__new__(cls, abs(val))
编写一个类,永远返回一个大写字母串:
class UpperStr(str):
def __new__(cls, s):
return super(UpperStr, cls).__new__(cls, s.upper())
此外,__new__方法还能用于实现单例(Singleton)和元类(MetaClass),属于更高阶的内容,在这里不细讲了。
(3)__del__
顾名思义,__del__方法在一个实例被销毁时调用,类似于C++中的析构函数,无论是GC自动销毁实例还是手动使用del关键字时,该方法都会被调用。
这个方法被用到的场景不多,因为该方法通常是由GC触发的,并没有一种很好的方法可以引发有意义的异常,一般只用于在标准输出窗口打印一些错误信息。
(4)强制类型转换:__str__、__int__、__bool__
先说__str__(以及容易混淆的__repr__),这应该是最常用的魔术方法,用于把一个实例转化为一个字符串,调用时机:显式调用str函数、调用print函数,使用格式化字符串%s。
例子:
class People:
def __init__(self, name='null', age=None):
self.name = name
self.age = age
def __str__(self):
return "My name is %s. I'm %d years old."%(self.name, self.age)
在命令行中调用:
me = People('Tom', 18)
print(me)
print(str(me))
都会输出:
My name is Tom. I'm 18 years old.
调用:
print("Introduction: %s"%me)
输出:
Introduction: My name is Tom. I'm 18 years old.
但是直接调用:
me
输出:
<__main__.People at 0x1b651d048d0>
这是由于在命令行中直接调用实例本身,会调用__repr__方法,而该方法默认返回实例在内存中的地址,下面再说说这个方法,同样是返回一个字符串,与__str__的区别在于:
调用时机:__repr__的调用时机为:直接在命令行中调用实例本身(如上),显式调用repr函数,以及使用%r格式化输出。
此外,可以将__repr__看作__str__的“备胎”,即实例不存在__str__方法时,将会默认调用__repr__方法,因此编写一个类时应该至少编写一个__repr__方法。
两者的区别除了调用时机,还有就是用途,一般__str__输出的是易于阅读的字符串,以自然语言风格为主,而__repr__输出的则是易于调试的字符串,例如可以令其返回XML格式的描述等等。
例子:
class People:
def __init__(self, name='null', age=None):
self.name = name
self.age = age
def __str__(self):
return "My name is %s. I'm %d years old."%(self.name, self.age)
def __repr__(self):
return "<People>\n<name>%s</name>\n<age>%d</age>\n</People>"%(self.name, self.age)
可以看出,给People重写了__repr__方法,令其返回一段XML格式的描述。
直接调用:
me
返回:
<People>
<name>Tom</name>
<age>18</age>
</People>
调用:
print("Introduction:\n %r"%me)
返回:
Introduction:
<People>
<name>Tom</name>
<age>18</age>
</People>
其余的:__int__、__bool__比较简单,不再赘述了,同样是接收一个位置参数(self),返回一个对应类型的值。
(5)二元比较:此类魔术方法需要两个位置函数,即self和other
判等:__eq__,相当于重载==运算符
例如上面的例子,编写__eq__方法:
def __eq__(self, other):
return self.name == other.name
即认为两个人名字相同就相等(返回True),否则返回False
判不等:__ne__,相当于重载!=运算符,一般无需定义,因为只需要对__eq__的结果取反即可,如果不希望直接对__eq__的结果取反,才需要定义__ne__方法。
判小于:__It__,相当于重载<运算符,定义和使用方法和上面很像,不再赘述。
其他二元比较运算符:__Ie__相当于<=, __gt__相当于>,__ge__相当于>=,但是只要定义了__eq__和__It__,解释器就可以推出这三种方法的返回值,因此无需显式定义。
此外,定义了二元比较方法,就可以对该对象组成的列表使用sorted方法。
(6)重载二元操作符
对于每一个二元操作符,Python定义了3种魔术方法:即普通(vanilla)方法、取反(reverse)方法和原地(in-place)方法。
普通方法:例如:x.__add__(y)相当于x+y。
取反方法:若x没有定义__add__方法,则x+y调用y.__radd__(x)。即在普通方法前面加上'r'。
原地(自操作)方法:也就是常见的自增、自减等等。x.__iadd__(y)相当于x+=y。即在普通方法前面加上'i'。
下面是常见的二元操作符对应的魔术方法。
(7)重载一元操作符:+、-和~
不再赘述,x.__pos__相当于+x,x.__neg__相当于-x,x.__invert__相当于~x。
(8)其余常用方法的重载
1)__len__方法
重载len函数,接收一个位置参数,返回一个非负整数值,用于描述对象的长度。
2)__hash__方法
重载hash函数,接收一个位置参数,返回一个整型值(可以为负),返回能唯一标识该对象的id值。
通常用于使一个类能够定义相等且要使其可哈希化的时候,为什么要使一个类的对象可哈希化呢?仅有可哈希化的对象可以作为字典(dict)的键值,以及可以在集合(set)中存在。在字典中,哈希值用于键查找,而在集合中,哈希值用于确定一个对象是否是集合中的成员。
3)__abs__与__round__方法
顾名思义,相当于重载abs和round函数,分别返回绝对值以及取整值。
4)__contains__方法:相当于重载 in 关键字
常用于判断一个对象与另一个对象是否属于”成员关系“,可以用于判断一个对象的值是否处于某个区间。
5)__getitem__、__setitem__、__delitem__方法
用于对象是一个集合(如字典、列表)的情况。
x.__getitem__(key)相当于x[key]
x.__setitem__(key, value)相当于x[key] = value,在执行上述语句的时候自动调用
x.__delitem__(key)则在执行del x[key]的时候被调用。
6)__getattr__, __setattr__, __hasattr__方法
__hasattr__用于查找对象是否包含某个属性,其中要查找的attr_name参数是字符串类型。
__getattr__用于查找对象的某个属性的值,并且在使用常规方法(即成员运算符".")无法找到属性时才会被调用。
__setattr__用于设置对象的某个属性的值,与getattr不同的是,只要显式定义了__setattr__方法,该方法就会被一直调用。
此外,还有__getattribute__方法,与__getattr__方法的区别在于,该方法是无条件被调用的。只有在该方法调用时引发了AttributeError异常时,才会调用__getattr__方法。
7)__iter__方法和__next__方法
这两个方法用于可迭代对象,用于定义该对象的迭代协议。在生成器那一章会讲到,这里不再叙述。
8)__enter__方法和__exit__方法
这两个方法用于上下文管理器,用于定义一个上下文管理器在生命周期开始时和结束时的行为。在上下文管理器那一章会讲到,这里不再叙述。