【问题标题】:How to bypass python function definition with decorator?如何用装饰器绕过python函数定义?
【发布时间】:2020-05-31 07:55:15
【问题描述】:

我想知道是否可以根据全局设置(例如操作系统)来控制 Python 函数定义。示例:

@linux
def my_callback(*args, **kwargs):
    print("Doing something @ Linux")
    return

@windows
def my_callback(*args, **kwargs):
    print("Doing something @ Windows")
    return

然后,如果有人使用 Linux,将使用my_callback 的第一个定义,而第二个将被忽略。

这不是关于确定操作系统,而是关于函数定义/装饰器。

【问题讨论】:

  • 第二个装饰器等同于my_callback = windows(<actual function definition>) - 所以名称my_callback 被覆盖,不管装饰器可能做什么。该函数的 Linux 版本可能最终出现在该变量中的唯一方法是 windows() 返回它 - 但该函数无法知道 Linux 版本。我认为实现这一点的更典型方法是将特定于操作系统的函数定义放在单独的文件中,并且有条件地 import 只有其中一个。
  • 你可能想看看functools.singledispatch 的界面,它的功能与你想要的类似。在那里,register 装饰器知道调度程序(因为它是调度函数的一个属性,并且特定于该特定调度程序),因此它可以返回调度程序并避免您的方法出现问题。
  • 虽然您在这里尝试做的事情令人钦佩,但值得一提的是,大多数 CPython 都遵循标准的“if/elif/else 中的检查平台”;例如,uuid.getnode()。 (也就是说,Todd 在这里的回答非常好。)

标签: python python-3.x function python-decorators


【解决方案1】:

下面的代码通过根据platform.system 的值有条件地定义一个修饰函数来工作。如果platform.system 匹配选定的字符串,则函数将按原样传递。但是当platform.system 不匹配时,如果还没有给出有效的定义,则该函数将被替换为引发NotImplemented 错误的函数。

我只在 Linux 系统上测试过这段代码,所以在其他平台上使用之前一定要自己测试一下。

import platform
from functools import wraps
from typing import Callable, Optional


def implement_for_os(os_name: str):
    """
    Produce a decorator that defines a function only if the
    platform returned by `platform.system` matches the given `os_name`.
    Otherwise, replace the function with one that raises `NotImplementedError`.
    """
    def decorator(previous_definition: Optional[Callable]):
        def _decorator(func: Callable):
            if previous_definition and hasattr(previous_definition, '_implemented_for_os'):
                # This function was already implemented for this platform. Leave it unchanged.
                return previous_definition
            elif platform.system() == os_name:
                # The current function is the correct impementation for this platform.
                # Mark it as such, and return it unchanged.
                func._implemented_for_os = True
                return func
            else:
                # This function has not yet been implemented for the current platform
                @wraps(func)
                def _not_implemented(*args, **kwargs):
                    raise NotImplementedError(
                        f"The function {func.__name__} is not defined"
                        f" for the platform {platform.system()}"
                    )

                return _not_implemented
        return _decorator

    return decorator


implement_linux = implement_for_os('Linux')

implement_windows = implement_for_os('Windows')

请注意,implement_for_os 本身并不是装饰器。它的工作是在给定与您希望装饰器响应的平台匹配的字符串时构建装饰器。

一个完整的例子如下所示:

@implement_linux(None)
def some_function():
    print('Linux')

@implement_windows(some_function)
def some_function():
   print('Windows')

implement_other_platform = implement_for_os('OtherPlatform')

@implement_other_platform(some_function)
def some_function():
   print('Other platform')

【讨论】:

    【解决方案2】:

    如果目标是在您的代码中产生与 #ifdef WINDOWS / #endif 相同的效果。这是一种方法(顺便说一句,我在 mac 上)。

    简单案例,无链接

    >>> def _ifdef_decorator_impl(plat, func, frame):
    ...     if platform.system() == plat:
    ...         return func
    ...     elif func.__name__ in frame.f_locals:
    ...         return frame.f_locals[func.__name__]
    ...     else:
    ...         def _not_implemented(*args, **kwargs):
    ...             raise NotImplementedError(
    ...                 f"Function {func.__name__} is not defined "
    ...                 f"for platform {platform.system()}.")
    ...         return _not_implemented
    ...             
    ...
    >>> def windows(func):
    ...     return _ifdef_decorator_impl('Windows', func, sys._getframe().f_back)
    ...     
    >>> def macos(func):
    ...     return _ifdef_decorator_impl('Darwin', func, sys._getframe().f_back)
    

    因此,通过此实现,您将获得与问题相同的语法。

    >>> @macos
    ... def zulu():
    ...     print("world")
    ...     
    >>> @windows
    ... def zulu():
    ...     print("hello")
    ...     
    >>> zulu()
    world
    >>> 
    

    上面的代码本质上是在平台匹配时将 zulu 分配给 zulu。如果平台不匹配,如果之前已定义,它将返回 zulu。如果未定义,则返回一个引发异常的占位符函数。

    如果您牢记这一点,装饰器在概念上很容易理解

    @mydecorator
    def foo():
        pass
    

    类似于:

    foo = mydecorator(foo)
    

    这是一个使用参数化装饰器的实现:

    >>> def ifdef(plat):
    ...     frame = sys._getframe().f_back
    ...     def _ifdef(func):
    ...         return _ifdef_decorator_impl(plat, func, frame)
    ...     return _ifdef
    ...     
    >>> @ifdef('Darwin')
    ... def ice9():
    ...     print("nonsense")
    

    参数化装饰器类似于foo = mydecorator(param)(foo)

    我已经更新了很多答案。作为对 cme​​ts 的回应,我扩展了它的原始范围以包括对类方法的应用以及涵盖在其他模块中定义的函数。在最后一次更新中,我已经能够大大降低确定函数是否已定义所涉及的复杂性。

    [这里有一点更新......我只是无法放下这个 - 这是一个有趣的练习]我一直在对此进行更多测试,发现它通常适用于可调用对象 - 而不仅仅是普通函数;您还可以装饰类声明是否可调用。而且它支持函数的内部函数,所以这样的事情是可能的(虽然可能不是很好的风格 - 这只是测试代码):

    >>> @macos
    ... class CallableClass:
    ...     
    ...     @macos
    ...     def __call__(self):
    ...         print("CallableClass.__call__() invoked.")
    ...     
    ...     @macos
    ...     def func_with_inner(self):
    ...         print("Defining inner function.")
    ...         
    ...         @macos
    ...         def inner():
    ...             print("Inner function defined for Darwin called.")
    ...             
    ...         @windows
    ...         def inner():
    ...             print("Inner function for Windows called.")
    ...         
    ...         inner()
    ...         
    ...     @macos
    ...     class InnerClass:
    ...         
    ...         @macos
    ...         def inner_class_function(self):
    ...             print("Called inner_class_function() Mac.")
    ...             
    ...         @windows
    ...         def inner_class_function(self):
    ...             print("Called inner_class_function() for windows.")
    

    上面演示了装饰器的基本机制,如何访问调用者的作用域,以及如何通过定义包含通用算法的内部函数来简化具有相似行为的多个装饰器。

    链式支持

    为了支持链接这些装饰器来指示一个函数是否适用于多个平台,装饰器可以这样实现:

    >>> class IfDefDecoratorPlaceholder:
    ...     def __init__(self, func):
    ...         self.__name__ = func.__name__
    ...         self._func    = func
    ...         
    ...     def __call__(self, *args, **kwargs):
    ...         raise NotImplementedError(
    ...             f"Function {self._func.__name__} is not defined for "
    ...             f"platform {platform.system()}.")
    ...
    >>> def _ifdef_decorator_impl(plat, func, frame):
    ...     if platform.system() == plat:
    ...         if type(func) == IfDefDecoratorPlaceholder:
    ...             func = func._func
    ...         frame.f_locals[func.__name__] = func
    ...         return func
    ...     elif func.__name__ in frame.f_locals:
    ...         return frame.f_locals[func.__name__]
    ...     elif type(func) == IfDefDecoratorPlaceholder:
    ...         return func
    ...     else:
    ...         return IfDefDecoratorPlaceholder(func)
    ...
    >>> def linux(func):
    ...     return _ifdef_decorator_impl('Linux', func, sys._getframe().f_back)
    

    这样你就支持链接:

    >>> @macos
    ... @linux
    ... def foo():
    ...     print("works!")
    ...     
    >>> foo()
    works!
    

    下面的 cmets 在当前状态下并不真正适用于此解决方案。它们是在寻找解决方案的第一次迭代中制作的,不再适用。例如语句,“请注意,这仅在 macOS 和 Windows 与 zulu 定义在同一模块中时才有效。” (upvoted 4次) 适用于最早的版本,但已在当前版本中解决;以下大多数陈述都是这种情况。 奇怪的是,验证当前解决方案的 cmets 已被删除。

    【讨论】:

    • 请注意,这仅在 macoswindowszulu 定义在同一模块中时才有效。我相信如果没有为当前平台定义函数,这也会导致函数保留为None,这会导致一些非常令人困惑的运行时错误。
    • 这不适用于未在模块全局范围内定义的方法或其他函数。
    • 谢谢@Monica。是的,我没有考虑在类的成员函数上使用它..好吧..我会看看我是否可以让我的代码更通用。
    • @Monica 好的。我更新了代码以说明类成员函数。你可以试试这个吗?
    • @Monica,好吧..我已经更新了代码以涵盖类方法并进行了一些测试以确保它有效 - 没有任何广泛的..如果你想试一试,告诉我进展如何。
    【解决方案3】:

    一个干净的解决方案是创建一个在sys.platform 上调度的专用函数注册表。这与functools.singledispatch 非常相似。这个函数的source code 为实现自定义版本提供了一个很好的起点:

    import functools
    import sys
    import types
    
    
    def os_dispatch(func):
        registry = {}
    
        def dispatch(platform):
            try:
                return registry[platform]
            except KeyError:
                return registry[None]
    
        def register(platform, func=None):
            if func is None:
                if isinstance(platform, str):
                    return lambda f: register(platform, f)
                platform, func = platform.__name__, platform  # it is a function
            registry[platform] = func
            return func
    
        def wrapper(*args, **kw):
            return dispatch(sys.platform)(*args, **kw)
    
        registry[None] = func
        wrapper.register = register
        wrapper.dispatch = dispatch
        wrapper.registry = types.MappingProxyType(registry)
        functools.update_wrapper(wrapper, func)
        return wrapper
    

    现在可以像singledispatch一样使用:

    @os_dispatch  # fallback in case OS is not supported
    def my_callback():
        print('OS not supported')
    
    @my_callback.register('linux')
    def _():
        print('Doing something @ Linux')
    
    @my_callback.register('windows')
    def _():
        print('Doing something @ Windows')
    
    my_callback()  # dispatches on sys.platform
    

    注册也可以直接作用于函数名:

    @os_dispatch
    def my_callback():
        print('OS not supported')
    
    @my_callback.register
    def linux():
        print('Doing something @ Linux')
    
    @my_callback.register
    def windows():
        print('Doing something @ Windows')
    

    【讨论】:

      【解决方案4】:

      虽然@decorator 语法看起来不错,但您可以通过简单的if 获得完全相同的 行为。

      linux = platform.system() == "Linux"
      windows = platform.system() == "Windows"
      macos = platform.system() == "Darwin"
      
      if linux:
          def my_callback(*args, **kwargs):
              print("Doing something @ Linux")
              return
      
      if windows:
          def my_callback(*args, **kwargs):
              print("Doing something @ Windows")
              return
      

      如果需要,这也允许轻松强制执行 some 案例确实匹配。

      if linux:
          def my_callback(*args, **kwargs):
              print("Doing something @ Linux")
              return
      
      elif windows:
          def my_callback(*args, **kwargs):
              print("Doing something @ Windows")
              return
      
      else:
           raise NotImplementedError("This platform is not supported")
      

      【讨论】:

      • +1,如果你要写两个不同的函数,那么这是要走的路。我可能想保留原始函数名称以进行调试(因此堆栈跟踪是正确的):def callback_windows(...)def callback_linux(...),然后是 if windows: callback = callback_windows 等。但无论哪种方式,这都更容易阅读、调试和维护.
      • 我同意这是满足您心目中的用例的最简单方法。然而,最初的问题是关于装饰器以及如何将它们应用于函数声明。所以范围可能不仅仅是条件平台逻辑。
      • 我会使用elif,因为它永远不会是预期的情况,不止一个linux/windows/macOS会是真的。事实上,我可能只定义一个变量p = platform.system(),然后使用if p == "Linux" 等,而不是多个布尔标志。不存在的变量不能不同步。
      • @chepner 如果很明显这些案例是互斥的,elif 肯定有其优势——具体来说,一个尾随 else + raise 以确保至少有一个案例 did 匹配。至于评估谓词,我更喜欢预先评估它们——它避免了重复并将定义和使用解耦。即使结果没有存储在变量中,现在也有硬编码的值可能会不同步。我可以永远记住不同方式的各种魔术字符串,例如platform.system() == "Windows"sys.platform == "win32",...
      • 您可以枚举字符串,无论是使用Enum 的子类还是仅使用一组常量。
      【解决方案5】:

      我在阅读其他答案之前编写了代码。完成我的代码后,我发现@Todd 的代码是最好的答案。 无论如何,我发布我的答案是因为我在解决这个问题时感到很有趣。多亏了这个好问题,我学到了新东西。 我的代码的缺点是每次调用函数时都存在检索字典的开销。

      from collections import defaultdict
      import inspect
      import os
      
      
      class PlatformFunction(object):
          mod_funcs = defaultdict(dict)
      
          @classmethod
          def get_function(cls, mod, func_name):
              return cls.mod_funcs[mod][func_name]
      
          @classmethod
          def set_function(cls, mod, func_name, func):
              cls.mod_funcs[mod][func_name] = func
      
      
      def linux(func):
          frame_info = inspect.stack()[1]
          mod = inspect.getmodule(frame_info.frame)
          if os.environ['OS'] == 'linux':
              PlatformFunction.set_function(mod, func.__name__, func)
      
          def call(*args, **kwargs):
              return PlatformFunction.get_function(mod, func.__name__)(*args,
                                                                       **kwargs)
      
          return call
      
      
      def windows(func):
          frame_info = inspect.stack()[1]
          mod = inspect.getmodule(frame_info.frame)
          if os.environ['OS'] == 'windows':
              PlatformFunction.set_function(mod, func.__name__, func)
      
          def call(*args, **kwargs):
              return PlatformFunction.get_function(mod, func.__name__)(*args,
                                                                       **kwargs)
      
          return call
      
      
      @linux
      def myfunc(a, b):
          print('linux', a, b)
      
      
      @windows
      def myfunc(a, b):
          print('windows', a, b)
      
      
      if __name__ == '__main__':
          myfunc(1, 2)
      

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 2022-01-09
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2011-12-20
        相关资源
        最近更新 更多