Bagaimana cara memotong definisi fungsi python dengan dekorator?

66

Saya ingin tahu apakah mungkin untuk mengontrol definisi fungsi Python berdasarkan pengaturan global (misalnya OS). Contoh:

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

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

Kemudian, jika seseorang menggunakan Linux, definisi pertama my_callbackakan digunakan dan yang kedua akan diabaikan.

Ini bukan tentang menentukan OS, ini tentang definisi fungsi / dekorator.

Pedro
sumber
10
Dekorator kedua itu sama dengan my_callback = windows(<actual function definition>)- sehingga namanya my_callback akan ditimpa, terlepas dari apa yang dekorator lakukan. Satu-satunya cara fungsi versi Linux bisa berakhir di variabel itu adalah jika windows()mengembalikannya - tetapi fungsi tersebut tidak memiliki cara untuk mengetahui tentang versi Linux. Saya pikir cara yang lebih khas untuk mencapai ini adalah memiliki definisi fungsi OS-spesifik dalam file terpisah, dan secara kondisional importhanya satu dari mereka.
jasonharper
7
Anda mungkin ingin melihat antarmuka functools.singledispatch, yang melakukan sesuatu yang mirip dengan yang Anda inginkan. Di sana, registerdekorator tahu tentang dispatcher (karena itu adalah atribut dari fungsi dispatcher, dan khusus untuk dispatcher tertentu), sehingga dapat mengembalikan dispatcher dan menghindari masalah dengan pendekatan Anda.
user2357112 mendukung Monica
5
Sementara apa yang Anda coba lakukan di sini sangat mengagumkan, perlu disebutkan bahwa sebagian besar CPython mengikuti standar "periksa platform di if / elif / else"; misalnya uuid.getnode(),. (Konon, jawaban Todd di sini cukup bagus.)
Brad Solomon

Jawaban:

58

Jika tujuannya adalah untuk memiliki efek yang sama dalam kode Anda yang memiliki #ifdef WINDOWS / #endif .. inilah cara untuk melakukannya (Saya menggunakan mac btw).

Kasus Sederhana, Tanpa Rantai

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

Jadi dengan implementasi ini Anda mendapatkan sintaks yang sama dengan yang Anda miliki dalam pertanyaan Anda.

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

Apa yang dilakukan kode di atas, pada dasarnya, adalah menugaskan zulu ke zulu jika platform cocok. Jika platform tidak cocok, itu akan mengembalikan zulu jika sudah ditentukan sebelumnya. Jika tidak ditentukan, ia mengembalikan fungsi placeholder yang menimbulkan pengecualian.

Penghias secara konsep mudah untuk mencari tahu jika Anda ingat itu

@mydecorator
def foo():
    pass

analog dengan:

foo = mydecorator(foo)

Berikut ini implementasi menggunakan dekorator berparameter:

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

Dekorator berparameter analog dengan foo = mydecorator(param)(foo).

Saya sudah memperbarui sedikit jawabannya. Menanggapi komentar, saya telah memperluas cakupan aslinya untuk memasukkan aplikasi ke metode kelas dan untuk mencakup fungsi yang didefinisikan dalam modul lain. Dalam pembaruan terakhir ini, saya dapat mengurangi kompleksitas yang terlibat dalam menentukan apakah suatu fungsi telah ditentukan.

[Sedikit pembaruan di sini ... Saya tidak bisa menghentikannya - ini merupakan latihan yang menyenangkan] Saya telah melakukan beberapa pengujian lagi, dan ternyata ini berfungsi secara umum pada kartu panggil - bukan hanya fungsi biasa; Anda juga bisa mendekorasi deklarasi kelas apakah dapat dipanggil atau tidak. Dan itu mendukung fungsi fungsi dalam, jadi hal-hal seperti ini dimungkinkan (walaupun mungkin bukan gaya yang baik - ini hanya kode uji):

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

Di atas menunjukkan mekanisme dasar dekorator, cara mengakses ruang lingkup pemanggil, dan bagaimana menyederhanakan banyak dekorator yang memiliki perilaku serupa dengan memiliki fungsi internal yang berisi algoritma umum yang ditentukan.

Dukungan Chaining

Untuk mendukung rantai dekorator ini menunjukkan apakah suatu fungsi berlaku untuk lebih dari satu platform, dekorator dapat diimplementasikan seperti:

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

Dengan begitu Anda mendukung rantai:

>>> @macos
... @linux
... def foo():
...     print("works!")
...     
>>> foo()
works!
Todd
sumber
4
Perhatikan bahwa ini hanya berfungsi jika macosdan windowsdidefinisikan dalam modul yang sama dengan zulu. Saya percaya ini juga akan menghasilkan fungsi yang dibiarkan seolah- Noneolah fungsi tersebut tidak didefinisikan untuk platform saat ini, yang akan menyebabkan beberapa kesalahan runtime yang sangat membingungkan.
Brian
1
Ini tidak akan berfungsi untuk metode atau fungsi lain yang tidak didefinisikan dalam lingkup modul-global.
user2357112 mendukung Monica
1
@Monica terima kasih. Ya, saya belum memperhitungkan menggunakan ini pada fungsi anggota kelas .. baiklah .. Saya akan melihat apakah saya dapat membuat kode saya lebih umum.
Todd
1
@Monica oke .. Saya memperbarui kode ke akun untuk fungsi anggota kelas. Bisakah Anda mencobanya?
Todd
2
@Monica, baiklah .. Saya telah memperbarui kode untuk mencakup metode kelas dan melakukan sedikit pengujian hanya untuk memastikan itu berfungsi - tidak ada yang luas .. jika Anda ingin menjalankannya, beri tahu saya bagaimana kelanjutannya.
Todd
37

Meskipun @decoratorsintaks terlihat bagus, Anda mendapatkan perilaku yang sama persis seperti yang diinginkan dengan sederhana 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

Jika diperlukan, ini juga memungkinkan untuk dengan mudah menegakkan bahwa beberapa kasus cocok.

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")
MisterMiyagi
sumber
8
+1, Jika Anda akan tetap menulis dua fungsi yang berbeda, maka inilah caranya. Saya mungkin ingin melestarikan nama fungsi asli untuk debugging (sehingga jejak stack sudah benar): def callback_windows(...)dan def callback_linux(...), kemudian if windows: callback = callback_windows, dll. Namun cara ini adalah cara yang lebih mudah untuk membaca, men-debug, dan memelihara.
Seth
Saya setuju ini adalah pendekatan paling sederhana untuk memuaskan use case yang ada dalam pikiran Anda. Namun, pertanyaan aslinya adalah tentang dekorator dan bagaimana mereka dapat diterapkan pada deklarasi fungsi. Jadi ruang lingkup mungkin melampaui logika platform bersyarat.
Todd
3
Saya akan menggunakan elif, karena tidak akan pernah menjadi kasus yang diharapkan bahwa lebih dari satu linux/ windows/ macOSakan benar. Bahkan, saya mungkin hanya mendefinisikan satu variabel p = platform.system(), lalu gunakan if p == "Linux", dll daripada beberapa boolean flags. Variabel yang tidak ada tidak dapat disinkronkan.
chepner
@chepner Jika itu jelas kasus saling eksklusif, eliftentu memiliki kelebihan - khususnya, trailing else+ raiseuntuk memastikan bahwa setidaknya satu kasus melakukan pertandingan. Sedangkan untuk mengevaluasi predikat, saya lebih suka memiliki mereka dievaluasi - itu menghindari duplikasi dan memisahkan definisi dan penggunaan. Bahkan jika hasilnya tidak disimpan dalam variabel, sekarang ada nilai-nilai hardcoded yang bisa keluar dari sinkronisasi sama saja. Saya tidak pernah dapat mengingat berbagai string sihir untuk cara yang berbeda, misalnya platform.system() == "Windows"versus sys.platform == "win32", ...
MisterMiyagi
Anda dapat menghitung string, apakah dengan subkelas Enumatau hanya seperangkat konstanta.
chepner
8

Di bawah ini adalah salah satu implementasi yang mungkin untuk mekanik ini. Seperti disebutkan dalam komentar, mungkin lebih baik untuk mengimplementasikan antarmuka "master dispatcher", seperti yang terlihat di functools.singledispatch, untuk melacak keadaan yang terkait dengan beberapa definisi kelebihan beban. Harapan saya adalah implementasi ini setidaknya akan menawarkan beberapa wawasan tentang masalah yang mungkin harus Anda hadapi ketika mengembangkan fungsi ini untuk basis kode yang lebih besar.

Saya hanya menguji bahwa implementasi di bawah ini berfungsi sebagaimana ditentukan pada sistem Linux, jadi saya tidak dapat menjamin bahwa solusi ini memungkinkan terciptanya fungsi khusus platform. Tolong jangan gunakan kode ini dalam pengaturan produksi tanpa mengujinya sendiri terlebih dahulu.

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

Untuk menggunakan dekorator ini, kita harus bekerja melalui dua tingkat tipuan. Pertama, kita harus menentukan platform apa yang kita ingin dekorator merespons. Ini dilakukan oleh garis implement_linux = implement_for_os('Linux')dan mitra Window-nya di atas. Selanjutnya, kita perlu menyampaikan definisi fungsi yang kelebihan beban. Langkah ini harus dilakukan di situs definisi, seperti yang ditunjukkan di bawah ini.

Untuk mendefinisikan fungsi khusus platform, Anda sekarang dapat menulis yang berikut:

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

Panggilan untuk some_function()akan dikirim secara tepat ke definisi spesifik platform yang disediakan.

Secara pribadi, saya tidak akan menyarankan menggunakan teknik ini dalam kode produksi. Menurut pendapat saya, lebih baik eksplisit tentang perilaku yang bergantung pada platform di setiap lokasi di mana perbedaan ini terjadi.

Brian
sumber
Bukankah itu @implement_for_os ("linux") dll ...
lltt
@ th0nk Tidak - fungsi implement_for_ostidak mengembalikan dekorator itu sendiri, melainkan mengembalikan fungsi yang akan menghasilkan dekorator yang pernah dilengkapi dengan definisi fungsi sebelumnya yang dimaksud.
Brian
5

Saya menulis kode sebelum membaca jawaban lain. Setelah saya menyelesaikan kode saya, saya menemukan kode @ Todd adalah jawaban terbaik. Pokoknya saya memposting jawaban saya karena saya merasa senang ketika saya sedang memecahkan masalah ini. Saya belajar hal-hal baru berkat pertanyaan yang bagus ini. Kelemahan dari kode saya adalah bahwa ada overhead untuk mengambil kamus setiap kali fungsi dipanggil.

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)
Junyeong Jeong
sumber
0

Solusi bersih adalah dengan membuat fungsi registri khusus yang dikirim sys.platform. Ini sangat mirip dengan functools.singledispatch. Kode sumber fungsi ini menyediakan titik awal yang baik untuk mengimplementasikan versi khusus:

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

Sekarang dapat digunakan mirip dengan 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

Registrasi juga berfungsi langsung pada nama fungsi:

@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')
seorang tamu
sumber