Comment contourner la définition de la fonction python avec le décorateur?

66

Je voudrais savoir s'il est possible de contrôler la définition de la fonction Python en fonction des paramètres globaux (par exemple OS). Exemple:

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

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

Ensuite, si quelqu'un utilise Linux, la première définition de my_callbacksera utilisée et la seconde sera silencieusement ignorée.

Il ne s'agit pas de déterminer le système d'exploitation, mais de définir des fonctions / décorateurs.

Pedro
la source
10
Ce deuxième décorateur est équivalent à my_callback = windows(<actual function definition>)- donc le nom my_callback sera écrasé, indépendamment de ce que le décorateur pourrait faire. La seule façon dont la version Linux de la fonction pourrait se retrouver dans cette variable est de la windows()renvoyer - mais la fonction n'a aucun moyen de connaître la version Linux. Je pense que la façon la plus typique d'y parvenir est d'avoir les définitions de fonctions spécifiques au système d'exploitation dans des fichiers séparés, et conditionnellement importune seule d'entre elles.
jasonharper
7
Vous voudrez peut-être jeter un œil à l'interface de functools.singledispatch, qui fait quelque chose de similaire à ce que vous voulez. Là, le registerdécorateur connaît le répartiteur (car c'est un attribut de la fonction de répartition, et spécifique à ce répartiteur particulier), il peut donc retourner le répartiteur et éviter les problèmes avec votre approche.
user2357112 prend en charge Monica
5
Bien que ce que vous essayez de faire ici soit admirable, il convient de mentionner que la plupart des CPython suivent une "plate-forme de vérification standard dans un if / elif / else"; par exemple uuid.getnode(),. (Cela dit, la réponse de Todd ici est assez bonne.)
Brad Solomon

Réponses:

58

Si le but est d'avoir le même type d'effet dans votre code que #ifdef WINDOWS / #endif .. voici un moyen de le faire (je suis sur un mac btw).

Boîtier simple, sans chaînage

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

Ainsi, avec cette implémentation, vous obtenez la même syntaxe que vous avez dans votre question.

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

Ce que le code ci-dessus fait, essentiellement, est d'attribuer zulu à zulu si la plate-forme correspond. Si la plate-forme ne correspond pas, elle renverra zulu si elle a été précédemment définie. S'il n'a pas été défini, il renvoie une fonction d'espace réservé qui déclenche une exception.

Les décorateurs sont conceptuellement faciles à comprendre si vous gardez à l'esprit que

@mydecorator
def foo():
    pass

est analogue à:

foo = mydecorator(foo)

Voici une implémentation utilisant un décorateur paramétré:

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

Les décorateurs paramétrés sont analogues à foo = mydecorator(param)(foo).

J'ai un peu mis à jour la réponse. En réponse aux commentaires, j'ai étendu sa portée d'origine pour inclure l'application aux méthodes de classe et pour couvrir les fonctions définies dans d'autres modules. Dans cette dernière mise à jour, j'ai pu réduire considérablement la complexité de déterminer si une fonction a déjà été définie.

[Une petite mise à jour ici ... Je ne pouvais tout simplement pas mettre cela de côté - cela a été un exercice amusant] J'ai fait encore plus de tests et j'ai constaté que cela fonctionne généralement sur les callables - pas seulement sur les fonctions ordinaires; vous pouvez également décorer les déclarations de classe appelables ou non. Et il prend en charge les fonctions internes des fonctions, donc des choses comme cela sont possibles (bien que ce ne soit probablement pas un bon style - ce n'est qu'un code de test):

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

Ce qui précède montre le mécanisme de base des décorateurs, comment accéder à la portée de l'appelant et comment simplifier plusieurs décorateurs qui ont un comportement similaire en ayant une fonction interne contenant l'algorithme commun défini.

Support de chaînage

Pour prendre en charge le chaînage de ces décorateurs en indiquant si une fonction s'applique à plusieurs plates-formes, le décorateur pourrait être implémenté comme suit:

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

De cette façon, vous soutenez le chaînage:

>>> @macos
... @linux
... def foo():
...     print("works!")
...     
>>> foo()
works!
Todd
la source
4
Notez que cela ne fonctionne que si macoset windowssont définis dans le même module que zulu. Je crois que cela entraînera également que la fonction soit laissée comme Nonesi la fonction n'était pas définie pour la plate-forme actuelle, ce qui entraînerait des erreurs d'exécution très déroutantes.
Brian
1
Cela ne fonctionnera pas pour les méthodes ou autres fonctions non définies dans une portée globale de module.
user2357112 prend en charge Monica
1
Merci @Monica. Ouais, je n'avais pas expliqué l'utilisation de ceci sur les fonctions membres d'une classe .. bien .. Je vais voir si je peux rendre mon code plus générique.
Todd
1
@Monica ok .. J'ai mis à jour le code pour tenir compte des fonctions des membres de la classe. Pouvez-vous essayer ceci?
Todd
2
@Monica, d'accord .. J'ai mis à jour le code pour couvrir les méthodes de classe et fait un peu de test juste pour m'assurer qu'il fonctionne - rien d'extraordinaire .. si vous voulez lui donner un run, faites-moi savoir comment ça se passe.
Todd
37

Bien que la @decoratorsyntaxe soit agréable, vous obtenez exactement le même comportement que vous le souhaitez avec un simple 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

Si nécessaire, cela permet également de faire en sorte que certains cas correspondent.

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")
MonsieurMiyagi
la source
8
+1, Si vous deviez quand même écrire deux fonctions différentes, c'est la voie à suivre. Je voudrais probablement conserver les noms de fonction d'origine pour le débogage (afin que les traces de pile soient correctes): def callback_windows(...)et def callback_linux(...)puis if windows: callback = callback_windows, etc. Mais dans les deux cas, c'est beaucoup plus facile à lire, à déboguer et à maintenir.
Seth
Je suis d'accord que c'est l'approche la plus simple pour satisfaire le cas d'utilisation que vous avez en tête. Cependant, la question initiale concernait les décorateurs et comment ils pouvaient être appliqués à la déclaration de fonction. La portée peut donc dépasser la simple logique de plate-forme conditionnelle.
Todd
3
J'utiliserais un elif, car ce ne sera jamais le cas attendu que plus d'un linux/ windows/ macOSsoit vrai. En fait, je définirais probablement une seule variable p = platform.system(), puis utiliserais if p == "Linux", etc. plutôt que plusieurs drapeaux booléens. Les variables qui n'existent pas ne peuvent pas être désynchronisées.
chepner
Si @chepner il est clair que les cas sont mutuellement exclusifs, elifa certainement ses avantages - en particulier, une fuite else+ raisepour veiller à ce qu'au moins un cas a fait partie. Quant à l'évaluation du prédicat, je préfère les pré-évaluer - cela évite la duplication et découples la définition et l'utilisation. Même si le résultat n'est pas stocké dans des variables, il existe maintenant des valeurs codées en dur qui peuvent se désynchroniser tout de même. Je ne me souviens jamais des différentes cordes magiques pour les différents moyens, par exemple platform.system() == "Windows"contre sys.platform == "win32", ...
MisterMiyagi
Vous pouvez énumérer les chaînes, que ce soit avec une sous-classe de Enumou simplement un ensemble de constantes.
chepner
8

Voici une implémentation possible pour ce mécanicien. Comme indiqué dans les commentaires, il peut être préférable de mettre en œuvre une interface "maître répartiteur", comme celle vue dans functools.singledispatch, pour garder une trace de l'état associé aux multiples définitions surchargées. J'espère que cette implémentation offrira au moins un aperçu des problèmes que vous devrez peut-être traiter lors du développement de cette fonctionnalité pour une base de code plus grande.

J'ai seulement testé que l'implémentation ci-dessous fonctionne comme spécifié sur les systèmes Linux, donc je ne peux pas garantir que cette solution permet de manière adéquate la création de fonctions spécialisées de plate-forme. Veuillez ne pas utiliser ce code dans un environnement de production sans l'avoir soigneusement testé vous-même au préalable.

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

Pour utiliser ce décorateur, nous devons travailler sur deux niveaux d'indirection. Tout d'abord, nous devons spécifier à quelle plateforme nous voulons que le décorateur réponde. Ceci est accompli par la ligne implement_linux = implement_for_os('Linux')et son homologue de Windows ci-dessus. Ensuite, nous devons transmettre la définition existante de la fonction surchargée. Cette étape doit être effectuée sur le site de définition, comme illustré ci-dessous.

Pour définir une fonction spécialisée de plate-forme, vous pouvez maintenant écrire ce qui suit:

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

Appels à some_function() seront correctement acheminés vers la définition spécifique à la plate-forme fournie.

Personnellement, je ne conseillerais pas d'utiliser cette technique dans le code de production. À mon avis, il vaut mieux être explicite sur le comportement dépendant de la plate-forme à chaque endroit où ces différences se produisent.

Brian
la source
Ne serait-ce pas @implement_for_os ("linux") etc ...
lltt
@ th0nk Non - la fonction implement_for_osne renvoie pas un décorateur lui-même, mais renvoie plutôt une fonction qui produira le décorateur une fois fourni avec la définition précédente de la fonction en question.
Brian
5

J'ai écrit mon code avant de lire d'autres réponses. Après avoir terminé mon code, j'ai trouvé que le code de @ Todd était la meilleure réponse. Quoi qu'il en soit, je poste ma réponse parce que je me suis senti amusant pendant que je résolvais ce problème. J'ai appris de nouvelles choses grâce à cette bonne question. L'inconvénient de mon code est qu'il existe une surcharge pour récupérer les dictionnaires à chaque appel de fonctions.

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
la source
0

Une solution propre consisterait à créer un registre de fonctions dédié qui serait distribué sys.platform. C'est très similaire à functools.singledispatch. Le code source de cette fonction fournit un bon point de départ pour implémenter une version personnalisée:

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

Maintenant, il peut être utilisé comme 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

L'enregistrement fonctionne également directement sur les noms des fonctions:

@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')
un invité
la source