데코레이터로 파이썬 함수 정의를 우회하는 방법은 무엇입니까?


66

전역 설정 (예 : OS)을 기반으로 Python 함수 정의를 제어 할 수 있는지 알고 싶습니다. 예:

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

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

그런 다음 누군가 Linux를 사용하는 경우 첫 번째 정의 my_callback가 사용되며 두 번째 정의는 자동으로 무시됩니다.

OS 결정에 관한 것이 아니라 함수 정의 / 데코레이터에 관한 것입니다.


10
두 번째 데코레이터는 다음과 같습니다 . 따라서 데코레이터의 기능에 관계없이 my_callback = windows(<actual function definition>)이름 my_callback 덮어 씁니다. 함수의 Linux 버전이 해당 변수로 끝나는 유일한 방법은 변수를 windows()반환하는 것입니다. 그러나 함수는 Linux 버전에 대해 알 방법이 없습니다. 이 작업을 수행하는 가장 일반적인 방법은 OS 특정 기능 정의를 별도의 파일에 저장하고 조건부 import중 하나만 갖는 것입니다.
Jasonharper

7
원하는 인터페이스 functools.singledispatch와 비슷한 인터페이스를 살펴볼 수 있습니다 . 거기에서, register데코레이터는 디스패처에 대해 알고 있습니다 (파견 기능의 속성이고 특정 디스패처에 따라 다르기 때문에) 디스패처를 리턴하고 접근 방식의 문제점을 피할 수 있습니다.
user2357112는

5
여기서하려는 것은 훌륭하지만, CPython의 대부분은 표준 "if / elif / else의 체크 플랫폼"을 따른다는 것을 언급 할 가치가 있습니다. 예를 들면 다음과 같습니다 uuid.getnode(). 토드의 대답은 꽤 좋다.
브래드 솔로몬

답변:


58

목표가 코드에서 #ifdef WINDOWS / #endif와 같은 종류의 효과를 갖는 것이라면 .. 여기 방법이 있습니다 (Mac btw에 있습니다).

간단한 케이스, 체인 없음

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

따라서이 구현을 사용하면 질문에서와 동일한 구문을 얻을 수 있습니다.

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

위의 코드가 본질적으로하는 것은 플랫폼이 일치하는 경우 zulu를 zulu에 할당하는 것입니다. 플랫폼이 일치하지 않으면 이전에 정의 된 경우 zulu를 반환합니다. 정의되지 않은 경우 예외를 발생시키는 자리 표시 자 함수를 반환합니다.

데코레이터는 개념적으로 쉽게 알아낼 수 있습니다.

@mydecorator
def foo():
    pass

다음과 유사합니다.

foo = mydecorator(foo)

다음은 매개 변수화 된 데코레이터를 사용한 구현입니다.

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

매개 변수화 된 데코레이터는와 유사합니다 foo = mydecorator(param)(foo).

나는 대답을 꽤 많이 업데이트했습니다. 의견에 따라 클래스 메서드에 응용 프로그램을 포함하고 다른 모듈에 정의 된 함수를 다루도록 원래 범위를 확장했습니다. 이 마지막 업데이트에서는 함수가 이미 정의되어 있는지 확인하는 데 관련된 복잡성을 크게 줄일 수있었습니다.

[여기에 약간의 업데이트가 있습니다 ... 나는 이것을 내려 놓을 수 없었습니다. 그것은 재미있는 운동이었습니다.] 나는 이것에 대해 좀 더 테스트를 해왔고, 일반적인 함수뿐만 아니라 일반적으로 콜 러블에서 작동한다는 것을 알았습니다. 호출 가능 여부에 관계없이 클래스 선언을 장식 할 수도 있습니다. 그리고 함수의 내부 기능을 지원하므로 다음과 같은 것이 가능합니다 (아마 좋은 스타일은 아니지만 테스트 코드 일뿐입니다).

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

위는 데코레이터의 기본 메커니즘, 호출자의 범위에 액세스하는 방법 및 공통 알고리즘을 포함하는 내부 함수를 정의하여 유사한 동작을 갖는 여러 데코레이터를 단순화하는 방법을 보여줍니다.

체인 지원

함수가 둘 이상의 플랫폼에 적용되는지 여부를 나타내는 이러한 데코레이터 체인을 지원하기 위해 다음과 같이 데코레이터를 구현할 수 있습니다.

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

그렇게하면 체인을 지원합니다.

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

4
있을 경우에만 작동합니다 macoswindows같은 모듈에 정의되어 있습니다 zulu. 나는 이것이 None현재 플랫폼에 대해 함수가 정의되어 있지 않은 것처럼 함수가 남게되어 매우 혼란스러운 런타임 오류를 초래할 것이라고 생각 합니다.
브라이언

1
모듈 전역 범위에 정의되지 않은 메소드 또는 다른 함수에는 작동하지 않습니다.
user2357112는

1
@Monica 감사합니다. 그래, 나는 클래스의 멤버 함수에 이것을 사용하는 것에 대해 설명하지 않았다. 좋아. 내 코드를보다 일반적인 것으로 만들 수 있는지 볼 것이다.
토드

1
@Monica okay .. 클래스 멤버 함수를 설명하기 위해 코드를 업데이트했습니다. 이것을 시도해 볼 수 있습니까?
토드

2
@Monica, 알았어 .. 클래스 메소드를 다루기 위해 코드를 업데이트하고 작동하는지 확인하기 위해 약간의 테스트를 수행했습니다. 광범위하지는 않습니다.
토드

37

하지만 @decorator구문 외모의 좋은, 당신은 얻을 동일한 간단한와 함께 원하는대로 행동을 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

필요한 경우 일부 사례가 일치 하도록 쉽게 시행 할 수도 있습니다 .

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, 만약 당신이 어쨌든 두 개의 다른 함수를 쓰려고한다면, 이것이 갈 길입니다. 아마 (스택 트레이스가 올바른지 때문에) 디버깅을 위해 원래의 함수 이름을 유지하려는 것 : def callback_windows(...)def callback_linux(...), 다음 if windows: callback = callback_windows등 그러나이 두 방법은 읽기, 디버그 및 유지 관리하는 방법은 쉽다.
세스

이것이 귀하가 염두에 둔 유스 케이스를 만족시키는 가장 간단한 접근법이라는 것에 동의합니다. 그러나 원래 질문은 데코레이터와 함수 선언에 어떻게 적용 할 수 있는지에 관한 것입니다. 따라서 범위는 조건부 플랫폼 논리를 넘어서는 것일 수 있습니다.
토드

3
나는를 사용하는 거라고 elif는 결코 될 것 없을 것 같은, 기대 이상 일 경우 linux/ windows/이 macOS사실이 될 것입니다. 사실, 아마도 하나의 변수를 정의 한 p = platform.system()다음 if p == "Linux"여러 부울 플래그 대신 등 을 사용 합니다. 존재하지 않는 변수는 동기화되지 않습니다.
chepner

@chepner 사례가 상호 배타적 elif이라는 것이 확실 하다면, 특히 하나 이상의 사례 일치 하도록 하는 후행 else+ 의 장점이 있습니다. 술어를 평가할 때는 사전 평가를 선호합니다. 중복을 피하고 정의와 사용을 분리합니다. 결과가 변수에 저장되지 않더라도 이제 동일한 동기화에서 벗어날 수있는 하드 코딩 된 값이 있습니다. 내가 할 수 결코 다른 수단에 대한 다양한 마법 문자열을 기억하지, 예를 들면 대 ...raiseplatform.system() == "Windows"sys.platform == "win32"
MisterMiyagi

서브 클래스 Enum또는 상수 세트만으로 문자열을 열거 할 수 있습니다 .
chepner

8

아래는이 정비공에 대한 가능한 구현입니다. 주석에서 언급했듯이 "마스터 디스패처"인터페이스를 구현하는 것이 바람직 할 수 있습니다 (예 :functools.singledispatch , 다중 과부하 정의와 관련된 상태를 추적하기 위해 . 이 구현이 더 큰 코드베이스를 위해이 기능을 개발할 때 처리해야 할 문제에 대한 통찰력을 제공 할 수 있기를 바랍니다.

필자는 아래 구현이 Linux 시스템에 지정된대로 작동하는지 테스트 한 결과,이 솔루션이 플랫폼 별 기능을 생성 할 수 있다고 보장 할 수 없습니다. 먼저 직접 테스트하지 않고 프로덕션 환경에서이 코드를 사용하지 마십시오.

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

이 데코레이터를 사용하려면 두 가지 수준의 간접 작업을 수행해야합니다. 먼저 데코레이터가 응답 할 플랫폼을 지정해야합니다. 이것은 implement_linux = implement_for_os('Linux')위의 줄 과 해당 창에 의해 수행됩니다 . 다음으로, 오버로드되는 함수의 기존 정의를 전달해야합니다. 이 단계는 아래에 설명 된대로 정의 사이트에서 수행해야합니다.

플랫폼 특화 기능을 정의하기 위해 다음을 작성할 수 있습니다.

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

전화 some_function() 은 제공된 플랫폼 별 정의로 적절하게 발송됩니다.

개인적 으로이 코드를 프로덕션 코드에서 사용하지 않는 것이 좋습니다. 제 생각에는 이러한 차이가 발생하는 각 위치에서 플랫폼에 따른 행동에 대해 명시하는 것이 좋습니다.


@implement_for_os ( "linux") 등이
아닐까요

@ th0nk 아니오-함수 implement_for_os는 데코레이터 자체를 반환하지 않고 해당 함수 의 이전 정의가 제공되면 데코레이터를 생성하는 함수를 반환합니다.
브라이언

5

다른 답변을 읽기 전에 코드를 작성했습니다. 코드를 완성한 후 @Todd의 코드가 가장 좋은 답변이라는 것을 알았습니다. 어쨌든 나는이 문제를 해결하는 동안 재미를 느꼈기 때문에 대답을 게시했습니다. 이 좋은 질문으로 새로운 것을 배웠습니다. 내 코드의 단점은 함수가 호출 될 때마다 사전을 검색하는 오버 헤드가 있다는 것입니다.

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

깨끗한 솔루션은에 전달되는 전용 함수 레지스트리를 작성하는 것입니다 sys.platform. 이것은와 매우 유사합니다 functools.singledispatch. 이 함수의 소스 코드 는 사용자 정의 버전을 구현하기위한 좋은 시작점을 제공합니다.

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

이제 다음과 유사하게 사용할 수 있습니다 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

등록은 함수 이름에서 직접 작동합니다.

@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')
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.