Wie kann man die Definition der Python-Funktion mit Decorator umgehen?

66

Ich würde gerne wissen, ob es möglich ist, die Python-Funktionsdefinition basierend auf globalen Einstellungen (z. B. Betriebssystem) zu steuern. Beispiel:

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

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

Wenn dann jemand Linux verwendet, wird die erste Definition von my_callbackverwendet und die zweite wird stillschweigend ignoriert.

Es geht nicht darum, das Betriebssystem zu bestimmen, sondern um Funktionsdefinitionen / Dekoratoren.

Pedro
quelle
10
Das zweite Dekorateur entspricht my_callback = windows(<actual function definition>)- so der Name my_callback wird überschrieben, unabhängig davon , was der Dekorateur tun könnte. Die Linux-Version der Funktion kann nur dann in diese Variable gelangen, wenn sie windows()zurückgegeben wird. Die Funktion kann jedoch nichts über die Linux-Version wissen. Ich denke, der typischere Weg, dies zu erreichen, besteht darin, die betriebssystemspezifischen Funktionsdefinitionen in separaten Dateien zu haben, und bedingt importnur in einer von ihnen.
Jasonharper
7
Vielleicht möchten Sie einen Blick auf die Benutzeroberfläche von werfen functools.singledispatch, die etwas Ähnliches wie das tut, was Sie wollen. Dort registerkennt der Dekorateur den Dispatcher (weil er ein Attribut der Dispatch-Funktion ist und für diesen bestimmten Dispatcher spezifisch ist), sodass er den Dispatcher zurückgeben und die Probleme mit Ihrem Ansatz vermeiden kann.
user2357112 unterstützt Monica
5
Während das, was Sie hier versuchen, bewundernswert ist, ist es erwähnenswert, dass der größte Teil von CPython einer Standard- "Check-Plattform in einem if / elif / else" folgt. zum Beispiel uuid.getnode(). (Das heißt, Todd's Antwort hier ist ziemlich gut.)
Brad Solomon

Antworten:

58

Wenn das Ziel darin besteht, den gleichen Effekt in Ihrem Code zu erzielen wie #ifdef WINDOWS / #endif .. Hier ist eine Möglichkeit, dies zu tun (ich bin übrigens auf einem Mac).

Einfacher Fall, keine Verkettung

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

Mit dieser Implementierung erhalten Sie also dieselbe Syntax, die Sie in Ihrer Frage haben.

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

Der obige Code weist Zulu im Wesentlichen Zulu zu, wenn die Plattform übereinstimmt. Wenn die Plattform nicht übereinstimmt, gibt sie zulu zurück, wenn es zuvor definiert wurde. Wenn es nicht definiert wurde, wird eine Platzhalterfunktion zurückgegeben, die eine Ausnahme auslöst.

Dekorateure sind konzeptionell leicht herauszufinden, wenn Sie dies berücksichtigen

@mydecorator
def foo():
    pass

ist analog zu:

foo = mydecorator(foo)

Hier ist eine Implementierung mit einem parametrisierten 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")

Parametrierte Dekorateure sind analog zu foo = mydecorator(param)(foo) .

Ich habe die Antwort ziemlich aktualisiert. Als Reaktion auf Kommentare habe ich den ursprünglichen Bereich um die Anwendung auf Klassenmethoden und auf Funktionen erweitert, die in anderen Modulen definiert sind. In diesem letzten Update konnte ich die Komplexität bei der Feststellung, ob eine Funktion bereits definiert wurde, erheblich reduzieren.

[Ein kleines Update hier ... Ich konnte das einfach nicht ablegen - es war eine lustige Übung] Ich habe dies noch einmal getestet und festgestellt, dass es im Allgemeinen bei Callables funktioniert - nicht nur bei normalen Funktionen. Sie können auch Klassendeklarationen dekorieren, ob aufrufbar oder nicht. Und es unterstützt innere Funktionen von Funktionen, so dass solche Dinge möglich sind (obwohl wahrscheinlich kein guter Stil - dies ist nur Testcode):

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

Das Obige zeigt den grundlegenden Mechanismus von Dekoratoren, wie auf den Bereich des Anrufers zugegriffen werden kann und wie mehrere Dekoratoren mit ähnlichem Verhalten vereinfacht werden können, indem eine interne Funktion mit dem definierten gemeinsamen Algorithmus definiert wird.

Verkettungsunterstützung

Um die Verkettung dieser Dekoratoren zu unterstützen und anzugeben, ob eine Funktion für mehr als eine Plattform gilt, könnte der Dekorator folgendermaßen implementiert werden:

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

Auf diese Weise unterstützen Sie die Verkettung:

>>> @macos
... @linux
... def foo():
...     print("works!")
...     
>>> foo()
works!
Todd
quelle
4
Beachten Sie, dass dies nur funktioniert, wenn macosund windowsim selben Modul wie definiert sind zulu. Ich glaube, dies wird auch dazu führen, dass die Funktion so belassen wird, als Noneob die Funktion nicht für die aktuelle Plattform definiert wäre, was zu einigen sehr verwirrenden Laufzeitfehlern führen würde.
Brian
1
Dies funktioniert nicht für Methoden oder andere Funktionen, die nicht in einem modulglobalen Bereich definiert sind.
user2357112 unterstützt Monica
1
Vielen Dank, dass Sie @Monica. Ja, ich hatte nicht berücksichtigt, dass dies für Mitgliedsfunktionen einer Klasse verwendet wird. Okay. Ich werde sehen, ob ich meinen Code allgemeiner gestalten kann.
Todd
1
@Monica okay .. Ich habe den Code aktualisiert, um die Funktionen der Klassenmitglieder zu berücksichtigen. Können Sie das versuchen?
Todd
2
@Monica, in Ordnung. Ich habe den Code aktualisiert, um Klassenmethoden abzudecken, und ein wenig getestet, um sicherzustellen, dass er funktioniert - nichts umfangreiches. Wenn Sie ihn ausprobieren möchten, lassen Sie mich wissen, wie er funktioniert.
Todd
37

Während die @decoratorSyntax gut aussieht, erhalten Sie mit einem einfachen genau das gleiche Verhalten wie gewünscht 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

Bei Bedarf kann auf diese Weise auch leicht erzwungen werden, dass ein Fall übereinstimmt.

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
quelle
8
+1, Wenn Sie sowieso zwei verschiedene Funktionen schreiben wollten, dann ist dies der richtige Weg. Ich würde wahrscheinlich die ursprünglichen Funktionsnamen für das Debuggen beibehalten wollen (damit die Stapelspuren korrekt sind): def callback_windows(...)und def callback_linux(...)dann if windows: callback = callback_windowsusw. Aber so oder so ist dies viel einfacher zu lesen, zu debuggen und zu warten.
Seth
Ich bin damit einverstanden, dass dies der einfachste Ansatz ist, um den von Ihnen beabsichtigten Anwendungsfall zu erfüllen. Die ursprüngliche Frage betraf jedoch Dekorateure und wie sie auf die Funktionsdeklaration angewendet werden können. Der Umfang kann also über die bedingte Plattformlogik hinausgehen.
Todd
3
Ich würde ein verwenden elif, da es niemals der erwartete Fall sein wird, dass mehr als eines von linux/ windows/ macOSwahr sein wird. In der Tat würde ich wahrscheinlich nur eine einzelne Variable definieren p = platform.system()und dann if p == "Linux"usw. anstelle mehrerer boolescher Flags verwenden. Nicht vorhandene Variablen können nicht nicht synchronisiert werden.
Chepper
@chepner Wenn es klar die Fälle gegenseitig ausschließen, elifsicherlich hat seine Vorteile - speziell ein nachlauf else+ , raiseum sicherzustellen , dass zumindest in einem Fall tat Spiel. Für die Bewertung des Prädikats bevorzuge ich eine Vorbewertung - dies vermeidet Doppelarbeit und entkoppelt die Definition und Verwendung. Auch wenn das Ergebnis nicht in Variablen gespeichert ist, gibt es jetzt fest codierte Werte, die trotzdem nicht mehr synchron sind. Ich kann mich nie an die verschiedenen magischen Saiten für die verschiedenen Mittel erinnern, zB platform.system() == "Windows"gegen sys.platform == "win32"...
MisterMiyagi
Sie können die Zeichenfolgen auflisten, unabhängig davon, ob es sich um eine Unterklasse Enumoder nur um eine Reihe von Konstanten handelt.
Chepper
8

Nachfolgend finden Sie eine mögliche Implementierung für diesen Mechaniker. Wie in den Kommentaren erwähnt, kann es vorzuziehen sein, eine "Master-Dispatcher" -Schnittstelle zu implementieren, wie die infunctools.singledispatch , um den Status zu verfolgen, der mit den mehreren überladenen Definitionen verbunden ist. Ich hoffe, dass diese Implementierung zumindest einen Einblick in die Probleme bietet, mit denen Sie möglicherweise bei der Entwicklung dieser Funktionalität für eine größere Codebasis zu kämpfen haben.

Ich habe nur getestet, ob die unten stehende Implementierung wie auf Linux-Systemen angegeben funktioniert, daher kann ich nicht garantieren, dass diese Lösung die Erstellung plattformspezifischer Funktionen angemessen ermöglicht. Bitte verwenden Sie diesen Code nicht in einer Produktionsumgebung, ohne ihn vorher gründlich zu testen.

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

Um diesen Dekorator verwenden zu können, müssen wir zwei Indirektionsebenen durcharbeiten. Zunächst müssen wir angeben, auf welche Plattform der Dekorateur reagieren soll. Dies wird durch die Linie implement_linux = implement_for_os('Linux')und das obige Gegenstück zum Fenster erreicht. Als nächstes müssen wir die vorhandene Definition der überladenen Funktion weitergeben. Dieser Schritt muss an der Definitionsstelle ausgeführt werden, wie unten gezeigt.

Um eine plattformspezifische Funktion zu definieren, können Sie jetzt Folgendes schreiben:

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

Anrufe an some_function() werden entsprechend an die angegebene plattformspezifische Definition weitergeleitet.

Persönlich würde ich nicht empfehlen, diese Technik im Produktionscode zu verwenden. Meiner Meinung nach ist es besser, das plattformabhängige Verhalten an jedem Ort, an dem diese Unterschiede auftreten, explizit zu beschreiben.

Brian
quelle
Wäre es nicht @implement_for_os ("linux") etc ...
lltt
@ th0nk Nein - Die Funktion implement_for_osgibt keinen Dekorator selbst zurück, sondern eine Funktion, die den Dekorator erzeugt, sobald die vorherige Definition der betreffenden Funktion vorliegt .
Brian
5

Ich habe meinen Code geschrieben, bevor ich andere Antworten gelesen habe. Nachdem ich meinen Code fertiggestellt hatte, fand ich, dass der Code von @ Todd die beste Antwort ist. Wie auch immer, ich poste meine Antwort, weil ich Spaß hatte, als ich dieses Problem löste. Dank dieser guten Frage habe ich neue Dinge gelernt. Der Nachteil meines Codes besteht darin, dass bei jedem Aufruf von Funktionen ein Overhead zum Abrufen von Wörterbüchern vorhanden ist.

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

Eine saubere Lösung wäre, eine dedizierte Funktionsregistrierung zu erstellen, die versendet sys.platform. Dies ist sehr ähnlich zu functools.singledispatch. Der Quellcode dieser Funktion bietet einen guten Ausgangspunkt für die Implementierung einer benutzerdefinierten Version:

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

Jetzt kann es ähnlich verwendet werden wie 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

Die Registrierung funktioniert auch direkt mit den Funktionsnamen:

@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')
ein Gast
quelle