8.1 类和描述
对象的一个基本特性就是它们可以被分类。每一个对象都属于一个类。这是对象和类之间的一种简单关系,它只需要一个简单的、单继承的设计。
如果考虑多重继承,分类的问题就变得复杂了。当我们审视真实世界中的物体时,例如咖啡杯,我们可以很容易地将它们归类为容器。毕竟,那是咖啡杯的主要用途。它们解决的问题就是装咖啡。但是,在另外一种环境中,我们可能会对其他的用途感兴趣。对于一个装饰性的陶瓷杯,相比于一个杯子在装咖啡方面的能力,我们可能对尺寸、形状和釉彩更感兴趣。
大多数对象与类之间都是简单的is-a关系。在我们装咖啡问题中,桌上的杯子是咖啡杯的同时也是一个容器。对象与其他一些类之间或许也能有acts-as的关系。如果我们把一个杯子当成陶瓷艺术品,就会考虑它的尺寸、形状和釉彩。如果把一个杯子当成纸镇,就会考虑它的重量和摩擦力。通常,这些额外的属性可以被看成是mixin类,它们定义了一个对象的附加接口或者行为。
进行面向对象设计时,通常都会先确定is-a的类和这个类的一些基本定义。其他的类型可以混合在对象所附加的接口或者行为中。我们会介绍如何创建和装饰类。会从定义函数和装饰开始,因为这比创建一个类要简单一些。
8.1.1 创建函数
创建一个函数分两步。第1步是带有原始定义声明的def语句。
| 理论上,是可以用lambda表达式和赋值语句创建一个函数的,但是我们会避免这样做。 |
一个def语句提供了一个名称、变量、默认值、一个docstring、一个代码块和一些其他的细节。一个函数是11个属性的集合,这是标准的类层次结构,定义在Python语言参考(Python Language Reference)的3.2节中。可以参考http://docs.python.org/3.3/reference/datamodel.html#the-standardtype-hierarchy。
第2步是将一个装饰器应用在原始定义上。当我们将装饰器(@d)应用到一个函数(F)上时,结果就好像是创建了一个新的函数,F’=@d(F)。函数名相同,但是依据增加、删除或者修改的属性不同,功能会有所不同。然后,我们会有下面这样的代码。
@decorate
def function():
pass
装饰器直接写在函数定义之前。内部发生的过程是:
def function():
pass
function= decorate( function )
装饰器修改函数定义,然后创建了一个新的函数。下面是函数的属性列表。
属性 | 说明 |
|---|---|
doc | docstring或者None |
name | 函数的初始名称 |
module | 函数所属的模块名称,或者None |
qualname | 函数的全名: module.name |
defaults | 默认的参数值,如果没有默认参数就是None |
kwdefaults | 只有关键字(keyword-only)的参数的默认值 |
code | 这个对象代表编译后的函数体 |
dict | 函数属性的命名空间 |
annotations | 参数的注释,包括“return”为返回值的注释 |
globals | 函数所属模块的全局命名空间;这个属性用于解析只读的全局变量 |
closure | 与函数中的自由变量(free variables)的绑定或者为None。它是只读的 |
装饰器可以改变除globals和closure之外的其他所有属性。但是,我们稍后会看到,这些摆弄得太深不是非常实际。
在实践中,装饰通常包括定义一个封装了现有函数的新函数。可能需要复制或者修改前面的一些属性。在实践中,这就限制了一个装饰器能做什么和应该做什么。
8.1.2 创建类
创建类是一组嵌套的两级过程。外部对类方法的引用让类的创建变得更加复杂,因为这涉及多步查找。对象的类中会定义方法解析顺序(Method Resolution Order,MRO)。这定义了一个基类如何定义一个属性或者方法。MRO会顺着继承层次向上查找;这意味子类中的名称会覆盖基类中的名称。用这种方式实现的搜索符合对继承的预期。
类创建的第1阶段是带有原始定义的class语句。这个阶段会首先执行元类型,然后执行赋值语句和类中的def语句。正如之前说的,类中的每一个def语句都会被翻译成一个嵌套的两级函数创建。装饰器可以作为创建类过程的一部分,应用于每个函数方法。
类创建的第 2 阶段是将一个全局的类装饰器应用于类定义。通常,一个 decorator函数可以增加功能,较为常见的是添加属性而不是添加方法。但是,我们也会看到有一些添加方法和函数的装饰器。
很明显,不可以通过装饰器修改从基类继承的功能,因为它们在方法解析查找的过程中是延迟解析的。这带来了一些重要的设计要素。通常,我们用类或者mixin类引入方法。但是,我们只用装饰器或者mixin类定义引入属性。下面是类中内置的一些属性,其他的许多属性是元类型的一部分,如下表所示。
属性 | 说明 |
|---|---|
doc | 类的文档字符串(documentation string),如果没有定义就是None |
name | 类名 |
module | 类所属的模块名 |
dict | 包含类命名空间的字典 |
bases | 包含了基类的元组(有可能为空或者是一个单例),基类以基类列表中的顺序存储;它用来处理方法的解析顺序 |
class | 当前类的基类,通常是type类型 |
类中另外的一些方法函数包括subclasshook、reduce和reduce_ex,都属于pickle接口。
8.1.3 一些类设计原则
当定义类时,我们的属性和方法有下面3种来源。
- class语句。
- 类级的装饰器。
- Mixin类和最后一个基类。
我们需要定义每种来源的可见级别。class语句是属性和方法的最明显来源。mixin和基类相比于类主体而言,显得不够明确。基类名称可以阐明它的基本用途这一点是很有帮助的。我们会尽量用现实世界中的对象来命名基类。
mixin类通常会定义类中额外的接口或者行为。了解如何使用mixin类对于创建最终的类很重要。docstring类是其中一个重要的部分,同时,docstring模块对于展示如何将多个不同部分组成一个正确的类也是非常重要的。
当使用class语句时,类自身的基类放在最后,mixin类在基类之前。这不仅仅是习惯而已。放在最后的类是is-a类。装饰器的应用将带来一些比较模糊的行为。通常,装饰器提供的行为相对少一些。关注其中的一个或者几个行为可以帮助我们弄清装饰器的作用。
8.1.4 面向方面编程
面向方面编程(Aspect-oriented programming)的某些部分与装饰器相关。我们的目的是利用一些面向方面编程的概念来讲解Python中的装饰器和mixin类。横切关注点是AOP的核心。这里有横切关注点的背景知识:http://en.wikipedia.org/wiki/Cross-cutting_concern。下面是一些关于横切关注点的常见示例。
- 日志(Logging):我们常常需要为不同的类实现统一的日志记录功能。需要确保日志的命名统一,并且日志事件也与类结构一致。
- 审计(Auditability):另外一种记录日志的方式是提供一种审计机制,用于追踪可变对象的每次转换。在许多商业应用程序中,交易是一种商业记录,它代表账单或者付款信息。处理一条商业记录的每个步骤都应该是可审核的,以确保在处理过程中没有任何错误。
- 安全(Security):我们的应用程序会经常有安全方面的需求,涵盖了每一个HTTP请求和网站下载的所有内容。这样做的目的是为了确保每个请求都是由有权利发起请求的认证用户发起的。我们必须始终使用 Cookies、安全套接字(secure sockets)和其他的加密技术来确保整个Web应用程序的安全性。
一些语言和工具对AOP提供了强大的支持。Python借用了其中的一些概念。Python风格的AOP包括下面的语言特性。
- 装饰器:利用装饰器,我们可以通过函数的两个简单连接点之一来创建统一的横切方面实现。我们可以在现有函数执行之前或者之后执行横切方面的逻辑。在函数的代码中,我们很难找到连接点。对于装饰器来说,封装函数或者方法并提供额外的功能是修改一个函数或者方法的最简单方式。
- mixin:利用mixin能够做到在一个单独的类层次结构之外定义另一个类。将mixin类与其他类一起使用可以为横切方面提供一致的实现。为此,被扩展的类必须使用mixin的API。通常,我们将mixin类当作抽象类,因为我们无法用一种有意义的方式来初始化它们。
