Як обійти визначення функції 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 буде використано , а друге буде мовчки ігноруватися.

Йдеться не про визначення ОС, а про визначення функції / декораторів.


10
Цей другий декоратор еквівалентний my_callback = windows(<actual function definition>)- тому ім'я my_callback буде перезаписано, незалежно від того, що може зробити декоратор. Єдиний спосіб, коли версія Linux може бути в цій змінній, це windows()повернути її - але функція не може знати про версію Linux. Я думаю, що більш типовим способом досягнення цього є визначення специфічних для ОС функцій в окремих файлах і, умовно, importлише в одному з них.
jasonharper

7
Ви можете поглянути на інтерфейс functools.singledispatch, який робить щось подібне до того, що ви хочете. Там registerдекоратор знає про диспетчера (адже це атрибут функції диспетчеризації, характерний саме для конкретного диспетчера), тому він може повернути диспетчера та уникнути проблем із вашим підходом.
user2357112 підтримує Моніку

5
Хоча те, що ви намагаєтеся зробити тут, є захоплюючим, варто згадати, що більшість CPython дотримується стандартної "контрольної платформи в if / elif / else"; наприклад, uuid.getnode(). (Отож, відповідь Тодда тут досить хороша.)
Бред Соломон

Відповіді:


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

Те, що робиться вище кодом, по суті, призначає зулу зулу, якщо платформа збігається. Якщо платформа не збігається, вона поверне зулу, якщо вона була визначена раніше. Якщо його не було визначено, він повертає функцію заповнення місця, яка викликає виняток.

Декораторів концептуально легко зрозуміти, якщо ви це пам’ятаєте

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

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або просто набором констант.
чепнер

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") тощо ...
lltt

@ 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')
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.