这篇短文中显示的代码取自我的小型开源项目DesignbyContract,它提供了一个类型化的装饰器。装饰器是一个非常有用的概念,您一定会在网上找到大量关于它们的介绍。简而言之,它们允许每次(之前和之后)调用装饰函数时执行代码。通过这种方式,您可以修改函数参数或返回值、测量执行时间、添加日志记录、执行执行时类型检查等。请注意,装饰器也可以为类编写,提供元编程的替代方法(例如在attrs包中完成的)。在最简单的形式中,装饰器的定义类似于以下代码:defmy_first_decorator(func):defwrapped(*args,**kwargs):#dosomethingbeforeresult=func(*args,**kwargs)#dosomething返回结果后returnwrapped@my_first_decoratordeffunc(a):returna如上代码,因为当defined被wrapped函数嵌套时,只要在某个地方使用该函数,它的周围变量就可以在函数内部访问并保存在内存中(这在函数式编程语言中称为闭包)。很简单,但这有一些缺点。最大的问题是一个装饰函数丢失了它之前的函数名(你可以通过inspect.signature看到这个),它的文档字符串,甚至它的名字,这些都是源代码文档工具(如sphinx)的问题,但它可以使用标准库中的functools.wraps装饰器很容易解决:fromfunctoolsimportwrapsfromtypingimportAny,Callable,TypeVar,ParamSpecP=ParamSpec("P")#requirespython>=3.10R=TypeVar("R")defmy_second_decorator(func:Callable[P,R])->Callable[P,R]:@wraps(func)defwrapped(*args:Any,**kwargs:Any)->R:#在result=func(*args,**kwargs)#在返回结果后做一些事情returnwrapped@my_second_decoratordeffunc2(a:int)->int:"""Doesnothing"""returnaprint(func2.__name__)#'func2'print(func2.__doc__)#'Doesnothing'在这个例子中,我添加了类型注释,注释和类型提示是对Python所做的最重要的添加。更好的可读性、IDE中的代码完成以及更大代码库的可维护性只是几个例子。上面的代码应该已经涵盖了大部分用例,但是没有办法对装饰器进行参数化。考虑编写一个装饰器来记录函数的执行时间,但前提是它超过一定的秒数。这个数字应该可以为每个装饰函数单独配置。如果未指定,则应使用默认值,并应使用不带括号的装饰器以便于使用:@time(threshold=2)deffunc1(a):...#Noparanthesiswhenusingdefaultthreshold@timedeffunc2(b):...如果你可以在第二种情况下使用括号,或者根本不为参数提供默认值,那么这个方法就足够了:fromfunctoolsimportwrapsfromtypingimportAny,Callable,TypeVar,ParamSpecP=ParamSpec("P")#要求python>=3.10R=TypeVar("R")defmy_third_decorator(threshold:int=1)->Callable[[Callable[P,R]],Callable[P,R]]:defdecorator(func:Callable[P,R])->Callable[P,R]:@wraps(func)defwrapper(*args:Any,**kwargs:Any)->R:#在使用`threshold`之前做一些事情result=func(*args,**kwargs)#返回结果后做一些事情returnwrapperreturndecorator@my_third_decorator(threshold=2)deffunc3a(a:int)->None:...#works@my_third_decorator()deffunc3b(a:int)->None:...#不起作用!@my_third_decoratordef有趣c3c(a:int)->None:...为了涵盖第三种情况,有包,即wraps和装饰器,他们实际上可以做的不仅仅是添加可选参数。虽然质量非常高,但它们引入了相当多的额外复杂性。使用wrapt-decorated函数,在远程集群上运行该函数时,我进一步遇到了序列化问题。据我所知,两者都不是完全类型化的,因??此静态类型检查器/linters(例如mypy)在严格模式下会失败。当我处理自己的包并决定编写自己的解决方案时,必须解决这些问题。它成为一种可以轻松重用但难以转换为库的模式。它使用标准库的重载装饰器。这样,可以指定相同的装饰器来使用我们的无参数。除此之外,它是上面两个片段的组合。这种方法的一个缺点是所有参数都需要作为关键字参数给出(毕竟这增加了可读性)来自输入importCallable,TypeVar,ParamSpecfromfunctoolsimportpartial,wrapsP=ParamSpec("P")#requirespython>=3.10R=TypeVar("R@overloaddeftyped_decorator(func:Callable[P,R])->Callable[P,R]:...@overloaddeftyped_decorator(*,first:str="x",second:bool=True)->Callable[[Callable[P,R]],Callable[P,R]]:...deftyped_decorator(func:Optional[Callable[P,R]]=None,*,first:str="x",second:bool=True)->Union[Callable[[Callable[P,R]],Callable[P,R]],Callable[P,R]]:"""描述装饰器应该做什么!Parameters----------first:str,optional第一个参数,默认为"x"。这是一个关键字参数!second:bool,optional第二个参数,默认为True。这是一个关键字-onlyargument!"""defwrapper(func:Callable[P,R],*args:Any,**kw:Any)->R:"""Theactuallogic"""#Dosomething使用first和second并产生类型为`R`的`result`返回结果#没有参数`func`直接传递给装饰器如果func不是None:ifnotcallable(func):raiseTypeError("Notacallable.你使用了非关键字参数吗?”)returnwraps(func)(partial(wrapper,func))#有了参数,我们需要返回一个接受函数的函数defdecorator(func:Callable[P,R])->Callable[P,R]:returnwraps(func)(partial(wrapper,func))returndecorator之后,我们可以单独使用不带参数的装饰器@typed_decoratordefspam(a:int)->int:returna@typed_decorator(first="ydefeggs(a:int)->int:returna这种模式肯定有一些开销,但是收益大于成本原文:https://lemonfold.io/posts/2022/dbc/typed_decorator/
