前言
Python是一种面向对象的语言,而特殊方法又是Python类中一个重点,因此学习Python类的特殊方法能够有助于设计出更加简洁、规范的代码架构。
Python类的特殊方法又称为魔术方法,它是以双下划线包裹一个词的形式出现,例如__init__。特殊方法不仅可以可以实现构造和初始化,而且可以实现比较、算数运算,此外,它还可以让类像一个字典、迭代器一样使用,可以设计出一些高级的代码,例如单例模式。
面向对象这个词大家应该都不陌生,在C++、Java等面向对象的语言中也经常出现,要想理解面向对象,首先要理解4个概念之间的关系:类、对象、实例、方法。
类:类是一种由不同属性、不同数据组成的一个集合。用直白的话来描述,它是由多种对象组成的一个组合,例如人是一个类,那么它包含男人、女人、儿童等对象。例如三角形是一个类,那么它包含等腰三角形、直角三角形、等边三角形等对象。
对象:前面介绍类中已经提到了对象这个词汇,一句话总结:对象具有具体状态和行为。例如直角三角形,它具有特定的状态和属性。
实例:对象就是类的一个实例。也许这有点绕,的确对象与实例之间的概念非常模糊。你可以理解为对象是一个概念性的存在,而实例是采取行为、动作的载体,以一段代码举例,
class Animal(object): pass animal = Animal()
其中Animal是一个类,而animal是一个实例,它可以访问类内的方法,实施“动作”和“行为”。
方法:定义在类外部的函数叫做函数,定义在类内部的函数称为方法。
这些概念在Python面向对象编程中非常概念,只有理解这些概念才能在后续学习中更加容易理解,对上述这些概念有一个简单的了解,在后续的讲解中会更加轻松。
完整代码
本讲所涉及的代码已经上传至github,需要的可以可以访问Jackpopc/advance-python完整链接,
https://github.com/Jackpopc/advance-python/blob/master/2-magic-method_1.ipynb
__new__与__init__
之所以把这个放在第一个,因为这个不仅非常常用,而且很容易被误解,甚至很多知名的书籍中都把这个特殊方法弄错。
很多博客和个别书籍中都把__init__当作类似于C++的构造方法,其实这个理解是错误的。
class Animal(object): def __new__(cls, *args, **kargs): instance = object.__new__(cls, *args, **kargs) print("{} in new method.".format(instance)) # return instance # 不返回实例 def __init__(self): print("{} in init method.".format(self)) animal = Animal() # 输出 <__main__.Animal object at 0x000002BB03001CF8> in new method.
以上面为例,我们对基类中的__new__进行重构,不让它返回实例,可以从输出结果可以看出,程序没有进入__init__方法。这是因为__new__是用来构造实例的,而__init__只是用来对返回的实例进行一些属性的初始化,我们在写一个类的时候首先都会写一个__init__方法去初始化变量,却很少使用__new__,因此就容易忽略__new__,其实在我们继承基类object(例如,class Animal(object))时同时就从基类中继承了__new__方法,所以就不需要重新在子类中实现,如果把上述注释取消掉,再看一下,
class Animal(object): def __new__(cls, *args, **kargs): instance = object.__new__(cls, *args, **kargs) print("{} in new method.".format(instance)) return instance def __init__(self): print("{} in init method.".format(self)) animal = Animal() # 输出 <__main__.Animal object at 0x000002BB03001B00> in new method. <__main__.Animal object at 0x000002BB03001B00> in init method.
可以看出,程序先运行到new中,然后进入init方法。
对于__init__应该都很熟悉,为什么很少使用__new__呢?因为大多数情况下我们是用不到它的。但是存在的即是合理的,它自然有自己的价值。
__new__在哪些场景能够用到呢?
当实现一些高级的软件设计模式可能会用到__new__方法,它主要有以下几点用处,
- 重构一些不可变方法,例如,int, str, tuple
- 实现单例模式(Singleton Pattern)
这里着重介绍一下单例模式。单例模式是一种常用的软件设计模式,有时候我们需要严格的限制一个类只有一个实例存在,一个系统只有一个全局对象,这样有利于协调系统的整体行为。先看一下我们常用的写法,
class NewInt(object): pass new1 = NewInt() new2 = NewInt() print(new1) print(new2) # 输出 <__main__.NewInt object at 0x000002BB03001390> <__main__.NewInt object at 0x000002BB02FF4080>
从输出可以看出,上述两个实例new1、new2地址不同,是两个实例。
然后通过__new__实现单例模式,
class NewInt(object): _singleton = None def __new__(cls, *args, **kwargs): if not cls._singleton: cls._singleton = object.__new__(cls, *args, **kwargs) return cls._singleton new1 = NewInt() new2 = NewInt() print(new1) print(new2) # 输出 <__main__.NewInt object at 0x000002BB02FF6080> <__main__.NewInt object at 0x000002BB02FF6080>
地址相同,指向同一个对象,所以每次实例化产生的实例都是完全相同的。
__enter__与__exit__
在介绍这两个特殊方法之前我们首先讲一下with语句。with语句主要用于对资源进行访问的场景,例如读取文件。以读取文件为例,我们可以使用open、close的方法,但是使用with语句有着无法比拟的优势。首先就是简洁,你不需要再写file.close的语句去关闭文件。其次,也是最重要的,它能够很好的做到异常处理,当处理过程中发生异常,它能够自动关闭、自动释放资源。以读取文件来对比一下两个功能,如果使用open、close方式需要打开、读取、关闭3个过程,
# file.txt fp = open("file.txt", "rb") fp.readline() fp.close()
而使用with语句只需要打开、读取两个过程,当执行完毕会自动关闭,
with open("file.txt", "rb") as fp: fp.readline()
说了这么多with语句的好处,这和__enter__与__exit__有什么关系?
__enter__与__exit__就是实现with的类特殊方法。
以一段代码来解释这两个特殊方法的使用,
class FileReader(object): def __init__(self): print("in init method") def __enter__(self): print("int enter method") return self def __exit__(self, exc_type, exc_val, exc_tb): print("in exit method") del self def read(self): print("in read") # with语句调用 with FileReader() as fr: fr.read() # 输出 in init method int enter method in read in exit method
从上面输出可以看出,程序先进去init方法进行初始化,然后进入enter特殊方法,然后通过fr.read调用read()方法,最后退出时调用exit方法。
这就是enter与exit的调用过程,
- __enter__:初始化后返回实例
- __exit__:退出时做处理,例如清理内存,关闭文件,删除冗余等
__str__与__repr__
一句话描述这两个特殊方法的功能:把类的实例变为字符串。我们都知道,我们可以用这种方法输出一个字符串,
print("Hello world!")
那我们怎么能够像字符串一样把实例输出出来?
可以通过__str__与__repr__来实现,
lass Person(object): def __init__(self, name, age): self.name = name self.age = age def __str__(self): return "str: {} now year is {} years old.".format(self.name, self.age) def __repr__(self): return "repr: {} now year is {} years old.".format(self.name, self.age) person = Person("Li", 27) print(person) # 输出 str: Li now year is 27 years old.
可以看出,当使用print语句打印实例person时,能够像输出字符串那样把实例信息输出出来。但是可以看出,程序进入__str__方法,并没有进入__repr__,这就引出了这两个方法的不同之处,
- __str__:用于用户调用
- __repr__:用于开发人员调用
这似乎不太好理解,因为对于写程序的我们无法理解,何为用户?何为开发人员?简单的来说,__str__是用些Python脚本(.py)时使用,用print语句输出字符串信息。__repr__是我们在交互式环境下测试使用,例如cmd下的Python、ipython,例如在交互式环境下调用,
>>> person = Person("li", 27) >>> person repr: li now year is 27 years old.
更为简单的理解就是:__str__需要用print语句打印,__repr__只需输入实例名称即可。
__setattr__、__getattr__、__getattribute__与__delattr__
了解上述这4个方法之前,我们先来解释一下什么是属性?
也许很多同学已经清楚,但是我觉得还是有必要介绍一下,因为这是要讲的这3个特殊方法的关键。
class Person(object): def __init__(self, name, age, home, work): self.name = name self.age = age self.home = home self.work = work person = Person("Li", 27, "China", "Python") print(person.name) print(person.age) # 输出 Li 27
例如上面我们定义一个Person类,name、age、home、work就是它的属性,当实例化之后我们可以通过点.来访问它的属性。
我可以可以通过传入参数,赋值给self来定义类的属性,但是这样未免太固定了,当实例化之后就不能更改它的属性了,如果我们想获取、添加、删除属性怎么办?这就用到这里要讲的4个特殊方法,__setattr__、__getattr__、__getattribute__与__delattr__,它们的功能分别是,
- __setattr__:设置属性
- __getattr__:访问不存的属性时调用,可能会有同学有疑问,访问不存的属性要它干吗?可以用来做异常处理!
- __getattribute__:访问存在的属性,如果访问属性不存在的时候随后会调用__getattr__
- __delattr__:删除属性
以一个例子来说明一下,
class Person(object): def __init__(self, name): self.name = name def __setattr__(self, key, value): object.__setattr__(self, key, value) def __getattribute__(self, item): print("in getattribute") return object.__getattribute__(self, item) def __getattr__(self, item): try: print("in getattr") return object.__getattribute__(self, item) except: return "Not find attribute: {}".format(item) def __delattr__(self, item): object.__delattr__(item) person = Person("Li") print(person.name) print(person.age) # 输出 in getattribute Li in getattribute in getattr Not find attribute: age
从上面输出来看一下就可以明白,当获取属性name时,由于已经有了,则进入__getattribute__中,获取对应的属性,当获取属性age时,由于没有这个属性,则先进入__getattribute__,然后进入__getattr__,没有找到属性返回异常信息。
然后再来看一下看一下设置属性和删除属性,
person.age = 27 print(person.age) delattr(person, "age") print(person.age) # 输出 in getattribute 27 in delattr in getattribute in getattr Not find attribute: age
从输出结果可以看出,通过instance.attribute的方式可以设置属性,通过delattr可以删除属性。