代码之家  ›  专栏  ›  技术社区  ›  Tobias Hermann

检查函数是否作为装饰器调用

  •  6
  • Tobias Hermann  · 技术社区  · 6 年前

    decorate 叫了两次。首次使用 @decorate ,第二次按正常函数调用 decorate(bar)

    def decorate(func):
        print(func.__name__)
        return func
    
    @decorate
    def bar():
        pass
    
    decorate(bar)
    

    有没有可能看到 装饰 如果调用是通过 @装饰 还是作为一个普通的函数调用?

    2 回复  |  直到 6 年前
        1
  •  12
  •   Olivier Melançon iacob    6 年前

    @decorator 句法只是句法上的糖分,因此两个例子的行为是相同的。这也意味着无论你在他们之间做什么区别,都可能没有你想象的那么有意义。

    不过,你可以用 inspect 阅读您的脚本并查看如何在上面的框架中调用decorator。

    import inspect
    
    def decorate(func):
        # See explanation below
        lines = inspect.stack(context=2)[1].code_context
        decorated = any(line.startswith('@') for line in lines)
    
        print(func.__name__, 'was decorated with "@decorate":', decorated)
        return func
    

    请注意,我们必须指定 context=2 inspect.stack 功能。这个 context 参数指示必须返回当前行周围的代码行数。在某些特定情况下,例如在装饰子类时,当前行位于类声明上,而不是装饰器上。 The exact reason for this behaviour has been explored here.

    例子

    @decorate
    def bar():
        pass
    
    def foo():
        pass
    foo = decorate(foo)
    
    @decorate
    class MyDict(dict):
        pass
    

    输出

    bar was decorated with "@decorate": True
    foo was decorated with "@decorate": False
    MyDict was decorated with "@decorate": True
    

    还有一些我们很难克服的极端情况,比如decorator和类声明之间的换行符。

    # This will fail
    @decorate
    
    class MyDict(dict):
        pass
    
        2
  •  3
  •   Philip DiSarro    6 年前

    奥利弗的回答把我脑子里的想法都给打乱了。然而,作为 inspect.stack() 是一个特别昂贵的电话,我会考虑选择使用一些大致如下:

    frame = inspect.getframeinfo(inspect.currentframe().f_back, context=1)
    if frame.code_context[0][0].startswith('@'): 
        print('Used as @decorate: True')
    else:
        print("Used as @decorate: False")
    
        3
  •  2
  •   MisterMiyagi    4 年前

    @decorator decorator(…) 并不完全等同。第一次跑步 名称绑定,后者 之后 名称绑定。对于顶级函数的常见用例,这允许廉价地测试哪种情况适用。

    import sys
    
    def decoraware(subject):
        """
        Decorator that is aware whether it was applied using `@deco` syntax
        """
        try:
            module_name, qualname = subject.__module__, subject.__qualname__
        except AttributeError:
            raise TypeError(f"subject must define '__module__' and '__qualname__' to find it")
        if '.' in qualname:
            raise ValueError(f"subject must be a top-level function/class")
        # see whether ``subject`` has been bound to its module
        module = sys.modules[module_name]
        if getattr(module, qualname, None) is not subject:
            print('@decorating', qualname)  # @decoraware
        else:
            print('wrapping()', qualname)   # decoraware()
        return subject
    

    这个例子将只打印它是如何应用的。

    >>> @decoraware
    ... def foo(): ...
    ...
    @decorating foo
    >>> decoraware(foo)
    wrapping() foo
    

    不过,同样的方法也可以用于在每个路径中运行任意代码。

    subject = inspect.unwrap(subject)


    同样的方法也可以用在CPython上。使用 sys._getframe(n).f_locals

    def decoraware(subject):
        """Decorator that is aware whether it was applied using `@deco` syntax"""
        modname, topname = subject.__module__, subject.__name__
        if getattr(sys.modules[modname], topname, None) is subject:
            print('wrapping()', topname, '[top-level]')
        else:
            at_frame = sys._getframe(1)
            if at_frame.f_locals.get(topname) is subject:
                print('wrapping()', topname, '[locals]')
            elif at_frame.f_globals.get(topname) is subject:
                print('wrapping()', topname, '[globals]')
            else:
                print('@decorating', topname)
        return subject
    

    请注意,类似于 pickle ,如果受试者 __qualname__ __name__ 被篡改或 del '从其定义命名空间中删除。

        4
  •  0
  •   Changaco    4 年前

    在前面两个答案的基础上,我编写了一个泛型函数,它应该在几乎所有实际情况下都能正常工作。我用python3.6、3.7和3.8测试了它。

    在将此函数复制粘贴到代码中之前,请确保使用 decorator module 相反。

    def am_I_called_as_a_decorator(default=False):
        """This function tries to determine how its caller was called.
    
        The value returned by this function should not be blindly trusted, it can
        sometimes be inaccurate.
    
        Arguments:
            default (bool): the fallback value to return when we're unable to determine
                            how the function was called
    
        >>> def f(*args):
        ...     if am_I_called_as_a_decorator():
        ...         print("called as decorator with args {!r}".format(args))
        ...         if len(args) == 1:
        ...             return args[0]
        ...         return f
        ...     else:
        ...         print("called normally with args {!r}".format(args))
        ...
        >>> f()
        called normally with args ()
        >>> @f                              #doctest: +ELLIPSIS
        ... def g(): pass
        ...
        called as decorator with args (<function g at ...>,)
        >>> @f()
        ... class Foobar: pass
        ...
        called as decorator with args ()
        called as decorator with args (<class 'state_chain.Foobar'>,)
        >>> @f(                             #doctest: +ELLIPSIS
        ...     'one long argument',
        ...     'another long argument',
        ... )
        ... def g(): pass
        ...
        called as decorator with args ('one long argument', 'another long argument')
        called as decorator with args (<function g at ...>,)
        >>> @f('one long argument',         #doctest: +ELLIPSIS
        ...    'another long argument')
        ... def g(): pass
        ...
        called as decorator with args ('one long argument', 'another long argument')
        called as decorator with args (<function g at ...>,)
        >>> @f(                             #doctest: +ELLIPSIS
        ...     # A weirdly placed comment
        ...   )
        ... @f
        ... def g(): pass
        ...
        called as decorator with args ()
        called as decorator with args (<function g at ...>,)
    
        """
    
        def get_indentation(line):
            for i, c in enumerate(line):
                if not c.isspace():
                    break
            return line[:i]
    
        # First, we try to look at the line where Python says the function call is.
        # Unfortunately, Python doesn't always give us the line we're interested in.
        call_frame = inspect.currentframe().f_back.f_back
        call_info = inspect.getframeinfo(call_frame, context=0)
        source_lines = linecache.getlines(call_info.filename)
        if not source_lines:
            # Reading the source code failed, return the fallback value.
            return default
        try:
            call_line = source_lines[call_info.lineno - 1]
        except IndexError:
            # The source file seems to have been modified.
            return default
        call_line_ls = call_line.lstrip()
        if call_line_ls.startswith('@'):
            # Note: there is a small probability of false positive here, if the
            # function call is on the same line as a decorator call.
            return True
        if call_line_ls.startswith('class ') or call_line_ls.startswith('def '):
            # Note: there is a small probability of false positive here, if the
            # function call is on the same line as a `class` or `def` keyword.
            return True
        # Next, we try to find and examine the line after the function call.
        # If that line doesn't start with a `class` or `def` keyword, then the
        # function isn't being called as a decorator.
        def_lineno = call_info.lineno
        while True:
            try:
                def_line = source_lines[def_lineno]
            except IndexError:
                # We've reached the end of the file.
                return False
            def_line_ls = def_line.lstrip()
            if def_line_ls[:1] in (')', '#', '@', ''):
                def_lineno += 1
                continue
            break
        if not (def_line_ls.startswith('class') or def_line_ls.startswith('def')):
            # Note: there is a small probability of false negative here, as we might
            # be looking at the wrong line.
            return False
        # Finally, we look at the lines above, taking advantage of the fact that a
        # decorator call is at the same level of indentation as the function or
        # class being decorated.
        def_line_indentation = get_indentation(def_line)
        for lineno in range(call_info.lineno - 1, 0, -1):
            line = source_lines[lineno - 1]
            line_indentation = get_indentation(line)
            if line_indentation == def_line_indentation:
                line_ls = line.lstrip()
                if line_ls[:1] in (')', ','):
                    continue
                return line_ls.startswith('@')
            elif len(line_indentation) < len(def_line_indentation):
                break
        return default