Jak ominąć definicję funkcji python za pomocą dekoratora?

66

Chciałbym wiedzieć, czy można kontrolować definicję funkcji Pythona w oparciu o ustawienia globalne (np. System operacyjny). Przykład:

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

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

Następnie, jeśli ktoś używa Linuksa, zostanie zastosowana pierwsza definicja, my_callbacka druga zostanie po cichu zignorowana.

Nie chodzi o określenie systemu operacyjnego, ale o definicję funkcji / dekoratorów.

Pedro
źródło
10
Ten drugi dekorator jest równoważny my_callback = windows(<actual function definition>)- więc nazwa my_callback zostanie zastąpiona, niezależnie od tego, co może zrobić dekorator. Jedynym sposobem, w jaki wersja Linux funkcji może znaleźć się w tej zmiennej, jest windows()jej zwrócenie - ale funkcja nie ma możliwości dowiedzenia się o wersji Linux. Myślę, że bardziej typowym sposobem osiągnięcia tego jest posiadanie definicji funkcji specyficznych dla systemu operacyjnego w osobnych plikach, a warunkowo importtylko jeden z nich.
jasonharper
7
Możesz spojrzeć na interfejs functools.singledispatch, który robi coś podobnego do tego, co chcesz. Tam registerdekorator wie o programie rozsyłającym (ponieważ jest to atrybut funkcji wysyłki i specyficzny dla tego konkretnego programu rozsyłającego), dzięki czemu może zwrócić program rozsyłający i uniknąć problemów z twoim podejściem.
user2357112 obsługuje Monikę
5
Chociaż to, co próbujesz tutaj zrobić, jest godne podziwu, warto wspomnieć, że większość CPython podąża za standardową „platformą kontrolną w if / elif / else”; na przykład uuid.getnode(). (To powiedziawszy, odpowiedź Todda tutaj jest całkiem dobra.)
Brad Solomon

Odpowiedzi:

58

Jeśli celem jest uzyskanie takiego samego efektu w kodzie, jaki ma #ifdef WINDOWS / #endif .. oto sposób na zrobienie tego (jestem na komputerze Mac).

Prosta obudowa, bez łączenia

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

Dzięki tej implementacji otrzymujesz taką samą składnię, jaką masz w swoim pytaniu.

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

Zasadniczo to, co robi powyższy kod, polega na przypisaniu zulu do zulu, jeśli platforma pasuje. Jeśli platforma nie pasuje, zwróci Zulu, jeśli została wcześniej zdefiniowana. Jeśli nie został zdefiniowany, zwraca funkcję zastępczą, która wywołuje wyjątek.

Dekoratorzy są koncepcyjnie łatwi do zrozumienia, jeśli weźmiesz to pod uwagę

@mydecorator
def foo():
    pass

jest analogiczny do:

foo = mydecorator(foo)

Oto implementacja wykorzystująca sparametryzowany dekorator:

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

Sparametryzowane dekoratory są analogiczne do foo = mydecorator(param)(foo).

Zaktualizowałem odpowiedź całkiem sporo. W odpowiedzi na komentarze rozszerzyłem swój pierwotny zakres o aplikacje do metod klasowych oraz o funkcje zdefiniowane w innych modułach. W tej ostatniej aktualizacji byłem w stanie znacznie zmniejszyć złożoność związaną z określeniem, czy funkcja została już zdefiniowana.

[Mała aktualizacja tutaj ... Po prostu nie mogłem tego odłożyć - to było zabawne ćwiczenie] Przeprowadziłem kilka testów tego i odkryłem, że działa ogólnie na wywołaniach - nie tylko zwykłych funkcjach; możesz również ozdobić deklaracje klasowe, czy to na żądanie, czy nie. I obsługuje wewnętrzne funkcje funkcji, więc takie rzeczy są możliwe (chociaż prawdopodobnie nie w dobrym stylu - to tylko kod testowy):

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

Powyżej pokazuje podstawowy mechanizm dekoratorów, jak uzyskać dostęp do zakresu dzwoniącego i jak uprościć wiele dekoratorów, które zachowują się podobnie, poprzez zdefiniowanie wewnętrznej funkcji zawierającej wspólny algorytm.

Łańcuchowe wsparcie

Aby wesprzeć tworzenie łańcuchów tych dekoratorów wskazujących, czy funkcja dotyczy więcej niż jednej platformy, dekorator można zaimplementować w następujący sposób:

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

W ten sposób wspierasz tworzenie łańcuchów:

>>> @macos
... @linux
... def foo():
...     print("works!")
...     
>>> foo()
works!
Todd
źródło
4
Zauważ, że działa to tylko wtedy, gdy macosi windowssą zdefiniowane w tym samym module co zulu. Wierzę, że spowoduje to również pozostawienie funkcji tak, Nonejakby funkcja nie została zdefiniowana dla bieżącej platformy, co doprowadziłoby do bardzo mylących błędów środowiska uruchomieniowego .
Brian
1
To nie zadziała w przypadku metod lub innych funkcji, które nie są zdefiniowane w zasięgu globalnym modułu.
user2357112 obsługuje Monikę
1
Dziękuję @Monica. Tak, nie zastanawiałem się nad użyciem tego w funkcjach członkowskich klasy ... w porządku .. Zobaczę, czy mogę uczynić mój kod bardziej ogólnym.
Todd
1
@Monica w porządku .. Zaktualizowałem kod, aby uwzględnić funkcje członków klasy. Czy możesz spróbować?
Todd
2
@Monica, w porządku .. Zaktualizowałem kod, aby uwzględnić metody klas i wykonałem trochę testów, aby upewnić się, że działa - nic obszernego .. jeśli chcesz go uruchomić, daj mi znać, jak to działa.
Todd
37

Chociaż @decoratorskładnia wygląda ładnie, uzyskuje się dokładnie takie samo zachowanie, jak pożądane za pomocą prostego 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

W razie potrzeby pozwala to również łatwo egzekwować, że niektóre przypadki pasują.

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
źródło
8
+1, jeśli i tak zamierzasz napisać dwie różne funkcje, to jest to właściwa droga. Prawdopodobnie chciałbym zachować oryginalne nazwy funkcji do debugowania (więc ślady stosu są poprawne): def callback_windows(...)a def callback_linux(...)potem if windows: callback = callback_windowsitd. Ale tak czy inaczej jest to o wiele łatwiejsze do odczytania, debugowania i utrzymania.
Seth
Zgadzam się, że jest to najprostsze podejście, aby zaspokoić rozważany przypadek użycia. Jednak pierwotne pytanie dotyczyło dekoratorów i tego, jak można je zastosować do deklaracji funkcji. Zatem zakres może wykraczać poza zwykłą logikę platformy.
Todd
3
Użyłbym elif, ponieważ nigdy nie będzie to oczekiwany przypadek, że więcej niż jeden z linux/ windows/ macOSbędzie prawdziwy. W rzeczywistości prawdopodobnie po prostu zdefiniowałbym jedną zmienną p = platform.system(), a następnie użyłbym if p == "Linux"etc, zamiast wielu flag boolowskich. Zmienne, które nie istnieją, nie mogą zostać zsynchronizowane.
chepner
@chepner Jeśli to oczywiste przypadki, wykluczają się wzajemnie, elifz pewnością ma swoje zalety - konkretnie, krocząca else+ raise, aby upewnić się, że co najmniej jeden przypadek zrobił mecz. Jeśli chodzi o ocenę predykatu, wolę, aby były one wstępnie ocenione - pozwala to uniknąć powielania i definiuje i używa oddzielenia. Nawet jeśli wynik nie jest przechowywany w zmiennych, istnieją teraz zakodowane wartości, które mogą wyjść z synchronizacji tak samo. I może nigdy nie pamiętam różne ciągi magiczne dla różnych środków, np platform.system() == "Windows"kontra sys.platform == "win32"...
MisterMiyagi
Możesz wyliczyć ciągi znaków, niezależnie od tego, czy są to podklasy Enum czy tylko z zestawem stałych.
chepner
8

Poniżej znajduje się jedna możliwa implementacja tego mechanika. Jak zauważono w komentarzach, może być preferowane wdrożenie interfejsu „master dispatcher”, takiego jak ten widziany w functools.singledispatch, w celu śledzenia stanu związanego z wieloma przeciążonymi definicjami. Mam nadzieję, że ta implementacja zapewni przynajmniej wgląd w problemy, z którymi możesz mieć do czynienia podczas opracowywania tej funkcji dla większej bazy kodu.

Testowałem tylko, że poniższa implementacja działa zgodnie ze specyfikacją w systemach Linux, więc nie mogę zagwarantować, że to rozwiązanie odpowiednio umożliwia tworzenie funkcji specjalistycznych dla platformy. Nie używaj tego kodu w środowisku produkcyjnym bez wcześniejszego dokładnego przetestowania go.

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

Aby użyć tego dekoratora, musimy przejść przez dwa poziomy pośredni. Najpierw musimy określić, na jaką platformę ma odpowiadać dekorator. Dokonuje się tego przez linię implement_linux = implement_for_os('Linux')i jej odpowiednik w oknie powyżej. Następnie musimy przekazać istniejącą definicję przeciążonej funkcji. Ten krok należy wykonać w miejscu definicji, jak pokazano poniżej.

Aby zdefiniować funkcję specjalizowaną w platformie, możesz teraz napisać:

@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():
   ...

Połączenia z some_function() zostaną odpowiednio wysłane do podanej definicji specyficznej dla platformy.

Osobiście nie radziłbym używać tej techniki w kodzie produkcyjnym. Moim zdaniem lepiej jest wyraźnie powiedzieć o zachowaniach zależnych od platformy w każdej lokalizacji, w której występują te różnice.

Brian
źródło
Czy nie byłby to @implement_for_os („linux”) itp.
lltt
@ th0nk Nie - funkcja implement_for_osnie zwraca samego dekoratora, ale zwraca funkcję, która wytworzy dekorator, gdy zostanie udostępniona poprzednia definicja danej funkcji.
Brian
5

Napisałem swój kod, zanim przeczytałem inne odpowiedzi. Po skończeniu kodu znalazłem, że kod @ Todda jest najlepszą odpowiedzią. W każdym razie zamieszczam swoją odpowiedź, ponieważ czułem się dobrze podczas rozwiązywania tego problemu. Nauczyłem się nowych rzeczy dzięki temu dobremu pytaniu. Wadą mojego kodu jest to, że istnieje obciążenie związane z wyszukiwaniem słowników za każdym razem, gdy wywoływane są funkcje.

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
źródło
0

Czystym rozwiązaniem byłoby utworzenie rejestru funkcji dedykowanych, który będzie wysyłany dalej sys.platform. To jest bardzo podobne do functools.singledispatch. Kod źródłowy tej funkcji stanowi dobry punkt wyjścia do implementacji niestandardowej wersji:

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

Teraz można go używać podobnie do 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

Rejestracja działa również bezpośrednio na nazwy funkcji:

@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')
gość
źródło