デコレータでPython関数定義をバイパスする方法は?


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が使用され、2番目の定義は黙って無視されます。

OSを決定することではなく、関数定義/デコレータについてです。


10
その2番目のデコレーターはmy_callback = windows(<actual function definition>)- と同等です。そのため、デコレーターが何をするかに関係なく、名前my_callback 上書きされます。関数のLinuxバージョンがその変数に入る唯一の方法は、windows()それが返された場合です-しかし、関数はLinuxバージョンを知る方法がありません。これを達成するためのより一般的な方法は、OS固有の関数定義を別々のファイルに入れ、条件付きimportでそれらの1つだけにすることだと思います。
jasonharper

7
のインターフェースを確認functools.singledispatchすることをお勧めします。これは、必要なものと同様の処理を実行します。そこで、registerデコレータはディスパッチャについて知っています(ディスパッチャの属性であり、その特定のディスパッチャに固有であるため)。これにより、デコレータはディスパッチャを返し、アプローチの問題を回避できます。
user2357112はモニカ

5
ここでやろうとしていることは立派ですが、CPythonのほとんどが標準の「if / elif / elseでプラットフォームをチェックする」に従っていることは言及に値します。例えば、uuid.getnode()。(とはいえ、ここでのトッドの答えはかなり良いです。)
ブラッドソロモン

回答:


58

#ifdef WINDOWS / #endifと同じような効果をコードに持たせることが目標の場合は、次の方法で行います(Macを使用しています)。

単純なケース、連鎖なし

>>> 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
@モニカありがとうございます。ええ、私はこれをクラスのメンバー関数で使用することを考慮していませんでした..わかりました..私のコードをより一般的にすることができるかどうかを確認します。
トッド

1
@モニカ大丈夫。クラスのメンバー関数を説明するようにコードを更新しました。これを試して頂けますか?
トッド

2
@モニカ、申し分なく..私はクラスメソッドをカバーするようにコードを更新し、それが機能することを確認するためだけに少しテストを行いました-大規模なものは何もありません。
トッド

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、とにかく2つの異なる関数を書くつもりなら、これは行く方法です。私はおそらく(スタックトレースが正しいので)、デバッグのために元の関数名を保持したい:def callback_windows(...)def callback_linux(...)、それからif windows: callback = callback_windows、などしかし、このいずれかの方法では、読み取りデバッグ、および維持する方法が容易です。
セス

これは、あなたが考えているユースケースを満たすための最も簡単なアプローチであることに同意します。ただし、元々の質問は、デコレータと、それを関数宣言に適用する方法についてでした。したがって、スコープは条件付きのプラットフォームロジックを超えている場合があります。
トッド

3
私が使用したいelif、決してするつもりないだとして、期待される複数の場合linux/ windows/はmacOStrueになります。実際、私はおそらく複数のブールフラグではなく、単一の変数を定義してから、などをp = platform.system()使用しますif p == "Linux"。存在しない変数は、同期できなくなります。
chepner

@chepnerケースが相互に排他的であることが明らかな場合、elif確かにその利点があります。具体的には、少なくとも1つのケース確実に一致するように末尾にelse+ raiseを付けます。述語の評価に関しては、私はそれらを事前評価することを好む–重複を避け、定義と使用を分離する。結果が変数に格納されていない場合でも、同じように非同期になる可能性のあるハードコードされた値があります。私はできないさまざまな手段のための様々な魔法の文字列を覚えていない、例えば対、...platform.system() == "Windows"sys.platform == "win32"
MisterMiyagi

Enum定数のサブクラスまたは定数のセットのいずれを使用しても、文字列を列挙できます。
chepner

8

以下は、このメカニズムの可能な実装の1つです。コメントに記載されているように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')

このデコレーターを使用するには、2レベルの間接参照を行う必要があります。まず、デコレータに応答させるプラットフォームを指定する必要があります。これは、上記のライン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")などではないでしょうか
lltt

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