如何使用Decorator绕过python函数定义?


66

我想知道是否有可能基于全局设置(例如OS)控制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而第二个定义将被忽略。

它与确定操作系统无关,而与功能定义/修饰符有关。


10
第二个修饰符等效于my_callback = windows(<actual function definition>)-因此,无论修饰符可能做什么,该名称my_callback 都将被覆盖。windows()返回该变量的Linux版本的唯一方法是返回该变量-但是该函数无法得知Linux版本。我认为实现此目的的更典型方法是将操作系统特定的功能定义放在单独的文件中,并且有条件地import仅其中之一。
jasonharper

7
您可能需要看一下的界面functools.singledispatch,该界面的功能类似于您想要的。register装饰者在那里知道了调度程序(因为它是调度功能的属性,并且特定于该特定调度程序),因此它可以返回调度程序并避免您的方法出现问题。
user2357112支持Monica

5
尽管您在这里尝试做的事令人钦佩,但值得一提的是,大多数CPython都遵循标准的“ if / elif / else中的检查平台”。例如,uuid.getnode()。(也就是说,托德在这里的答案是相当不错的。)
布拉德·所罗门

Answers:


58

如果目标是使代码中的效果与#ifdef WINDOWS / #endif具有的效果相同。.这是一种实现方法(我在Mac btw上)。

简单案例,无需链接

>>> 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)

我已经相当多地更新了答案。为了回应评论,我扩展了其原始范围,将应用程序包括到类方法中,并涵盖了其他模块中定义的功能。在最后的更新中,我已经能够大大降低确定功能是否已经定义的复杂性。

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

>>> @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!

4
请注意,仅当macoswindows在与相同的模块中定义时才有效zulu。我相信这也将导致该函数被遗留,就None好像该函数未为当前平台定义一样,这将导致一些非常令人困惑的运行时错误。
布赖恩

1
这不适用于未在模块全局范围内定义的方法或其他函数。
user2357112支持Monica

1
谢谢@莫妮卡。是的,我没有考虑在类的成员函数上使用它。..好吧..我将看看是否可以使我的代码更通用。
托德

1
@Monica好的..我更新了代码以说明类成员函数。你可以试试看吗?
托德(Todd)

2
@Monica,好吧..我已经更新了代码以覆盖类方法,并做了一些测试,以确保它能正常工作-没什么大不了的。
托德

37

虽然@decorator语法看起来不错,但是您可以通过simple 获得期望完全相同的行为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

如果需要,这还可以轻松地强制某些情况确实匹配。

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")

8
+1,如果您仍然要编写两个不同的函数,那么这就是要走的路。我可能希望保留原始函数名称以进行调试(因此堆栈跟踪是正确的):def callback_windows(...)def callback_linux(...),然后if windows: callback = callback_windows是等等。但是无论哪种方式,这都更易于阅读,调试和维护。
赛斯

我同意这是满足您所想到的用例的最简单方法。但是,最初的问题是关于装饰器以及如何将它们应用于函数声明。因此,范围可能不仅限于条件平台逻辑。
托德

3
我会使用elif,因为永远不会出现/ / 多个以上的预期情况。实际上,我可能只定义一个变量,然后使用,而不是多个布尔标志。不存在的变量不会不同步。linuxwindowsmacOSp = platform.system()if p == "Linux"
chepner

@chepner如果它是清除的情况下是相互排斥的,elif肯定有它的优势-特别是尾随else+ raise,以保证至少有一个案件没有比赛。至于评估谓词,我更喜欢对其进行预先评估–避免重复,并使定义和使用脱钩。即使结果没有存储在变量中,现在也有一些硬编码的值可能会完全不同步。我永远都记不起来各种手段不同的魔术弦,例如platform.system() == "Windows"vs sys.platform == "win32",...
MisterMiyagi

您可以枚举字符串,无论是使用的子类Enum还是仅使用一组常量。
chepner

8

以下是该机制的一种可能的实现。如评论中所述,可能更可取的是实现一个“主调度程序”接口,例如所见functools.singledispatch,以跟踪与多个重载定义关联的状态。我的希望是,这种实现方式至少可以为您在为更大的代码库开发此功能时可能要解决的问题提供一些见解。

我仅测试了以下实现可以在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 provided 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_linux = implement_for_os('Linux')及其上方的Window对应项完成的。接下来,我们需要传递要重载的函数的现有定义。该步骤必须在定义站点上完成,如下所示。

要定义平台专用功能,您现在可以编写以下内容:

@implement_linux(None)
def some_function():
    ...

@implement_windows(some_function)
def some_function():
   ...

implement_other_platform = implement_for_os('OtherPlatform')

@implement_other_platform(some_function)
def some_function():
   ...

对的调用some_function()将被适当地调度到所提供的特定于平台的定义。

就个人而言,我不建议在生产代码中使用此技术。我认为,最好在发生这些差异的每个位置都明确说明与平台相关的行为。


是不是@implement_for_os(“ linux”)等...
th0nk-

@ th0nk否-该函数implement_for_os本身不返回装饰器,而是返回一个函数,该函数一旦提供了该函数的先前定义,便会生成装饰器。
布赖恩

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)

0

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

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')
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.