【问题标题】:How to strip decorators from a function in Python如何从 Python 中的函数中去除装饰器
【发布时间】:2010-11-13 01:26:50
【问题描述】:

假设我有以下内容:

def with_connection(f):
    def decorated(*args, **kwargs):
        f(get_connection(...), *args, **kwargs)
    return decorated

@with_connection
def spam(connection):
    # Do something

我想测试spam 函数,而无需经历设置连接(或装饰器正在做的任何事情)的麻烦。

给定spam,我如何从中剥离装饰器并获得底层的“未装饰”功能?

【问题讨论】:

    标签: python decorator


    【解决方案1】:

    一般情况下,你不能,因为

    @with_connection
    def spam(connection):
        # Do something
    

    等价于

    def spam(connection):
        # Do something
    
    spam = with_connection(spam)
    

    这意味着“原始”垃圾邮件甚至可能不再存在。一个(不太漂亮的)hack 是这样的:

    def with_connection(f):
        def decorated(*args, **kwargs):
            f(get_connection(...), *args, **kwargs)
        decorated._original = f
        return decorated
    
    @with_connection
    def spam(connection):
        # Do something
    
    spam._original(testcon) # calls the undecorated function
    

    【讨论】:

    • 如果你要修改代码调用_original你不妨把装饰器注释掉。
    • 这对于 Python 3 来说已经过时了。请参阅下面的答案:stackoverflow.com/a/33024739/13969
    【解决方案2】:

    看哪,FuglyHackThatWillWorkForYourExampleButICantPromiseAnythingElse:

     orig_spam = spam.func_closure[0].cell_contents
    

    编辑:对于多次修饰的函数/方法以及更复杂的修饰器,您可以尝试使用以下代码。它依赖于这样一个事实,即修饰函数的 __name__d 与原始函数不同。

    def search_for_orig(decorated, orig_name):
        for obj in (c.cell_contents for c in decorated.__closure__):
            if hasattr(obj, "__name__") and obj.__name__ == orig_name:
                return obj
            if hasattr(obj, "__closure__") and obj.__closure__:
                found = search_for_orig(obj, orig_name)
                if found:
                    return found
        return None
    
     >>> search_for_orig(spam, "spam")
     <function spam at 0x027ACD70>
    

    但这并不是万无一失的。如果从装饰器返回的函数名与被装饰的函数名相同,它将失败。 hasattr() 检查的顺序也是启发式的,有装饰链在任何情况下都会返回错误的结果。

    【讨论】:

    • func_closure 在 3.x 中被 __closure__ 取代,并且已经在 2.6 中
    • 我在玩函数时看到了这一点,但如果你在一个函数上使用多个装饰器,它就会变得复杂。你最终会打电话给.func_closure[0].cell_contents,直到cell_contents is None。我希望有一个更优雅的解决方案。
    • 如果装饰器使用 functools.wraps 可能不起作用
    • 想出了相同的解决方案,赞 ^^ @EvgeniiPuchkaryov 它似乎与 functools.wrap 一起使用
    【解决方案3】:

    使用这个元装饰器可以使 balpha 的解决方案更通用:

    def include_original(dec):
        def meta_decorator(f):
            decorated = dec(f)
            decorated._original = f
            return decorated
        return meta_decorator
    

    然后你可以用@include_original 来装饰你的装饰器,每个装饰器里面都会有一个可测试(未装饰)的版本。

    @include_original
    def shout(f):
        def _():
            string = f()
            return string.upper()
        return _
    
    
    
    @shout
    def function():
        return "hello world"
    
    >>> print function()
    HELLO_WORLD
    >>> print function._original()
    hello world
    

    【讨论】:

    • 有没有办法扩展它,以便在最外层的装饰函数中可以访问最深层的原件,所以我不必为包装在三个装饰器中的函数做 ._original._original._original ?
    • @jcdyer 装饰你的装饰器到底是什么意思?我可以做一些类似 \@include_original (next line) \@decorator_which_I_dont_control (next line) function_definition 的事情吗?
    • @Harshdeep:你会想做类似now_i_control = include_original(decorator_i_dont_control) 的事情,然后用@now_i_control\ndef function(): 装饰你的函数。请注意,y = foo(y) 在语法上等同于@foo\ndef y():。如果你尝试了你的建议,你最终会得到include_original(decorator_i_dont_control(function)),而你想要的是include_original(decorator_i_dont_control)(function)
    • @Harshdeep 我刚刚用示例用法编辑了我的回复。同样,如果你没有自己定义装饰器,你可以用decorator = include_original(decorator) 包裹它
    • @jcdyer 谢谢 :) .. 我也找到了类似的解决方案 def include_original(f): @wraps(f) def decorated(*args, **kwargs): return f(*args, **kwargs) decorated._original = f return decorated
    【解决方案4】:

    测试此类函数的常用方法是使任何依赖项(例如 get_connection)可配置。然后你可以在测试时用模拟覆盖它。与 Java 世界中的依赖注入基本相同,但由于 Python 的动态特性而简单得多。

    它的代码可能如下所示:

    # decorator definition
    def with_connection(f):
        def decorated(*args, **kwargs):
            f(with_connection.connection_getter(), *args, **kwargs)
        return decorated
    
    # normal configuration
    with_connection.connection_getter = lambda: get_connection(...)
    
    # inside testsuite setup override it
    with_connection.connection_getter = lambda: "a mock connection"
    

    根据您的代码,您可以找到比装饰器更好的对象来粘贴工厂功能。将它放在装饰器上的问题是您必须记住在 teardown 方法中将其恢复为旧值。

    【讨论】:

      【解决方案5】:

      而不是做...

      def with_connection(f):
          def decorated(*args, **kwargs):
              f(get_connection(...), *args, **kwargs)
          return decorated
      
      @with_connection
      def spam(connection):
          # Do something
      
      orig_spam = magic_hack_of_a_function(spam)
      

      你可以这样做......

      def with_connection(f):
          ...
      
      def spam_f(connection):
          ...
      
      spam = with_connection(spam_f)
      

      ...这就是@decorator 语法所做的全部 - 然后您显然可以正常访问原始spam_f

      【讨论】:

      • 不错的方法,太聪明了!
      • 使用装饰器的重点在于简单地重用它。如果我在需要装饰器的地方都有代码,这不会扩展。
      【解决方案6】:

      添加一个无所事事的装饰器:

      def do_nothing(f):
          return f
      

      在定义或导入 with_connection 之后,但在使用它作为装饰器的方法之前,添加:

      if TESTING:
          with_connection = do_nothing
      

      然后,如果您将全局 TESTING 设置为 True,您将用一个无所事事的装饰器替换 with_connection。

      【讨论】:

        【解决方案7】:

        这个问题有一些更新。如果您使用的是 Python 3,则可以将 __wrapped__ 属性用于 stdlib 中的装饰器。

        这是来自Python Cookbook, 3rd edition, section 9.3 Unwrapping decorators的示例

        >>> @somedecorator
        >>> def add(x, y):
        ...     return x + y
        ...
        >>> orig_add = add.__wrapped__
        >>> orig_add(3, 4)
        7
        >>>
        

        如果您尝试从自定义装饰器中解包函数,则装饰器函数需要使用来自functoolswraps 函数请参阅Python Cookbook, 3rd edition, section 9.2 Preserving function metadata when writing decorators 中的讨论

        >>> from functools import wraps
        >>> def somedecorator(func):
        ...    @wraps(func)
        ...    def wrapper(*args, **kwargs):
        ...       # decorator implementation here
        ...       # ......
        ...       return func(*args, **kwargs)
        ...
        ...    return wrapper
        

        【讨论】:

        • Python3 的胜利!
        • 这是不真实的。如果使用functools.wraps 装饰,则装饰函数只有__wrapped__ 属性。此外,链接已失效。
        • 我在实现自己的装饰器时修复了本书的链接并扩展了案例的答案。
        • 这对我有用。附加信息:如果您的函数有多个装饰器,您可以链接多个 .__wrapped__ 以访问原始函数。
        【解决方案8】:

        您现在可以使用undecorated 包:

        >>> from undecorated import undecorated
        >>> undecorated(spam)
        

        它经历了挖掘不同装饰器的所有层直到到达底部功能的麻烦,并且不需要更改原始装饰器。它适用于 Python 2 和 Python 3。

        【讨论】:

        • 这正是我一直在寻找的!
        • 史诗!也需要这个。
        【解决方案9】:

        最好用functools.wraps 来装饰装饰器,如下所示:

        import functools
        
        def with_connection(f):
            @functools.wraps(f)
            def decorated(*args, **kwargs):
                f(get_connection(...), *args, **kwargs)
            return decorated
        
        @with_connection
        def spam(connection):
            # Do something
        

        从 Python 3.2 开始,这将自动添加一个 __wrapped__ 属性,让您可以检索原始的、未修饰的函数:

        >>> spam.__wrapped__
        <function spam at 0x7fe4e6dfc048>
        

        不过,与其手动访问__wrapped__ 属性,不如使用inspect.unwrap

        >>> inspect.unwrap(spam)
        <function spam at 0x7fe4e6dfc048>
        

        【讨论】:

          【解决方案10】:

          原始函数存储在spam.__closure__[0].cell_contents
          装饰器使用闭包将原始功能与额外的功能层绑定。原始函数必须存储在一个闭包单元中,该闭包单元由装饰器嵌套结构中的一个函数保存。
          示例:

          >>> def add(f):
          ...     def _decorator(*args, **kargs):
          ...             print('hello_world')
          ...             return f(*args, **kargs)
          ...     return _decorator
          ... 
          >>> @add
          ... def f(msg):
          ...     print('f ==>', msg)
          ... 
          >>> f('alice')
          hello_world
          f ==> alice
          >>> f.__closure__[0].cell_contents
          <function f at 0x7f5d205991e0>
          >>> f.__closure__[0].cell_contents('alice')
          f ==> alice
          

          这是undecorated的核心原理,具体可以参考源码。

          【讨论】:

            猜你喜欢
            • 2023-02-08
            • 2019-08-08
            • 2020-07-13
            • 2017-01-06
            • 1970-01-01
            • 2018-01-21
            • 2019-12-26
            • 1970-01-01
            相关资源
            最近更新 更多