Làm thế nào để bỏ qua định nghĩa chức năng python với trang trí?


66

Tôi muốn biết liệu có thể kiểm soát định nghĩa hàm Python dựa trên các cài đặt chung hay không (ví dụ: HĐH). Thí dụ:

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

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

Sau đó, nếu ai đó đang sử dụng Linux, định nghĩa đầu tiên my_callbacksẽ được sử dụng và định nghĩa thứ hai sẽ bị bỏ qua trong âm thầm.

Nó không phải là về việc xác định hệ điều hành, mà là về định nghĩa / trang trí chức năng.


10
Công cụ trang trí thứ hai đó tương đương với my_callback = windows(<actual function definition>)- vì vậy tên my_callback sẽ bị ghi đè, bất kể nhà trang trí có thể làm gì. Cách duy nhất phiên bản Linux của chức năng có thể kết thúc trong biến đó là nếu windows()trả về nó - nhưng chức năng không có cách nào để biết về phiên bản Linux. Tôi nghĩ rằng cách điển hình hơn để thực hiện điều này là có các định nghĩa chức năng dành riêng cho hệ điều hành trong các tệp riêng biệt và importchỉ có một trong số chúng.
jasonharper

7
Bạn có thể muốn xem giao diện của functools.singledispatch, giao diện này tương tự như những gì bạn muốn. Ở đó, người registertrang trí biết về bộ điều phối (vì đó là một thuộc tính của chức năng điều phối và cụ thể cho bộ điều phối cụ thể đó), vì vậy nó có thể trả lại bộ điều phối và tránh các vấn đề với cách tiếp cận của bạn.
user2357112 hỗ trợ Monica

5
Mặc dù những gì bạn đang cố gắng làm ở đây thật đáng ngưỡng mộ, nhưng điều đáng nói là hầu hết CPython đều tuân theo một "nền tảng kiểm tra tiêu chuẩn trong if / elif / other"; ví dụ , uuid.getnode(). (Điều đó nói rằng, câu trả lời của Todd ở đây khá tốt.)
Brad Solomon

Câu trả lời:


58

Nếu mục tiêu là có cùng loại hiệu ứng trong mã của bạn mà #ifdef WINDOWS / #endif có .. đây là một cách để làm điều đó (Tôi đang sử dụng mac btw).

Trường hợp đơn giản, không có chuỗi

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

Vì vậy, với việc thực hiện này, bạn nhận được cùng một cú pháp bạn có trong câu hỏi của bạn.

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

Những gì đoạn mã trên đang làm, về cơ bản, là gán zulu cho zulu nếu nền tảng khớp. Nếu nền tảng không khớp, nó sẽ trả về zulu nếu nó được xác định trước đó. Nếu nó không được xác định, nó sẽ trả về một hàm giữ chỗ làm tăng ngoại lệ.

Khái niệm trang trí là khái niệm dễ dàng để tìm ra nếu bạn nhớ rằng

@mydecorator
def foo():
    pass

tương tự như:

foo = mydecorator(foo)

Đây là một triển khai sử dụng một trang trí tham số:

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

Trang trí tham số là tương tự như foo = mydecorator(param)(foo) .

Tôi đã cập nhật câu trả lời khá nhiều. Đáp lại các bình luận, tôi đã mở rộng phạm vi ban đầu của nó để bao gồm ứng dụng cho các phương thức lớp và để bao hàm các hàm được định nghĩa trong các mô-đun khác. Trong bản cập nhật cuối cùng này, tôi đã có thể giảm đáng kể độ phức tạp liên quan đến việc xác định xem một chức năng đã được xác định chưa.

[Một bản cập nhật nhỏ ở đây ... Tôi chỉ không thể đặt nó xuống - đó là một bài tập thú vị] Tôi đã thực hiện thêm một số thử nghiệm về điều này, và thấy nó hoạt động chung trên các thiết bị gọi - không chỉ là các chức năng thông thường; bạn cũng có thể trang trí khai báo lớp cho dù có thể gọi được hay không. Và nó hỗ trợ các chức năng bên trong của các chức năng, vì vậy những thứ như thế này là có thể (mặc dù có lẽ không phải là phong cách tốt - đây chỉ là mã thử nghiệm):

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

Ở trên trình bày cơ chế cơ bản của trình trang trí, cách truy cập phạm vi của người gọi và cách đơn giản hóa nhiều trình trang trí có hành vi tương tự bằng cách có một hàm bên trong chứa thuật toán chung được xác định.

Hỗ trợ xích

Để hỗ trợ xâu chuỗi các trình trang trí này cho biết liệu một chức năng có áp dụng cho nhiều hơn một nền tảng hay không, trình trang trí có thể được thực hiện như vậy:

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

Bằng cách đó, bạn hỗ trợ xích:

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

4
Lưu ý rằng điều này chỉ hoạt động nếu macoswindowsđược xác định trong cùng một mô-đun như zulu. Tôi tin rằng điều này cũng sẽ dẫn đến chức năng bị bỏ lại như Nonethể chức năng không được xác định cho nền tảng hiện tại, điều này sẽ dẫn đến một số lỗi thời gian chạy rất khó hiểu.
Brian

1
Điều này sẽ không hoạt động đối với các phương thức hoặc các chức năng khác không được xác định trong phạm vi toàn cầu mô-đun.
user2357112 hỗ trợ Monica

1
Cảm ơn bạn @Monica. Vâng, tôi đã không tính đến việc sử dụng điều này cho các chức năng thành viên của một lớp .. được rồi .. tôi sẽ xem liệu tôi có thể làm cho mã của mình chung chung hơn không.
Todd

1
@Monica được rồi .. Tôi đã cập nhật mã vào tài khoản cho các chức năng thành viên lớp. Bạn có thể thử cái này không?
Todd

2
@Monica, được rồi .. Tôi đã cập nhật mã để bao quát các phương thức lớp và thực hiện một chút thử nghiệm chỉ để đảm bảo nó hoạt động - không có gì rộng rãi .. nếu bạn muốn chạy thử, hãy cho tôi biết cách thực hiện.
Todd

37

Trong khi @decoratorcú pháp có vẻ tốt, bạn có được hành vi chính xác như mong muốn với một đơn giản 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

Nếu được yêu cầu, điều này cũng cho phép dễ dàng thực thi rằng một số trường hợp đã khớp.

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, Nếu bạn định viết hai hàm khác nhau, thì đây là cách để đi. Tôi có thể muốn giữ lại các tên hàm ban đầu để gỡ lỗi (vì vậy dấu vết ngăn xếp là chính xác): def callback_windows(...)def callback_linux(...), sau đó if windows: callback = callback_windows, v.v. Nhưng cả hai cách này đều dễ đọc, gỡ lỗi và bảo trì hơn.
Seth

Tôi đồng ý đây là cách tiếp cận đơn giản nhất để đáp ứng trường hợp sử dụng mà bạn có trong đầu. Tuy nhiên, câu hỏi ban đầu là về trang trí và cách chúng có thể được áp dụng để khai báo hàm. Vì vậy, phạm vi có thể vượt ra ngoài logic nền tảng có điều kiện.
Todd

3
Tôi sẽ sử dụng một elif, vì nó sẽ không bao giờ là trường hợp được mong đợi rằng nhiều hơn một linux/ windows/ macOSsẽ là sự thật. Trong thực tế, có lẽ tôi chỉ định nghĩa một biến duy nhất p = platform.system(), sau đó sử dụng if p == "Linux", v.v. chứ không phải là nhiều cờ boolean. Các biến không tồn tại không thể không đồng bộ.
chepner

@chepner Nếu rõ ràng các trường hợp là loại trừ lẫn nhau, elifchắc chắn có những ưu điểm của nó - cụ thể là, dấu else+ raiseđể đảm bảo rằng ít nhất một trường hợp đã khớp. Đối với việc đánh giá vị ngữ, tôi thích đánh giá chúng trước - nó tránh sự trùng lặp và tách rời định nghĩa và sử dụng. Ngay cả khi kết quả không được lưu trữ trong các biến, hiện tại có các giá trị được mã hóa cứng có thể không đồng bộ giống nhau. Tôi không bao giờ có thể nhớ các chuỗi ma thuật khác nhau cho các phương tiện khác nhau, ví dụ như platform.system() == "Windows"so với sys.platform == "win32", ...
MisterMiyagi

Bạn có thể liệt kê các chuỗi, cho dù với một lớp con Enumhoặc chỉ một tập các hằng số.
chepner

8

Dưới đây là một thực hiện có thể cho cơ khí này. Như đã lưu ý trong các bình luận, có thể tốt hơn là triển khai giao diện "bộ điều phối chính", như giao diện đã thấy functools.singledispatch, để theo dõi trạng thái được liên kết với nhiều định nghĩa quá tải. Tôi hy vọng việc triển khai này ít nhất sẽ cung cấp một số hiểu biết sâu sắc về các vấn đề mà bạn có thể phải giải quyết khi phát triển chức năng này cho một cơ sở mã lớn hơn.

Tôi chỉ thử nghiệm rằng việc triển khai bên dưới hoạt động như được chỉ định trên các hệ thống Linux, vì vậy tôi không thể đảm bảo rằng giải pháp này cho phép tạo ra các chức năng chuyên biệt nền tảng. Vui lòng không sử dụng mã này trong cài đặt sản xuất mà không tự kiểm tra kỹ trước.

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

Để sử dụng trang trí này, chúng ta phải làm việc thông qua hai cấp độ gián tiếp. Đầu tiên, chúng ta phải xác định nền tảng nào chúng ta muốn người trang trí đáp ứng. Điều này được thực hiện bởi dòng implement_linux = implement_for_os('Linux')và đối tác của Window ở trên. Tiếp theo, chúng ta cần chuyển theo định nghĩa hiện có của hàm đang bị quá tải. Bước này phải được thực hiện tại trang web định nghĩa, như được trình bày dưới đây.

Để xác định chức năng chuyên biệt nền tảng, bây giờ bạn có thể viết như sau:

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

Gọi đến some_function() sẽ được gửi một cách thích hợp đến định nghĩa cụ thể của nền tảng được cung cấp.

Cá nhân, tôi sẽ không khuyên sử dụng kỹ thuật này trong mã sản xuất. Theo tôi, tốt hơn là nên rõ ràng về hành vi phụ thuộc vào nền tảng tại mỗi địa điểm nơi những khác biệt này xảy ra.


Sẽ không phải là @imây_for_os ("linux"), v.v ...
sẽ

@ th0nk Không - hàm implement_for_oskhông trả về chính bộ trang trí, mà trả về một hàm sẽ tạo ra bộ trang trí một khi được cung cấp với định nghĩa trước của hàm được đề cập.
Brian

5

Tôi đã viết mã của mình trước khi đọc các câu trả lời khác. Sau khi tôi hoàn thành mã của mình, tôi thấy mã của @ Todd là câu trả lời tốt nhất. Dù sao tôi cũng đăng câu trả lời của mình vì tôi cảm thấy vui trong khi giải quyết vấn đề này. Tôi đã học được những điều mới nhờ câu hỏi hay này. Hạn chế của mã của tôi là tồn tại chi phí để truy xuất từ ​​điển mỗi khi các hàm được gọi.

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

Một giải pháp sạch sẽ là tạo ra một sổ đăng ký chức năng chuyên dụng gửi đi sys.platform. Điều này rất giống với functools.singledispatch. Mã nguồn của chức năng này cung cấp một điểm khởi đầu tốt để triển khai phiên bản tùy chỉnh:

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

Bây giờ nó có thể được sử dụng tương tự như 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

Đăng ký cũng hoạt động trực tiếp trên các tên chức năng:

@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')
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.