Bagaimana cara membuat dekorator Python yang dapat digunakan dengan atau tanpa parameter?

90

Saya ingin membuat dekorator Python yang dapat digunakan baik dengan parameter:

@redirect_output("somewhere.log")
def foo():
    ....

atau tanpa mereka (misalnya untuk mengarahkan output ke stderr secara default):

@redirect_output
def foo():
    ....

Apakah itu mungkin?

Perhatikan bahwa saya tidak mencari solusi berbeda untuk masalah pengalihan keluaran, ini hanya contoh sintaks yang ingin saya capai.

elifiner
sumber
Tampilan default @redirect_outputsangat tidak informatif. Saya menyarankan itu ide yang buruk. Gunakan formulir pertama dan sederhanakan hidup Anda.
S. Lotot
pertanyaan menarik meskipun - sampai saya melihatnya dan melihat-lihat dokumentasi, saya akan berasumsi bahwa @f sama dengan @f (), dan saya masih berpikir itu harus, jujur ​​saja (argumen yang diberikan hanya akan ditempelkan ke argumen fungsi)
rog

Jawaban:

68

Saya tahu pertanyaan ini sudah lama, tetapi beberapa komentar baru, dan sementara semua solusi yang layak pada dasarnya sama, kebanyakan tidak terlalu bersih atau mudah dibaca.

Seperti jawaban thobe katakan, satu-satunya cara untuk menangani kedua kasus tersebut adalah dengan memeriksa kedua skenario. Cara termudah adalah dengan memeriksa untuk melihat apakah ada satu argumen dan itu adalah callabe (CATATAN: pemeriksaan tambahan akan diperlukan jika dekorator Anda hanya mengambil 1 argumen dan kebetulan itu adalah objek yang dapat dipanggil):

def decorator(*args, **kwargs):
    if len(args) == 1 and len(kwargs) == 0 and callable(args[0]):
        # called as @decorator
    else:
        # called as @decorator(*args, **kwargs)

Dalam kasus pertama, Anda melakukan apa yang dilakukan dekorator normal mana pun, mengembalikan versi modifikasi atau gabungan dari fungsi yang diteruskan.

Dalam kasus kedua, Anda mengembalikan dekorator 'baru' yang entah bagaimana menggunakan informasi yang diteruskan dengan * args, ** kwargs.

Ini bagus dan semuanya, tetapi harus menuliskannya untuk setiap dekorator yang Anda buat bisa sangat mengganggu dan tidak sebersih. Alih-alih, alangkah baiknya dapat secara otomatis memodifikasi dekorator kami tanpa harus menulis ulang ... tapi itulah gunanya dekorator!

Dengan menggunakan dekorator dekorator berikut, kita dapat mendeokrasi dekorator kita sehingga bisa digunakan dengan atau tanpa argumen:

def doublewrap(f):
    '''
    a decorator decorator, allowing the decorator to be used as:
    @decorator(with, arguments, and=kwargs)
    or
    @decorator
    '''
    @wraps(f)
    def new_dec(*args, **kwargs):
        if len(args) == 1 and len(kwargs) == 0 and callable(args[0]):
            # actual decorated function
            return f(args[0])
        else:
            # decorator arguments
            return lambda realf: f(realf, *args, **kwargs)

    return new_dec

Sekarang, kita dapat menghias dekorator kita dengan @doublewrap, dan mereka akan bekerja dengan dan tanpa argumen, dengan satu peringatan:

Saya sebutkan di atas tetapi harus diulangi di sini, centang di dekorator ini membuat asumsi tentang argumen yang dapat diterima dekorator (yaitu tidak dapat menerima satu pun, argumen yang dapat dipanggil). Karena kami membuatnya dapat diterapkan ke generator apa pun sekarang, itu perlu diingat, atau dimodifikasi jika akan bertentangan.

Berikut ini menunjukkan penggunaannya:

def test_doublewrap():
    from util import doublewrap
    from functools import wraps    

    @doublewrap
    def mult(f, factor=2):
        '''multiply a function's return value'''
        @wraps(f)
        def wrap(*args, **kwargs):
            return factor*f(*args,**kwargs)
        return wrap

    # try normal
    @mult
    def f(x, y):
        return x + y

    # try args
    @mult(3)
    def f2(x, y):
        return x*y

    # try kwargs
    @mult(factor=5)
    def f3(x, y):
        return x - y

    assert f(2,3) == 10
    assert f2(2,5) == 30
    assert f3(8,1) == 5*7
bj0
sumber
31

Menggunakan argumen kata kunci dengan nilai default (seperti yang disarankan oleh kquinn) adalah ide yang bagus, tetapi Anda harus menyertakan tanda kurung:

@redirect_output()
def foo():
    ...

Jika Anda menginginkan versi yang berfungsi tanpa tanda kurung pada dekorator, Anda harus memperhitungkan kedua skenario dalam kode dekorator Anda.

Jika Anda menggunakan Python 3.0, Anda dapat menggunakan argumen hanya kata kunci untuk ini:

def redirect_output(fn=None,*,destination=None):
  destination = sys.stderr if destination is None else destination
  def wrapper(*args, **kwargs):
    ... # your code here
  if fn is None:
    def decorator(fn):
      return functools.update_wrapper(wrapper, fn)
    return decorator
  else:
    return functools.update_wrapper(wrapper, fn)

Di Python 2.x ini dapat ditiru dengan trik varargs:

def redirected_output(*fn,**options):
  destination = options.pop('destination', sys.stderr)
  if options:
    raise TypeError("unsupported keyword arguments: %s" % 
                    ",".join(options.keys()))
  def wrapper(*args, **kwargs):
    ... # your code here
  if fn:
    return functools.update_wrapper(wrapper, fn[0])
  else:
    def decorator(fn):
      return functools.update_wrapper(wrapper, fn)
    return decorator

Salah satu versi ini memungkinkan Anda untuk menulis kode seperti ini:

@redirected_output
def foo():
    ...

@redirected_output(destination="somewhere.log")
def bar():
    ...
thobe
sumber
1
Apa yang kamu masukkan your code here? Bagaimana Anda memanggil fungsi yang didekorasi? fn(*args, **kwargs)tidak bekerja.
lum
Saya pikir ada jawaban yang lebih sederhana, buat kelas yang akan menjadi dekorator dengan argumen opsional. buat fungsi lain dengan argumen yang sama dengan default dan kembalikan instance baru dari kelas dekorator. harus terlihat seperti: def f(a = 5): return MyDecorator( a = a) dan class MyDecorator( object ): def __init__( self, a = 5 ): .... maaf sulit menuliskannya di komentar tetapi saya harap ini cukup sederhana untuk dipahami
Omer Ben Haim
17

Saya tahu ini adalah pertanyaan lama, tetapi saya benar-benar tidak menyukai teknik yang diusulkan jadi saya ingin menambahkan metode lain. Saya melihat bahwa django menggunakan metode yang sangat bersih di login_requireddekoratordjango.contrib.auth.decorators mereka di . Seperti yang Anda lihat di dokumen dekorator , ini bisa digunakan sendiri sebagai @login_requiredatau dengan argumen @login_required(redirect_field_name='my_redirect_field'),.

Cara mereka melakukannya cukup sederhana. Mereka menambahkan kwarg( function=None) sebelum argumen dekoratornya. Jika dekorator digunakan sendiri, functionakan menjadi fungsi sebenarnya yang didekorasi, sedangkan jika dipanggil dengan argumen, functionakan None.

Contoh:

from functools import wraps

def custom_decorator(function=None, some_arg=None, some_other_arg=None):
    def actual_decorator(f):
        @wraps(f)
        def wrapper(*args, **kwargs):
            # Do stuff with args here...
            if some_arg:
                print(some_arg)
            if some_other_arg:
                print(some_other_arg)
            return f(*args, **kwargs)
        return wrapper
    if function:
        return actual_decorator(function)
    return actual_decorator

@custom_decorator
def test1():
    print('test1')

>>> test1()
test1

@custom_decorator(some_arg='hello')
def test2():
    print('test2')

>>> test2()
hello
test2

@custom_decorator(some_arg='hello', some_other_arg='world')
def test3():
    print('test3')

>>> test3()
hello
world
test3

Saya menemukan pendekatan yang digunakan Django ini lebih elegan dan lebih mudah untuk dipahami daripada teknik lain yang diusulkan di sini.

dgel
sumber
Ya, saya suka metode ini. Perhatikan bahwa Anda harus menggunakan kwargs saat memanggil dekorator jika tidak, argumen posisi pertama ditetapkan ke functiondan kemudian semuanya akan rusak karena dekorator mencoba memanggil argumen posisi pertama itu seolah-olah itu adalah fungsi dekorasi Anda.
Dustin Wyatt
12

Anda perlu mendeteksi kedua kasus, misalnya menggunakan tipe argumen pertama, dan karenanya mengembalikan pembungkus (bila digunakan tanpa parameter) atau dekorator (bila digunakan dengan argumen).

from functools import wraps
import inspect

def redirect_output(fn_or_output):
    def decorator(fn):
        @wraps(fn)
        def wrapper(*args, **args):
            # Redirect output
            try:
                return fn(*args, **args)
            finally:
                # Restore output
        return wrapper

    if inspect.isfunction(fn_or_output):
        # Called with no parameter
        return decorator(fn_or_output)
    else:
        # Called with a parameter
        return decorator

Saat menggunakan @redirect_output("output.log")sintaks, redirect_outputdipanggil dengan satu argumen "output.log", dan harus mengembalikan dekorator yang menerima fungsi untuk didekorasi sebagai argumen. Saat digunakan sebagai @redirect_output, itu dipanggil langsung dengan fungsi yang akan didekorasi sebagai argumen.

Atau dengan kata lain: @sintaks harus diikuti oleh ekspresi yang hasilnya adalah fungsi yang menerima fungsi yang akan didekorasi sebagai argumen tunggalnya, dan mengembalikan fungsi yang didekorasi. Ekspresi itu sendiri dapat berupa pemanggilan fungsi, seperti halnya dengan @redirect_output("output.log"). Berbelit-belit, tapi benar :-)

Remy Blank
sumber
9

Beberapa jawaban di sini sudah mengatasi masalah Anda dengan baik. Berkenaan dengan gaya, bagaimanapun, saya lebih suka memecahkan kesulitan dekorator ini menggunakan functools.partial, seperti yang disarankan dalam Python Cookbook 3 David Beazley :

from functools import partial, wraps

def decorator(func=None, foo='spam'):
    if func is None:
         return partial(decorator, foo=foo)

    @wraps(func)
    def wrapper(*args, **kwargs):
        # do something with `func` and `foo`, if you're so inclined
        pass

    return wrapper

Meskipun ya, Anda bisa melakukannya

@decorator()
def f(*args, **kwargs):
    pass

tanpa solusi yang funky, saya merasa terlihat aneh, dan saya suka memiliki pilihan untuk hanya mendekorasi @decorator.

Adapun tujuan misi sekunder, mengalihkan output fungsi dibahas dalam posting Stack Overflow ini .


Jika Anda ingin mendalami lebih dalam, lihat Bab 9 (Metaprogramming) dengan Python Cookbook 3 , yang tersedia secara gratis untuk dibaca secara online .

Beberapa dari materi itu didemokan secara langsung (ditambah lagi!) Di video YouTube Beazley yang mengagumkan, Python 3 Metaprogramming .

Selamat coding :)

henrywallace
sumber
8

Dekorator python dipanggil dengan cara yang berbeda secara fundamental tergantung pada apakah Anda memberikan argumen atau tidak. Dekorasi sebenarnya hanyalah ekspresi (dibatasi secara sintaksis).

Dalam contoh pertama Anda:

@redirect_output("somewhere.log")
def foo():
    ....

fungsi redirect_outputtersebut dipanggil dengan argumen yang diberikan, yang diharapkan mengembalikan fungsi dekorator, yang dengan sendirinya dipanggil dengan foosebagai argumen, yang (akhirnya!) diharapkan mengembalikan fungsi dekorasi akhir.

Kode yang setara terlihat seperti ini:

def foo():
    ....
d = redirect_output("somewhere.log")
foo = d(foo)

Kode yang setara untuk contoh kedua Anda terlihat seperti:

def foo():
    ....
d = redirect_output
foo = d(foo)

Jadi Anda dapat melakukan apa yang Anda suka tetapi tidak dengan cara yang sepenuhnya mulus:

import types
def redirect_output(arg):
    def decorator(file, f):
        def df(*args, **kwargs):
            print 'redirecting to ', file
            return f(*args, **kwargs)
        return df
    if type(arg) is types.FunctionType:
        return decorator(sys.stderr, arg)
    return lambda f: decorator(arg, f)

Ini akan baik-baik saja kecuali Anda ingin menggunakan suatu fungsi sebagai argumen untuk dekorator Anda, dalam hal ini dekorator akan salah berasumsi bahwa ia tidak memiliki argumen. Ini juga akan gagal jika dekorasi ini diterapkan pada dekorasi lain yang tidak mengembalikan jenis fungsi.

Metode alternatif hanya mengharuskan fungsi dekorator selalu dipanggil, meskipun tanpa argumen. Dalam kasus ini, contoh kedua Anda akan terlihat seperti ini:

@redirect_output()
def foo():
    ....

Kode fungsi dekorator akan terlihat seperti ini:

def redirect_output(file = sys.stderr):
    def decorator(file, f):
        def df(*args, **kwargs):
            print 'redirecting to ', file
            return f(*args, **kwargs)
        return df
    return lambda f: decorator(file, f)
rog
sumber
2

Faktanya, kasus peringatan dalam solusi @ bj0 dapat diperiksa dengan mudah:

def meta_wrap(decor):
    @functools.wraps(decor)
    def new_decor(*args, **kwargs):
        if len(args) == 1 and len(kwargs) == 0 and callable(args[0]):
            # this is the double-decorated f. 
            # Its first argument should not be a callable
            doubled_f = decor(args[0])
            @functools.wraps(doubled_f)
            def checked_doubled_f(*f_args, **f_kwargs):
                if callable(f_args[0]):
                    raise ValueError('meta_wrap failure: '
                                'first positional argument cannot be callable.')
                return doubled_f(*f_args, **f_kwargs)
            return checked_doubled_f 
        else:
            # decorator arguments
            return lambda real_f: decor(real_f, *args, **kwargs)

    return new_decor

Berikut adalah beberapa kasus uji untuk versi aman-gagal ini meta_wrap.

    @meta_wrap
    def baddecor(f, caller=lambda x: -1*x):
        @functools.wraps(f)
        def _f(*args, **kwargs):
            return caller(f(args[0]))
        return _f

    @baddecor  # used without arg: no problem
    def f_call1(x):
        return x + 1
    assert f_call1(5) == -6

    @baddecor(lambda x : 2*x) # bad case
    def f_call2(x):
        return x + 1
    f_call2(5)  # raises ValueError

    # explicit keyword: no problem
    @baddecor(caller=lambda x : 100*x)
    def f_call3(x):
        return x + 1
    assert f_call3(5) == 600
Ainz Titor
sumber
1
Terima kasih. Ini sangat membantu!
Pragy Agarwal
0

Untuk memberikan jawaban yang lebih lengkap dari yang di atas:

"Adakah cara untuk membangun dekorator yang bisa digunakan dengan dan tanpa argumen?"

Tidak ada cara umum karena saat ini ada sesuatu yang hilang dalam bahasa python untuk mendeteksi dua kasus penggunaan yang berbeda.

Namun Ya seperti yang telah ditunjukkan oleh jawaban lain seperti bj0s , ada solusi kikuk yaitu memeriksa jenis dan nilai dari argumen posisi pertama yang diterima (dan untuk memeriksa apakah tidak ada argumen lain yang memiliki nilai non-default). Jika Anda dijamin bahwa pengguna tidak akan pernah meneruskan callable sebagai argumen pertama dekorator Anda, Anda dapat menggunakan solusi ini. Perhatikan bahwa ini sama untuk dekorator kelas (ganti dapat dipanggil oleh kelas di kalimat sebelumnya).

Untuk memastikan hal di atas, saya melakukan sedikit penelitian di luar sana dan bahkan menerapkan perpustakaan bernama decopatchyang menggunakan kombinasi dari semua strategi yang dikutip di atas (dan banyak lagi, termasuk introspeksi) untuk melakukan "solusi apa pun yang paling cerdas" bergantung sesuai kebutuhan Anda.

Tapi sejujurnya yang terbaik adalah tidak membutuhkan perpustakaan apa pun di sini dan mendapatkan fitur itu langsung dari bahasa python. Jika, seperti saya, Anda merasa sayang sekali bahwa bahasa python tidak mampu memberikan jawaban yang tepat untuk pertanyaan ini, jangan ragu untuk mendukung gagasan ini di bugtracker python : https: //bugs.python .org / issue36553 !

Terima kasih banyak atas bantuan Anda membuat python menjadi bahasa yang lebih baik :)

smarie
sumber
0

Ini melakukan pekerjaan tanpa repot:

from functools import wraps

def memoize(fn=None, hours=48.0):
  def deco(fn):
    @wraps(fn)
    def wrapper(*args, **kwargs):
      return fn(*args, **kwargs)
    return wrapper

  if callable(fn): return deco(fn)
  return deco
j4hangir.dll
sumber
0

Karena tidak ada yang menyebutkan ini, ada juga solusi yang menggunakan kelas yang dapat dipanggil yang menurut saya lebih elegan, terutama dalam kasus di mana dekoratornya rumit dan orang mungkin ingin membaginya menjadi beberapa metode (fungsi). Solusi ini menggunakan __new__metode ajaib untuk melakukan apa yang pada dasarnya telah ditunjukkan oleh orang lain. Pertama-tama deteksi bagaimana dekorator digunakan, kemudian sesuaikan kembali dengan tepat.

class decorator_with_arguments(object):

    def __new__(cls, decorated_function=None, **kwargs):

        self = super().__new__(cls)
        self._init(**kwargs)

        if not decorated_function:
            return self
        else:
            return self.__call__(decorated_function)

    def _init(self, arg1="default", arg2="default", arg3="default"):
        self.arg1 = arg1
        self.arg2 = arg2
        self.arg3 = arg3

    def __call__(self, decorated_function):

        def wrapped_f(*args):
            print("Decorator arguments:", self.arg1, self.arg2, self.arg3)
            print("decorated_function arguments:", *args)
            decorated_function(*args)

        return wrapped_f

@decorator_with_arguments(arg1=5)
def sayHello(a1, a2, a3, a4):
    print('sayHello arguments:', a1, a2, a3, a4)

@decorator_with_arguments()
def sayHello(a1, a2, a3, a4):
    print('sayHello arguments:', a1, a2, a3, a4)

@decorator_with_arguments
def sayHello(a1, a2, a3, a4):
    print('sayHello arguments:', a1, a2, a3, a4)

Jika dekorator digunakan dengan argumen, maka ini sama dengan:

result = decorator_with_arguments(arg1=5)(sayHello)(a1, a2, a3, a4)

Dapat dilihat bahwa argumen arg1diteruskan dengan benar ke konstruktor dan fungsi yang didekorasi diteruskan ke__call__

Tetapi jika dekorator digunakan tanpa argumen, maka ini sama dengan:

result = decorator_with_arguments(sayHello)(a1, a2, a3, a4)

Anda melihat bahwa dalam kasus ini fungsi yang didekorasi diteruskan langsung ke konstruktor dan panggilan ke __call__dihilangkan seluruhnya. Itulah mengapa kita perlu menggunakan logika untuk menangani kasus ini dengan __new__metode ajaib.

Mengapa kita tidak bisa menggunakan __init__alih-alih __new__? Alasannya sederhana: python melarang mengembalikan nilai selain dari None from__init__

PERINGATAN

Pendekatan ini memiliki satu efek samping. Itu tidak akan mempertahankan tanda tangan fungsi!

Majo
sumber
-1

Sudahkah Anda mencoba argumen kata kunci dengan nilai default? Sesuatu seperti

def decorate_something(foo=bar, baz=quux):
    pass
kquinn
sumber
-2

Umumnya Anda dapat memberikan argumen default dengan Python ...

def redirect_output(fn, output = stderr):
    # whatever

Tidak yakin apakah itu berfungsi dengan dekorator juga. Saya tidak tahu alasan mengapa itu tidak terjadi.

David Z
sumber
2
Jika Anda mengatakan @dec (abc) fungsinya tidak langsung diteruskan ke dec. dec (abc) mengembalikan sesuatu, dan nilai pengembalian ini digunakan sebagai dekorator. Jadi dec (abc) harus mengembalikan sebuah fungsi, yang kemudian membuat fungsi yang didekorasi dilewatkan sebagai parameter. (Juga lihat kode thobes)
sth
-2

Membangun jawaban vartec:

imports sys

def redirect_output(func, output=None):
    if output is None:
        output = sys.stderr
    if isinstance(output, basestring):
        output = open(output, 'w') # etc...
    # everything else...
muhuk
sumber
ini tidak dapat digunakan sebagai dekorator seperti pada @redirect_output("somewhere.log") def foo()contoh di pertanyaan.
ehabkost