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.
@redirect_output
sangat tidak informatif. Saya menyarankan itu ide yang buruk. Gunakan formulir pertama dan sederhanakan hidup Anda.Jawaban:
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
sumber
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(): ...
sumber
your code here
? Bagaimana Anda memanggil fungsi yang didekorasi?fn(*args, **kwargs)
tidak bekerja.def f(a = 5): return MyDecorator( a = a)
danclass MyDecorator( object ): def __init__( self, a = 5 ): ....
maaf sulit menuliskannya di komentar tetapi saya harap ini cukup sederhana untuk dipahamiSaya 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_required
dekoratordjango.contrib.auth.decorators
mereka di . Seperti yang Anda lihat di dokumen dekorator , ini bisa digunakan sendiri sebagai@login_required
atau 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,function
akan menjadi fungsi sebenarnya yang didekorasi, sedangkan jika dipanggil dengan argumen,function
akanNone
.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.
sumber
function
dan kemudian semuanya akan rusak karena dekorator mencoba memanggil argumen posisi pertama itu seolah-olah itu adalah fungsi dekorasi Anda.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_output
dipanggil 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 :-)sumber
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 :)
sumber
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_output
tersebut dipanggil dengan argumen yang diberikan, yang diharapkan mengembalikan fungsi dekorator, yang dengan sendirinya dipanggil denganfoo
sebagai 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)
sumber
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
sumber
Untuk memberikan jawaban yang lebih lengkap dari yang di atas:
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
bj0
s , 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
decopatch
yang 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 :)
sumber
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
sumber
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
arg1
diteruskan dengan benar ke konstruktor dan fungsi yang didekorasi diteruskan ke__call__
Tetapi jika dekorator digunakan tanpa argumen, maka ini sama dengan:
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!
sumber
Sudahkah Anda mencoba argumen kata kunci dengan nilai default? Sesuatu seperti
def decorate_something(foo=bar, baz=quux): pass
sumber
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.
sumber
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...
sumber
@redirect_output("somewhere.log") def foo()
contoh di pertanyaan.