在类的初印象中,我们已经简单的介绍了类,包括类的定义、类对象和实例对象。本文将进一步学习类的继承、迭代器、发生器等等。
一、类的继承
单继承
派生类的定义如下:1
2
3
4
5
6class DerivedClassName(BaseClassName):
<statement-1>
.
.
.
<statement-N>
基类名 BaseClassName 对于派生类来说必须是可见的。也可以继承在其他模块中定义的基类:1
class DerivedClassName(module.BaseClassName):
对于派生类的属性引用:首先会在当前的派生类中搜索,如果没有找到,则会递归地去基类中寻找。
从C++术语上讲,Python 类中所有的方法都是vitual
的,所以派生类可以覆写(override)基类的方法。在派生类中一个覆写的方法可能需要调用基类的方法,可以通过以下方式:1
BaseClassName.method(self, arguments)
介绍两个函数:
isinstance(object, class_name)
:内置函数,用于判断实例对象 object 是不是类 classname 或其派生类的实例,即`object._class `是 class_name 或其派生类时返回 True。issubclass(class1, class2)
:内置函数,用于检查类 class1 是不是 class2 的派生类。例如issubclass(bool, int)
会返回 True,因为 bool 是 int 的派生类。
多重继承
Python支持多重继承,一个多重继承的定义形如:1
2
3
4
5
6class DerivedClassName(Base1, Base2, Base3):
<statement-1>
.
.
.
<statement-N>
大多数的情况(未使用super)下,多重继承中属性搜索的方式是,深度优先,从左到右。在继承体系中,同样的类只会被搜寻一次。如果一个属性在当前类中没有被找到,它就会搜寻 Base1,然后递归地搜寻 Base1 的基类,然后如果还是没有找到,那么就会搜索 Base2,依次类推。
对于菱形继承,Python 3采用了 C3 线性化算法去搜索基类,保证每个基类只搜寻一次。所以对于使用者,无须担心这个问题,如果你想了解更多细节,可以看看Python类的方法解析顺序。
二、自定义异常类
在《Python3的错误和异常》中,我们简单地介绍了Python中的异常处理、异常抛出以及清理动作。在学习了类的继承以后,我们就可以定义自己的异常类了。
自定义异常需要从 Exception 类派生,既可以是直接也可以是间接。例如:1
2
3
4
5
6
7
8
9
10
11class MyError(Exception):
def __init__(self, value):
self.value = value
def __str__(self):
return repr(self.value)
try:
raise MyError(2*2)
except MyError as e:
print('My exception occurred, value:', e.value)
# 输出:My exception occurred, value: 4
在这个例子中, Exception 的默认方法 __init__() 被覆写了,现在新的异常类可以像其他的类一样做任何的事。当创建一个模块时,可能会有多种不同的异常,一种常用的做法就是,创建一个基类,然后派生出各种不同的异常:1
2
3
4
5
6
7
8
9
10
11
12
13
14class Error(Exception):
"""Base class for exceptions in this module."""
pass
class InputError(Error):
def __init__(self, expression, message):
self.expression = expression
self.message = message
class TransitionError(Error):
def __init__(self, previous, next, message):
self.previous = previous
self.next = next
self.message = message
需要特别注意的是,如果一个 except 后跟了一个异常类,则这个 except 语句不能捕获该异常类的基类,但能够捕获该异常类的子类。例如:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16class B(Exception):
pass
class C(B):
pass
class D(C):
pass
for e in [B, C, D]:
try:
raise e()
except D:
print('D')
except C:
print('C')
except B:
print('B')
上面的代码会按顺序输出B、C、D。如果将三个 except 语句逆序,则会打印B、B、B。
三、迭代器(Iterator)
到目前为止,你可能注意到,大多数的容器对象都可以使用 for 来迭代:1
2
3
4
5
6
7
8
9
10for element in [1, 2, 3]:
print(element)
for element in (1, 2, 3):
print(element)
for key in {'one':1, 'two':2}:
print(key)
for char in "123":
print(char)
for line in open("myfile.txt"):
print(line)
这种形式可以说是简洁明了。其实,for 语句在遍历容器的过程中隐式地调用了iter()
,这个函数返回一个迭代器对象,迭代器对象定义了 __next__()
方法,用以在每次访问时得到一个元素。当没有任何元素时,__next__() 将产生 StopIteration 异常来告诉 for 语句停止迭代。
内置函数 next()
可以用来调用 __next__() 方法,示例:1
2
3
4
5
6
7
8
9
10
11
12>>> s = 'abc'
>>> it = iter(s) # 获取迭代器对象
>>> next(it)
'a'
>>> next(it)
'b'
>>> next(it)
'c'
>>> next(it)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
StopIteration
在了解了迭代器的机制之后,就可以很简单的将迭代行为增加到你的类中。定义一个 __iter__()
方法返回一个具有 __next__() 的对象,如果这个类定义了 __next__() , 那么 __iter__() 仅需要返回 self:1
2
3
4
5
6
7
8
9
10
11
12class Reverse:
""" 逆序迭代一个序列 """
def __init__(self, data):
self.data = data
self.index = len(data)
def __iter__(self):
return self
def __next__(self):
if self.index == 0:
raise StopIteration
self.index -= 1
return self.data[self.index]
测试:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16# 测试
rev = Reverse('spam')
for c in rev:
print(c, end=' ') # 输出:m a p s
# 单步测试
>>> rev = Reverse('spam')
>>> it = iter(rev) # 返回的 self 本身
>>> next(it) # 相当于 next(rev),因为iter(rev)返回本身
'm'
>>> next(it)
'a'
>>> next(it)
'p'
>>> next(it)
's'
四、生成器(Generator)
生成器(Generator)是用来创建迭代器的工具,它的形式跟函数一样,唯一的不同是生成器使用 yield
语句返回,而不是 return 语句。
有了生成器,我们不再需要自定义迭代器类(例如上面的 class Reverse),因为自定义迭代器类需要手动实现 __iter__() 和 __next__() 方法,也是有点麻烦。而生成器则会自动创建 __iter()__ 和 __next__(),可以更方便地生成一个迭代器,而且代码也会更短更简洁。例如,这里用生成器实现与 class Reverse 相同作用的迭代器:1
2
3def Reverse(data):
for idx in range(len(data)-1, -1, -1):
yield data[idx]
原来要十多行代码写一个迭代器类,现在使用生成器只需要3行代码!来测试一下:1
2
3
4
5
6
7
8
9
10
11
12
13
14# 测试
for c in Reverse('spam'):
print(c, end=' ') # 输出:m a p s
# 单步测试
>>> rev = Reverse('spam')
>>> next(rev)
'm'
>>> next(rev)
'a'
>>> next(rev)
'p'
>>> next(rev)
's'
怎么样?现在感受到生成器的强大了吧。确实,生成器让我们可以方便的创建迭代器,而不必去自定义迭代器类那么麻烦。下面我们来了解一下生成器的工作过程:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18def generator_func():
""" 这是一个简单的生成器 """
yield 1
yield 2
yield 3
# 测试
>>> g = generator_func()
>>> next(g)
1
>>> next(g)
2
>>> next(g)
3
>>> next(g)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
StopIteration
执行过程大致如下:
- 调用生成器函数将返回一个生成器。
- 第一次调用生成器的 next 方法时,生成器才开始执行生成器函数。直到遇到 yield 时暂停执行(挂起),并且将 yield 的参数作为此次的返回值。
- 之后每次调用 next 方法,生成器将从上次暂停的位置恢复并继续执行,直到再次遇到yield 时暂停,同样将 yield 的参数返回。
- 当调用 next 方法时生成器函数结束,则此次调用将抛出 StopIteration 异常(for循环终止条件)。
所以说,生成器的神奇之处在于每次使用 next() 执行生成器函数遇到 yield 返回时,生成器函数的“状态”会被冻结,所有的数据值和执行位置会被记住,一旦 next() 再次被调用,生成器函数会从它上次离开的地方继续执行。
五、类用作ADT
有些时候,类似于 Pascal 的“record”或 C 的“struct”这样的数据类型非常有用,绑定一些命名的数据。在 Python 中一个空的类定义就可以做到:1
2
3
4
5
6
7
8
9class Employee:
pass
john = Employee() # Create an empty employee record
# Fill the fields of the record
john.name = 'John Doe'
john.dept = 'computer lab'
john.salary = 1000
一段 Python 代码中如果需要一个抽象数据类型,那么可以通过传递一个类给那个方法,就好像有了那个数据类型一样。
例如,如果你有一个函数用于格式化某些从文件对象中读取的数据,你可以定义一个有 read() 和 readline() 方法的类用于读取数据,然后将这个类作为一个参数传递给那个函数。
附:类变量与实例变量的区别
类变量(class variable)是类的属性和方法,它们会被类的所有实例共享。而实例变量(instance variable)是实例对象所特有的数据。如下:1
2
3
4
5
6
7
8
9
10
11class animal:
kind = 'dog' # class variable shared by all instances
def __init__(self, color):
self.color = color # instance variable unique to each instance
a1 = animal('black')
a2 = animal('white')
print(a1.kind, a2.kind) # shared by all animals
print(a1.color, a2.color) # unique to each animal
当类变量(被所有实例共享)是一个可变的对象时,如 list 、dict ,那么在一个实例对象中改变该属性,其他实例的这个属性也会发生变化。这应该不难理解,例如:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16class animal:
actions = [] # class variable shared by all instances
def __init__(self, color):
self.color = color # instance variable unique to each instance
def addActions(self, action):
self.actions.append(action)
a1 = animal('black')
a2 = animal('white')
a1.addActions('run') # 动物a1会跑
a2.addActions('fly') # 动物a2会飞
print(a1.actions, a2.actions) # 输出:['run', 'fly'] ['run', 'fly']
输出结果显示:动物 a1 和 a2 总是又相同的行为(actions),显然这不是我们想要的,因为不同的动物有不同的行为,比如狗会跑、鸟会飞、鱼会游……
对这个问题进行改进,我们只需要将 actions 这个属性变成实例变量,让它对每个实例对象都 unique ,而不是被所有实例共享:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16class animal:
def __init__(self, color):
self.color = color # instance variable
self.actions = [] # instance variable
def addActions(self, action):
self.actions.append(action)
a1 = animal('black')
a2 = animal('white')
a1.addActions('run') # 动物a1会跑
a2.addActions('fly') # 动物a2会飞
print(a1.actions, a2.actions) # 输出:['run'] ['fly']
(全文完)