Süslü işlevlerin imzalarını korumak


111

Çok genel bir şey yapan bir dekoratör yazdığımı varsayalım. Örneğin, tüm argümanları belirli bir türe dönüştürebilir, günlüğe kaydetme yapabilir, notlandırma uygulayabilir, vb.

İşte bir örnek:

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

Şimdiye kadar her şey yolunda. Ancak bir sorun var. Dekore edilmiş işlev, orijinal işlevin belgelerini korumaz:

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

g(*args, **kwargs)

Neyse ki, bir çözüm var:

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

Bu sefer işlev adı ve belgeler doğrudur:

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

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

Ancak yine de bir sorun var: işlev imzası yanlış. "* Args, ** kwargs" bilgisi neredeyse yararsızdır.

Ne yapalım? İki basit ama kusurlu çözüm düşünebilirim:

1 - Doküman dizisine doğru imzayı ekleyin:

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

Bu, çoğaltma nedeniyle kötü. İmza, otomatik olarak oluşturulan belgelerde yine düzgün şekilde gösterilmeyecektir. İşlevi güncellemek ve belge dizesini değiştirmeyi unutmak veya bir yazım hatası yapmak kolaydır. [ Ve evet, doktrinin zaten işlev gövdesini kopyaladığı gerçeğinin farkındayım. Lütfen bunu dikkate almayın; funny_function sadece rastgele bir örnektir. ]

2 - Her özel imza için bir dekoratör veya özel amaçlı bir dekoratör kullanmayın:

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

Bu, aynı imzaya sahip bir dizi işlev için iyi çalışır, ancak genel olarak işe yaramaz. Başlangıçta da söylediğim gibi, dekoratörleri tamamen genel bir şekilde kullanabilmek istiyorum.

Tamamen genel ve otomatik bir çözüm arıyorum.

Öyleyse soru şu: dekore edilmiş işlev imzasını oluşturulduktan sonra düzenlemenin bir yolu var mı?

Aksi takdirde, dekore edilmiş işlevi oluştururken işlev imzasını çıkaran ve bu bilgileri "* kwargs, ** kwargs" yerine kullanan bir dekoratör yazabilir miyim? Bu bilgiyi nasıl çıkarırım? Exec ile dekore edilmiş işlevi nasıl oluşturmalıyım?

Başka yaklaşım var mı?


1
Asla "güncel değil" demedim. inspect.SignatureSüslü işlevlerle uğraşmaya neyin eklendiğini az çok merak ediyordum .
NightShadeQueen

Yanıtlar:


79
  1. Dekoratör modülünü kurun :

    $ pip install decorator
  2. Tanımını uyarlayın 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()stdlib, Python 3.4'ten beri imzaları korur:

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()en azından Python 2.5'ten beri mevcuttur , ancak buradaki imzayı korumaz:

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

Uyarı: *args, **kwargsyerine x, y, z=3.


İlk cevap senin değildi, ama şimdiye kadarki en kapsamlı olanı :-) Aslında üçüncü taraf bir modülü içermeyen bir çözümü tercih ederim, ancak dekoratör modülünün kaynağına baktığımda, yapabileceğim kadar basit sadece kopyalayın.
Fredrik Johansson

1
@MarkLodato: functools.wraps()Python 3.4+ sürümünde imzaları zaten koruyor (cevapta belirtildiği gibi). wrapper.__signature__Önceki sürümlerde yardımcı olmayı mı kastediyorsunuz ? (hangi sürümleri test ettiniz?)
jfs

1
@MarkLodato: help()Python 3.4'te doğru imzayı gösterir. Neden functools.wraps()IPython değil de bozuk olduğunu düşünüyorsunuz ?
jfs

1
@MarkLodato: Bunu düzeltmek için kod yazmamız gerekiyorsa bozulur. Bunun help()doğru sonucu verdiğini düşünürsek, soru hangi yazılım parçasının düzeltilmesi gerektiğidir: functools.wraps()veya IPython? Her durumda, elle atama __signature__en iyi ihtimalle geçici bir çözümdür - uzun vadeli bir çözüm değildir.
jfs

1
Görünüşe göre inspect.getfullargspec()hala functools.wrapspython 3.4 için uygun imza döndürmüyor ve bunun inspect.signature()yerine kullanmanız gerekiyor .
Tuukka Mustonen

16

Bu, Python'un standart kitaplığı functoolsve özellikle functools.wraps" bir sarmalayıcı işlevini sarılmış işlev gibi görünecek şekilde güncellemek " için tasarlanmış işleviyle çözülür . Davranışı Python sürümüne bağlıdır, ancak aşağıda gösterildiği gibi. Sorudaki örneğe uygulandığında, kod şöyle görünecektir:

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

Python 3'te çalıştırıldığında, bu aşağıdakileri üretir:

>>> 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

Tek dezavantajı Python 2'de fonksiyonun argüman listesini güncellememesidir. Python 2'de çalıştırıldığında, şunları üretecektir:

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

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

Sphinx olup olmadığından emin değilim, ancak sarmalanmış işlev bir sınıfın yöntemi olduğunda bu işe yaramıyor gibi görünüyor. Sphinx, dekoratörün çağrı imzasını bildirmeye devam ediyor.
alphabetasoup

9

Kullanabileceğiniz dekoratörlü bir dekoratör modülü var 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)

Ardından yöntemin imzası ve yardımı korunur:

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

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

DÜZENLEME: JF Sebastian, args_as_intsişlevi değiştirmediğime dikkat çekti - şimdi düzeltildi.



6

İkinci seçenek:

  1. Wrapt modülünü kurun:

$ easy_install wrapt

wrapt bir bonus alır, sınıf imzasını korur.


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

2

Yukarıda jfs'nin cevabında yorumlandığı gibi ; İmza ( help, ve inspect.signature) açısından endişe duyuyorsanız , kullanmak functools.wrapstamamen iyidir.

Davranış açısından imzayla ilgileniyorsanız (özellikle TypeErrorargüman uyumsuzluğu durumunda), functools.wrapsonu korumaz. Bunun decoratoriçin ya da adı verilen çekirdek motoru hakkındaki genellememi kullanmayı tercih etmelisiniz 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)

Bu gönderiyefunctools.wraps de bakın .


1
Ayrıca, sonucu inspect.getfullargspecaranarak saklanmaz functools.wraps.
laike9m

Faydalı ek yorum için teşekkürler @ laike9m!
smarie
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.