根据这个问题,我知道如何检查我的函数是否被修饰了。
只是我需要更多的信息,即实际应用到函数上的装饰器(或者如果函数更适合的话,在调用函数时调用(。
为了避免这个答案中提到的危险,我使用functools.wraps
。这样,我就不必为所使用的包装器的任何命名重新定义而小心。
这就是我目前所拥有的:
from functools import wraps
def decorator_wraps(function):
@wraps(function)
def _wrapper(*a, **kw): ...
return _wrapper
def is_decorated(func):
return hasattr(func, '__wrapped__')
@decorator_wraps
def foo(x, y): ...
print(is_decorated(foo)) # True
但我需要的是:
from functools import wraps
def decorator_wraps_1(function):
@wraps(function)
def _wrapper(*a, **kw): ...
return _wrapper
def decorator_wraps_2(function):
@wraps(function)
def _wrapper(*a, **kw): ...
return _wrapper
def decorators(func):
# returns list of decorators on `func`
# OR
def is_decorated_by(func, decorator):
# returns True if `func` is decorated by `decorator`
@decorator_wraps_1
@decorator_wraps_2
def foo(x, y): ...
print(decorators(foo)) # [decorator_wraps_1, decorator_wraps_2]
print(is_decorated_by(foo, decorator_wraps_1)) # True
TLDR
我想决定我的函数是否被修饰,我也需要这些修饰函数的名称。
知道如何做到这一点吗?
TL;DR
滚动您自己的@wraps
。
import functools
def update_wrapper(wrapper, wrapped, decorator, **kwargs):
wrapper = functools.update_wrapper(wrapper, wrapped, **kwargs)
if decorator is not None:
__decorators__ = getattr(wrapper, "__decorators__", [])
setattr(wrapper, "__decorators__", __decorators__ + [decorator])
return wrapper
def wraps(wrapped, decorator, **kwargs):
return functools.partial(
update_wrapper, wrapped=wrapped, decorator=decorator, **kwargs
)
def get_decorators(func):
return getattr(func, "__decorators__", [])
def is_decorated_by(func, decorator):
return decorator in get_decorators(func)
用法:
def test_decorator_1(function):
@wraps(function, test_decorator_1)
def wrapper(*args, **kwargs):
return function(*args, **kwargs)
return wrapper
def test_decorator_2(function):
@wraps(function, test_decorator_2)
def wrapper(*args, **kwargs):
return function(*args, **kwargs)
return wrapper
@test_decorator_1
@test_decorator_2
def foo(x: str, y: int) -> None:
print(x, y)
assert get_decorators(foo) == [test_decorator_2, test_decorator_1]
assert is_decorated_by(foo, test_decorator_1)
自定义@wraps
概念
据我所知,这并没有内在的方式。创建(函数(装饰器所需要的就是定义一个函数,该函数以另一个函数为参数并返回一个函数。没有关于";外部";函数通过装饰神奇地印在返回的函数上。
然而,我们可以依靠functools.wraps
方法,并简单地推出我们自己的变体。我们可以这样定义它,即它不仅引用封装函数作为参数,还引用外部装饰器。
与functools.update_wrapper
在其输出的包装器上定义额外的__wrapped__
属性的方式相同,我们可以定义自己的自定义__decorators__
属性,它将只是按照应用程序顺序(符号的相反顺序(的所有装饰器的列表。
代码
正确的类型注释有点棘手,但这里有一个完整的工作示例:
import functools
from collections.abc import Callable
from typing import Any, ParamSpec, TypeAlias, TypeVar
P = ParamSpec("P")
T = TypeVar("T")
AnyFunc: TypeAlias = Callable[..., Any]
def update_wrapper(
wrapper: Callable[P, T],
wrapped: AnyFunc,
decorator: AnyFunc | None = None,
assigned: tuple[str, ...] = functools.WRAPPER_ASSIGNMENTS,
updated: tuple[str, ...] = functools.WRAPPER_UPDATES,
) -> Callable[P, T]:
"""
Same as `functools.update_wrapper`, but can also add `__decorators__`.
If provided a `decorator` argument, it is appended to the the
`__decorators__` attribute of `wrapper` before returning it.
If `wrapper` has no `__decorators__` attribute, a list with just
`decorator` in it is created and set as that attribute on `wrapper`.
"""
wrapper = functools.update_wrapper(
wrapper,
wrapped,
assigned=assigned,
updated=updated,
)
if decorator is not None:
__decorators__ = getattr(wrapper, "__decorators__", [])
setattr(wrapper, "__decorators__", __decorators__ + [decorator])
return wrapper
def wraps(
wrapped: AnyFunc,
decorator: AnyFunc | None,
assigned: tuple[str, ...] = functools.WRAPPER_ASSIGNMENTS,
updated: tuple[str, ...] = functools.WRAPPER_UPDATES
) -> Callable[[Callable[P, T]], Callable[P, T]]:
"""Same as `functools.wraps`, but uses custom `update_wrapper` inside."""
return functools.partial(
update_wrapper, # type: ignore[arg-type]
wrapped=wrapped,
decorator=decorator,
assigned=assigned,
updated=updated,
)
def get_decorators(func: AnyFunc) -> list[AnyFunc]:
return getattr(func, "__decorators__", [])
def is_decorated_by(func: AnyFunc, decorator: AnyFunc) -> bool:
return decorator in get_decorators(func)
def test() -> None:
def test_decorator_1(function: Callable[P, T]) -> Callable[P, T]:
@wraps(function, test_decorator_1)
def wrapper(*args: P.args, **kwargs: P.kwargs) -> T:
print(f"Called wrapper from {test_decorator_1.__name__}")
return function(*args, **kwargs)
return wrapper
def test_decorator_2(function: Callable[P, T]) -> Callable[P, T]:
@wraps(function, test_decorator_2)
def wrapper(*args: P.args, **kwargs: P.kwargs) -> T:
print(f"Called wrapper from {test_decorator_2.__name__}")
return function(*args, **kwargs)
return wrapper
@test_decorator_1
@test_decorator_2
def foo(x: str, y: int) -> None:
print(x, y)
assert get_decorators(foo) == [test_decorator_2, test_decorator_1]
assert is_decorated_by(foo, test_decorator_1)
assert hasattr(foo, "__wrapped__")
foo("a", 1)
if __name__ == '__main__':
test()
输出当然是:
Called wrapper from test_decorator_1
Called wrapper from test_decorator_2
a 1
细节和注意事项
有了这种方法,functools.wraps
的原始功能就不会丢失。和原来的一样,这个@wraps
装饰器显然依赖于你为整个事件传递正确的参数,以使其最终有意义。如果将无意义的参数传递给@wraps
,它将向包装器添加无意义的信息。
不同的是,您现在必须提供两个函数引用,而不是一个,即被包装的函数(如前所述(和外部装饰器(如果出于某种原因想要抑制该信息,则可以提供None
(。因此,您通常会将其用作@wraps(function, decorator)
。
如果您不喜欢decorator
参数是强制性的,可以将其默认为None
。但我认为这种方式更好,因为关键是要有一种一致的方式来跟踪谁装饰了谁,所以省略decorator
参考应该是一个有意识的选择。
请注意,我选择按该顺序实现__decorators__
,因为虽然它们是按相反的顺序编写的,但它们是按该顺序应用的。因此,在本例中,foo
首先用@test_decorator_2
进行装饰,然后用@test_decorator_1
进行装饰。对我来说,我们的名单反映出这一顺序更有意义。
静态类型检查
有了给定的类型注释,mypy --strict
也很满意,任何IDE都应该像预期的那样提供自动建议。唯一让我反感的是,mypy
抱怨我使用update_wrapper
作为functools.partial
的论据。我不知道为什么,所以我在那里添加了一个# type: ignore
。
注意:如果使用Python<3.10
,则可能需要调整导入,并以typing_extensions
中的ParamSpec
为例。另外,您需要使用typing.Optional[T]
来代替T | None
。或者升级您的Python版本。🙂