Melestarikan tanda tangan dari fungsi yang didekorasi

111

Misalkan saya telah menulis seorang dekorator yang melakukan sesuatu yang sangat umum. Misalnya, ini mungkin mengubah semua argumen ke tipe tertentu, melakukan logging, mengimplementasikan memoization, dll.

Berikut ini contohnya:

def args_as_ints(f):
    def g(*args, **kwargs):
        args = [int(x) for x in args]
        kwargs = dict((k, int(v)) for k, v in kwargs.items())
        return f(*args, **kwargs)
    return g

@args_as_ints
def funny_function(x, y, z=3):
    """Computes x*y + 2*z"""
    return x*y + 2*z

>>> funny_function("3", 4.0, z="5")
22

Semuanya baik-baik saja sejauh ini. Namun, ada satu masalah. Fungsi dekorasi tidak menyimpan dokumentasi dari fungsi aslinya:

>>> help(funny_function)
Help on function g in module __main__:

g(*args, **kwargs)

Untungnya, ada solusi:

def args_as_ints(f):
    def g(*args, **kwargs):
        args = [int(x) for x in args]
        kwargs = dict((k, int(v)) for k, v in kwargs.items())
        return f(*args, **kwargs)
    g.__name__ = f.__name__
    g.__doc__ = f.__doc__
    return g

@args_as_ints
def funny_function(x, y, z=3):
    """Computes x*y + 2*z"""
    return x*y + 2*z

Kali ini, nama fungsi dan dokumentasinya benar:

>>> help(funny_function)
Help on function funny_function in module __main__:

funny_function(*args, **kwargs)
    Computes x*y + 2*z

Tapi masih ada masalah: tanda tangan fungsi salah. Informasi "* args, ** kwargs" hampir tidak berguna.

Apa yang harus dilakukan? Saya dapat memikirkan dua solusi sederhana namun cacat:

1 - Sertakan tanda tangan yang benar di docstring:

def funny_function(x, y, z=3):
    """funny_function(x, y, z=3) -- computes x*y + 2*z"""
    return x*y + 2*z

Ini buruk karena duplikasi. Tanda tangan tetap tidak akan ditampilkan dengan benar dalam dokumentasi yang dibuat secara otomatis. Sangat mudah untuk memperbarui fungsi dan melupakan tentang mengubah docstring, atau salah ketik. [ Dan ya, saya menyadari fakta bahwa docstring sudah menduplikasi badan fungsi. Harap abaikan ini; funny_function hanyalah contoh acak. ]

2 - Tidak menggunakan dekorator, atau menggunakan dekorator tujuan khusus untuk setiap tanda tangan tertentu:

def funny_functions_decorator(f):
    def g(x, y, z=3):
        return f(int(x), int(y), z=int(z))
    g.__name__ = f.__name__
    g.__doc__ = f.__doc__
    return g

Ini berfungsi dengan baik untuk sekumpulan fungsi yang memiliki tanda tangan identik, tetapi tidak berguna secara umum. Seperti yang saya katakan di awal, saya ingin dapat menggunakan dekorator secara umum.

Saya mencari solusi yang sepenuhnya umum, dan otomatis.

Jadi pertanyaannya adalah: apakah ada cara untuk mengedit tanda tangan fungsi yang didekorasi setelah dibuat?

Jika tidak, dapatkah saya menulis dekorator yang mengekstrak tanda tangan fungsi dan menggunakan informasi itu daripada "* kwargs, ** kwargs" saat membuat fungsi yang didekorasi? Bagaimana cara mengekstrak informasi itu? Bagaimana saya harus membangun fungsi yang didekorasi - dengan exec?

Ada pendekatan lain?

Fredrik Johansson
sumber
1
Tidak pernah mengatakan "ketinggalan zaman". Saya kurang lebih bertanya-tanya apa yang inspect.Signatureditambahkan untuk berurusan dengan fungsi dekorasi.
NightShadeQueen

Jawaban:

79
  1. Pasang modul dekorator :

    $ pip install decorator
  2. Sesuaikan definisi dari args_as_ints():

    import decorator
    
    @decorator.decorator
    def args_as_ints(f, *args, **kwargs):
        args = [int(x) for x in args]
        kwargs = dict((k, int(v)) for k, v in kwargs.items())
        return f(*args, **kwargs)
    
    @args_as_ints
    def funny_function(x, y, z=3):
        """Computes x*y + 2*z"""
        return x*y + 2*z
    
    print funny_function("3", 4.0, z="5")
    # 22
    help(funny_function)
    # Help on function funny_function in module __main__:
    # 
    # funny_function(x, y, z=3)
    #     Computes x*y + 2*z

Python 3.4+

functools.wraps()from stdlib mempertahankan tanda tangan sejak Python 3.4:

import functools


def args_as_ints(func):
    @functools.wraps(func)
    def wrapper(*args, **kwargs):
        args = [int(x) for x in args]
        kwargs = dict((k, int(v)) for k, v in kwargs.items())
        return func(*args, **kwargs)
    return wrapper


@args_as_ints
def funny_function(x, y, z=3):
    """Computes x*y + 2*z"""
    return x*y + 2*z


print(funny_function("3", 4.0, z="5"))
# 22
help(funny_function)
# Help on function funny_function in module __main__:
#
# funny_function(x, y, z=3)
#     Computes x*y + 2*z

functools.wraps()tersedia setidaknya sejak Python 2.5 tetapi tidak mempertahankan tanda tangannya di sana:

help(funny_function)
# Help on function funny_function in module __main__:
#
# funny_function(*args, **kwargs)
#    Computes x*y + 2*z

Perhatikan: *args, **kwargsalih-alih x, y, z=3.

jfs
sumber
Jawaban Anda bukanlah jawaban pertama, tetapi yang paling komprehensif sejauh ini :-) Saya sebenarnya lebih suka solusi yang tidak melibatkan modul pihak ketiga, tetapi melihat sumber modul dekorator, cukup sederhana sehingga saya dapat melakukannya salin saja.
Fredrik Johansson
1
@MarkLodato: functools.wraps()sudah mempertahankan tanda tangan di Python 3.4+ (seperti yang dikatakan dalam jawaban). Apakah maksud Anda pengaturan wrapper.__signature__membantu pada versi sebelumnya? (versi mana yang sudah Anda uji?)
jfs
1
@MarkLodato: help()menunjukkan tanda tangan yang benar pada Python 3.4. Menurut Anda mengapa functools.wraps()rusak dan bukan IPython?
jfs
1
@ MarkLodato: rusak jika kita harus menulis kode untuk memperbaikinya. Mengingat itu help()menghasilkan hasil yang benar, pertanyaannya adalah perangkat lunak apa yang harus diperbaiki: functools.wraps()atau IPython? Bagaimanapun, menetapkan secara manual __signature__adalah solusi terbaik - ini bukan solusi jangka panjang.
jfs
1
Sepertinya inspect.getfullargspec()masih tidak mengembalikan tanda tangan yang benar untuk functools.wrapspython 3.4 dan inspect.signature()sebagai gantinya Anda harus menggunakannya .
Tuukka Mustonen
16

Ini diselesaikan dengan pustaka standar Python functools dan functools.wrapsfungsi khusus , yang dirancang untuk " memperbarui fungsi pembungkus agar terlihat seperti fungsi yang dibungkus ". Perilakunya tergantung pada versi Python, seperti yang ditunjukkan di bawah ini. Diterapkan pada contoh dari pertanyaan, kodenya akan terlihat seperti:

from functools import wraps

def args_as_ints(f):
    @wraps(f) 
    def g(*args, **kwargs):
        args = [int(x) for x in args]
        kwargs = dict((k, int(v)) for k, v in kwargs.items())
        return f(*args, **kwargs)
    return g


@args_as_ints
def funny_function(x, y, z=3):
    """Computes x*y + 2*z"""
    return x*y + 2*z

Saat dieksekusi dengan Python 3, ini akan menghasilkan yang berikut:

>>> funny_function("3", 4.0, z="5")
22
>>> help(funny_function)
Help on function funny_function in module __main__:

funny_function(x, y, z=3)
    Computes x*y + 2*z

Satu-satunya kekurangannya adalah bahwa di Python 2, ia tidak memperbarui daftar argumen fungsi. Saat dijalankan dengan Python 2, itu akan menghasilkan:

>>> help(funny_function)
Help on function funny_function in module __main__:

funny_function(*args, **kwargs)
    Computes x*y + 2*z
Timur
sumber
Tidak yakin apakah itu Sphinx, tetapi ini sepertinya tidak berfungsi ketika fungsi yang dibungkus adalah metode kelas. Sphinx terus melaporkan tanda tangan panggilan dari dekorator.
alphabetasoup
9

Ada moduldecorator dekorator dengan dekorator yang dapat Anda gunakan:

@decorator
def args_as_ints(f, *args, **kwargs):
    args = [int(x) for x in args]
    kwargs = dict((k, int(v)) for k, v in kwargs.items())
    return f(*args, **kwargs)

Kemudian tanda tangan dan bantuan metode tersebut dipertahankan:

>>> help(funny_function)
Help on function funny_function in module __main__:

funny_function(x, y, z=3)
    Computes x*y + 2*z

EDIT: JF Sebastian menunjukkan bahwa saya tidak mengubah args_as_intsfungsi - sudah diperbaiki sekarang.

DzinX
sumber
8

Lihatlah modul dekorator - khususnya dekorator dekorator, yang memecahkan masalah ini.

Brian
sumber
6

Opsi kedua:

  1. Instal modul wrapt:

$ easy_install wrapt

wrapt mendapat bonus, pertahankan tanda tangan kelas.


import wrapt
import inspect

@wrapt.decorator def args_as_ints(wrapped, instance, args, kwargs): if instance is None: if inspect.isclass(wrapped): # Decorator was applied to a class. return wrapped(*args, **kwargs) else: # Decorator was applied to a function or staticmethod. return wrapped(*args, **kwargs) else: if inspect.isclass(instance): # Decorator was applied to a classmethod. return wrapped(*args, **kwargs) else: # Decorator was applied to an instancemethod. return wrapped(*args, **kwargs) @args_as_ints def funny_function(x, y, z=3): """Computes x*y + 2*z""" return x * y + 2 * z >>> funny_function(3, 4, z=5)) # 22 >>> help(funny_function) Help on function funny_function in module __main__: funny_function(x, y, z=3) Computes x*y + 2*z
macm
sumber
2

Seperti yang dikomentari di atas dalam jawaban jfs ; jika Anda khawatir dengan tanda tangan dalam hal tampilan ( help, dan inspect.signature), maka penggunaan functools.wrapstidak masalah.

Jika Anda khawatir dengan tanda tangan dalam hal perilaku (khususnya TypeErrordalam kasus ketidakcocokan argumen), functools.wrapsjangan pertahankan. Anda lebih suka menggunakan decoratoruntuk itu, atau generalisasi saya tentang mesin intinya, bernama makefun.

from makefun import wraps

def args_as_ints(func):
    @wraps(func)
    def wrapper(*args, **kwargs):
        print("wrapper executes")
        args = [int(x) for x in args]
        kwargs = dict((k, int(v)) for k, v in kwargs.items())
        return func(*args, **kwargs)
    return wrapper


@args_as_ints
def funny_function(x, y, z=3):
    """Computes x*y + 2*z"""
    return x*y + 2*z


print(funny_function("3", 4.0, z="5"))
# wrapper executes
# 22

help(funny_function)
# Help on function funny_function in module __main__:
#
# funny_function(x, y, z=3)
#     Computes x*y + 2*z

funny_function(0)  
# observe: no "wrapper executes" is printed! (with functools it would)
# TypeError: funny_function() takes at least 2 arguments (1 given)

Lihat juga postingan ini tentangfunctools.wraps .

smarie
sumber
1
Selain itu, hasil dari inspect.getfullargspectidak disimpan dengan menelepon functools.wraps.
laike9m
Terima kasih atas komentar tambahan yang berguna @ laike9m!
smarie