软件设计通常具有跨多个类、函数或方法应用的方面。我们可能有一个问题,比如日志记录、审计或安全性,必须一致地实现。面向对象编程中重用功能的一种通用方法是通过类层次结构进行继承。然而,继承并不总是成功的。例如,软件设计的一个方面可能与类层次结构正交。这些有时被称为交叉关注点。它们跨越类,使设计更加复杂。
装饰器提供了一种定义不绑定到继承层次结构的功能的方法。我们可以使用 decorator 来设计应用程序的一个方面,然后跨类、方法或函数应用 decorator。
此外,我们可以使用多个继承来创建横切方面。我们将考虑基类加 MIXIN 类定义来介绍特性。通常,我们将使用 mixin 类来构建横切方面。
需要注意的是,横切关注点很少特定于手头的应用程序。它们通常是一般性的考虑因素。日志记录、审计和安全性的常见示例可以视为独立于应用程序细节的基础设施。
Python 附带了许多 decorator,我们可以扩展这个标准的 decorator 集。有几个不同的用例。本章将从类定义和类的含义开始。在这个上下文中,我们将研究简单函数修饰符、带参数的函数修饰符、类修饰符和方法修饰符。
在本章中,我们将介绍以下主题:
- 阶级与意义
- 使用内置装饰器
- 使用标准库 mixin 类
- 编写一个简单的函数装饰器
- 参数化装饰器
- 创建方法函数装饰器
- 创建类装饰器
- 向类添加方法
- 使用装饰器实现安全性
本章的代码文件可在上找到 https://git.io/fj2UV 。
对象的一个基本特征是可以分类:每个对象都属于一个类。当使用简单的单继承设计时,这将导致对象和类之间的直接关系。
使用多重继承,分类问题可能变得复杂。当我们观察现实世界中的物体时,比如咖啡杯,我们可以毫不费力地将它们归类为容器。毕竟,这是他们的主要用例。他们解决的问题是拿咖啡。然而,在另一个上下文中,我们可能对其他用例感兴趣。在一个装饰性的陶瓷杯收藏中,我们可能更感兴趣的是尺寸、形状和釉面,而不是杯子中的咖啡杯。
大多数对象与类有一个简单的is-a关系。在我们的咖啡持有问题领域,坐在桌子上的杯子属于咖啡杯类,也属于容器类。对象还可能具有多个作为与其他类的关系。杯子就像一件陶瓷艺术品,具有大小、形状和釉面特性。马克杯作为纸张砝码,具有质量和摩擦特性。
通常,这些其他特性可以看作是 mixin 类,它们定义了对象的附加接口或行为。mixin 类可以有自己的层次结构;例如,陶瓷艺术是一个更一般的雕塑和艺术类专业。
在 Python 中进行面向对象设计时,识别是一个类以及该类定义的基本方面是很有帮助的。其他类提供了作为方面,它混合了对象的其他接口或行为。
在下一节中,我们将介绍函数定义和修饰,因为它比类构造更简单。在了解了函数装饰是如何工作的之后,我们将回到 mixin 类和类装饰。
我们分两个阶段构造装饰函数。第一阶段是具有原始定义的def语句。
def语句提供名称、参数、默认值、一个docstring、一个代码对象和许多其他细节。一个函数是 11 个属性的集合,在Python 标准库的第 3.2 节中定义,这是标准类型层次结构。
第二阶段涉及对原始定义应用装饰器。当我们将修饰符(@d)应用于函数(F时,效果就像我们创建了一个新函数
。名称F相同,但功能可能不同,具体取决于添加、删除或修改的功能类型。通常,我们可以编写以下代码:
@decorate
def function():
pass 修饰符直接写在函数定义前面。实现这一点会发生什么,可以从以下几点看出:
def function():
pass
function = decorate(function) 装饰器修改函数定义以创建新函数。这里的基本技术是 decorator 函数接受函数并返回该函数的修改版本。因此,装饰器有一个相当复杂的类型提示。
第二种样式function=decorate(function)也适用于通过将 lambda 赋值给变量而创建的函数。它也适用于可调用对象。@decorate符号仅适用于def语句。
当存在多个装饰器时,它们将作为嵌套函数调用应用。考虑下面的例子:
@decorator1
@decorator2
def function(): ...这相当于function=decorator1(decorator2(function))。当装饰物有副作用时,装饰物的使用顺序就很重要了。例如,在 Flask 框架中,@app.route装饰应始终位于装饰器堆栈的顶部,以便最后应用,并包括其他装饰器行为的结果。
以下是定义装饰器所需的一组典型类型提示:
from typing import Any, Callable, TypeVar, cast
FuncType = Callable[..., Any]
F = TypeVar('F', bound=FuncType)
def my_decorator(func: F) -> F:
...我们根据Callable类型提示定义了一个函数类型FuncType。由此,类型变量F派生为任何遵守FuncType协议的内容的通用描述。这将包括函数、lambda 和可调用对象。装饰函数my_decorator()接受类型提示为F的参数func,并使用类型提示为F返回函数。重要的是,任何具有可调用协议的对象都可以被描述为具有非常通用的FuncType的上限。我们暂时省略了my_decorator()的细节。此代码段旨在展示类型提示的一般方法。
类的装饰器更简单,因为签名是def class_decorator(class: Type) -> Type: ...。有几种方法可以创建类,上限已经定义为类型提示Type。
现在,让我们检查函数的不同属性。
装饰器可以更改函数的属性。以下是函数的属性列表:
| __doc__ | docstring,或无 |
| __name__ | 函数的原始名称 |
| __module__ | 在其中定义函数的模块的名称,或无 |
| __qualname__ | 函数的完全限定名__module__.__name__ |
| __defaults__ | 默认参数值,如果没有默认值,则为“无” |
| __kwdefaults__ | 仅关键字参数的默认值 |
| __code__ | 表示已编译函数体的代码对象 |
| __dict__ | 函数属性的命名空间 |
| __annotations__ | 参数注释,包括返回注释的'return' |
| __globals__ | 在其中定义函数的模块的全局命名空间;这用于解析全局变量,并且是只读的 |
| __closure__ | 函数的自由变量绑定或无绑定;它是只读的 |
除了__globals__和__closure__之外,装饰师可以更改这些属性中的任何一个。实际上,最好只将__name__和__doc__从原始功能复制到装饰功能。大多数其他属性虽然是可变的,但通过在 decorator 中定义新函数并返回新函数的简单技术更容易管理。我们将在以下几个示例中对此进行研究。
现在,让我们看看如何构造装饰类。
修饰类构造是一组嵌套的两阶段过程。使类构造更复杂是对类方法进行引用的方式。引用涉及多步骤查找。对象的类将定义一个方法解析顺序(MRO)。这定义了如何搜索基类来定位属性或方法名。MRO 沿着继承层次结构向上运行;这就是子类名称如何覆盖超类中的名称
嵌套的最外层是将class语句作为一个整体进行处理。这有两个阶段:构建类和应用装饰函数。在class语句处理中,各个方法定义也可以有修饰,每个修饰都是一个两阶段的过程。
类构造的第一个阶段是执行class语句。这一阶段涉及元类的评估,然后在class中执行赋值序列和def语句。如前所述,类中的每个def语句都扩展为嵌套的两阶段函数构造。作为构建类过程的一部分,装饰器可以应用于每个方法函数。
类构造的第二阶段是对类定义应用总体类装饰器。通常,这可以添加功能。添加属性比添加方法更常见。虽然修饰符可以添加方法函数,但软件维护人员定位修饰符注入的方法的源代码可能会令人困惑。这些类型的功能需要精心设计。
从超类继承的特性不能通过装饰器修改,因为它们是通过方法解析查找延迟解析的。这导致了一些重要的设计考虑。我们通常希望通过类和 mixin 类引入方法和属性。我们应该限制自己通过 decorator 定义新属性。
下面是为类构建的一些属性的列表。许多附加属性是元类的一部分;下表中对其进行了说明:
| __doc__ | 类的文档字符串,如果未定义,则为无 |
| __name__ | 类名 |
| __module__ | 在其中定义类的模块名称 |
| __dict__ | 包含类的命名空间的字典 |
| __bases__ | 包含基类的元组(可能为空或单元组),按它们在基类列表中的出现顺序排列;它用于计算方法的分辨率顺序 |
| __class__ | 此类的超类,通常为类型 |
属于类的一些附加方法函数包括__subclasshook__、__reduce__和__reduce_ex__,它们是pickle接口的一部分。
定义类时,我们有以下属性和方法的来源:
- 应用于类定义的任何修饰符。最后将这些应用于定义。
- 类语句的主体。
- 任何混合类。这些定义倾向于覆盖方法解析顺序算法中的基类定义。
- 基类。如果未指定,则基类为
object,它提供了一组最小的定义。
它们按可见性的顺序显示。装饰器的最终更改将覆盖它下面的所有内容,使这些更改最为可见。class 语句的主体重写从 mixin 或基类继承的任何内容。基类是用于解析名称的最后一个位置
我们需要认识到,对于软件维护人员来说,看到其中每一个是多么容易。class语句是查找属性或方法定义的最明显的地方。mixin 和基类在某种程度上不如类主体那么明显。确保基类名称澄清其作用并使用显然至关重要的术语是很有帮助的。例如,它有助于以真实对象命名基类。
对类应用 decorator 可能会导致模糊的特性。对一个或几个特性的强烈关注有助于澄清装饰者的工作。虽然应用程序的某些方面可能适合于通用装饰器,但缺乏可见性会使它们难以测试、调试和维护。
mixin 类通常会定义类的其他接口或行为。明确 mixin 类如何用于构建最终的类定义是很重要的。虽然docstring类是其中的一个重要部分,但整个docstring模块对于展示如何从各个部分组装一个合适的类也很重要。
编写class语句时,首先列出 mixin,最后列出基本超类。这是名称解析的搜索顺序。最后列出的类是定义基本的is-a关系的类。列表上的最后一个类定义了事物是什么。前面的类名可以定义的功能。mixin 提供了重写或扩展此基本行为的方法。
下一节将讨论面向方面编程。
面向方面编程(AOP的部分内容由 Python 中的修饰符实现。我们在这里的目的是利用一些面向方面的概念来帮助展示 Python 中 decorator 和 mixin 的用途。交叉关注点的理念是 AOP 的核心。而维基百科页面(http://en.wikipedia.org/wiki/Cross-cutting_concern 通常是最新的,此处提供较旧的信息:https://web.archive.org/web/20150919015041/http://www.aosd.net/wiki/index.php?title=Glossary 。Spring 框架提供了一些想法;另见https://docs.spring.io/spring-python/1.2.x/sphinx/html/aop.html 。交叉关注点有几个常见的例子,如下所示:
- 日志记录:我们经常需要在许多类中实现一致的日志记录功能。我们希望确保记录器的命名一致,并且日志事件以一致的方式遵循类结构。
- 可审计性:日志主题的一个变体是提供一个审计跟踪,显示可变对象的每次转换。在许多面向商业的应用程序中,事务是表示账单或付款的业务记录。业务记录处理过程中的每个步骤都需要可审核,以表明处理过程中没有引入错误。
- 安全:我们的应用程序通常会在每个 HTTP 请求和网站下载的每个内容中都有安全方面。其思想是确认每个请求都涉及一个经过身份验证的用户,该用户有权提出请求。Cookie、安全套接字和其他加密技术必须始终如一地使用,以确保整个 web 应用程序的安全。
一些语言和工具对 AOP 有深入、正式的支持。Python 借用了其中一些概念。AOP 的 python 方法包括以下语言特性:
- Decorators:使用 decorator,我们可以在函数中的两个简单连接点之一建立一致的方面实现。我们可以在现有函数之前或之后执行方面的处理。我们无法在函数的代码中轻松找到连接点。对于装饰者来说,用附加功能包装函数或方法来转换函数或方法是最容易的。
- mixin:使用 mixin,我们可以定义一个作为多个类层次结构的一部分存在的类。mixin 类可以与基类一起使用,以提供横切方面的一致实现。通常,mixin 类被认为是抽象的,因为它们不能被有意义地实例化。
下一节将展示如何使用内置装饰器
Python 有几个内置的装饰器,它们是该语言的一部分。@property、@classmethod和@staticmethod修饰符用于注释类的方法。@property装饰器将方法函数转换为描述符。@property修饰符应用于方法函数时,会将函数更改为对象的属性。属性修饰符应用于方法时,还会创建一对附加属性,可用于创建setter和deleter属性。我们在第 4 章、属性访问、属性和描述符中了解了这一点。
@classmethod和@staticmethod修饰符将方法函数转换为类级函数。修饰的方法现在是类的一部分,而不是对象。对于静态方法,没有对类的显式引用。另一方面,对于类方法,类是方法函数的第一个参数。下面是一个包含@staticmethod和一些@property定义的类的示例:
class Angle(float):
__slots__ = ("_degrees",)
@staticmethod
def from_radians(value: float) -> 'Angle':
return Angle(180 * value / math.pi)
def __init__(self, degrees: float) -> None:
self._degrees = degrees
@property
def radians(self) -> float:
return math.pi * self._degrees / 180
@property
def degrees(self) -> float:
return self._degrees此类定义了一个可以用度或弧度表示的Angle。构造函数需要学位。但是,我们还定义了一个from_radians()方法函数,它发出类的一个实例。此函数不会像__init__()那样设置现有实例变量的值;它创建类的一个新实例
此外,我们还提供了经过修饰的degrees()和radians()方法函数,以使它们成为属性。在引擎盖下,这些装饰器创建一个描述符,以便访问属性名degrees或radians将调用命名的方法函数。我们可以使用static方法创建一个实例,然后使用property方法访问一个方法函数,如下所示:
>>> b = Angle.from_radians(.227)
>>> round(b.degrees, 1)
13.0静态方法类似于函数,因为它不绑定到self实例变量。它的优点是它在语法上绑定到类。使用Angle.from_radians比使用名为angle_from_radians的函数更有帮助。使用这些装饰器可以确保正确且一致地处理实现。
现在,让我们看看如何使用标准库装饰器。
标准库有许多装饰器。诸如contextlib、functools、unittest、atexit、importlib和reprlib等模块包含装饰器,这些装饰器是软件设计交叉方面的优秀示例。
一个特殊的例子,functools库提供了定义比较运算符的total_ordering装饰器。它利用__eq__()和__lt__()、__le__()、__gt__()或__ge__()创建一套完整的比较。
首先,我们需要这个类来完全定义一张扑克牌,如下所示:
from enum import Enum
class Suit(Enum):
Clubs = "♣"
Diamonds = "♦"
Hearts = "♥"
Spades = "♠" 此类提供扑克牌套装的枚举值。
下面是Card类的一个变体,它只定义了两个比较:
import functools
@functools.total_ordering
class CardTO:
__slots__ = ( "rank" , "suit" )
def __init__ ( self , rank: int , suit: Suit) -> None :
self .rank = rank
self .suit = suit
def __eq__ ( self , other: Any) -> bool :
return self .rank == cast(CardTO, other).rank
def __lt__ ( self , other: Any) -> bool :
return self .rank < cast(CardTO, other).rank
def __str__ ( self ) -> str :
return f" { self .rank : d }{ self .suit : s } " 我们的类CardTO由类级装饰师@functools.total_ordering包装。此装饰程序创建缺少的方法函数,以确保所有比较工作正常。从一些运算符组合中,可以导出余数。一般的想法是提供某种形式的等式(或不等式)测试和排序测试,其余的操作可以从这两种测试中逻辑推导出来。
在本例中,我们提供了
我们可以使用该类创建可以使用所有比较运算符进行比较的对象,即使只有两个运算符定义如下:
>>> c1 = Card( 3, '♠' )
>>> c2 = Card( 3, '♥' )
>>> c1 == c2
True
>>> c1 < c2
False
>>> c1 <= c2
True
>>> c1 >= c2
True 这种交互表明,我们能够进行原始类中未定义的比较。装饰器将所需的方法函数添加到原始类定义中。
让我们在下一节中了解如何使用标准库 mixin 类。
标准库使用 mixin 类定义。有几个模块包含示例,包括io、socketserver、urllib.request、contextlib和collections.abc。接下来,我们将看一个使用enum模块中Enum类的混合特性的示例。
当我们基于collections.abc抽象基类定义自己的集合时,我们使用 mixin 来确保容器的横切方面得到一致的定义。顶级集合(Set、Sequence和Mapping都是由多个 mixin 构建的。查看Python 标准库的第 8.4 节非常重要,以了解 mixin 在整体结构由片段构建时如何贡献功能。
只看一行,Sequence的摘要,我们看到它继承了Sized、Iterable和Container。这些 mixin 类产生了__contains__()、__iter__()、__reversed__()、index()和count()等方法。
list类的最终行为是其定义中存在的每个 mixin 的方面的组合。从根本上说,它是一个Container,添加了许多协议。
在下一节中,让我们看看如何将 enum 与 mixin 类一起使用。
enum模块提供Enum类。此类的一个常见用例是定义枚举的值域;例如,我们可以用它来列举四套扑克牌。
枚举类型具有以下两个特性:
- **成员名称:**成员名称是枚举值的正确 Python 标识符。
- **成员值:**成员值可以是任何 Python 对象。
在前面的几个示例中,我们对枚举成员使用了过于简单的定义。下面是一个典型的类定义:
from enum import Enum
class Suit(Enum):
Clubs = "♣"
Diamonds = "♦"
Hearts = "♥"
Spades = "♠"这提供了四名成员。我们可以使用Suit.Clubs引用特定字符串。我们还可以使用list(Suit)创建枚举成员列表
基本Enum类对将成为该类一部分的成员名称或值施加约束。我们可以使用 mixin 类定义来缩小定义的范围。具体而言,Enum 类可以使用数据类型以及其他要素定义。
我们通常希望枚举成员的基础值有一个更丰富的定义。本例显示了str与Enum的混合:
class SuitS( str , Enum):
Clubs = "♣"
Diamonds = "♦"
Hearts = "♥"
Spades = "♠"基类是Enum。str类的功能将提供给每个成员。定义的顺序很重要:首先列出 mixin;基类列在最后
当str被混入时,它向成员本身提供所有字符串方法,而不必显式引用每个成员的内部value。例如,SuitS.Clubs.center(5)将发出以长度为 5 的字符串为中心的字符串值
我们还可以在Enum中加入其他功能。在本例中,我们将添加一个类级功能来枚举值:
class EnumDomain:
@classmethod
def domain(cls: Type) -> List[str]:
return [m.value for m in cls]
class SuitD( str , EnumDomain, Enum):
Clubs = "♣"
Diamonds = "♦"
Hearts = "♥"
Spades = "♠"将以下两个 mixin 协议添加到此类:
- str 方法将直接应用于每个成员。
- 该类还有一个
domain()方法,该方法将只发出值。我们可以使用SuitD.domain()获取与成员关联的字符串值列表。
这种 mixin 技术允许我们将特性捆绑在一起,从不同的方面创建复杂的类定义
A mixin design is better than copy and paste among several related classes.
创建足够泛型的类以用作 mixin 可能很困难。一种方法是跨多个类查找重复的代码。重复代码的存在表明可能存在重构和消除重复的混合。
让我们在下一节中了解如何编写一个简单的函数装饰器
decorator是一个函数(或可调用对象),它接受一个函数作为参数并返回一个新函数。装饰的结果是一个被包装的功能。通常,包装的附加功能围绕原始功能,通过转换实际参数值或转换结果值。这是函数中两个随时可用的连接点。
当我们使用 decorator 时,我们希望确保生成的修饰函数具有原始函数的名称和docstring。这些细节可以由装饰师为我们处理,以构建我们的装饰师。使用functools.wraps编写新的装饰师简化了我们需要做的工作,因为簿记是为我们处理的。
此外,decorators 的类型提示可能会令人困惑,因为参数和返回基本上都是Callable类型。为了正确的泛型,我们将使用一个上限类型定义来定义一个类型F,它包含可调用对象或函数的任何变体。
为了说明可以插入功能的两个地方,我们可以创建一个调试跟踪装饰器,它将记录参数并从函数返回值。这会将功能放在被调用函数之前和之后。下面是一个定义好的函数,some_function,我们想要包装它。实际上,我们希望代码的行为如下所示:
logging.debug("function(%r, %r)", args, kw)
result = some_function(*args, **kw)
logging.debug("result = %r", result) 这段代码显示了我们将如何编写新的日志来包装原始的some_function()函数。
以下是在函数求值前后插入日志记录的调试装饰程序:
import logging, sys
import functools
from typing import Callable, TypeVar
FuncType = Callable[..., Any]
F = TypeVar( 'F' , bound =FuncType)
def debug(function: F) -> F:
@functools.wraps (function)
def logged_function(*args, **kw):
logging.debug( "%s(%r, %r)" , function. __name__ , args, kw)
result = function(*args, **kw)
logging.debug( "%s = %r" , function. __name__ , result)
return result
return cast(F, logged_function)我们使用了@functools.wraps装饰器来确保原始函数名和 docstring 作为结果函数的属性被保留。logged_function()定义是debug()修饰符返回的结果函数。内部,logged_function()执行一些日志记录,然后调用修饰函数function,并在返回修饰函数的结果之前执行更多日志记录。在此示例中,未执行参数值或结果的转换。
使用记录器时,f 字符串不是最好的主意。它有助于提供单个值,以便日志过滤器可用于编辑或排除敏感日志中的条目
有了这个@debug装饰器,我们可以用它来产生嘈杂的、详细的调试。例如,我们可以这样做,将 decorator 应用于函数ackermann(),如下所示:
@debug
def ackermann(m: int , n: int ) -> int :
if m == 0 :
return n + 1
elif m > 0 and n == 0 :
return ackermann(m - 1 , 1 )
elif m > 0 and n > 0 :
return ackermann(m - 1 , ackermann(m, n - 1 ))
else :
raise Exception (f "Design Error: {vars()}" )此定义使用调试信息包装ackermann()功能,调试信息通过日志模块写入root记录器。我们没有对函数定义做任何实质性更改。@debug装饰器作为一个单独的方面注入日志细节
我们按照以下方式配置记录器:
logging.basicConfig(stream=sys.stderr, level=logging.DEBUG) 我们将在第 16 章、日志和警告模块中详细回顾日志记录。我们在评估ackermann(2,4)时会看到这样的结果,如下所示:
DEBUG:root:ackermann((2, 4), {})
DEBUG:root:ackermann((2, 3), {})
DEBUG:root:ackermann((2, 2), {})
.
.
.
DEBUG:root:ackermann((0, 10), {})
DEBUG:root:ackermann = 11
DEBUG:root:ackermann = 11
DEBUG:root:ackermann = 11 在下一节中,我们将看到如何创建单独的记录器。
作为日志优化,我们可能希望为每个包装函数使用特定的记录器,而不是过度使用根记录器进行此类调试输出。我们将返回到第 16 章、日志和警告模块中的记录器。
以下是我们的 decorator 版本,它为每个单独的函数创建单独的记录器:
def debug2(function: F) -> F:
log = logging.getLogger(function. __name__ )
@functools.wraps (function)
def logged_function(*args, **kw):
log.debug( "call(%r, %r)" , args, kw)
result = function(*args, **kw)
log.debug( "result = %r" , result)
return result
return cast(F, logged_function)此版本将输出修改为如下所示:
DEBUG:ackermann:call((2, 4), {})
DEBUG:ackermann:call((2, 3), {})
DEBUG:ackermann:call((2, 2), {})
.
.
.
DEBUG:ackermann:call( (0, 10), {} )
DEBUG:ackermann:result = 11
DEBUG:ackermann:result = 11
DEBUG:ackermann:result = 11 函数名现在是记录器名称。这可用于微调调试输出。我们现在可以为单个函数启用日志记录,而不是为所有函数启用调试
请注意,我们不能简单地更改装饰器,而期望装饰函数也会更改。在对 decorator 进行更改之后,我们需要将修改后的 decorator 应用于函数。这意味着调试和实验装饰程序不能从>>>交互提示中简单地完成。
Decorator 开发通常涉及创建和重新运行脚本来定义 Decorator 并将其应用于示例函数。在某些情况下,此脚本还将包括测试或演示,以显示一切都按预期工作。
现在,让我们看看如何参数化装饰器。
有时,我们需要向装饰者提供参数。我们的想法是定制包装功能。当我们这样做的时候,装饰变成了一个两步的过程。
下面是一个片段,展示了如何为函数定义提供参数化装饰器:
@decorator(arg)
def func( ):
pass 实施情况如下:
def func( ):
pass
func = decorator(arg)(func) 我们做了以下三件事:
- 定义了一个函数,
func - 将抽象修饰符应用于其参数,以创建一个具体的修饰符
decorator(arg) - 将具体修饰符应用于定义的函数,以创建函数的装饰版本
decorator(arg)(func)
将func = decorate(arg)(func)视为具有以下实现可能会有所帮助:
concrete = decorate(arg)
func = concrete(func) 这意味着带有参数的装饰器是作为最终函数的间接构造实现的。现在,让我们再次调整调试修饰符。我们想做以下工作:
@debug("log_name")
def some_function( args ):
pass 这种代码允许我们指定调试输出将转到的日志的名称。这意味着我们不会使用根记录器,也不会为每个函数创建不同的记录器。
参数化装饰器的轮廓如下所示:
def decorator( config ) -> Callable[[F], F]:
def concrete_decorator(function: F) -> F:
def wrapped(*args, **kw):
return function(*args, **kw)
return cast(F, wrapped)
return concrete_decorator在看示例之前,让我们先剥去洋葱的几层。decorator 定义(def decorator(config))显示了我们在使用 decorator 时将向其提供的参数。它的主体是混凝土装饰器,在参数绑定到它之后返回。然后将混凝土装饰符(def concrete_decorator(function):应用于目标函数。具体的 decorator 类似于上一节中所示的简单函数 decorator。它构建包装函数(def wrapped(*args, **kw):,并返回该函数。
以下是调试的命名记录器版本:
def debug_named(log_name: str ) -> Callable[[F], F]:
log = logging.getLogger(log_name)
def concrete_decorator(function: F) -> F:
@functools.wraps (function)
def wrapped(*args, **kw):
log.debug( "%s(%r, %r)" , function. __name__ , args, kw)
result = function(*args, **kw)
log.debug( "%s = %r" , function. __name__ , result)
return result
return cast(F, wrapped)
return concrete_decorator此@debug_named装饰器接受一个参数,该参数是要使用的日志的名称。它创建并返回一个具体的 decorator 函数,其中绑定了一个具有给定名称的记录器。当此具体修饰符应用于函数时,具体修饰符返回给定函数的包装版本。当函数按以下方式使用时,装饰器会添加嘈杂的调试行。
下面是使用给定函数的输出创建日志命名递归的示例:
@debug_named ( "recursion" )
def ackermann3(m: int , n: int ) -> int :
if m == 0 :
return n + 1
elif m > 0 and n == 0 :
return ackermann3(m - 1 , 1 )
elif m > 0 and n > 0 :
return ackermann3(m - 1 , ackermann3(m, n - 1 ))
else :
raise Exception ( f"Design Error: { vars () } " )decorator 将给定的ackermann3()函数包装为日志输出。由于 decorator 接受一个参数,因此我们可以提供一个记录器名称。我们可以重用 decorator 将任意数量的单个函数放入单个记录器中,从而提供对应用程序调试输出的更多控制。
现在,让我们看看如何创建方法函数装饰器。
类定义的方法的修饰符与独立函数的修饰符相同。虽然它在不同的上下文中使用,但它的定义将与其他任何装饰器一样。不同上下文的一个小结果是,我们通常必须在用于方法的 decorators 中显式地命名self变量。
方法装饰的一个应用是为对象状态更改生成审计跟踪。业务应用程序通常创建有状态记录;通常,它们在关系数据库中表示为行。我们将在第 10 章、序列化和保存–JSON、YAML、Pickle、CSV 和 XML、第 11 章、通过搁置存储和检索对象和第 12 章、中了解对象表示通过 SQLite存储和检索对象。
When we have stateful records, the state changes often need to be auditable. An audit can confirm that appropriate changes have been made to the records. In order to do the audit, the before and after version of each record must be available somewhere. Stateful database records are a long-standing tradition but are not in any way required. Immutable database records are a viable design alternative.
当我们设计一个有状态类时,任何 setter 方法都会导致状态改变。如果我们这样做,我们可以折叠一个@audit装饰器,它可以跟踪对象的变化,这样我们就有了正确的变化轨迹。我们将通过logging模块创建审计日志。我们将使用__repr__()方法函数生成一个完整的文本表示,用于检查更改。
以下是审核装饰器:
def audit(method: F) -> F:
@functools.wraps (method)
def wrapper(self, *args, **kw):
template = "%s \n before %s \n after %s"
audit_log = logging.getLogger( "audit" )
before = repr (self) # preserve state as text
try :
result = method(self, *args, **kw)
except Exception as e :
after = repr (self)
audit_log.exception(template, method. __qualname__ , before, after)
raise
after = repr (self)
audit_log.info(template, method. __qualname__ , before, after)
return result
return cast(F, wrapper)此审核跟踪通过创建对象设置前和设置后状态的文本纪念品来工作。捕获before状态后,应用原始方法函数。如果存在异常,审核日志条目将包含异常详细信息。否则,将使用对象的方法名、beforememento 和aftermemento 的限定名写入一个INFO条目。
下面是对Hand类的修改,显示了我们如何使用此装饰器:
class Hand:
def __init__ ( self , *cards: CardDC) -> None :
self ._cards = list (cards)
@audit
def __iadd__ ( self , card: CardDC) -> "Hand" :
self ._cards.append(card)
self ._cards.sort( key = lambda c: c.rank)
return self
def __repr__ ( self ) -> str :
cards = ", " .join( map ( str , self ._cards))
return f" { self .__class__. __name__ } ( { cards } )"此定义修改了__iadd__()方法函数,以便添加卡成为可审核事件。此装饰器将执行审核操作,并在操作前后保存Hand的文本纪念品。
这种方法装饰器的使用使得一个可见的声明,即一个特定的方法将进行重大的状态更改。我们可以很容易地使用代码审查来确保所有适当的方法函数都标记为这样的审计
如果我们想要审核对象创建和状态更改,我们不能在__init__()方法函数上使用此audit修饰符。那是因为在执行__init__()之前没有 before 图像。对此,我们可以采取以下两种补救措施:
- 我们可以添加一个
__new__()方法,以确保将空_cards属性作为空集合播种到类中。 - 我们可以调整
audit()装饰器以容忍__init__()处理过程中出现的AttributeError。
第二种选择要灵活得多。我们可以做到以下几点:
try:
before = repr(self)
except AttributeError as e:
before = repr(e) 这将在初始化期间为before状态记录一条消息,如AttributeError: 'Hand' object has no attribute '_cards'。
在下一节中,我们将看到如何创建类装饰器。
与修饰函数类似,我们可以编写一个类修饰器来向类定义添加特性。基本规则是相同的。装饰器是一个函数(或可调用对象);它接收一个类对象作为参数,并返回一个类对象作为结果。
我们在一个类定义中有数量有限的连接点作为一个整体。在大多数情况下,类装饰器可以将其他属性折叠到类定义中。虽然在技术上可以创建一个封装原始类定义的新类,但作为一种设计模式,这似乎不是很有用。还可以创建一个新类,它是原始修饰类定义的子类。这可能会让 decorator 的用户感到困惑。也可以从类定义中删除特性,但这看起来非常糟糕。
前面展示了一个复杂的类装饰器。functools.Total_Ordering装饰器将许多新的方法函数注入到类定义中。此实现中使用的技术是创建 lambda 对象并将其分配给类的属性。
通常,添加属性通常会导致mypy类型的提示检查出现问题。当我们在装饰器中向类添加属性时,mypy基本上看不到这些属性。
作为一个例子,考虑调试对象创建的需要。通常,我们希望每个类都有一个唯一的记录器。
我们经常被迫做以下事情:
class UglyClass1:
def __init__ ( self ) -> None :
self .logger = logging.getLogger( self .__class__. __qualname__ )
self .logger.info( "New thing" )
def method( self , *args: Any) -> int :
self .logger.info( "method %r" , args)
return 42这个类的缺点是它创建了一个logger实例变量,它实际上不是类操作的一部分,而是类的一个独立方面。我们希望避免这一额外的方面污染课堂。尽管logging.getLogger()非常高效,但成本不是零。我们希望在每次创建UglyClass1实例时避免额外的开销。
这里有一个稍微好一点的版本。记录器升级为类级实例变量,并与类的每个单独对象分开:
class UglyClass2:
logger = logging.getLogger( "UglyClass2" )
def __init__ ( self ) -> None :
self .logger.info( "New thing" )
def method( self , *args: Any) -> int :
self .logger.info( "method %r" , args)
return 42这样做的好处是它只执行一次logging.getLogger()。然而,它有一个深刻的不要重复自己(干燥问题。我们无法在类定义中自动设置类名。该类尚未创建,因此我们不得不重复该名称。
干燥问题可由小型装饰师部分解决,如下所示:
def logged(class_: Type) -> Type:
class_.logger = logging.getLogger(class_. __qualname__ )
return class_此装饰器调整类定义,将logger引用添加为类级属性。现在,每个方法都可以使用self.logger生成审计或调试信息。当我们想要使用这个特性时,我们可以在整个类上使用@logged装饰器。
这给mypy带来了一个深刻的问题,使用 mixin 比使用 decorator 更容易解决。
继续使用类装饰器,下面是一个记录类的示例,SomeClass:
@logged
class SomeClass:
def __init__ ( self ) -> None :
self .logger.info( "New thing" ) # mypy error
def method( self , *args: Any) -> int :
self .logger.info( "method %r" , args) # mypy error
return 42decorator 保证该类具有可由任何方法使用的logger属性。logger属性不是每个实例的特性,而是整个类的特性。此属性还有一个额外的好处,即它在模块导入期间创建记录器实例,略微减少了日志记录的开销。让我们将其与UglyClass1进行比较,其中logging.getLogger()针对每个实例创建进行评估。
我们已经注释了两行,它们将报告mypy错误。类型提示检查装饰器注入的属性是否不够健壮,无法检测附加属性。装饰师无法轻松创建mypy可见的属性。最好使用以下类型的混合器:
class LoggedWithHook:
def __init_subclass__ ( cls , name= None ):
cls .logger = logging.getLogger(name or cls . __qualname__ )这个 mixin 类定义了__init_subclass__()方法,将一个附加属性注入到类定义中。这由mypy识别,使得logger属性可见且有用。如果提供了参数的名称,它将成为记录器的名称,否则将使用子类的名称。下面是一个使用此 mixin 的示例类:
class SomeClass4(LoggedWithHook):
def __init__ ( self ) -> None :
self .logger.info( "New thing" )
def method( self , *args: Any) -> int :
self .logger.info( "method %r" , args)
return 42该类将在创建该类时生成一个记录器。它将由类的所有实例共享。并且附加属性将对mypy可见。在大多数普通的应用程序编程中,类级修饰符非常少见。使用__init_subclass__()方法几乎可以完成任何需要的事情。
一些复杂的框架,如@dataclasses.dataclass装饰器,涉及从可用的脚手架扩展类。mypy使用的属性中引入名称所需的代码是不寻常的。
让我们在下一节中了解如何向类添加方法。
类装饰器可以使用两步过程创建新方法。首先,它必须创建一个方法函数,然后将其插入到类定义中。这通常通过 mixin 类比 decorator 更好地完成。mixin 的明显和预期用途是插入方法。插入带有装饰器的方法不太明显,阅读代码并试图找到类的方法定义在哪里的人可能会感到惊讶。
在Total_Ordering装饰器的示例中,插入的确切方法函数是灵活的,并且取决于已经提供的内容。这是一种特殊情况,不会让阅读代码的人感到惊讶。
我们将介绍一种通过创建对象的文本纪念品来创建对象状态快照的技术。这可以通过标准化的memento()方法实现。我们希望在各种类中包含此标准方法函数。首先,我们来看一个装饰器实现。之后,我们将看到此设计的 mixin 版本。
以下是添加此标准化memento()方法的装饰版本:
def memento(class_: Type) -> Type:
def memento_method(self):
return f" { self.__class__. __qualname__ } (** { vars (self) !r} )"
class_.memento = memento_method
return class_此装饰器包括插入到类中的方法函数定义。vars(self)表达式公开了实例变量,这些变量通常保存在实例的内部__dict__属性中。这将生成一个可包含在输出字符串值中的字典。
下面是我们如何使用这个@memento装饰器将memento()方法添加到类中:
@memento
class StatefulClass:
def __init__ ( self , value: Any) -> None :
self .value = value
def __repr__ ( self ) -> str :
return f" { self .value } "装饰器将一个新方法memento()合并到装饰类中。下面是使用此类并提取总结对象状态的纪念品的示例:
>>> st = StatefulClass(2.7)
>>> print(st.memento())
StatefulClass(**{'value': 2.7}) 这种实现有以下缺点:
- 我们不能覆盖
memento()方法函数的实现来处理特殊情况。它在定义之后内置到类中。 - 我们不能轻易地扩展 decorator 函数。要做到这一点,可能需要创建一个非常复杂的
memento()方法,也可能需要一些其他笨拙的设计来包含某种插件特性。
另一种方法是使用 mixin 类。此类上的变体允许自定义。以下是添加标准方法的 mixin 类:
class Memento:
def memento( self ) -> str :
return (
f"{self.__class__.__qualname__}"
f"(**{vars(self)!r})"
)下面是我们如何使用这个Mementomixin 类来定义应用程序类:
class StatefulClass2(Memento):
def __init__ ( self , value):
self .value = value
def __repr__ ( self ):
return f" { self .value } "mixin 提供了一种新的方法memento();这是 mixin 的预期、典型用途。我们可以更容易地扩展Mementomixin 类来添加特性。此外,我们可以重写memento()方法函数来处理特殊情况。
现在,让我们看看如何使用装饰器来实现安全性。
软件充满了交叉关注点,这些方面需要一致地实现,即使它们位于单独的类层次结构中。试图围绕横切关注点强加类层次结构通常是错误的。我们已经看了一些例子,比如日志记录和审计。
我们不能合理地要求可能需要写入日志的每个类也是某个Loggable超类的子类。设计Loggable混音器或@loggable装饰器要容易得多。这些不会干扰正确的继承层次结构,我们需要设计这种继承层次结构以使多态性正常工作。
一些重要的交叉关注点围绕着安全。在 web 应用程序中,安全问题有两个方面,如下所示:
- 认证:我们知道是谁提出的请求吗?
- 授权:是否允许经过身份验证的用户进行请求?
一些 web 框架允许我们用安全需求来修饰请求处理程序。例如,Django 框架有许多装饰器,允许我们为视图函数或视图类指定安全要求。
其中一些装饰师如下所示:
user_passes_test:这是一个非常通用的低级装饰器,用于构建其他两个装饰器。它需要一个测试功能;与请求关联的登录User对象必须通过给定的函数。如果User实例无法通过给定的测试,则会将其重定向到登录页面,以便此人可以提供发出请求所需的凭据。login_required:此装饰器基于user_passes_test。它确认登录用户已通过身份验证。这种装饰器用于适用于所有访问站点的人的 web 请求。请求(如更改密码或注销)不应要求任何更具体的权限。permission_required:此装饰器使用 Django 内部定义的数据库权限方案。它确认登录的用户(或用户组)与给定的权限相关联。这种装饰器用于需要特定管理权限才能发出请求的 web 请求。
其他包和框架也有表达 web 应用程序这一交叉方面的方法。在许多情况下,web 应用程序可能有更严格的安全考虑。我们可能有一个 web 应用程序,其中根据合同条款和条件有选择地解锁用户功能。也许,额外的费用将解锁一个功能。我们可能必须设计如下所示的测试:
def user_has_feature(feature_name):
def has_feature(user):
return feature_name in (f.name for f in user.feature_set())
return user_passes_test(has_feature) 此装饰器通过在特定的特性测试中绑定来定制 Djangouser_passes_test()装饰器的一个版本。has_feature()函数检查每个User对象的feature_set()值。这不是 Django 中内置的。feature_set()方法将是一个扩展,添加到 DjangoUser类定义中。其思想是让应用程序扩展 Django 定义以定义其他特性。
has_feature()函数检查命名特征是否与当前User实例的feature_set()结果关联。我们已经将我们的has_feature()函数与 Django 的user_passes_testdecorator 一起使用,创建了一个新的 decorator,可以应用于相关的view函数。
然后我们可以创建一个view函数,如下所示:
@user_has_feature('special_bonus')
def bonus_view(request):
pass 这确保了安全问题将在许多view函数中得到一致的应用。
我们已经研究了如何使用装饰器来修改函数和类定义。我们还研究了 mixin,它允许我们将更大的类分解为编织在一起的组件。
这两种技术的思想都是将特定于应用程序的特性与通用特性(如安全性、审计或日志记录)分开。我们将区分类的固有特性和不是固有的但又是附加关注点的方面。固有特性是显式设计的一部分。它们是继承层次结构的一部分;它们定义了对象是什么。其他方面可以是混合或装饰;它们定义了对象的行为方式。
在大多数情况下,是一个和作为的角色之间的这种区分是非常清楚的。固有特性是整个问题域的一部分。当谈到模拟 21 点游戏时,诸如纸牌、手牌、下注、击球和站立等显然是问题领域的一部分。同样,数据收集和结果统计分析也是解决方案的一部分。其他事情,比如日志记录、调试、安全检查和审计,都不是问题域的一部分;这些其他方面与解决方案技术相关。在某些情况下,它们是法规遵从性的一部分或使用软件的另一个背景环境。
虽然大多数情况都很清楚,但内在和装饰方面之间的分界线可以很好。在某些情况下,它可能会转化为审美判断。通常,在编写框架和基础结构类时,决策变得很困难,因为它们不关注特定的问题。创造良好设计的一般策略如下:
- 问题的核心方面将直接影响到类定义。许多类是基于问题域中的名词和动词。这些类形成简单的层次结构;与真实对象相比,数据对象之间的多态性工作正常。
- 某些方面与问题无关,将导致 mixin 类定义。这些都与使用软件的操作方面有关,而不是解决基本问题。
一个包含 mixin 的类可以说是多维。它有多个独立轴;方面属于正交设计考虑因素。当我们定义单独的混合时,我们可以为混合拥有单独的继承层次结构。对于我们的赌场游戏模拟,有两个方面:游戏规则和博彩策略。这些都是正交的考虑。最终的玩家模拟类必须具有来自两个类层次结构的 mixin 元素。
装饰器的类型提示可能会变得复杂。在最一般的情况下,装饰器可以概括为一个函数,其参数为Callable,结果为Callable。如果我们想具体说明可调用的参数和结果,将有复杂的类型提示,通常涉及类型变量,以显示Callable参数和Callable结果如何对齐。如果装饰程序通过修改参数或结果来更改装饰函数的签名,这可能会变得非常复杂。
如前所述,面向对象编程允许我们遵循多种设计策略,如下所示:
- 组合:我们通过将一个类包装成另一个类来引入功能。这可能涉及立面下各个方面的组成。它可能涉及使用 mixin 类来添加特性,或者使用 decorator 来添加特性。
- 扩展:这是继承的普通情况。这适用于类定义之间存在明确的is-a关系的情况。当超类是子类细节的一个毫不奇怪的泛化时,它的效果最好。在这种情况下,普通的继承技术效果很好。
接下来的章节将改变方向。我们已经看到了几乎所有 Python 的特殊方法名。接下来的五章将重点讨论对象持久化和序列化。我们将首先以各种外部符号序列化和保存对象,包括 JSON、YAML、Pickle、CSV 和 XML。
序列化和持久化为我们的类引入了更多面向对象的设计考虑。我们还将了解对象关系及其表示方式。我们还将研究序列化和反序列化对象的成本复杂性,以及与来自不可信源的对象的反序列化相关的安全问题。
