डेकोरेटर के साथ पायथन फ़ंक्शन परिभाषा को कैसे बायपास करें?


66

मैं जानना चाहूंगा कि वैश्विक सेटिंग्स (जैसे ओएस) के आधार पर पायथन फ़ंक्शन की परिभाषा को नियंत्रित करना संभव है या नहीं। उदाहरण:

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

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

फिर, अगर कोई लिनक्स का उपयोग कर रहा है, की पहली परिभाषा my_callback उपयोग किया जाएगा और दूसरा चुपचाप नजरअंदाज कर दिया जाएगा।

यह ओएस का निर्धारण करने के बारे में नहीं है, इसके बारे में फ़ंक्शन परिभाषा / सज्जाकार।


10
यह दूसरा डेकोरेटर के बराबर है my_callback = windows(<actual function definition>)- इसलिए नाम my_callback को ओवरराइट किया जाएगा , भले ही डेकोरेटर क्या कर सकता है। फ़ंक्शन के लिनक्स संस्करण का एकमात्र तरीका उस चर में समाप्त हो सकता है यदि windows()इसे वापस कर दिया जाए - लेकिन फ़ंक्शन के पास लिनक्स संस्करण के बारे में जानने का कोई तरीका नहीं है। मुझे लगता है कि इसे पूरा करने का अधिक विशिष्ट तरीका अलग-अलग फ़ाइलों में ओएस-विशिष्ट फ़ंक्शन परिभाषाएं हैं, और सशर्त रूप importसे उनमें से केवल एक है।
जस्सोन्पर

7
आप के इंटरफ़ेस पर एक नज़र डालना चाह सकते हैं functools.singledispatch, जो कुछ वैसा ही करता है जैसा आप चाहते हैं। वहां, registerडेकोरेटर डिस्पैचर के बारे में जानता है (क्योंकि यह प्रेषण समारोह की विशेषता है, और उस विशेष डिस्पैचर के लिए विशिष्ट है), इसलिए यह डिस्पैचर को वापस कर सकता है और आपके दृष्टिकोण के साथ समस्याओं से बच सकता है।
user2357112

5
जबकि आप यहाँ क्या करने की कोशिश कर रहे हैं, यह सराहनीय है, यह ध्यान देने योग्य है कि ज्यादातर सीपीथॉन एक मानक "चेक प्लेटफ़ॉर्म इन एफ़ / इलिफ़ / और" है; उदाहरण के लिए, uuid.getnode()। (उस ने कहा, टॉड का यहाँ उत्तर काफी अच्छा है।)
ब्रैड सोलोमन

जवाबों:


58

यदि लक्ष्य आपके कोड में उसी तरह का प्रभाव रखता है जो #ifdef WINDOWS / #endif के पास है .. तो इसे करने का एक तरीका है (मैं मैक 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
>>> 

उपरोक्त कोड क्या कर रहा है, अनिवार्य रूप से, प्लेटफ़ॉर्म से मेल खाने पर ज़ुलु को असाइन कर रहा है। यदि मंच मेल नहीं खाता है, तो यह पहले से परिभाषित होने पर ज़ुलु वापस आ जाएगा। यदि इसे परिभाषित नहीं किया गया था, तो यह एक प्लेसहोल्डर फ़ंक्शन लौटाता है जो एक अपवाद उठाता है।

यदि आप ध्यान में रखते हैं कि डेकोरेटर्स का पता लगाना आसान है

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

Parameterized सज्जाकार के अनुरूप हैं 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
ध्यान दें कि यह तभी काम करता है macosऔर windowsउसी के रूप में मॉड्यूल में परिभाषित कर रहे हैं 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, यदि आप वैसे भी दो अलग-अलग फ़ंक्शन लिखने जा रहे हैं, तो यह जाने का तरीका है। मैं शायद डिबगिंग के लिए मूल फ़ंक्शन नामों को संरक्षित करना चाहता हूं (इसलिए स्टैक के निशान सही हैं): def callback_windows(...)और def callback_linux(...), फिर if windows: callback = callback_windows, आदि। लेकिन या तो इस तरह से पढ़ना, डिबग करना और बनाए रखना आसान है।
सेठ

मैं मानता हूं कि आपके मन में उपयोग के मामले को संतुष्ट करने के लिए यह सबसे सरल तरीका है। हालांकि, मूल प्रश्न सज्जाकारों के बारे में था और उन्हें फ़ंक्शन घोषणा पर कैसे लागू किया जा सकता है। इसलिए गुंजाइश सिर्फ सशर्त मंच तर्क से परे हो सकती है।
टोड

3
मैं एक का उपयोग करता हूं elif, क्योंकि यह कभी भी अपेक्षित मामला नहीं होगा जो कि एक से अधिक linux/ windows/ macOSसच होगा। वास्तव में, मैं शायद एक एकल चर को परिभाषित करूंगा p = platform.system(), फिर if p == "Linux"कई बूलियन झंडे के बजाय उपयोग , आदि। चर जो मौजूद नहीं हैं, वे सिंक से बाहर नहीं जा सकते।
चेपनर

@chepner यदि यह स्पष्ट है कि मामले परस्पर अनन्य हैं, तो elifनिश्चित रूप से इसके फायदे हैं - विशेष रूप से, एक अनुगामीelse + raiseकम से कम एक मामले सुनिश्चित करने के लिए किया था मैच। विधेय का मूल्यांकन करने के लिए, मैं उन्हें पूर्व-मूल्यांकन करना पसंद करता हूं - यह दोहराव से बचता है और परिभाषा और उपयोग को रोकता है। यहां तक ​​कि अगर परिणाम चर में संग्रहीत नहीं किया जाता है, तो अब हार्डकोडेड मान हैं जो सिंक से बाहर जा सकते हैं। मैं विभिन्न साधनों, जैसे बनाम , के लिए विभिन्न जादू के तार कभी याद नहीं कर सकता ...platform.system() == "Windows"sys.platform == "win32"
मिस्टरमियागी

आप स्ट्रिंग्स की गणना कर सकते हैं, चाहे एक उपवर्ग के साथ Enumया बस स्थिरांक का एक सेट।
चेजनर

8

नीचे इस मैकेनिक के लिए एक संभावित कार्यान्वयन है। जैसा कि टिप्पणियों में कहा गया है, "मास्टर डिस्पैचर" इंटरफ़ेस को लागू करना बेहतर हो सकता है, जैसे कि इसमें देखा गयाfunctools.singledispatch , कई अतिभारित परिभाषाओं से जुड़े राज्य का ट्रैक रखने के लिए। मेरी आशा है कि यह कार्यान्वयन कम से कम उन समस्याओं में कुछ अंतर्दृष्टि प्रदान करेगा जो आपको इस कार्यशीलता को एक बड़े कोडबेस के लिए विकसित करते समय सामना करना पड़ सकता है।

मैंने केवल परीक्षण किया है कि नीचे दिए गए कार्यान्वयन लिनक्स सिस्टम पर निर्दिष्ट के रूप में काम करते हैं, इसलिए मैं यह गारंटी नहीं दे सकता कि यह समाधान पर्याप्त रूप से प्लेटफ़ॉर्म-विशेष कार्यों के निर्माण में सक्षम बनाता है। कृपया इस कोड का उपयोग किसी प्रोडक्शन सेटिंग में न करके, इसे पहले स्वयं जांचे बिना करें।

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") आदि नहीं होगा ...
2:16

@ th0nk नहीं - फ़ंक्शन implement_for_osस्वयं एक डेकोरेटर वापस नहीं करता है, बल्कि एक फ़ंक्शन देता है जो फ़ंक्शन में पिछली परिभाषा के साथ एक बार प्रदान किए गए डेकोरेटर का उत्पादन करेगा।
ब्रायन

5

अन्य उत्तर पढ़ने से पहले मैंने अपना कोड लिखा। अपना कोड समाप्त करने के बाद, मैंने पाया कि @ टॉड का कोड सबसे अच्छा उत्तर है। वैसे भी मैं अपना जवाब पोस्ट करता हूं क्योंकि मुझे इस समस्या को हल करने के दौरान मज़ा आया। मैंने इस अच्छे सवाल के लिए नई चीजें सीखीं। मेरे कोड का दोष यह है कि हर बार फ़ंक्शंस कहे जाने वाले शब्दकोशों को पुनः प्राप्त करने के लिए ओवरहेड मौजूद है।

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')
हमारी साइट का प्रयोग करके, आप स्वीकार करते हैं कि आपने हमारी Cookie Policy और निजता नीति को पढ़ और समझा लिया है।
Licensed under cc by-sa 3.0 with attribution required.