类的命名空间
下面两条语句大致等价:
def foo(x): return x * x
foo = lambda x: x * x
它们都创建一个返回参数平方的函数,并将这个函数关联到变量foo。可以在全局(模块)作用域内定义名称foo,也可以在函数或方法内定义。定义类时情况亦如此:在class语句中定义的代码都是在一个特殊的命名空间(类的命名空间)内执行的,而类的所有成员都可访问这个命名空间。类定义其实就是要执行的代码段,并非所有的Python程序员都知道这一点,但知道这一点很有帮助。例如,在类定义中,并非只能包含def语句。对于成员变量(属性),有些语言支持多种私有程度。
>>> class C:
... print('Class C being defined...')
...
Class C being defined...
>>>
这有点傻,但请看下面的代码:
class MemberCounter:
members = 0
def init(self):
MemberCounter.members += 1
>>> m1 = MemberCounter()
>>> m1.init()
>>> MemberCounter.members
1
>>> m2 = MemberCounter()
>>> m2.init()
>>> MemberCounter.members
2
上述代码在类作用域内定义了一个变量,所有的成员(实例)都可访问它,这里使用它来计算类实例的数量。注意到这里使用了init来初始化所有实例,这个初始化过程自动化,也就是将init转换为合适的构造函数。
每个实例都可访问这个类作用域内的变量,就像方法一样。
>>> m1.members
2
>>> m2.members
2
如果你在一个实例中给属性members赋值,结果将如何呢?
>>> m1.members = 'Two'
>>> m1.members
'Two'
>>> m2.members
2
新值被写入m1的一个属性中,这个属性遮住了类级变量。这类似于“遮盖的问题”所讨论的,函数中局部变量和全局变量之间的关系。
抽象基类
然而,有比手工检查各个方法更好的选择。在历史上的大部分时间内,Python几乎都只依赖于鸭子类型,即假设所有对象都能完成其工作,同时偶尔使用hasattr来检查所需的方法是否存在。很多其他语言(如Java和Go)都采用显式指定接口的理念,而有些第三方模块提供了这种理念的各种实现。最终,Python通过引入模块abc提供了官方解决方案。这个模块为所谓的抽象基类提供了支持。一般而言,抽象类是不能(至少是不应该)实例化的类,其职责是定义子类应实现的一组抽象方法。下面是一个简单的示例:
from abc import ABC, abstractmethod
class Talker(ABC):
@abstractmethod
def talk(self):
pass
形如@this的东西被称为装饰器,这里的要点是你使用@abstractmethod来将方法标记为抽象的——在子类中必须实现的方法。
如果你使用的是较旧的Python版本,将无法在模块abc中找到ABC类。在这种情况下,需要导入ABCMeta,并在类定义开头包含代码行__metaclass__ = ABCMeta(紧跟在class语句后面并缩进)。如果你使用的是3.4之前的Python 3版本,也可使用Talker(metaclass=ABCMeta)代替Talker(ABC)。
抽象类(即包含抽象方法的类)最重要的特征是不能实例化。
>>> Talker()
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: Can't instantiate abstract class Talker with abstract methods talk
假设像下面这样从它派生出一个子类:
class Knigget(Talker):
pass
由于没有重写方法talk,因此这个类也是抽象的,不能实例化。如果你试图这样做,将出现类似于前面的错误消息。然而,你可重新编写这个类,使其实现要求的方法。
class Knigget(Talker):
def talk(self):
print("Ni!")
现在实例化它没有任何问题。这是抽象基类的主要用途,而且只有在这种情形下使用isinstance才是妥当的:如果先检查给定的实例确实是Talker对象,就能相信这个实例在需要的情况下有方法talk。
>>> k = Knigget()
>>> isinstance(k, Talker)
True
>>> k.talk()
Ni!
然而,还缺少一个重要的部分——让isinstance的多态程度更高的部分。正如你看到的,抽象基类让我们能够本着鸭子类型的精神使用这种实例检查!我们不关心对象是什么,只关心对象能做什么(它实现了哪些方法)。因此,只要实现了方法talk,即便不是Talker的子类,依然能够通过类型检查。下面来创建另一个类。
class Herring:
def talk(self):
print("Blub.")
这个类的实例能够通过是否为Talker对象的检查,可它并不是Talker对象。
>>> h = Herring()
>>> isinstance(h, Talker)
False
诚然,你可从Talker派生出Herring,这样就万事大吉了,但Herring可能是从他人的模块中导入的。在这种情况下,就无法采取这样的做法。为解决这个问题,你可将Herring注册为Talker(而不从Herring和Talker派生出子类),这样所有的Herring对象都将被视为Talker对象。
>>> Talker.register(Herring)
<class '__main__.Herring'>
>>> isinstance(h, Talker)
True
>>> issubclass(Herring, Talker)
True
然而,这种做法存在一个缺点,就是直接从抽象类派生提供的保障没有了。
>>> class Clam:
... pass
...
>>> Talker.register(Clam)
<class '__main__.Clam'>
>>> issubclass(Clam, Talker)
True
>>> c = Clam()
>>> isinstance(c, Talker)
True
>>> c.talk()
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
AttributeError: 'Clam' object has no attribute 'talk'
换而言之,应将isinstance返回True视为一种意图表达。在这里,Clam有成为Talker的意图。
本着鸭子类型的精神,我们相信它能承担Talker的职责,但可悲的是它失败了。
接口继承与归一化设计(工厂方法模式)
工厂方法模式:
在父类中继承abc模块,装饰abc抽象基类,但父类中不去实现其方法,而是去子类中实现,如果子类中没有完全继承实现父类中的方法,会报错,作用在于可以严格匹配一致。
import abc
class All_file(metaclass=abc.ABCMeta):
@abc.abstractmethod
def read(self):
pass
@abc.abstractmethod
def write(self):
pass
class Disk(All_file):
def read(self):
print('disk read')
def write(self):
print('disk write')
class Cdrom(All_file):
def read(self):
print('cdrom read')
def write(self):
print('cdrom write')
class Mem(All_file):
def read(self):
print('mem read')
def write(self):
print('mem write')
m1=Mem()
m1.read()
m1.write()