Como ignorar a definição de função python com o decorador?

66

Gostaria de saber se é possível controlar a definição da função Python com base nas configurações globais (por exemplo, SO). Exemplo:

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

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

Então, se alguém estiver usando Linux, a primeira definição de my_callbackserá usada e a segunda será ignorada silenciosamente.

Não se trata de determinar o sistema operacional, mas de definição de função / decoradores.

Pedro
fonte
10
Esse segundo decorador é equivalente a my_callback = windows(<actual function definition>)- portanto, o nome my_callback será substituído, independentemente do que o decorador possa fazer. A única maneira pela qual a versão Linux da função poderia terminar nessa variável é se a windows()retornasse - mas a função não tem como saber sobre a versão Linux. Eu acho que a maneira mais típica de conseguir isso é ter as definições de função específicas do SO em arquivos separados e, condicionalmente, importapenas um deles.
jasonharper 16/02
7
Você pode dar uma olhada na interface de functools.singledispatch, que faz algo semelhante ao que você deseja. Lá, o registerdecorador conhece o expedidor (porque é um atributo da função de expedição e específico para esse expedidor específico), para que ele possa retornar o expedidor e evitar os problemas com sua abordagem.
user2357112 suporta Monica
5
Enquanto o que você está tentando fazer aqui é admirável, vale a pena mencionar que a maior parte do CPython segue uma "plataforma de verificação padrão em um if / elif / else"; por exemplo uuid.getnode(),. (Dito isto, a resposta de Todd aqui é bastante boa.)
Brad Solomon

Respostas:

58

Se o objetivo é ter o mesmo tipo de efeito em seu código que #ifdef WINDOWS / #endif tem ... aqui está uma maneira de fazer isso (estou em um mac btw).

Caso simples, sem encadeamento

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

Portanto, com esta implementação, você obtém a mesma sintaxe que possui na sua pergunta.

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

O que o código acima está fazendo, essencialmente, é atribuir zulu a zulu se a plataforma corresponder. Se a plataforma não corresponder, retornará o zulu se tiver sido definido anteriormente. Se não foi definido, ele retorna uma função de espaço reservado que gera uma exceção.

Os decoradores são conceitualmente fáceis de descobrir se você tiver em mente que

@mydecorator
def foo():
    pass

é análogo a:

foo = mydecorator(foo)

Aqui está uma implementação usando um 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")

Decoradores parametrizados são análogos a foo = mydecorator(param)(foo).

Eu atualizei a resposta um pouco. Em resposta aos comentários, ampliei seu escopo original para incluir aplicativos em métodos de classe e para cobrir funções definidas em outros módulos. Nesta última atualização, pude reduzir bastante a complexidade envolvida na determinação de se uma função já foi definida.

[Uma pequena atualização aqui ... Eu simplesmente não pude deixar isso para trás - foi um exercício divertido] Eu tenho testado mais isso e descobri que geralmente funciona em chamadas - não apenas em funções comuns; você também pode decorar declarações de classe, chamadas ou não. E ele suporta funções internas de funções, para que coisas como essa sejam possíveis (embora provavelmente não seja um bom estilo - este é apenas um código de teste):

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

O exemplo acima demonstra o mecanismo básico dos decoradores, como acessar o escopo do chamador e como simplificar vários decoradores que têm comportamento semelhante ao ter uma função interna contendo o algoritmo comum definido.

Suporte de encadeamento

Para suportar o encadeamento desses decoradores indicando se uma função se aplica a mais de uma plataforma, o decorador pode ser implementado da seguinte maneira:

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

Dessa forma, você apoia o encadeamento:

>>> @macos
... @linux
... def foo():
...     print("works!")
...     
>>> foo()
works!
Todd
fonte
4
Observe que isso só funciona se macose windowsestiver definido no mesmo módulo que zulu. Acredito que isso também resultará na função ser deixada como Nonese a função não estivesse definida para a plataforma atual, o que levaria a alguns erros de tempo de execução muito confusos.
Brian
11
Isso não funcionará para métodos ou outras funções não definidas no escopo global do módulo.
user2357112 suporta Monica
11
Obrigado @Monica. Sim, eu não tinha explicado isso nas funções de membro de uma classe ... tudo bem ... vou ver se consigo tornar meu código mais genérico.
Todd
11
@ Monica ok .. Eu atualizei o código para dar conta das funções de membro da classe. Você pode tentar?
Todd
2
@ Monica, tudo bem. Atualizei o código para abranger os métodos de classe e fiz um pouco de teste apenas para garantir que funcione - nada extenso.
Todd
37

Embora a @decoratorsintaxe pareça agradável, você obtém exatamente o mesmo comportamento desejado com um simples 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 necessário, isso também permite impor facilmente que algum caso corresponda.

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
fonte
8
+1, se você escrever duas funções diferentes, então este é o caminho a seguir. Eu provavelmente gostaria de preservar os nomes das funções originais para depuração (para que os rastreamentos da pilha estejam corretos): def callback_windows(...)e def callback_linux(...), então if windows: callback = callback_windows, etc. Mas, de qualquer maneira, é muito mais fácil ler, depurar e manter.
Seth
Concordo que esta é a abordagem mais simples para satisfazer o caso de uso que você tem em mente. No entanto, a pergunta original era sobre decoradores e como eles poderiam ser aplicados à declaração de funções. Portanto, o escopo pode estar além da lógica da plataforma condicional.
Todd
3
Eu usaria um elif, pois nunca será o caso esperado que mais de um de linux/ windows/ macOSseja verdadeiro. Na verdade, eu provavelmente apenas definiria uma única variável p = platform.system()e depois usaria if p == "Linux"etc., em vez de vários sinalizadores booleanos. Variáveis ​​que não existem não podem sair de sincronia.
chepner 17/02
@chepner Se é limpar os casos são mutuamente exclusivas, elifcertamente tem suas vantagens - especificamente, um à direita else+ raisepara garantir que pelo menos um caso fez jogo. Quanto à avaliação do predicado, prefiro tê-los pré-avaliados - evita duplicação e desacopla a definição e o uso. Mesmo que o resultado não seja armazenado em variáveis, agora existem valores codificados que podem sair da sincronização da mesma forma. Eu posso não lembrar as várias cordas mágicas para os diferentes meios, por exemplo, platform.system() == "Windows"contra sys.platform == "win32", ...
MisterMiyagi
Você pode enumerar as seqüências de caracteres, com uma subclasse Enumou apenas um conjunto de constantes.
chepner 17/02
8

Abaixo está uma implementação possível para esse mecânico. Conforme observado nos comentários, pode ser preferível implementar uma interface "master dispatcher", como a vista em functools.singledispatch, para acompanhar o estado associado às várias definições sobrecarregadas. Minha esperança é que essa implementação, pelo menos, ofereça algumas dicas sobre os problemas com os quais você pode ter que lidar ao desenvolver essa funcionalidade para uma base de código maior.

Eu testei apenas que a implementação abaixo funciona conforme especificado nos sistemas Linux, portanto, não posso garantir que esta solução permita adequadamente a criação de funções especializadas na plataforma. Por favor, não use esse código em uma configuração de produção sem antes testá-lo completamente.

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 usar esse decorador, precisamos trabalhar com dois níveis de indireção. Primeiro, devemos especificar a qual plataforma queremos que o decorador responda. Isso é realizado pela linha implement_linux = implement_for_os('Linux')e pela contraparte da janela acima. Em seguida, precisamos repassar a definição existente da função que está sendo sobrecarregada. Esta etapa deve ser realizada no site de definição, conforme demonstrado abaixo.

Para definir uma função especializada em plataforma, você pode agora escrever o seguinte:

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

As chamadas para some_function()serão enviadas adequadamente para a definição específica da plataforma fornecida.

Pessoalmente, eu não recomendaria o uso dessa técnica no código de produção. Na minha opinião, é melhor ser explícito sobre o comportamento dependente da plataforma em cada local onde essas diferenças ocorrem.

Brian
fonte
Não seria @implement_for_os ("linux") etc ...
lltt 16/02
@ th0nk Não - a função implement_for_osnão retorna um decorador, mas retorna uma função que produzirá o decorador uma vez fornecida com a definição anterior da função em questão.
Brian
5

Eu escrevi meu código antes de ler outras respostas. Depois que terminei meu código, achei que o código de @ Todd é a melhor resposta. De qualquer forma, postei minha resposta porque me senti divertido enquanto resolvia esse problema. Aprendi coisas novas graças a essa boa pergunta. A desvantagem do meu código é que existe uma sobrecarga para recuperar dicionários toda vez que funções são chamadas.

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

Uma solução limpa seria criar um registro de função dedicado que é despachado sys.platform. Isso é muito parecido com functools.singledispatch. O código fonte desta função fornece um bom ponto de partida para implementar uma versão 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

Agora pode ser usado semelhante 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

O registro também funciona diretamente nos nomes das funções:

@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')
um convidado
fonte