Как обойти определение функции Python с декоратором?

66

Я хотел бы знать, возможно ли управлять определением функции 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будет использовано первое определение , а второе будет игнорироваться.

Дело не в определении ОС, а в определении функций / декораторах.

Pedro
источник
10
Этот второй декоратор эквивалентен my_callback = windows(<actual function definition>)- поэтому имя my_callback будет перезаписано независимо от того, что может делать декоратор. Единственный способ, которым версия функции Linux может оказаться в этой переменной, - это windows()вернуть ее, но функция не может знать о версии Linux. Я думаю, что более типичный способ сделать это - иметь определения функций для конкретной ОС в отдельных файлах, и условно importтолько в одном из них.
Джейсон Харпер
7
Возможно, вы захотите взглянуть на интерфейс functools.singledispatch, который делает что-то похожее на то, что вы хотите. Там registerдекоратор знает о диспетчере (потому что это атрибут функции диспетчеризации и специфичен для этого конкретного диспетчера), поэтому он может вернуть диспетчер и избежать проблем с вашим подходом.
user2357112 поддерживает Monica
5
Хотя то, что вы пытаетесь сделать здесь, достойно восхищения, стоит отметить, что большая часть CPython следует стандартной «проверочной платформе в if / elif / else»; например uuid.getnode(),. (Тем не менее, ответ Тодда здесь довольно хороший.)
Брэд Соломон

Ответы:

58

Если цель состоит в том, чтобы иметь такой же эффект в вашем коде, как у #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
>>> 

То, что делает код выше, по сути, назначает зулу зулу, если платформа соответствует. Если платформа не совпадает, она вернет зулу, если она была определена ранее. Если он не был определен, он возвращает функцию-заполнитель, которая вызывает исключение.

Декораторы концептуально легко понять, если учесть, что

@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
Обратите внимание, что это работает, только если macosи windowsопределены в том же модуле, что и zulu. Я полагаю, что это также приведет к тому, что функция будет оставлена ​​так, как Noneесли бы она не была определена для текущей платформы, что привело бы к очень запутанным ошибкам во время выполнения.
Брайан
1
Это не будет работать для методов или других функций, не определенных в глобальной области модуля.
user2357112 поддерживает Монику
1
Спасибо @Monica. Да, я не учел использовать это в функциях-членах класса ... хорошо ... я посмотрю, смогу ли я сделать свой код более универсальным.
Тодд
1
@Monica хорошо .. Я обновил код, чтобы учесть функции члена класса. Можешь попробовать?
Тодд
2
@Monica, хорошо .. Я обновил код, чтобы охватить методы класса, и провел небольшое тестирование, просто чтобы убедиться, что он работает - ничего особенного ... Если вы хотите запустить его, дайте мне знать, как он работает.
Тодд
37

Хотя @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

При необходимости это также позволяет легко обеспечить соответствие того или иного случая.

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")
MisterMiyagi
источник
8
+1, если вы все равно собираетесь написать две разные функции, то это путь. Вероятно, я бы хотел сохранить исходные имена функций для отладки (чтобы трассировки стека были правильными): def callback_windows(...)и def callback_linux(...), затем if windows: callback = callback_windows, и т. Д. Но в любом случае это легче читать, отлаживать и поддерживать.
Сет
Я согласен, что это самый простой подход для удовлетворения сценария использования, который вы имеете в виду. Однако первоначальный вопрос был о декораторах и о том, как их можно применять для объявления функций. Таким образом, область действия может выходить за рамки просто условной логики платформы.
Тодд
3
Я бы использовать elif, так как он никогда не станет ожидать , так , что более чем один из linux/ windows/ macOSбудет истинным. На самом деле, я бы просто определил одну переменную p = platform.system(), затем использовал if p == "Linux"и т. Д., А не несколько логических флагов. Переменные, которые не существуют, не могут быть синхронизированы.
Чепнер
@chepner Если это ясно случаи являются взаимоисключающими, elifбезусловно , имеет свои преимущества - в частности, замыкающие else+ , raiseчтобы гарантировать , что по крайней мере один случай сделал матч. Что касается оценки предиката, я предпочитаю предварительно оценивать их - это позволяет избежать дублирования и разъединяет определение и использование. Даже если результат не сохраняется в переменных, теперь существуют жестко закодированные значения, которые могут точно так же не синхронизироваться. Я никогда не могу вспомнить различные магические строки для разных средств, например, platform.system() == "Windows"против 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')и аналогом ее Окна выше. Далее нам нужно передать существующее определение перегружаемой функции. Этот шаг должен быть выполнен на сайте определения, как показано ниже.

Чтобы определить платформо-специализированную функцию, вы можете написать следующее:

@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") и т. Д.
11
@ th0nk Нет - функция implement_for_osне возвращает сам декоратор, а скорее возвращает функцию, которая создаст декоратор, как только будет предоставлено предыдущее определение рассматриваемой функции.
Брайан
5

Я написал свой код, прежде чем читать другие ответы. После того, как я закончил свой код, я нашел, что код @ Тодда - лучший ответ. В любом случае я публикую свой ответ, потому что мне было весело, когда я решал эту проблему. Благодаря этому хорошему вопросу я узнал что-то новое. Недостаток моего кода заключается в том, что при каждом вызове функций возникают словари для извлечения словарей.

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)
Junyeong Jeong
источник
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')
Гость
источник