Python3基础(十一) 类的拓展

类的初印象中,我们已经简单的介绍了类,包括类的定义、类对象和实例对象。本文将进一步学习类的继承、迭代器、发生器等等。


一、类的继承

单继承

派生类的定义如下:

1
2
3
4
5
6
class 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
6
class 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
11
class 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
14
class 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
16
class 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
10
for 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
12
class 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
3
def 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
18
def 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

执行过程大致如下:

  1. 调用生成器函数将返回一个生成器。
  2. 第一次调用生成器的 next 方法时,生成器才开始执行生成器函数。直到遇到 yield 时暂停执行(挂起),并且将 yield 的参数作为此次的返回值。
  3. 之后每次调用 next 方法,生成器将从上次暂停的位置恢复并继续执行,直到再次遇到yield 时暂停,同样将 yield 的参数返回。
  4. 当调用 next 方法时生成器函数结束,则此次调用将抛出 StopIteration 异常(for循环终止条件)。

所以说,生成器的神奇之处在于每次使用 next() 执行生成器函数遇到 yield 返回时,生成器函数的“状态”会被冻结,所有的数据值和执行位置会被记住,一旦 next() 再次被调用,生成器函数会从它上次离开的地方继续执行。


五、类用作ADT

有些时候,类似于 Pascal 的“record”或 C 的“struct”这样的数据类型非常有用,绑定一些命名的数据。在 Python 中一个空的类定义就可以做到:

1
2
3
4
5
6
7
8
9
class 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
11
class 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
16
class 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
16
class 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']

(全文完)