Functools.wraps ne yapıyor?


650

Başka bir sorunun bu cevabı üzerine yapılan bir yorumda , birisi ne functools.wrapsyaptığını bilmediğini söyledi . Bu nedenle, ileride başvurmak üzere StackOverflow üzerinde bir kayıt olacak şekilde bu soruyu soruyorum: functools.wrapstam olarak ne yapar ?

Yanıtlar:


1069

Bir dekoratör kullandığınızda, bir işlevi başka bir işlevle değiştirirsiniz. Başka bir deyişle, bir dekoratörünüz varsa

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

o zaman dediğin zaman

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

tam olarak söylemekle aynı

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

ve işleviniz işlev file değiştirilir with_logging. Ne yazık ki, bu demek ki

print(f.__name__)

yazdırılacaktır with_loggingçünkü bu yeni işlevinizin adıdır. Aslında, eğer doktora bakarsanız, fboş olacaktır çünkü with_loggingdoktora olmadığı için yazdığınız doktora artık orada olmayacaktır. Ayrıca, bu işlev için pydoc sonucuna bakarsanız, tek bir argüman almak olarak listelenmez x; bunun yerine alma olarak listelenir *argsve **kwargsçünkü with_logging bunu alır.

Bir dekoratör kullanmak her zaman bir işlevle ilgili bu bilgileri kaybetmek anlamına geliyorsa, bu ciddi bir sorun olacaktır. Bu yüzden var functools.wraps. Bu dekoratör kullanılan bir işlevi alır ve vb, fonksiyon adının üzerine kopyalama docstring'ini argümanlar liste işlevselliğini ekler Ve bu yana wrapskendisini bir dekoratör olan, aşağıdaki kod doğru şeyi yapar:

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'

7
Evet, dekoratör modülünden kaçınmayı tercih ediyorum çünkü functools.wraps standart kütüphanenin bir parçası ve bu nedenle başka bir dış bağımlılık getirmiyor. Ancak dekoratör modülü, umarım functools.wraps'ın bir gün de olacağı yardım sorununu çözüyor.
Eli Courtwright

6
Burada, sargı kullanmazsanız ne olabileceğine bir örnek vardır: doctools testleri aniden kaybolabilir. çünkü wrato () gibi bir şey onları kopyalamazsa, doctools testleri dekore edilmiş işlevlerde bulamaz.
andrew cooke

88
neden functools.wrapsbu iş için ihtiyacımız var , sadece dekoratör deseninin bir parçası olmamalı? ne zaman olacağını değil @wraps kullanmak istiyor?
wim

56
@wim: @wrapsKopyalanan değerler üzerinde değişik tipte modifikasyon veya ek açıklama yapmak için kendi sürümlerini yapan bazı dekoratörler yazdım . Temel olarak, açık bir şekilde örtükten daha iyi olan ve özel durumların kuralları ihlal edecek kadar özel olmadığı Python felsefesinin bir uzantısıdır. (Kod çok daha basittir ve bir @wrapstür özel devre dışı bırakma mekanizması kullanmak yerine manuel olarak sağlanması gerekip gerekmediğini anlamak daha kolaydır .)
ssokolow

35
@LucasMalor Tüm dekoratörler süsledikleri işlevleri sarmaz. Bazıları, bir tür arama sistemine kaydolmak gibi yan etkiler uygular.
ssokolow

22

Dekoratörlerim için çok işlevler yerine sınıflar kullanıyorum. Bir nesne bir işlevden beklenen tüm aynı özniteliklere sahip olmayacağı için bu konuda bazı sorunlar yaşıyordum. Örneğin, bir nesnenin özniteliği olmaz __name__. Django "nesne hiçbir özniteliği ' __name__'" hata bildiriyor nerede izlemek oldukça zor bu belirli bir sorun vardı . Ne yazık ki, sınıf tarzı dekoratörler için, @wrap'ın işi yapacağına inanmıyorum. Bunun yerine böyle bir temel dekoratör sınıfı oluşturduk:

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)

Bu sınıf, dekore edilen işleve yapılan tüm öznitelikleri temsil eder. Böylece, 2 bağımsız değişkenin şu şekilde belirtildiğini kontrol eden basit bir dekoratör oluşturabilirsiniz:

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

        return self.func(*args)

7
Dokümanların @wrapsdediği gibi @wraps, sadece bir kolaylık fonksiyonudur functools.update_wrapper(). Sınıf dekoratörü durumunda, update_wrapper()doğrudan __init__()yönteminizden arayabilirsiniz . Yani, oluşturmak gerekmez DecBasehiç, sadece üzerinde içerebilir __init__()ait process_loginhat: update_wrapper(self, func). Bu kadar.
Fabiano

14

Python 3.5+ itibariyle:

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

İçin bir takma addır g = functools.update_wrapper(g, f). Tam olarak üç şey yapar:

  • kopyalar __module__, __name__, __qualname__, __doc__, ve __annotations__sıfatları füzerinde g. Bu varsayılan liste içerisindedir WRAPPER_ASSIGNMENTS, listeyi functools kaynağında görebilirsiniz .
  • güncellediği __dict__ait gtüm unsurları ile f.__dict__. ( WRAPPER_UPDATESkaynağa bakın)
  • __wrapped__=füzerinde yeni bir özellik ayarlıyorg

Sonuç golarak, aynı ada, öğretiye, modül adına ve imzasına sahip olarak görünür f. Tek sorun, imza ile ilgili olarak bunun gerçek olmadığıdır: sadece inspect.signaturevarsayılan olarak sarıcı zincirleri takip eder. Dokümandainspect.signature(g, follow_wrapped=False) açıklandığı gibi kullanarak kontrol edebilirsiniz . Bunun can sıkıcı sonuçları var:

  • sarmalayıcı kodu, sağlanan bağımsız değişkenler geçersiz olsa bile yürütülür.
  • sarmalayıcı kodu, alınan * args, ** kwargs öğesinden adını kullanarak bir bağımsız değişkene kolayca erişemez. Gerçekten de tüm vakaları (konumsal, anahtar kelime, varsayılan) ele almak ve bu nedenle böyle bir şey kullanmak zorunda kalacaktır Signature.bind().

Şimdi functools.wrapsve dekoratörler arasında biraz karışıklık var , çünkü dekoratörleri geliştirmek için çok sık kullanılan bir durum fonksiyonları sarmaktır. Ancak her ikisi de tamamen bağımsız kavramlardır. Farkı anlamak istiyorsanız, her ikisi için de yardımcı kütüphaneler uyguladım: dekoratörleri kolayca yazmak için decopatch ve imza koruyucu bir değiştirme sağlamak için makefun@wraps . Not makefunünlü ile aynı kanıtlanmış hile dayanır decoratorkütüphanede.


3

Bu, tamamlamalarla ilgili kaynak koddur:

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)

2
  1. Önkoşul: Dekoratörleri nasıl kullanacağınızı ve özellikle sargılarla bilmelisiniz. Bu yorum biraz açıklıyor veya bu bağlantı da oldukça iyi açıklıyor.

  2. Ne zaman kullanırsak Örneğin: @wraps ve ardından kendi sarmalayıcı fonksiyonumuz. Bu linkte verilen detaylara göre,

functools.wraps, bir sarmalayıcı işlevi tanımlanırken update_wrapper () işlevini işlev dekoratörü olarak çağırmak için kolaylık işlevidir.

Kısmen eşdeğerdir (update_wrapper, wrapped = wrapped, atanan = atanan, updated = updated).

Yani @wraps dekoratör aslında functools.partial (func [, * args] [, ** anahtar kelimeler]) çağrısı yapar.

Functools.partial () tanımı

Partial () işlevi, bir işlevin bağımsız değişkenlerinin ve / veya anahtar sözcüklerinin bir kısmını "imzalı" bir şekilde kısaltılmış işlev uygulaması için kullanılır. Örneğin, partial argümanı varsayılan olarak iki olduğunda int () işlevi gibi davranan bir callable oluşturmak için partial () kullanılabilir:

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

Bu da beni şu sonuca getiriyor: @wraps, partial () işlevini çağırır ve sarma işlevinizi parametre olarak iletir. Sondaki kısmi (), basitleştirilmiş sürümü, yani sarma işlevinin kendisini değil, sarma işlevinin içindekinin nesnesini döndürür.


-4

Kısacası, functools.wraps sadece normal bir işlevdir. Bu resmi örneği ele alalım . Kaynak kodun yardımıyla , uygulama ve çalışan adımlar hakkında daha fazla ayrıntıyı aşağıdaki gibi görebiliriz:

  1. wras (f) bir nesneyi döndürür, örneğin O1 . Kısmi sınıfının bir nesnesidir
  2. Bir sonraki adım, pitondaki dekoratör notasyonu olan @ O1'dir . Anlamı

sargı = O1 .__ arama __ (sargı)

Uygulanmasını kontrol edilmesi __call__ , bu aşamada, (sol taraf) sonra görüyoruz sarma nesne ile sonuçlanmıştır olur (* self.args, * bağımsız değişken, ** yeni anahtar kelimeleri) self.func oluşturulmasını kontrol edilmesi O1 içinde __New__ biz biliyorum self.func fonksiyonudur update_wrapper . Bu parametre kullanır * args , sağ taraftaki sarmalayıcı onun 1 parametresi olarak. Update_wrapper öğesinin son adımına bakıldığında , sağ taraftaki sarıcı döndürüldüğünde, bazı öznitelikler gerektiği gibi değiştirilir.

Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.