Apa yang dilakukan functools.wraps?

650

Dalam komentar tentang jawaban untuk pertanyaan lain ini , seseorang mengatakan bahwa mereka tidak yakin apa yang functools.wrapssedang dilakukan. Jadi, saya mengajukan pertanyaan ini sehingga akan ada catatan di StackOverflow untuk referensi di masa mendatang: apa yang functools.wrapsdilakukan, tepatnya?

Eli Courtwright
sumber

Jawaban:

1070

Saat Anda menggunakan dekorator, Anda mengganti satu fungsi dengan yang lain. Dengan kata lain, jika Anda memiliki dekorator

def logged(func):
    def with_logging(*args, **kwargs):
        print(func.__name__ + " was called")
        return func(*args, **kwargs)
    return with_logging

lalu ketika Anda mengatakannya

@logged
def f(x):
   """does some math"""
   return x + x * x

persis sama dengan mengatakan

def f(x):
    """does some math"""
    return x + x * x
f = logged(f)

dan fungsi Anda fdiganti dengan fungsi with_logging. Sayangnya, ini berarti jika Anda mengatakannya

print(f.__name__)

itu akan dicetak with_loggingkarena itulah nama fungsi baru Anda. Bahkan, jika Anda melihat docstring untuk f, itu akan kosong karena with_loggingtidak memiliki docstring, sehingga docstring yang Anda tulis tidak akan ada lagi. Juga, jika Anda melihat hasil pydoc untuk fungsi itu, itu tidak akan terdaftar sebagai mengambil satu argumen x; alih-alih itu akan terdaftar sebagai pengambilan *argsdan **kwargskarena itulah yang mengambil with_logging.

Jika menggunakan dekorator selalu berarti kehilangan informasi ini tentang suatu fungsi, itu akan menjadi masalah serius. Itu sebabnya kami punya functools.wraps. Ini mengambil fungsi yang digunakan dalam dekorator dan menambahkan fungsi menyalin di atas nama fungsi, docstring, daftar argumen, dll. Dan karena wrapsitu sendiri adalah dekorator, kode berikut melakukan hal yang benar:

from functools import wraps
def logged(func):
    @wraps(func)
    def with_logging(*args, **kwargs):
        print(func.__name__ + " was called")
        return func(*args, **kwargs)
    return with_logging

@logged
def f(x):
   """does some math"""
   return x + x * x

print(f.__name__)  # prints 'f'
print(f.__doc__)   # prints 'does some math'
Eli Courtwright
sumber
7
Yap, saya lebih suka menghindari modul dekorator karena functools.wraps adalah bagian dari perpustakaan standar dan karenanya tidak memperkenalkan ketergantungan eksternal lain. Tetapi modul dekorator memang memecahkan masalah bantuan, yang diharapkan bisa berfungsi juga.
Eli Courtwright
6
inilah contoh apa yang bisa terjadi jika Anda tidak menggunakan wraps: tes doctools tiba-tiba bisa hilang. itu karena doctools tidak dapat menemukan tes dalam fungsi yang didekorasi kecuali sesuatu seperti wraps () telah menyalinnya.
andrew cooke
88
mengapa kita membutuhkan functools.wrapspekerjaan ini, bukankah itu hanya menjadi bagian dari pola dekorator? kapan Anda tidak ingin menggunakan @wraps?
wim
56
@ wim: Saya telah menulis beberapa dekorator yang melakukan versi mereka sendiri @wrapsuntuk melakukan berbagai jenis modifikasi atau anotasi pada nilai yang disalin. Pada dasarnya, ini merupakan perpanjangan dari filosofi Python yang eksplisit lebih baik daripada kasus implisit dan khusus tidak cukup istimewa untuk melanggar aturan. (Kode ini jauh lebih sederhana dan bahasanya lebih mudah dimengerti jika @wrapsharus disediakan secara manual, daripada menggunakan semacam mekanisme penyisih khusus.)
ssokolow
35
@LucasMalor Tidak semua dekorator membungkus fungsi yang mereka hias. Beberapa menerapkan efek samping, seperti mendaftarkannya di semacam sistem pencarian.
ssokolow
22

Saya sangat sering menggunakan kelas, bukan fungsi, untuk dekorator saya. Saya mengalami beberapa masalah dengan ini karena suatu objek tidak akan memiliki semua atribut yang sama yang diharapkan dari suatu fungsi. Misalnya, suatu objek tidak akan memiliki atribut __name__. Saya punya masalah khusus dengan ini yang cukup sulit dilacak di mana Django melaporkan kesalahan "objek tidak memiliki atribut ' __name__'". Sayangnya, untuk dekorator gaya kelas, saya tidak percaya bahwa @wrap akan melakukan pekerjaan. Saya malah membuat kelas dekorator dasar seperti:

class DecBase(object):
    func = None

    def __init__(self, func):
        self.__func = func

    def __getattribute__(self, name):
        if name == "func":
            return super(DecBase, self).__getattribute__(name)

        return self.func.__getattribute__(name)

    def __setattr__(self, name, value):
        if name == "func":
            return super(DecBase, self).__setattr__(name, value)

        return self.func.__setattr__(name, value)

Kelas ini proksi semua panggilan atribut ke fungsi yang sedang didekorasi. Jadi, Anda sekarang dapat membuat dekorator sederhana yang memeriksa bahwa 2 argumen ditentukan seperti:

class process_login(DecBase):
    def __call__(self, *args):
        if len(args) != 2:
            raise Exception("You can only specify two arguments")

        return self.func(*args)
Josh
sumber
7
Seperti yang dikatakan oleh docs @wraps, @wrapshanyalah fungsi kenyamanan untuk functools.update_wrapper(). Dalam hal dekorator kelas, Anda dapat menelepon update_wrapper()langsung dari __init__()metode Anda . Jadi, Anda tidak perlu membuat DecBasesama sekali, Anda hanya dapat mencakup pada __init__()dari process_loginbaris: update_wrapper(self, func). Itu saja.
Fabiano
14

Pada python 3.5+:

@functools.wraps(f)
def g():
    pass

Adalah alias untuk g = functools.update_wrapper(g, f). Ia melakukan tiga hal:

  • itu salinan __module__, __name__, __qualname__, __doc__, dan __annotations__atribut fpada g. Daftar default ini ada WRAPPER_ASSIGNMENTS, Anda dapat melihatnya di sumber functools .
  • itu update __dict__dari gsemua elemen dari f.__dict__. (lihat WRAPPER_UPDATESdi sumber)
  • itu menetapkan __wrapped__=fatribut barug

Konsekuensinya adalah yang gmuncul memiliki nama yang sama, docstring, nama modul, dan tanda tangan daripada f. Satu-satunya masalah adalah bahwa mengenai tanda tangan ini sebenarnya tidak benar: hanya saja inspect.signaturemengikuti rantai pembungkus secara default. Anda dapat memeriksanya dengan menggunakan inspect.signature(g, follow_wrapped=False)seperti yang dijelaskan dalam dokumen . Ini memiliki konsekuensi yang menjengkelkan:

  • kode pembungkus akan mengeksekusi bahkan ketika argumen yang diberikan tidak valid.
  • kode wrapper tidak dapat dengan mudah mengakses argumen menggunakan namanya, dari argumen * yang diterima, ** kwargs. Memang kita harus menangani semua kasus (posisi, kata kunci, default) dan karenanya menggunakan sesuatu seperti Signature.bind().

Sekarang ada sedikit kebingungan antara functools.wrapsdan dekorator, karena kasus penggunaan yang sangat sering untuk mengembangkan dekorator adalah untuk membungkus fungsi. Tetapi keduanya adalah konsep yang sepenuhnya independen. Jika Anda tertarik untuk memahami perbedaannya, saya menerapkan perpustakaan pembantu untuk keduanya: dekopatch untuk menulis dekorator dengan mudah, dan makefun untuk menyediakan pengganti yang mempertahankan tanda tangan @wraps. Perhatikan bahwa makefunmengandalkan trik terbukti yang sama dari decoratorperpustakaan terkenal .

nyonya
sumber
3

ini adalah kode sumber tentang wraps:

WRAPPER_ASSIGNMENTS = ('__module__', '__name__', '__doc__')

WRAPPER_UPDATES = ('__dict__',)

def update_wrapper(wrapper,
                   wrapped,
                   assigned = WRAPPER_ASSIGNMENTS,
                   updated = WRAPPER_UPDATES):

    """Update a wrapper function to look like the wrapped function

       wrapper is the function to be updated
       wrapped is the original function
       assigned is a tuple naming the attributes assigned directly
       from the wrapped function to the wrapper function (defaults to
       functools.WRAPPER_ASSIGNMENTS)
       updated is a tuple naming the attributes of the wrapper that
       are updated with the corresponding attribute from the wrapped
       function (defaults to functools.WRAPPER_UPDATES)
    """
    for attr in assigned:
        setattr(wrapper, attr, getattr(wrapped, attr))
    for attr in updated:
        getattr(wrapper, attr).update(getattr(wrapped, attr, {}))
    # Return the wrapper so this can be used as a decorator via partial()
    return wrapper

def wraps(wrapped,
          assigned = WRAPPER_ASSIGNMENTS,
          updated = WRAPPER_UPDATES):
    """Decorator factory to apply update_wrapper() to a wrapper function

   Returns a decorator that invokes update_wrapper() with the decorated
   function as the wrapper argument and the arguments to wraps() as the
   remaining arguments. Default arguments are as for update_wrapper().
   This is a convenience function to simplify applying partial() to
   update_wrapper().
    """
    return partial(update_wrapper, wrapped=wrapped,
                   assigned=assigned, updated=updated)
Baliang
sumber
2
  1. Prasyarat: Anda harus tahu cara menggunakan dekorator dan khusus menggunakan pembungkus. Komentar ini menjelaskannya dengan cukup jelas atau tautan ini juga menjelaskannya dengan cukup baik.

  2. Setiap kali kita menggunakan Untuk misalnya: @wraps diikuti oleh fungsi pembungkus kita sendiri. Sesuai dengan rincian yang diberikan dalam tautan ini , dikatakan demikian

functools.wraps adalah fungsi kenyamanan untuk menjalankan update_wrapper () sebagai dekorator fungsi, ketika mendefinisikan fungsi wrapper.

Itu sama dengan parsial (update_wrapper, dibungkus = dibungkus, ditugaskan = ditugaskan, diperbarui = diperbarui).

Jadi @wraps dekorator benar-benar memberikan panggilan ke functools.partial (func [, * args] [, ** kata kunci]).

Definisi functools.partial () mengatakan itu

Parsial () digunakan untuk aplikasi fungsi parsial yang “membekukan” sebagian argumen fungsi dan / atau kata kunci yang menghasilkan objek baru dengan tanda tangan yang disederhanakan. Misalnya, partial () dapat digunakan untuk membuat callable yang berperilaku seperti fungsi int () di mana argumen dasar default ke dua:

>>> from functools import partial
>>> basetwo = partial(int, base=2)
>>> basetwo.__doc__ = 'Convert base 2 string to an int.'
>>> basetwo('10010')
18

Yang membawa saya pada kesimpulan bahwa, @wraps memberikan panggilan ke parsial () dan melewati fungsi wrapper Anda sebagai parameter untuk itu. Parsial () pada akhirnya mengembalikan versi yang disederhanakan yaitu objek dari apa yang ada di dalam fungsi wrapper dan bukan fungsi wrapper itu sendiri.

3rdi
sumber
-4

Singkatnya, functools.wraps hanyalah fungsi biasa. Mari kita perhatikan contoh resmi ini . Dengan bantuan kode sumber , kita dapat melihat detail lebih lanjut tentang implementasi dan langkah-langkah yang berjalan sebagai berikut:

  1. wraps (f) mengembalikan objek, katakanlah O1 . Ini adalah objek dari Partial kelas
  2. Langkah selanjutnya adalah @ O1 ... yang merupakan notasi dekorator dalam python. Itu berarti

wrapper = O1 .__ call __ (wrapper)

Memeriksa implementasi __call__ , kita melihat bahwa setelah langkah ini, (sisi kiri) pembungkus menjadi objek yang dihasilkan oleh self.func (* self.args, * args, ** newkeywords) Memeriksa pembuatan O1 di __new__ , kami tahu self.func adalah fungsi update_wrapper . Ia menggunakan parameter * args , pembungkus sisi kanan , sebagai parameter pertama. Memeriksa langkah terakhir dari update_wrapper , orang dapat melihat pembungkus sisi kanan dikembalikan, dengan beberapa atribut diubah sesuai kebutuhan.

Yong Yang
sumber