如何使用装饰器绕过python函数定义?

Ped*_*dro 70 python function python-3.x python-decorators

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

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

@windows
def my_callback(*args, **kwargs):
    print("Doing something @ Windows")
    return
Run Code Online (Sandbox Code Playgroud)

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

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

Tod*_*odd 61

如果目标是在您的代码中具有与 #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)
Run Code Online (Sandbox Code Playgroud)

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

>>> @macos
... def zulu():
...     print("world")
...     
>>> @windows
... def zulu():
...     print("hello")
...     
>>> zulu()
world
>>> 
Run Code Online (Sandbox Code Playgroud)

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

如果您记住,装饰器在概念上很容易弄清楚

@mydecorator
def foo():
    pass
Run Code Online (Sandbox Code Playgroud)

类似于:

foo = mydecorator(foo)
Run Code Online (Sandbox Code Playgroud)

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

>>> 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")
Run Code Online (Sandbox Code Playgroud)

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

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

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

>>> @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.")
Run Code Online (Sandbox Code Playgroud)

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

链式支持

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

>>> 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)
Run Code Online (Sandbox Code Playgroud)

这样你就支持链接:

>>> @macos
... @linux
... def foo():
...     print("works!")
...     
>>> foo()
works!
Run Code Online (Sandbox Code Playgroud)

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

  • 请注意,只有当“macos”和“windows”与“zulu”在同一模块中定义时,这才有效。我相信,如果没有为当前平台定义该函数,这也会导致该函数被保留为“无”,这将导致一些非常令人困惑的运行时错误。 (4认同)
  • @Monica,好吧..我已经更新了代码以涵盖类方法,并做了一些测试以确保它有效 - 没有什么广泛的..如果你想运行它,请让我知道它是如何进行的。 (2认同)

Mis*_*agi 41

虽然@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
Run Code Online (Sandbox Code Playgroud)

如果需要,这也允许轻松地强制执行某些情况确实匹配。

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")
Run Code Online (Sandbox Code Playgroud)

  • +1,如果您无论如何都要编写两个不同的函数,那么这就是要走的路。我可能想保留原始函数名称以进行调试(因此堆栈跟踪是正确的):“defcallback_windows(...)”和“defcallback_linux(...)”,然后是“ifwindows:callback=callback_windows”等等。但无论哪种方式,这都更容易阅读、调试和维护。 (9认同)
  • 我会使用“elif”,因为“linux”/“windows”/“macOS”中的一个以上的情况永远不会是“预期”的情况。事实上,我可能只定义一个变量 `p = platform.system()`,然后使用 `if p == "Linux"` 等,而不是多个布尔标志。不存在的变量不会失去同步。 (3认同)

Bri*_*ian 8

下面的代码通过根据 的值有条件地定义一个装饰函数来工作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')
Run Code Online (Sandbox Code Playgroud)

请注意,这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')

Run Code Online (Sandbox Code Playgroud)


Jun*_*ong 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)
Run Code Online (Sandbox Code Playgroud)