¿Cómo omitir la definición de la función python con decorador?


66

Me gustaría saber si es posible controlar la definición de la función Python en función de la configuración global (por ejemplo, SO). Ejemplo:

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

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

Luego, si alguien usa Linux, se usará la primera definición de my_callbacky la segunda se ignorará en silencio.

No se trata de determinar el sistema operativo, se trata de la definición de funciones / decoradores.


10
Ese segundo decorador es equivalente a my_callback = windows(<actual function definition>), por lo que el nombre my_callback se sobrescribirá, independientemente de lo que el decorador pueda hacer. La única forma en que la versión de Linux de la función podría terminar en esa variable es si se windows()devuelve, pero la función no tiene forma de conocer la versión de Linux. Creo que la forma más típica de lograr esto es tener las definiciones de funciones específicas del sistema operativo en archivos separados, y condicionalmente importsolo una de ellas.
jasonharper

77
Es posible que desee echar un vistazo a la interfaz de functools.singledispatch, que hace algo similar a lo que desea. Allí, el registerdecorador conoce el despachador (porque es un atributo de la función de despacho y específico de ese despachador en particular), por lo que puede devolver el despachador y evitar los problemas con su enfoque.
user2357112 es compatible con Monica

55
Si bien lo que intenta hacer aquí es admirable, vale la pena mencionar que la mayoría de CPython sigue una "plataforma de verificación estándar en un if / elif / else"; por ejemplo uuid.getnode(),. (Dicho esto, la respuesta de Todd aquí es bastante buena.)
Brad Solomon

Respuestas:


58

Si el objetivo es tener el mismo tipo de efecto en su código que #ifdef WINDOWS / #endif ... aquí hay una manera de hacerlo (estoy en una Mac por cierto).

Caso simple, sin encadenamiento

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

Entonces, con esta implementación, obtienes la misma sintaxis que tienes en tu pregunta.

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

Lo que está haciendo el código anterior, esencialmente, es asignar zulu a zulu si la plataforma coincide. Si la plataforma no coincide, devolverá zulu si se definió previamente. Si no se definió, devuelve una función de marcador de posición que genera una excepción.

Los decoradores son conceptualmente fáciles de entender si tienes en cuenta que

@mydecorator
def foo():
    pass

es análogo a:

foo = mydecorator(foo)

Aquí hay una implementación que usa un decorador parametrizado:

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

Los decoradores parametrizados son análogos a foo = mydecorator(param)(foo).

He actualizado bastante la respuesta. En respuesta a los comentarios, amplié su alcance original para incluir la aplicación a los métodos de clase y para cubrir las funciones definidas en otros módulos. En esta última actualización, he podido reducir en gran medida la complejidad involucrada en determinar si una función ya ha sido definida.

[Una pequeña actualización aquí ... simplemente no pude dejar esto - ha sido un ejercicio divertido] He estado haciendo algunas pruebas más de esto, y descubrí que funciona en general en callables, no solo en funciones comunes; También puede decorar declaraciones de clase, ya sean invocables o no. Y es compatible con las funciones internas de las funciones, por lo que cosas como esta son posibles (aunque probablemente no sea un buen estilo, esto es solo un código de prueba):

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

Lo anterior demuestra el mecanismo básico de los decoradores, cómo acceder al alcance de la persona que llama y cómo simplificar múltiples decoradores que tienen un comportamiento similar al tener una función interna que contiene el algoritmo común definido.

Encadenamiento de apoyo

Para admitir el encadenamiento de estos decoradores que indican si una función se aplica a más de una plataforma, el decorador podría implementarse así:

>>> 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 esa manera, apoyas el encadenamiento:

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

44
Tenga en cuenta que esto solo funciona si macosy windowsse definen en el mismo módulo que zulu. Creo que esto también dará como resultado que la función se deje como Nonesi la función no estuviera definida para la plataforma actual, lo que conduciría a algunos errores de tiempo de ejecución muy confusos.
Brian

1
Esto no funcionará para métodos u otras funciones no definidas en un ámbito global de módulo.
user2357112 es compatible con Monica

1
Gracias @Monica. Sí, no había tenido en cuenta el uso de esto en las funciones miembro de una clase ... está bien ... Veré si puedo hacer que mi código sea más genérico.
Todd

1
@Monica está bien ... Actualicé el código para tener en cuenta las funciones de los miembros de la clase. ¿Puedes probar esto?
Todd

2
@Monica, está bien ... He actualizado el código para cubrir los métodos de clase y he hecho un poco de prueba solo para asegurarme de que funciona, nada extenso ... si quieres probarlo, avísame cómo funciona.
Todd

37

Si bien la @decoratorsintaxis se ve bien, obtienes exactamente el mismo comportamiento deseado con 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 es necesario, esto también permite hacer cumplir fácilmente que algún caso coincidió.

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, si ibas a escribir dos funciones diferentes de todos modos, entonces este es el camino a seguir. Probablemente me gustaría conservar los nombres de las funciones originales para la depuración (para que los seguimientos de la pila sean correctos): def callback_windows(...)y def callback_linux(...), luego if windows: callback = callback_windows, etc. Pero de cualquier manera, esto es mucho más fácil de leer, depurar y mantener.
Seth

Estoy de acuerdo en que este es el enfoque más simple para satisfacer el caso de uso que tiene en mente. Sin embargo, la pregunta original era sobre decoradores y cómo podrían aplicarse a la declaración de función. Por lo tanto, el alcance puede estar más allá de la lógica de plataforma condicional.
Todd

3
Yo usaría un elif, ya que nunca será el caso esperado que más de uno de linux/ windows/ macOSsea ​​cierto. De hecho, probablemente solo definiría una sola variable p = platform.system(), luego usaría if p == "Linux", etc. en lugar de múltiples banderas booleanas. Las variables que no existen no se pueden sincronizar.
Chepner

@chepner Si está claro que los casos son mutuamente excluyentes, elifsin duda tiene sus ventajas - en concreto, un arrastre else+ raisepara asegurar que al menos un caso hizo partido. En cuanto a la evaluación del predicado, prefiero que se evalúen previamente: evita la duplicación y desacopla la definición y el uso. Incluso si el resultado no se almacena en variables, ahora hay valores codificados que pueden salir de la sincronización de la misma manera. Puedo no recordar las diversas cadenas mágicas para los diferentes medios, por ejemplo, platform.system() == "Windows"frente sys.platform == "win32", ...
MisterMiyagi

Puede enumerar las cadenas, ya sea con una subclase de Enumo solo con un conjunto de constantes.
Chepner

8

A continuación se muestra una posible implementación para esta mecánica. Como se señaló en los comentarios, puede ser preferible implementar una interfaz de "despachador maestro", como la que se ve en functools.singledispatch, para realizar un seguimiento del estado asociado con las múltiples definiciones sobrecargadas. Espero que esta implementación al menos ofrezca una idea de los problemas que puede tener que enfrentar al desarrollar esta funcionalidad para una base de código más grande.

Solo he probado que la implementación a continuación funciona como se especifica en los sistemas Linux, por lo que no puedo garantizar que esta solución permita adecuadamente la creación de funciones especializadas en la plataforma. No utilice este código en una configuración de producción sin probarlo usted mismo primero.

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

Para utilizar este decorador, debemos trabajar a través de dos niveles de indirección. Primero, debemos especificar a qué plataforma queremos que responda el decorador. Esto se logra mediante la línea implement_linux = implement_for_os('Linux')y su contraparte de la ventana anterior. A continuación, debemos transmitir la definición existente de la función que se está sobrecargando. Este paso debe realizarse en el sitio de definición, como se demuestra a continuación.

Para definir una función especializada en la plataforma, ahora puede escribir lo siguiente:

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

Las llamadas a se some_function()enviarán adecuadamente a la definición específica de plataforma proporcionada.

Personalmente, no recomendaría usar esta técnica en el código de producción. En mi opinión, es mejor ser explícito sobre el comportamiento dependiente de la plataforma en cada ubicación donde se producen estas diferencias.


¿No sería @implement_for_os ("linux") etc ...
lltt

@ th0nk No: la función implement_for_osno devuelve un decorador en sí, sino que devuelve una función que producirá el decorador una vez que se proporcione la definición previa de la función en cuestión.
Brian

5

Escribí mi código antes de leer otras respuestas. Después de terminar mi código, encontré que el código de @ Todd es la mejor respuesta. De todos modos, publico mi respuesta porque me sentí divertido mientras resolvía este problema. Aprendí cosas nuevas gracias a esta buena pregunta. El inconveniente de mi código es que existe una sobrecarga para recuperar diccionarios cada vez que se llaman funciones.

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 solución limpia sería crear un registro de funciones dedicado que se despache sys.platform. Esto es muy similar a functools.singledispatch. El código fuente de esta función proporciona un buen punto de partida para implementar una versión personalizada:

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

Ahora se puede usar de manera similar 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

El registro también funciona directamente en los nombres de las funciones:

@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')
Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.