Python很早就引入了裝飾器——在PEP-318中,作為一種簡化函數和方法定義方式的機制,這些函數和方法在初始定義之後必須進行修改。
這樣做的最初動機之一是,使用classmethod和staticmethod等函數來轉換方法的原始定義,但是它們需要額外的一行程式碼來修改函數的初始定義。
一般來說,每次必須對函數應用轉換時,我們必須使用modifier函數呼叫它,然後將它重新分配到函數初始定義時的名稱中。
例如,假設有一個叫作original的函數,在它上面有一個改變original行為的函數(叫作modifier),那麼我們必須這樣寫:
def original(...): ... original = modifier(original)
(相關免費學習推薦:python影片教學)
請注意我們是如何更改函數並將其重新分配到相同的名稱中去的。這是令人困惑的,很容易出錯(假設有人忘記重新分配函數,或者重新分配了函數,但不在函數定義之後的行中,而是在更遠的地方),而且很麻煩。出於這個原因,Python語言增加了一些語法支援。
前面的範例可以改寫為如下樣式:
@modifier def original(...): ...
這表示裝飾器只是語法糖,用於呼叫裝飾器之後的內容作為裝飾器本身的第一個參數,結果將是裝飾器返回的內容。
為了與Python的術語一致,在我們的範例中modifier稱為裝飾器,original是裝飾函數,通常也被稱為包裝物件。
雖然該函數最初被認為是用於方法和函數的,但實際的語法允許它修飾任何類型的對象,因此我們將研究應用於函數、方法、生成器和類別的裝飾器。
最後一點要注意的是,雖然裝飾器的名稱是正確的(畢竟,裝飾器實際上是在對包裝函數進行更改、擴展或處理),但不要將它與裝飾器設計模式混淆。
5.1.1 裝飾器函數
函數可能是可以裝飾的Python物件的最簡單的表示形式。我們可以在函數上使用裝飾器來應用各種邏輯——我們可以驗證參數、檢查前置條件、完全改變行為、修改其簽名、快取結果(創建原始函數的記憶體版本)等。
例如,我們將創建一個實現retry機制的基本裝飾器,控制一個特定的域級異常並重試一定的次數:
# decorator_function_1.py class ControlledException(Exception): """A generic exception on the program's domain.""" def retry(operation): @wraps(operation) def wrapped(*args, **kwargs): last_raised = None RETRIES_LIMIT = 3 for _ in range(RETRIES_LIMIT): try: return operation(*args, **kwargs) except ControlledException as e: logger.info("retrying %s", operation.__qualname__) last_raised = e raise last_raised return wrapped
現在可以忽略@wrap的使用,因為它將在另一節中討論。在for迴圈中使用“_”,表示這個數字被分配給我們目前不感興趣的變量,因為它不在for迴圈中使用(在Python中,將被忽略的值命名為“_”是一個常見的習慣用法)。
retry裝飾器不會接收任何參數,所以它可以很容易地應用於任何函數,如下所示:
@retry def run_operation(task): """Run a particular task, simulating some failures on its execution.""" return task.run()
正如一開始所解釋的,在run_operation之上@retry的定義只是Python提供的語法糖,用於實際執行run_operation = retry(run_operation)。
在這個有限的範例中,我們可以看到如何用裝飾器建立一個通用的retry操作,在某些確定的條件下(在本範例中,表示為可能與逾時相關的例外) ,該操作將允許多次調用裝飾後的程式碼。
5.1.2 裝飾類別
類別也可以被裝飾(PEP-3129),其裝飾方法與語法函數的裝飾方法相同。唯一的區別是,在為裝飾器編寫程式碼時,我們必須考慮到所接收的是一個類,而不是一個函數。
一些實踐者可能會認為裝飾類別是相當複雜的事情,這樣的場景可能會損害可讀性,因為我們將在類別中聲明一些屬性和方法,但是在幕後,裝飾器可能會應用一些變化,從而呈現一個完全不同的類別。
這種評定是正確的,但只有在裝飾類別技術被嚴重濫用的情況下才成立。客觀上,這與裝飾功能沒有什麼不同;畢竟,類別和函數一樣,都只是Python生態系中的一種類型的物件而已。在5.4節中,我們將再次審視這個問題的優缺點,但是這裡只探索裝飾器的優點,尤其是適用於類的裝飾器的優點。
(1)重複使用程式碼和DRY原則的所有好處。類別裝飾器的一個有效情況是,強制多個類別符合特定的介面或標準(透過僅在將應用於多個類別的裝飾器中進行一次檢查)。
(2)可以創建更小或更簡單的類別——這些類別稍後將由裝飾器進行增強。
(3)如果使用裝飾器,那麼需要應用到特定類別上的轉換邏輯將更容易維護,而不會使用更複雜的(通常是不鼓勵使用的)方法,例如元類別。
在装饰器的所有可能应用程序中,我们将探索一个简单的示例,以了解装饰器可以用于哪些方面。记住,这不是类装饰器的唯一应用程序类型,而且给出的代码还可以有许多其他解决方案。所有这些解决方案都有优缺点,之所以选择装饰器,是为了说明它们的用处。
回顾用于监视平台的事件系统,现在需要转换每个事件的数据并将其发送到外部系统。然而,在选择如何发送数据时,每种类型的事件可能都有自己的特殊性。
特别是,登录事件可能包含敏感信息,例如我们希望隐藏的凭据。时间戳等其他领域的字段可能也需要一些转换,因为我们希望以特定的格式显示它们。符合这些要求的第一次尝试很简单,就像有一个映射到每个特定事件的类,并知道如何序列化它那样:
class LoginEventSerializer: def __init__(self, event): self.event = event def serialize(self) -> dict: return { "username": self.event.username, "password": "**redacted**", "ip": self.event.ip, "timestamp": self.event.timestamp.strftime("%Y-%m-%d %H:%M"), } class LoginEvent: SERIALIZER = LoginEventSerializer def __init__(self, username, password, ip, timestamp): self.username = username self.password = password self.ip = ip self.timestamp = timestamp def serialize(self) -> dict: return self.SERIALIZER(self).serialize()
在这里,我们声明一个类。该类将直接映射到登录事件,其中包含它的一些逻辑——隐藏密码字段,并根据需要格式化时间戳。
虽然这是可行的,可能开始看起来是一个不错的选择,但随着时间的推移,若要扩展系统,就会发现一些问题。
(1)类太多。随着事件数量的增多,序列化类的数量将以相同的量级增长,因为它们是一一映射的。
(2)解决方案不够灵活。如果我们需要重用部分组件(例如,需要把密码藏在也有类似需求的另一个类型的事件中),就不得不将其提取到一个函数,但也要从多个类中调用它,这意味着我们没有重用那么多代码。
(3)样板文件。serialize()方法必须出现在所有事件类中,同时调用相同的代码。尽管我们可以将其提取到另一个类中(创建mixin),但这似乎没有很好地使用继承。
另一种解决方案是能够动态构造一个对象:给定一组过滤器(转换函数)和一个事件实例,该对象能够通过将过滤器应用于其字段的方式序列化它。然后,我们只需要定义转换每种字段类型的函数,并通过组合这些函数创建序列化器。
一旦有了这个对象,我们就可以装饰类以添加serialize()方法。该方法只会调用这些序列化对象本身:
def hide_field(field) -> str: return "**redacted**" def format_time(field_timestamp: datetime) -> str: return field_timestamp.strftime("%Y-%m-%d %H:%M") def show_original(event_field): return event_field class EventSerializer: def __init__(self, serialization_fields: dict) -> None: self.serialization_fields = serialization_fields def serialize(self, event) -> dict: return { field: transformation(getattr(event, field)) for field, transformation in self.serialization_fields.items() } class Serialization: def __init__(self, **transformations): self.serializer = EventSerializer(transformations) def __call__(self, event_class): def serialize_method(event_instance): return self.serializer.serialize(event_instance) event_class.serialize = serialize_method return event_class @Serialization( username=show_original, password=hide_field, ip=show_original, timestamp=format_time, ) class LoginEvent: def __init__(self, username, password, ip, timestamp): self.username = username self.password = password self.ip = ip self.timestamp = timestamp
注意,装饰器让你更容易知道如何处理每个字段,而不必查看另一个类的代码。仅通过读取传递给类装饰器的参数,我们就知道用户名和IP地址将保持不变,密码将被隐藏,时间戳将被格式化。
现在,类的代码不需要定义serialize()方法,也不需要从实现它的mixin类进行扩展,因为这些都将由装饰器添加。实际上,这可能是创建类装饰器的唯一理由,因为如果不是这样的话,序列化对象可能是LoginEvent的一个类属性,但是它通过向该类添加一个新方法来更改类,这使得创建该类装饰器变得不可能。
我们还可以使用另一个类装饰器,通过定义类的属性来实现init方法的逻辑,但这超出了本例的范围。
通过使用Python 3.7+ 中的这个类装饰器(PEP-557),可以按更简洁的方式重写前面的示例,而不使用init的样板代码,如下所示:
from dataclasses import dataclass from datetime import datetime @Serialization( username=show_original, password=hide_field, ip=show_original, timestamp=format_time, ) @dataclass class LoginEvent: username: str password: str ip: str timestamp: datetime
5.1.3 其他类型的装饰器
既然我们已经知道了装饰器的@语法的实际含义,就可以得出这样的结论:可以装饰的不仅是函数、方法或类;实际上,任何可以定义的东西(如生成器、协同程序甚至是装饰过的对象)都可以装饰,这意味着装饰器可以堆叠起来。
前面的示例展示了如何链接装饰器。我们先定义类,然后将@dataclass应用于该类——它将该类转换为数据类,充当这些属性的容器。之后,通过@Serialization把逻辑应用到该类上,从而生成一个新类,其中添加了新的serialize()方法。
装饰器另一个好的用法是用于应该用作协同程序的生成器。我们将在第7章中探讨生成器和协同程序的细节,其主要思想是,在向新创建的生成器发送任何数据之前,必须通过调用next()将后者推进到下一个yield语句。这是每个用户都必须记住的手动过程,因此很容易出错。我们可以轻松创建一个装饰器,使其接收生成器作为参数,调用next(),然后返回生成器。
5.1.4 将参数传递给装饰器
至此,我们已经将装饰器看作Python中的一个强大工具。如果我们可以将参数传递给装饰器,使其逻辑更加抽象,那么其功能可能会更加强大。
有几种实现装饰器的方法可以接收参数,但是接下来我们只讨论最常见的方法。第一种方法是将装饰器创建为带有新的间接层的嵌套函数,使装饰器中的所有内容深入一层。第二种方法是为装饰器使用一个类。
通常,第二种方法更倾向于可读性,因为从对象的角度考虑,其要比3个或3个以上使用闭包的嵌套函数更容易。但是,为了完整起见,我们将对这两种方法进行探讨,以便你可以选择使用最适合当前问题的方法。
1.带有嵌套函数的装饰器
粗略地说,装饰器的基本思想是创建一个返回函数的函数(通常称为高阶函数)。在装饰器主体中定义的内部函数将是实际被调用的函数。
现在,如果希望将参数传递给它,就需要另一间接层。第一个函数将接收参数,在该函数中,我们将定义一个新函数(它将是装饰器),而这个新函数又将定义另一个新函数,即装饰过程返回的函数。这意味着我们将至少有3层嵌套函数。
如果你到目前为止还不明白上述内容的含义,也不用担心,待查看下面给出的示例之后,就会明白了。
第一个示例是,装饰器在一些函数上实现重试功能。这是个好主意,只是有个问题:实现不允许指定重试次数,只允许在装饰器中指定一个固定的次数。
现在,我们希望能够指出每个示例有多少次重试,也许甚至可以为这个参数添加一个默认值。为了实现这个功能,我们需要用到另一层嵌套函数——先用于参数,然后用于装饰器本身。
这是因为如下代码:
@retry(arg1, arg2,... )
必须返回装饰器,因为@语法将把计算结果应用到要装饰的对象上。从语义上讲,它可以翻译成如下内容:
<original_function> = retry(arg1, arg2, ....)(<original_function>)
除了所需的重试次数,我们还可以指明希望控制的异常类型。支持新需求的新版本代码可能是这样的:
RETRIES_LIMIT = 3 def with_retry(retries_limit=RETRIES_LIMIT, allowed_exceptions=None): allowed_exceptions = allowed_exceptions or (ControlledException,) def retry(operation): @wraps(operation) def wrapped(*args, **kwargs): last_raised = None for _ in range(retries_limit): try: return operation(*args, **kwargs) except allowed_exceptions as e: logger.info("retrying %s due to %s", operation, e) last_raised = e raise last_raised return wrapped return retry
下面是这个装饰器如何应用于函数的一些示例,其中显示了它接收的不同选项:
# decorator_parametrized_1.py @with_retry() def run_operation(task): return task.run() @with_retry(retries_limit=5) def run_with_custom_retries_limit(task): return task.run() @with_retry(allowed_exceptions=(AttributeError,)) def run_with_custom_exceptions(task): return task.run() @with_retry( retries_limit=4, allowed_exceptions=(ZeropisionError, AttributeError) ) def run_with_custom_parameters(task): return task.run()
2.装饰器对象
前面的示例需要用到3层嵌套函数。首先,这将是一个用于接收我们想要使用的装饰器的参数。在这个函数中,其余的函数是使用这些参数和装饰器逻辑的闭包。
更简洁的实现方法是用一个类定义装饰器。在这种情况下,我们可以在__init__方法中传递参数,然后在名为__call__的魔法方法上实现装饰器的逻辑。
装饰器的代码如下所示:
class WithRetry: def __init__(self, retries_limit=RETRIES_LIMIT, allowed_exceptions=None): self.retries_limit = retries_limit self.allowed_exceptions = allowed_exceptions or (ControlledException,) def __call__(self, operation): @wraps(operation) def wrapped(*args, **kwargs): last_raised = None for _ in range(self.retries_limit): try: return operation(*args, **kwargs) except self.allowed_exceptions as e: logger.info("retrying %s due to %s", operation, e) last_raised = e raise last_raised return wrapped
这个装饰器可以像之前的一样应用,就像这样:
@WithRetry(retries_limit=5) def run_with_custom_retries_limit(task): return task.run()
注意Python语法在这里是如何起作用的,这一点很重要。首先,我们创建对象,这样在应用@操作之前,对象已经创建好了,并且其参数传递给它了,用这些参数初始化这个对象,如init方法中定义的那样。在此之后,我们将调用@操作,这样该对象将包装名为run_with_custom_reries_limit
的函数,而这意味着它将被传递给call这个魔法方法。
在call这个魔法方法中,我们定义了装饰器的逻辑,就像通常所做的那样——包装了原始函数,返回一个新的函数,其中包含所要的逻辑。
5.1.5 充分利用装饰器
本节介绍一些充分利用装饰器的常见模式。在有些常见的场景中使用装饰器是个非常好的选择。
可用于应用程序的装饰器数不胜数,下面仅列举几个最常见或相关的。
(1)转换参数。更改函数的签名以公开更好的API,同时封装关于如何处理和转换参数的详细信息。
(2)跟踪代码。记录函数及其参数的执行情况。
(3)验证参数。
(4)实现重试操作。
(5)通过把一些(重复的)逻辑移到装饰器中来简化类。
接下来详细讨论前两个应用程序。
1.转换参数
前文提到,装饰器可以用来验证参数(甚至在DbC的概念下强制一些前置条件或后置条件),因此你可能已经了解到,这是一些处理或者操控参数时使用装饰器的常用方法。
特别是,在某些情况下,我们会发现自己反复创建类似的对象,或者应用类似的转换,而我们希望将这些转换抽象掉。大多数时候,我们可以通过简单地用装饰器实现这一点。
2.跟踪代码
在本节中讨论跟踪时,我们将提到一些更通用的内容,这些内容与处理所要监控的函数的执行有关,具体是指:
(1)實際追蹤函數的執行(例如,透過記錄函數執行的行);
(2)監控函數的一些指標(如CPU使用量或記憶體佔用);
(3)測量函數的運行時間;
(4)函數被呼叫時的日誌,以及傳遞給它的參數。
我們將在5.2節剖析一個簡單的裝飾器範例,該範例記錄了函數的執行情況,包括函數名稱和運行時間。
本文摘自《編寫整齊的Python程式碼》
#本書介紹Python軟體工程的主要實踐和原則,旨在幫助讀者編寫更易於維護和更整潔的代碼。全書共10章:第1章介紹Python語言的基礎知識及建構Python開發環境所需的主要工具;第2章描述Python風格程式碼,介紹Python中的第一個習慣用法;第3章總結好程式碼的一般特徵,回顧軟體工程中的一般原則;第4章介紹一套物件導向軟體設計的原則,即SOLID原則;第5章介紹裝飾器,它是Python的**特性之一;第6章探討描述符,介紹如何透過描述符從物件中獲取更多的資訊;第7章和第8章介紹生成器以及單元測試和重構的相關內容;第9章回顧Python中最常見的設計模式;第10章再次強調程式碼整潔是實現良好架構的基礎。
本書適合所有Python程式設計愛好者、對程式設計感興趣的人,以及其他想學習更多Python知識的軟體工程的從業人員。
#相關免費學習推薦:python教學(影片)
以上是Python中的裝飾器是什麼?裝飾器是如何運作的?的詳細內容。更多資訊請關注PHP中文網其他相關文章!