Come bypassare la definizione della funzione Python con Decorator?


66

Vorrei sapere se è possibile controllare la definizione della funzione Python in base alle impostazioni globali (ad es. OS). Esempio:

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

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

Quindi, se qualcuno sta usando Linux, verrà utilizzata la prima definizione di my_callbacke la seconda verrà silenziosamente ignorata.

Non si tratta di determinare il sistema operativo, si tratta di definizione delle funzioni / decoratori.


10
Quel secondo decoratore equivale a my_callback = windows(<actual function definition>)- quindi il nome my_callback verrà sovrascritto, indipendentemente da ciò che il decoratore potrebbe fare. L'unico modo in cui la versione Linux della funzione potrebbe finire in quella variabile è se windows()restituita, ma la funzione non ha modo di conoscere la versione Linux. Penso che il modo più tipico per ottenere questo risultato sia avere le definizioni delle funzioni specifiche del sistema operativo in file separati, e condizionatamente importsolo uno di essi.
Jasonharper,

7
Potresti dare un'occhiata all'interfaccia di functools.singledispatch, che fa qualcosa di simile a quello che vuoi. Lì, il registerdecoratore conosce il dispatcher (perché è un attributo della funzione di dispacciamento e specifico per quel particolare dispatcher), quindi può restituire il dispatcher ed evitare i problemi con il tuo approccio.
user2357112 supporta Monica il

5
Mentre quello che stai cercando di fare qui è ammirevole, vale la pena ricordare che la maggior parte di CPython segue una "piattaforma di controllo standard in un if / elif / else"; per esempio uuid.getnode(),. (Detto questo, la risposta di Todd qui è abbastanza buona.)
Brad Solomon il

Risposte:


58

Se l'obiettivo è avere lo stesso tipo di effetto nel tuo codice che ha #ifdef WINDOWS / #endif .. ecco un modo per farlo (sono su un mac btw).

Caso semplice, senza concatenamento

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

Quindi con questa implementazione ottieni la stessa sintassi che hai nella tua domanda.

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

Ciò che sta facendo il codice sopra, in sostanza, è assegnare zulu a zulu se la piattaforma corrisponde. Se la piattaforma non corrisponde, restituirà zulu se è stata precedentemente definita. Se non è stato definito, restituisce una funzione segnaposto che genera un'eccezione.

I decoratori sono concettualmente facili da capire se lo ricordi

@mydecorator
def foo():
    pass

è analogo a:

foo = mydecorator(foo)

Ecco un'implementazione che utilizza un decoratore con parametri:

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

I decoratori con parametri sono analoghi a foo = mydecorator(param)(foo).

Ho aggiornato abbastanza la risposta. In risposta ai commenti, ho ampliato il suo ambito originale per includere l'applicazione ai metodi di classe e per coprire le funzioni definite in altri moduli. In questo ultimo aggiornamento, sono stato in grado di ridurre notevolmente la complessità necessaria per determinare se una funzione è già stata definita.

[Un piccolo aggiornamento qui ... Non riuscivo proprio a metterlo giù - è stato un esercizio divertente] Ho fatto qualche altro test su questo, e ho scoperto che funziona generalmente su callable - non solo funzioni ordinarie; puoi anche decorare le dichiarazioni di classe che possono essere richiamate o meno. E supporta le funzioni interne di funzioni, quindi cose come questa sono possibili (anche se probabilmente non è un buon stile - questo è solo un codice di prova):

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

Quanto sopra dimostra il meccanismo di base dei decoratori, come accedere all'ambito del chiamante e come semplificare più decoratori che hanno un comportamento simile avendo una funzione interna contenente l'algoritmo comune definito.

Supporto per il concatenamento

Per supportare il concatenamento di questi decoratori indicando se una funzione si applica a più di una piattaforma, il decoratore potrebbe essere implementato in questo modo:

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

In questo modo supporti il ​​concatenamento:

>>> @macos
... @linux
... def foo():
...     print("works!")
...     
>>> foo()
works!

4
Nota che funziona solo se macose windowssono definiti nello stesso modulo di zulu. Ritengo che ciò comporterà anche che la funzione venga lasciata come Nonese la funzione non fosse definita per la piattaforma corrente, il che porterebbe a errori di runtime molto confusi.
Brian,

1
Questo non funzionerà con metodi o altre funzioni non definite in un ambito globale del modulo.
user2357112 supporta Monica il

1
Grazie @Monica. Sì, non avevo tenuto conto dell'utilizzo di questo nelle funzioni membro di una classe .. va bene ... Vedrò se posso rendere il mio codice più generico.
Todd

1
@Monica ok .. Ho aggiornato il codice per tenere conto delle funzioni dei membri della classe. Puoi provarlo?
Todd,

2
@Monica, va bene .. Ho aggiornato il codice per coprire i metodi di classe e ho fatto un po 'di test solo per assicurarmi che funzionasse - niente di esteso .. se vuoi provarlo, fammi sapere come va.
Todd

37

Mentre la @decoratorsintassi sembra carina, ottieni lo stesso comportamento desiderato con un semplice 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

Se necessario, ciò consente anche di applicare facilmente la corrispondenza di alcuni casi.

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, Se avessi comunque scritto due diverse funzioni, questa è la strada da percorrere. Probabilmente vorrei preservare i nomi delle funzioni originali per il debug (quindi le tracce dello stack sono corrette): def callback_windows(...)e def callback_linux(...), quindi if windows: callback = callback_windows, ecc. Ma in entrambi i casi questo è molto più facile da leggere, eseguire il debug e mantenere.
Seth,

Sono d'accordo che questo è l'approccio più semplice per soddisfare il caso d'uso che hai in mente. Tuttavia, la domanda originale riguardava i decoratori e come potevano essere applicati alla dichiarazione di funzione. Quindi l'ambito potrebbe andare oltre la logica della piattaforma condizionale.
Todd,

3
Userei un elif, poiché non sarà mai il caso previsto che più di uno di linux/ windows/ macOSsarà vero. In effetti, probabilmente definirei solo una singola variabile p = platform.system(), quindi uso if p == "Linux", ecc. Anziché più flag booleani. Le variabili che non esistono non possono andare fuori sincrono.
Chepner,

@chepner Se è chiaro casi si escludono a vicenda, elifha sicuramente i suoi vantaggi - in particolare, un trailing else+ raiseper assicurare che almeno in un caso ha fatto partita. Per quanto riguarda la valutazione del predicato, preferisco averli pre-valutati - evita la duplicazione e disaccoppia la definizione e l'uso. Anche se il risultato non è archiviato in variabili, ora ci sono valori hardcoded che possono andare fuori sincrono allo stesso modo. Non riesco mai a ricordare le varie corde magiche per i diversi mezzi, ad esempio platform.system() == "Windows"contro sys.platform == "win32"...
MisterMiyagi,

È possibile enumerare le stringhe, con una sottoclasse Enumo solo un insieme di costanti.
Chepner,

8

Di seguito è una possibile implementazione per questo meccanico. Come notato nei commenti, può essere preferibile implementare un'interfaccia "master dispatcher", come quella vista in precedenza functools.singledispatch, per tenere traccia dello stato associato alle molteplici definizioni sovraccaricate. La mia speranza è che questa implementazione offrirà almeno alcune informazioni sui problemi che potresti dover affrontare quando sviluppi questa funzionalità per una base di codice più ampia.

Ho solo testato che l'implementazione di seguito funziona come specificato sui sistemi Linux, quindi non posso garantire che questa soluzione consenta adeguatamente la creazione di funzioni specializzate in piattaforma. Non utilizzare questo codice in un'impostazione di produzione senza prima testarlo a fondo.

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

Per utilizzare questo decoratore, dobbiamo lavorare attraverso due livelli di indiretta. Innanzitutto, dobbiamo specificare a quale piattaforma vogliamo che il decoratore risponda. Ciò è realizzato dalla linea implement_linux = implement_for_os('Linux')e dalla sua controparte di cui sopra. Successivamente, dobbiamo passare lungo la definizione esistente della funzione sovraccaricata. Questo passaggio deve essere eseguito nel sito di definizione, come dimostrato di seguito.

Per definire una funzione specializzata in piattaforma, è ora possibile scrivere quanto segue:

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

Le chiamate a some_function()verranno inviate in modo appropriato alla definizione specifica della piattaforma fornita.

Personalmente, non consiglierei l'uso di questa tecnica nel codice di produzione. A mio avviso, è meglio essere espliciti sul comportamento dipendente dalla piattaforma in ogni posizione in cui si verificano queste differenze.


Non sarebbe @implement_for_os ("linux") ecc ...
Il

@ th0nk No - la funzione implement_for_osnon restituisce un decoratore stesso, ma piuttosto restituisce una funzione che produrrà il decoratore una volta fornita con la definizione precedente della funzione in questione.
Brian,

5

Ho scritto il mio codice prima di leggere altre risposte. Dopo aver finito il mio codice, ho scoperto che il codice di @ Todd è la risposta migliore. In ogni caso, pubblico la mia risposta perché mi sono divertito mentre risolvo questo problema. Ho imparato cose nuove grazie a questa bella domanda. Lo svantaggio del mio codice è che esiste un sovraccarico per recuperare i dizionari ogni volta che vengono chiamate funzioni.

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

Una soluzione pulita sarebbe quella di creare un registro delle funzioni dedicato su cui inviare sys.platform. Questo è molto simile a functools.singledispatch. Il codice sorgente di questa funzione fornisce un buon punto di partenza per l'implementazione di una versione personalizzata:

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

Ora può essere usato in modo simile a 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

La registrazione funziona anche direttamente sui nomi delle funzioni:

@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')
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.