Python面向对象高级篇(01)--特殊方法

Python中, 以__开头的方法基本上都是特殊方法, 虽然不会被直接调用(在代码里面完全找不到这些方法是在哪里调用的), 但是这些方法却是Python灵活, 优雅的基石.

0. Define: python-version: 3.5.2

1. 前言

在基础篇中大致的梳理了面向对象的三大特性继承封装以及多态在Python语言中的体现, 此外还有一些更加高级的特性, 比如对象切片, 对象迭代, 属性描述符以及元编程, 这些属于Python语言所特有的特性. 而这些高级的特性均是建立在以__开头的特殊方法之上的.

2. 自定义类相关

2.1 __init__

之所以把__init__方法放在最前面, 是因为该方法可能是我们日常使用中最常定义的. 该方法在一个类被实例化成一个具体的对象时调用, 由Python自动调用, 作为对象初始化的方法.

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

smart = Person("smart")
smart.name

__init__方法的作用是将一个类的固有属性(变量)在初始化时进行赋值. 由于Python为动态语言, 我们不将属性放在__init__方法之中也可以, 即在实例化一个对象之后进行动态赋值. 只不过这样一来, 就无法使得所有的实例化对象均具有某种共同的属性了. 所以, __init__方法的根本作用其实就是使得所有的实例化对象拥有相同的属性, 但具体的属性值可以由对象自己来确定.

1
2
keyerror = Person("keyerror")
jack = Person("jack")

smart, keyerror, jack均有name属性, 但是值却不相同.

2.2 __new__

__init__方法和__new__方法常常拿来进行比较, 包括使用方法, 调用时机等等.
__init__方法是在实例化一个对象时被调用, 而__new__方法则是在此之前被调用. 该方法返回一个对象, 即实例. 返回的实例将会传递给__init__方法中的self参数. __new__方法其实才是真正的构造方法.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class Person:
def __new__(cls, *args, **kwargs):
print(args, kwargs) ①
print("__new__ method called") ②
cls = super(Person, cls).__new__(cls, *args, **kwargs)
print(cls) ③
return cls

def __init__(self):
print("__init__ method called") ④


if __name__ == "__main__":
a = Person()
print(a) ⑤

①: () {}
②: __new__ method called __new__方法首先被调用
③: <__main__.Person object at 0x7f22a92eb2b0>
④: __init__ method called
⑤: <__main__.Person object at 0x7f22a92eb2b0> 可以看到__new__方法返回的实例就是我们所实例化的对象.

那么我们可以使用__new__方法来做什么事情呢? 实现单例模式和元编程. 通常来讲该方法的用处并不是非常广泛, 一般根本不会重写. 但是一旦使用了__new__方法, 将会给我们的代码带来魔术般的体验.

2.3 __del__

前面的两个方法均与实例的创建有关, 而__del__方法则是与实例的销毁有关.
Python的垃圾收回机制主要是以引用计数为主, 以分代回收为辅. 当一个对象的引用计数为0时, 对象立刻被销毁: Python会调用__del__方法(如果定义了的话), 然后释放分配给对象的内存.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
In [11]: class Person(object):
...: def __init__(self, name):
...: self.name = name
...:
...: def __repr__(self):
...: return self.name
...:
...: def __del__(self):
...: print("对象即将被销毁")
...:

In [12]: smart = Person("smart")

In [13]: del smart
对象即将被销毁

In [14]: smart
---------------------------------------------------------------------------
NameError Traceback (most recent call last)
<ipython-input-14-1a93619c3d61> in <module>()
----> 1 smart

NameError: name 'smart' is not defined

其实__del__方法就是对象在被干掉的弥留之际, 做的最后一点儿事情. 不管这个事情抛不抛异常, 对象都会被干掉. 在日常中通常我们并不会定义该方法, 循环引用导致的内存泄露问题通常采用弱引用来进行解决.

2.4 __str____repr__

两个与对象的字符串表现形式有关的函数, 其返回值为能够具体表示某个对象的字符串. 其中__str__将于print函数中进行调用, __repr__则在交互式环境下进行调用. 通常来讲我们实现两个方法中的任意一个即可.

2.5 __call__

__call__方法其实就是实例对象能否向函数一样进行调用.

1
2
3
4
5
6
7
8
9
10
11
class Person:
def __init__(self, name):
self.name = name

def __call__(self):
print("__call__" + self.name)

In [3]: smart = Person("smart")

In [4]: smart()
smart

2.6 __hash__

当我们调用hash(object)方法时, 会触发__hash__方法的调用. 在Python3.5.2版本中:

1
hash(object) = id(object) / 16

id函数实际上是返回对象在内存中的地址

2.7 __iter____next__

这两个方法经常成对出现, 前者返回一个迭代器, 后者对迭代器进行next方法的调用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class Person(object):
def __init__(self, name):
self.name = name
self.count = 0

def __iter__(self):
return self

def __next__(self):
while self.count < 10:
self.count += 1
return self.count
raise StopIteration

In [24]: for i in Person("smart"):
...: print(i, end=" ")
1 2 3 4 5 6 7 8 9 10

其实迭代的原理也就是这样, 获取迭代器, 然后不断的调用迭代器的next方法, 直到抛出StopIteration的异常, 该异常在for循环中Python会进行处理.
通过使用__iter____next__两个特殊方法, 我们就可以将我们的对象进行迭代了.

2.8 __enter____exit__

这两个方法与上下文管理器有关, 用处非常广泛. 上下文管理器可以认为是面向切面编程的一种实现: 在执行某段代码之后的动作可交由上下管理器来完成, 我们只关心核心逻辑.
最常见的, 比如数据库连接. 首先我们建立一个连接, 执行SQL语句, 执行完毕后关闭该连接. 这个过程我们就可以使用上下文管理器来完成.

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
class MySQLConnection:

def connect(self):
print("connecting...")

def execute_sql(self):
print("executing...")

def close(self):
print("closing...")


class ContextManager:
def __init__(self):
self.connection = MySQLConnection()

def __enter__(self):
self.connection.connect()
return self.connection

def __exit__(self, exc_type, exc_val, exc_tb):
self.connection.close()


try:
with ContextManager() as connection:
connection.execute_sql()
except Exception as e:
print(e)

简陋版的数据库连接上下文管理器就是这么个样子, 值得注意的是__exit__方法所接受的参数, 依次为异常类型(类对象), 异常信息(message), 异常堆栈(traceback). 当我们在执行sql语句出错抛异常的时候, 连接也能够正常的被关闭. 这是一个非常方便的特性. 另外要说明的是, 在__exit__只能处理__exit__方法本身产生的异常, 比如调用close方法出现了异常, 然后进行异常捕获并处理, 是重试还是直接抛出视实际情况而定.
并且, 如果在with语句块内和__exit__都向外抛出了异常, 最外层的异常捕获只能捕获最后一次的异常, 也就是__exit__方法中抛出的异常.
其实__exit__接受异常的参数, 我认为这也就说明了应该在__exit__就将这个异常消化掉, 最外层的异常捕获精确捕获语句块儿中产生的异常.

2.9 __getattr____getattribute__

这两个方法看起来非常的相似, 同时也非常的容易混淆.

  • 当某个类同时定义了这两个方法时, __getattr__方法静默, 不会被调用, 除非显示调用. 此时仅调用__getattribute__方法.
  • __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
class Person:
class_attribute = "class_attribute"

def __init__(self, instance_attribute):
self.instance_attribute = instance_attribute

def __getattr__(self, item):
if item == "smart":
return "smart"
raise ValueError

In [27]: jack = Person("jack")

In [28]: jack.class_attribute # 访问存在的类属性, 不会调用__getattr__
Out[28]: 'class_attribute'

In [29]: jack.instance_attribute # 访问存在的实例属性, 不会调用__getattr__
Out[29]: 'jack'

In [30]: jack.hello # 访问不存在的属性, 触发__getattr__
---------------------------------------------------------------------------
ValueError Traceback (most recent call last)
<ipython-input-30-c690048f9567> in <module>()
----> 1 jack.hello

<ipython-input-26-3f561d30e387> in __getattr__(self, item)
8 if item == "smart":
9 return "smart"
---> 10 raise ValueError
11

ValueError:

In [31]: jack.smart
Out[31]: 'smart'
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class Person:
class_attribute = "class_attribute"

def __init__(self, instance_attribute):
self.instance_attribute = instance_attribute

def __getattribute__(self, item):
return "smart"

In [36]: jack = Person("jack")

In [37]: jack.class_attribute
Out[37]: 'smart'

In [38]: jack.instance_attribute
Out[38]: 'smart'

In [39]: jack.jack
Out[39]: 'smart'

如果在__getattribute__方法去调用类属性或者实例属性的话, 会造成递归调用, 直到栈慢抛异常.

大致上我们常用的特殊方法也就是这些, 当然在自定义类里面还有一个属性描述符__get__, 这个放在下一篇再写. 下面是与集合相关的特殊方法.

3. 与集合相关

在我们使用集合时, 常常使用list[0], dict[key]这样的形式来直接获取集合中的对象, 那么在这背后其实就是__getitem__, __setitem__方法.

3.1 __getitem__
1
2
3
4
5
6
7
8
9
10
11
12
class Person:
def __init__(self):
self.names = [i for i in range(10) if i % 2 == 0]
print(self.names)

def __getitem__(self, item):
return self.names[item] # 注意一下这里并不会产生递归调用, 因为Person的__getitem__方法和list的__getitem__方法并不是同一个


if __name__ == "__main__":
smart = Person()
print(smart[4]) # 得到数值8

__getitem__的另外一个作用就是用于迭代, 在前面提到了对象的迭代可以使用__iter__+__next__来实现, 那么如果说我们对一个对象的迭代恰好是对对象中的某个容器进行迭代的话, 那么就可以使用__getitem__方法.

1
2
for i in smart:
print(i, end=" ") # 0 2 4 6 8

将迭代委托给了__getitem__方法, 这样一来就不需要定义额外的特殊方法了.

那么__setitem__, __delitem__其实也就对应着容器对象的赋值, 容器对象的删除, 没有什么额外需要写的.

4. 小结

大致上来讲我们经常会用到的特殊方法也就是上面这些, 除此之外还有一些关于运算的特殊方法, 很少很少会用的到, 所以也就不再写了.
需要注意的是: 特殊方法应该正确的被使用, 不能为了使用特殊方法而特意的使用, 这样会使得代码难以维护, 还可能会出现一些隐藏的BUG.