จะบายพาสนิยามฟังก์ชัน python ด้วย decorator ได้อย่างไร?


66

ฉันอยากจะรู้ว่ามันเป็นไปได้ในการควบคุมการกำหนดฟังก์ชั่น Python ตามการตั้งค่าทั่วโลก (เช่น OS) ตัวอย่าง:

@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จะถูกนำมาใช้และคำที่สองจะถูกเพิกเฉยอย่างเงียบ ๆ

มันไม่เกี่ยวกับการพิจารณาระบบปฏิบัติการมันเกี่ยวกับฟังก์ชั่นนิยาม / มัณฑนากร


10
มัณฑนากรที่สองนั้นเทียบเท่าmy_callback = windows(<actual function definition>)- ดังนั้นชื่อmy_callback จะถูกเขียนทับโดยไม่คำนึงถึงสิ่งที่มัณฑนากรอาจทำ วิธีเดียวที่ฟังก์ชั่นรุ่นลีนุกซ์อาจสิ้นสุดลงในตัวแปรนั้นคือถ้าwindows()ส่งคืน - แต่ฟังก์ชั่นนั้นไม่มีทางรู้เกี่ยวกับรุ่นลีนุกซ์ ฉันคิดว่าวิธีทั่วไปในการบรรลุเป้าหมายนี้คือการมีคำจำกัดความของฟังก์ชั่นเฉพาะระบบปฏิบัติการในไฟล์แยกกันและมีimportเพียงหนึ่งเงื่อนไขเท่านั้น
jasonharper

7
คุณอาจต้องการดูอินเทอร์เฟซของfunctools.singledispatchซึ่งทำสิ่งที่คล้ายกับสิ่งที่คุณต้องการ ที่นั่นregisterมัณฑนากรรู้เกี่ยวกับดิสแพตเชอร์ (เพราะเป็นคุณสมบัติของฟังก์ชั่นการจัดส่งและเฉพาะสำหรับดิสแพตเชอร์นั้น) เพื่อให้สามารถส่งคืนผู้เลือกจ่ายและหลีกเลี่ยงปัญหาด้วยวิธีการของคุณ
user2357112 รองรับ Monica

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

สิ่งที่โค้ดข้างต้นกำลังทำคือการกำหนดซูลูให้กับซูลูหากแพลตฟอร์มตรงกัน หากแพลตฟอร์มไม่ตรงกันจะส่งคืนซูลูหากมีการกำหนดไว้ก่อนหน้านี้ หากไม่ได้กำหนดไว้จะส่งคืนฟังก์ชันตัวยึดตำแหน่งที่ยกข้อยกเว้น

นักตกแต่งเป็นแนวคิดที่ง่ายต่อการเข้าใจหากคุณจำได้ว่า

@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)ตกแต่งแปรเป็นคล้ายกับ

ฉันได้อัพเดตคำตอบแล้ว เพื่อตอบสนองต่อความคิดเห็นฉันได้ขยายขอบเขตดั้งเดิมเพื่อรวมแอปพลิเคชันไปยังวิธีการเรียนและครอบคลุมฟังก์ชั่นที่กำหนดในโมดูลอื่น ๆ ในการอัพเดทครั้งล่าสุดนี้ฉันสามารถลดความซับซ้อนที่เกี่ยวข้องในการพิจารณาว่าฟังก์ชั่นได้รับการกำหนดแล้ว

[อัพเดทเล็ก ๆ น้อย ๆ ที่นี่ ... ฉันไม่สามารถวางมันลงได้ - มันเป็นแบบฝึกหัดที่สนุก] ฉันทำการทดสอบเพิ่มเติมแล้วและพบว่ามันใช้งานได้โดยทั่วไปใน callables - ไม่ใช่แค่ฟังก์ชั่นธรรมดา คุณสามารถตกแต่งการประกาศคลาสว่า callable หรือไม่ และรองรับฟังก์ชั่นด้านในของฟังก์ชั่นดังนั้นสิ่งนี้เป็นไปได้ (แม้ว่าอาจจะไม่ใช่สไตล์ที่ดี - นี่เป็นเพียงรหัสทดสอบ):

>>> @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ว่าฟังก์ชั่นไม่ได้กำหนดไว้สำหรับแพลตฟอร์มปัจจุบันซึ่งจะนำไปสู่ข้อผิดพลาด runtime สับสนมาก
ไบรอัน

1
สิ่งนี้จะไม่ทำงานสำหรับวิธีการหรือฟังก์ชั่นอื่น ๆ ที่ไม่ได้กำหนดไว้ในขอบเขตของโมดูลทั่วโลก
user2357112 รองรับ Monica

1
ขอบคุณ @Monica ใช่ฉันไม่ได้คิดเรื่องการใช้สิ่งนี้ในฟังก์ชั่นสมาชิกของคลาส .. เอาล่ะ .. ฉันจะดูว่าฉันสามารถสร้างรหัสทั่วไปได้ไหม
ทอดด์

1
@Monica โอเค .. ฉันอัปเดตรหัสเป็นบัญชีสำหรับฟังก์ชั่นสมาชิกคลาส คุณลองดูไหม
ทอดด์

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+ raiseเพื่อให้มั่นใจว่ากรณีอย่างน้อยหนึ่งได้จับคู่ สำหรับการประเมินภาคแสดงฉันชอบให้พวกเขาประเมินล่วงหน้า - มันหลีกเลี่ยงการทำซ้ำและแยกความหมายและการใช้งาน แม้ว่าผลลัพธ์จะไม่ถูกเก็บไว้ในตัวแปร แต่ตอนนี้มีค่าฮาร์ดโค้ดที่สามารถซิงค์กันได้เหมือนกัน ฉันจะไม่เคยจำสตริงมายากลต่างๆสำหรับวิธีการที่แตกต่างกันเช่นplatform.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") ฯลฯ ...
lltt

@ th0nk ไม่ - ฟังก์ชั่นimplement_for_osจะไม่ส่งคืน decorator เอง แต่จะส่งคืนฟังก์ชันที่จะสร้างมัณฑนากรเมื่อให้กับคำจำกัดความก่อนหน้าของฟังก์ชันที่เป็นปัญหา
ไบรอัน

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.