Python 系列学习十三:面向对象编程 - 重载内置方法

前言

打算写一系列文章来记录自己学习 Python 3 的点滴;本章主要介绍 Python 有关面向对象编程中的通过重载内置方法自定义类的类型和外观;

本文为作者的原创作品,转载需注明出处;

重载内置方法

首先,内置方法在 Python 中约定使用 __<name>__ 的形式进行定义的;通过重载 Python 的内置方法,可以自定义你的类,

  1. 使得它的默认行为发生改变;
  2. 并且使得它的类的类型发生变化

object 对象

Python 为类定义了大量的内置方法,而这些内置方法都是通过继承对象 object 得到的,看看 object 的源码,

builtins.py

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
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
class object:
""" The most base type """
def __delattr__(self, *args, **kwargs): # real signature unknown
""" Implement delattr(self, name). """
pass

def __dir__(self): # real signature unknown; restored from __doc__
"""
__dir__() -> list
default dir() implementation
"""
return []

def __eq__(self, *args, **kwargs): # real signature unknown
""" Return self==value. """
pass

def __format__(self, *args, **kwargs): # real signature unknown
""" default object formatter """
pass

def __getattribute__(self, *args, **kwargs): # real signature unknown
""" Return getattr(self, name). """
pass

def __ge__(self, *args, **kwargs): # real signature unknown
""" Return self>=value. """
pass

def __gt__(self, *args, **kwargs): # real signature unknown
""" Return self>value. """
pass

def __hash__(self, *args, **kwargs): # real signature unknown
""" Return hash(self). """
pass

def __init_subclass__(self, *args, **kwargs): # real signature unknown
"""
This method is called when a class is subclassed.

The default implementation does nothing. It may be
overridden to extend subclasses.
"""
pass

def __init__(self): # known special case of object.__init__
""" Initialize self. See help(type(self)) for accurate signature. """
pass

def __le__(self, *args, **kwargs): # real signature unknown
""" Return self<=value. """
pass

def __lt__(self, *args, **kwargs): # real signature unknown
""" Return self<value. """
pass

@staticmethod # known case of __new__
def __new__(cls, *more): # known special case of object.__new__
""" Create and return a new object. See help(type) for accurate signature. """
pass

def __ne__(self, *args, **kwargs): # real signature unknown
""" Return self!=value. """
pass

def __reduce_ex__(self, *args, **kwargs): # real signature unknown
""" helper for pickle """
pass

def __reduce__(self, *args, **kwargs): # real signature unknown
""" helper for pickle """
pass

def __repr__(self, *args, **kwargs): # real signature unknown
""" Return repr(self). """
pass

def __setattr__(self, *args, **kwargs): # real signature unknown
""" Implement setattr(self, name, value). """
pass

def __sizeof__(self): # real signature unknown; restored from __doc__
"""
__sizeof__() -> int
size of object in memory, in bytes
"""
return 0

def __str__(self, *args, **kwargs): # real signature unknown
""" Return str(self). """
pass

@classmethod # known case
def __subclasshook__(cls, subclass): # known special case of object.__subclasshook__
"""
Abstract classes can override this to customize issubclass().

This is invoked early on by abc.ABCMeta.__subclasscheck__().
It should return True, False or NotImplemented. If it returns
NotImplemented, the normal algorithm is used. Otherwise, it
overrides the normal algorithm (and the outcome is cached).
"""
pass

__class__ = None # (!) forward: type, real value is ''
__dict__ = {}
__doc__ = ''
__module__ = ''

可见,里面定义了大量的内置方法,下面,笔者就几个常用的内置函数进行梳理;

__str__

这个等价于 Java 对象的 String toString() 方法,当该对象将要被打印出来的时候,会被重定向到该方法并输出其调用结果;

1
2
3
4
5
6
>>> class Student(object):
... def __init__(self, name):
... self.name = name
...
>>> print(Student('Linda'))
<__main__.Student object at 0x589cdf190>

可见,打印出来的东西实际上于我们而言帮助不大;是否能够重新定义其输出内容使其输出更有价值的内容呢?

1
2
3
4
5
6
>>> class Student(object):
... def __init__(self, name):
... self.name = name
... def __str__(self):
... return 'Student object (name: %s)' % self.name
...

通过覆盖 Student 的默认内置方法 __str__,便重写了 Student 实例的默认打印输出格式;

1
2
>>> print(Student('Linda'))
Student object (name: Linda)

__repr__

通过__str__的方式,我们重写了使用 print 方法打印实例的输出内容,但是,如果,我们直接使用下面的这种方式,可以发现,输出的还是以前的方式

1
2
3
>>> linda = Student('Linda'))
>>> linda
<__main__.Student object at 0x589cdf190>

这个时候,我们可以通过重写 __repr__ 来重定义其输出内容;

1
2
3
4
5
6
class Student(object):
def __init__(self, name):
self.name = name
def __str__(self):
return 'Student object (name=%s)' % self.name
__repr__ = __str__

再执行

1
2
3
>>> linda = Student('Linda'))
>>> linda
Student object (name: Linda)

__iter__

__iter__比较有意思,其作用相当于 Java 对象实现了接口 Iterator 一样,使得当前类可以被迭代操作,当然 Python 中最典型的迭代操作就是for ... in ...的方式了;首先要特别注意的一点是,__iter__方法必须返回的是一个Iterator对象,有关 Iterator 的介绍参考 Python 系列学习六:迭代类型 Iterator / Iterable,下面做一个 Negative 的测试,返回一个 list(list 是一个 Iterable 对象,不是 Iterator);

1
2
3
class Numbers(object):
def __iter__(self):
return list(range(1,11))

执行,

1
2
3
4
5
6
7
>>> n = Numbers()
>>> for i in n:
... print(i)
...
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: iter() returned non-iterator of type 'list'

可见,在执行内置方法 iter() 的时候抛出了一个类型异常的错误,该错误很明显的指出了,类型不匹配,返回的对象 list 不是一个 Iterator;所以,需要做如下的修改该,将其改为 Iterator 对象,如何将 list 转变为 Iterator 对象参考 Iterable -> Iterator

1
2
3
class Numbers(object):
def __iter__(self):
return iter(list(range(1,11)))

通过内置方法iter(Iterable)将 list 转换为了Iterator,再试试;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
>>> n = Numbers()
>>> for i in n:
... print(i)
...
1
2
3
4
5
6
7
8
9
10

类型的变化

别急,很多人学到这里,就匆匆忙忙的走了,却忘了街边还有一幅最美丽的风景,那就是实例 n 的类型现在是什么?

1
2
>>> type(n)
<class '__main__.Numbers'>

切,楼主,还以为你要说什么呢?我知道呀,它本来就是一个 Numbers 对象嘛…. 等等,别着急,看看下面这个,

1
2
3
>>> from collections import Iterable
>>> isinstance(n, Iterable)
True

有意思吧,当一个类的定义当中,如果重载了内置方法__iter__(self),他将会成为Iterable类型的对象了;

为了确保以上的论述是正确的,我们再来看看,

1
2
3
4
5
6
>>> class Numbers(object):
... pass
...
>>> n = Numbers()
>>> isinstance(n, Iterable)
False

ok,当 Numbers 类没有重载内置方法__iter__的时候,它并不是一个Iterable对象;

1
2
3
4
5
6
7
>>> class Numbers(object):
... def __iter__(self):
... pass
...
>>> n = Numbers()
>>> isinstance(n, Iterable)
True

ok,一旦 Numbers 重载了内置方法__iter__,它的实例就是一个Iterable对象了;

有意思吧,这里势必要记住这样的特性;

结合 __next__

结合__next__方法,使得该对象本身成为一个Iterator对象,满足__iter__接口方法的需要;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class Numbers(object):
def __init__(self):
self.nums = list(range(1, 11))
self.index = 0
def __iter__(self):
return self
def __next__(self):
if( self.index <= 9 ):
r = self.nums[self.index]
self.index += 1
return r
else:
raise StopIteration()
return r

上述实现中,通过 __iter__ 返回对象实例自己 self,表示,该实例自己就是一个 Iterator 类型的实例,那为什么说该实例自己就是一个 Iterator 实例呢?答案就在,Numbers 类实现了 __next__方法,使其成为了Iterator实例,也就满足了接口__iter__对返回对象是Iterator类型的需要;执行下看看,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
>>> n = Numbers()
>>> for i in n:
... print(i)
...
1
2
3
4
5
6
7
8
9
10

再来看看 Numbers 的实例 n 的类型,

1
2
3
>>> from collections import Iterator
>>> isinstance(n, Iterator)
True

果然,只要实现了默认内置接口__next__方法,该实例的类型就自动的转变为了Iterator

__getitem__

这里,我们依然使用到 __iter__ 中所使用到的例子,为了后续的演示,将它做了适当的改动,将 list 赋值给了实例变量l

1
2
3
4
5
class Numbers(object):
def __init__(self):
self.l = list(range(1,11))
def __iter__(self):
return iter(self.l)

虽然,Numbers 的实例可以进行迭代

1
2
3
4
5
6
7
8
9
10
11
12
13
14
>>> n = Numbers()
>>> for i in n:
... print(i)
...
1
2
3
4
5
6
7
8
9
10

但是,它不支持 indexing 和切片的操作,比如

1
2
3
4
>>> n[3]
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: 'Numbers' object does not support indexing
1
2
3
4
>>> n[1,5]
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: 'Numbers' object is not subscriptable

要想实现 indexing 和切片的操作,那么就必须实现内置接口__getitem__

实现 indexing

那么,笔者首先带领大家通过重载接口方法__getitem__来实现 indexing 操作,

1
2
3
4
5
6
7
class Numbers(object):
def __init__(self):
self.l = list(range(1,11))
def __iter__(self):
return iter(self.l)
def __getitem__(self, n):
return self.l[n]

执行测试,

1
2
3
>>> n = Numbers()
>>> n[5]
6

可见,通过重载接口方法__getitem__我们非常容易的实现了 indexing 操作;那切片操作呢?

实现切片

因为切片对应的是另一种参数类型 slice,所以,为了同时兼容 indexing 和切片,就需要在__getitem__方法中进行判断两种不同的类型并且分别实现对应的逻辑,如下所述,

1
2
3
4
5
6
7
8
9
10
class Numbers(object):
def __init__(self):
self.l = list(range(1,11))
def __iter__(self):
return iter(self.l)
def __getitem__(self, n):
if isinstance(n, int):
return self.l[n]
if isinstance(n, slice):
return self.l[n.start:n.stop]

执行试试,

1
2
3
4
5
>>> n = Numbers()
>>> n[5]
6
>>> n[5:11]
[6, 7, 8, 9, 10]

这样,我们就兼容了 indexing 和切片的操作;

__getattr__

当在获取默认属性时,没有找到的前提下,会调用__getattr__检索;例如,我们有如下的测试用例,

1
2
class Student(object):
pass

当我们获取 linda 的成员变量 name 的时候,自然是报错的,提示属性 name 不存在;

1
2
3
4
5
>>> linda = Student()
>>> linda.name
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
AttributeError: 'Student' object has no attribute 'name'

这个时候,我们就可以利用__getattr__来处理这种情况了,进一步修改 Student

1
2
3
4
class Student(object):
def __getattr__(self, attr):
if attr == 'name':
return 'default person'

再次调用,

1
2
3
>>> linda = Student()
>>> linda.name
'default person'

可见,当实现了默认接口__getattr__以后,当实例中没有对应的属性的时候,会交给__getattr__方法进行处理;不过返回的是默认值;

也可以返回不存在的方法调用的默认值,

1
2
3
4
5
6
class Student(object):
def __getattr__(self, attr):
if attr == 'name':
return 'default person'
if attr == 'age':
return lambda: 16

试试,

1
2
3
>>> linda = Student()
>>> linda.age()
16

注意,当实现了默认接口__getattr__以后,如果某个属性找不到,直接返回None,并不会有任何的错误提示信息;

1
2
>>> linda.score
>>>

可见,再次访问一个不存在的属性 score,并且__getattr__也没有进行处理的情况下,直接返回None;所以,我们需要定制错误提示的信息;

1
2
3
4
5
6
7
class Student(object):
def __getattr__(self, attr):
if attr == 'name':
return 'default person'
if attr == 'age':
return lambda: 16
raise AttributeError('\'Student\' object has no attribute \'%s\'' % attr)

再次调用

1
2
3
4
5
>>> linda.score
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "<stdin>", line 7, in __getattr__
AttributeError: 'Student' object has no attribute 'score'

__call__

有时候,某些实例有默认的调用方法,那么为了简化,能不能直接以instance()的方法进行调用呢?答案同样是有的,只要是利于简化的东西,在 Python 中都是被考虑的,这个简化动作就是由__call__内置接口函数负责实现的;看下面这个例子,

1
2
3
4
5
class Student(object):
def __init__(self, name):
self.name = name
def __call__(self):
print('Student name is %s.' % self.name)

执行测试,

1
2
3
>>> linda = Student('Linda')
>>> linda()
Student name is Linda.

可见,实例对象本身,化身成为了一个可被调用的函数,可以直接通过linda()来进行调用,该调用执行的正是__call__方法;

判断一个对象是不是一个直接可被调用的对象,可以使用如下两种判断的方式

  1. 使用内置方法callable(instance)

    1
    2
    3
    4
    5
    6
    7
    8
    >>> callable(linda)
    True
    >>> callable(max)
    True
    >>> callable([1, 2, 3])
    False
    >>> callable(None)
    False
  2. 使用isinstance

    1
    2
    3
    4
    5
    6
    7
    8
    9
    >>> from collections import Callable
    >>> isinstance(linda, Callable)
    True
    >>> isinstance(max, Callable)
    True
    >>> isinstance([1, 2, 3], Callable)
    False
    >>> isinstance(None, Callable)
    False