Dekoratör ile python fonksiyon tanımını nasıl atlayabilirim?


66

Küresel ayarlara (örneğin OS) dayalı Python fonksiyon tanımını kontrol etmenin mümkün olup olmadığını bilmek istiyorum. Misal:

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

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

Sonra, birisi Linux kullanıyorsa, my_callback kullanılır ve ikincisi sessizce göz ardı edilir.

Bu işletim sistemi belirlemekle ilgili değil, işlev tanımı / dekoratörler hakkında.


10
Bu ikinci dekoratör eşdeğerdir my_callback = windows(<actual function definition>)- adı bu yüzden my_callback olacak bakılmaksızın dekoratör yapabileceklerine istinaden, üzerine yazılır olun. İşlevin Linux sürümünün bu değişkenle sonuçlanabilmesinin tek yolu, windows()döndürüldüğünde ortaya çıkabilir - ancak işlevin Linux sürümü hakkında hiçbir bilgisi yoktur. Bunu başarmanın daha tipik bir yolunun, ayrı dosyalarda ve koşullu importolarak bunlardan sadece birinde OS'ye özgü işlev tanımlarına sahip olmak olduğunu düşünüyorum .
jasonharper

7
Arayüzüne bir göz atmak isteyebilirsiniz functools.singledispatch, bu da istediğinize benzer bir şey yapar. Orada, registerdekoratör dağıtıcıyı bilir (çünkü dağıtım işlevinin bir özniteliği ve belirli dağıtım programına özgüdür), böylece dağıtım görevlisini iade edebilir ve yaklaşımınızla ilgili sorunları önleyebilir.
user2357112 Monica

5
Burada yapmaya çalıştığınız takdire şayan olsa da, CPython'un çoğunun standart bir "if / elif / else içinde kontrol platformunu" izlediğini belirtmek gerekir; örneğin uuid.getnode(),. (Bu, Todd'un cevabı oldukça iyi dedi.)
Brad Solomon

Yanıtlar:


58

Amaç #ifdef WINDOWS / #endif sahip olduğu kodunuzda aynı etkiyi yaratmaksa .. işte bunu yapmanın bir yolu var (mac btw kullanıyorum).

Basit Kasa, Zincirleme Yok

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

Yani bu uygulama ile aynı sözdizimine sahip olacaksınız.

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

Yukarıdaki kodun yaptığı şey, platform eşleşirse, zulu'ya zulu'yu atamaktır. Platform eşleşmezse, daha önce tanımlanmışsa zulu değerini döndürür. Tanımlanmamışsa, istisna yaratan bir yer tutucu işlevi döndürür.

Dekoratörler,

@mydecorator
def foo():
    pass

şuna benzer:

foo = mydecorator(foo)

Parametreli bir dekoratör kullanan bir uygulama:

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

Parametreli dekoratörler buna benzerdir foo = mydecorator(param)(foo).

Cevabı biraz güncelledim. Yorumlara yanıt olarak, orijinal kapsamını sınıf yöntemlerine uygulamayı içerecek ve diğer modüllerde tanımlanan işlevleri kapsayacak şekilde genişlettim. Bu son güncellemede, bir işlevin önceden tanımlanıp tanımlanmadığını belirleme konusundaki karmaşıklığı büyük ölçüde azaltabildim.

[Buradaki küçük bir güncelleme ... Bunu başaramadım - eğlenceli bir alıştırma oldu] Bunu biraz daha test ediyorum ve genel olarak callables üzerinde çalıştığını gördüm - sadece sıradan işlevler değil; çağrılabilir olsun veya olmasın sınıf bildirimlerini de dekore edebilirsiniz. Ve fonksiyonların iç işlevlerini destekler, bu nedenle böyle şeyler mümkündür (muhtemelen iyi bir stil olmasa da - bu sadece test kodudur):

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

Yukarıda, dekoratörlerin temel mekanizması, arayanın kapsamına nasıl erişileceği ve ortak algoritmayı içeren bir dahili fonksiyona sahip olarak benzer davranışa sahip birden fazla dekoratörün nasıl basitleştirileceği gösterilmektedir.

Zincirleme Desteği

Bir fonksiyonun birden fazla platforma uygulanıp uygulanmadığını gösteren bu dekoratörlerin zincirlenmesini desteklemek için dekoratör şu şekilde uygulanabilir:

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

Bu şekilde zincirlemeyi desteklersiniz:

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

4
Bunun yalnızca macosve windowsile aynı modülde tanımlanmışsa işe yaradığını unutmayın zulu. Bunun, fonksiyonun Nonemevcut platform için tanımlanmamış gibi bırakılmasına neden olacağına inanıyorum, bu da çok kafa karıştırıcı çalışma zamanı hatalarına yol açacaktır .
Brian

1
Bu, modül genel kapsamında tanımlanmayan yöntemler veya diğer işlevler için çalışmaz.
user2357112 Monica

1
Teşekkürler @Monica. Evet, bunu bir sınıfın üye işlevlerinde kullanmaktan sorumlu değildim .. tamam .. Kodumu daha genel hale getirip getiremeyeceğimi göreceğim.
Todd

1
@Monica tamam .. Sınıf üyesi işlevlerini hesaba katmak için kodu güncelledim. Bunu deneyebilir misin?
Todd

2
@Monica, tamam .. Kodu sınıf yöntemlerini kapsayacak şekilde güncelledim ve sadece çalıştığından emin olmak için biraz test yaptım - kapsamlı bir şey değil ... eğer bir çalışma yapmak istiyorsanız, nasıl gittiğini bana bildirin.
Todd

37

İken @decoratorsözdizimi Hoş görünüyor, o kadar aynı bir basit ile istendiği gibi davranış 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

Gerekirse, bu aynı zamanda bazı vakaların eşleştiğini kolayca uygulamanızı sağlar .

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, Yine de iki farklı işlev yazacak olsaydınız, o zaman bu yol. Muhtemelen hata ayıklama için orijinal işlev adlarını korumak istiyorum (böylece yığın izleri doğru): def callback_windows(...)ve def callback_linux(...)sonra if windows: callback = callback_windows, vb. Ama her iki durumda da bu okumak, hata ayıklamak ve korumak için daha kolay.
Seth

Bunun, aklınızdaki kullanım durumunu karşılamak için en basit yaklaşım olduğuna katılıyorum. Bununla birlikte, asıl soru dekoratörler ve bunların işlev bildirimine nasıl uygulanabileceği ile ilgiliydi. Yani kapsam sadece koşullu platform mantığının ötesinde olabilir.
Todd

3
Bir kullanmayı tercih ediyorum elifasla olacak şekilde, beklenen birden fazla vaka linux/ windows/ macOSgeçerli olacak. Aslında, muhtemelen tek bir değişken tanımlayabilirim p = platform.system(), sonra if p == "Linux"birden fazla boole bayrağı yerine, vb kullanın . Var olmayan değişkenler senkronize edilemez.
chepner

@chepner Eğer, olgu birbirini dışlayan temizlemek var elifkesinlikle avantajları vardır - özellikle bir eğik else+ raiseen az bir vaka sağlamak için yaptığı maçı. Yükümlülüğü değerlendirmek için, onları önceden değerlendirmeyi tercih ederim - çoğaltmayı önler ve tanım ve kullanımı ayırır. Sonuç değişkenlerde depolanmasa bile, şimdi aynı senkronizasyondan çıkabilen sabit kodlanmış değerler var. Ben yapabilirsiniz asla farklı yollarla için çeşitli sihirli dizeleri hatırlamak, örneğin platform.system() == "Windows"karşı sys.platform == "win32", ...
MisterMiyagi

Bir alt sınıfla Enumya da sadece bir sabit kümesiyle dizeleri numaralandırabilirsiniz .
chepner

8

Aşağıda bu tamirci için olası bir uygulama yer almaktadır. Yorumlarda belirtildiği gibi functools.singledispatch, çoklu aşırı yüklenmiş tanımlarla ilişkili durumu takip etmek için görüldüğü gibi bir "ana dağıtım programı" arayüzünün uygulanması tercih edilebilir . Benim umarım bu uygulama en azından daha büyük bir kod tabanı için bu işlevselliği geliştirirken uğraşmak zorunda kalabileceğiniz sorunlar hakkında bazı bilgiler sunacak olmasıdır.

Yalnızca aşağıdaki uygulamanın Linux sistemlerinde belirtildiği gibi çalıştığını test ettim, bu nedenle bu çözümün platforma özel işlevlerin oluşturulmasını yeterince etkinleştirdiğini garanti edemem. Lütfen bu kodu önce kendiniz kapsamlı bir şekilde test etmeden bir üretim ortamında kullanmayın.

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

Bu dekoratörü kullanmak için iki dolaylı yol seviyesiyle çalışmalıyız. İlk olarak, dekoratörün yanıt vermesini istediğimiz platformu belirtmeliyiz. Bu, çizgi implement_linux = implement_for_os('Linux')ve penceresinin yukarıdaki karşılığı ile gerçekleştirilir. Daha sonra, aşırı yüklenen işlevin mevcut tanımını geçmemiz gerekiyor. Bu adım, aşağıda gösterildiği gibi tanım yerinde gerçekleştirilmelidir.

Platforma özel bir işlev tanımlamak için, şimdi aşağıdakileri yazabilirsiniz:

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

Çağrılar some_function(), sağlanan platforma özgü tanımlamaya uygun şekilde gönderilir.

Şahsen, bu tekniği üretim kodunda kullanmanızı tavsiye etmem. Benim düşünceme göre, bu farklılıkların meydana geldiği her yerde platforma bağımlı davranış hakkında açık olmak daha iyidir.


@İmplement_for_os ("linux") vb.
Olmaz mı

@ th0nk Hayır - işlev implement_for_osbir dekoratörün kendisini döndürmez, bunun yerine söz konusu işlevin önceki tanımı ile birlikte dekoratörü üretecek bir işlevi döndürür.
Brian

5

Diğer cevapları okumadan önce kodumu yazdım. Kodumu bitirdikten sonra @ Todd'un kodunun en iyi yanıt olduğunu buldum. Her neyse cevabımı gönderiyorum çünkü bu problemi çözerken çok eğlendim. Bu iyi soru sayesinde yeni şeyler öğrendim. Kodumun dezavantajı, işlevler her çağrıldığında sözlükleri almak için ek yük var olmasıdır.

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

Temiz bir çözüm, gönderilecek özel bir işlev kayıt defteri oluşturmak olacaktır sys.platform. Bu çok benzer functools.singledispatch. Bu işlevin kaynak kodu , özel bir sürümün uygulanması için iyi bir başlangıç ​​noktası sağlar:

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

Şimdi benzer kullanılabilir 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

Kayıt ayrıca doğrudan işlev adları üzerinde de çalışır:

@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')
Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.