Збереження підписів оформлених функцій


111

Припустимо, я написав декоратора, який робить щось дуже загальне. Наприклад, він може перетворити всі аргументи на певний тип, виконати журнал, здійснити запам'ятовування тощо.

Ось приклад:

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

Все добре поки що. Однак є одна проблема. Оформлена функція не зберігає документацію початкової функції:

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

g(*args, **kwargs)

На щастя, існує рішення:

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

Цього разу ім’я функції та документація є правильними:

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

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

Але проблема все ж є: підпис функції неправильний. Інформація "* args, ** kwargs" поруч із марною.

Що робити? Я можу придумати два простих, але хибних способи вирішення:

1 - Включіть правильну підпис у docstring:

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

Це погано через дублювання. Підпис все ще не відображатиметься належним чином у автоматично сформованій документації. Оновити функцію легко і забути про зміну докстрингу або зробити друк. [ І так, мені відомо про те, що docstring вже копіює тіло функції. Будь ласка, проігноруйте це; funny_function - лише випадковий приклад. ]

2 - Не використовуйте декоратор або використовуйте декоратор спеціального призначення для кожного конкретного підпису:

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

Це добре працює для набору функцій, які мають однакову підпис, але взагалі марно. Як я вже говорив на початку, я хочу вміти використовувати декоратори цілком загально.

Я шукаю рішення, яке є загальним та автоматичним.

Отже, питання: чи існує спосіб редагування оформленої підпису функції після її створення?

В іншому випадку чи можу я написати декоратор, який витягує функцію підпису і використовує цю інформацію замість "* kwargs, ** kwargs" під час побудови оформленої функції? Як отримати цю інформацію? Як я повинен побудувати оформлену функцію - за допомогою exec?

Будь-які інші підходи?


1
Ніколи не говорив "застаріло". Мені було більш-менш цікаво, що inspect.Signatureдодало до роботи з оформленими функціями.
NightShadeQueen

Відповіді:


78
  1. Встановити модуль декоратора :

    $ pip install decorator
  2. Адаптове визначення 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:

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()доступний принаймні з Python 2.5, але він не зберігає підпис там:

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

Зверніть увагу: *args, **kwargsзамість x, y, z=3.


Ваша відповідь була не першою, але найвичерпнішою на даний момент :-) Я б фактично вважав за краще рішення, яке не стосується стороннього модуля, а дивлячись на джерело модуля декоратора, це досить просто, що я зможу просто скопіюйте його.
Фредрік Йоханссон

1
@MarkLodato: functools.wraps()вже зберігає підписи в Python 3.4+ (як сказано у відповіді). Ви маєте на увазі, що налаштування wrapper.__signature__допомагає на більш ранніх версіях? (які версії ви протестували?)
jfs

1
@MarkLodato: help()показує правильний підпис на Python 3.4. Як ви думаєте functools.wraps(), чому зламаний, а не IPython?
jfs

1
@MarkLodato: він порушений, якщо нам доведеться написати код, щоб виправити його. З огляду на те, що help()дає правильний результат, питання полягає в тому, яку частину програмного забезпечення слід виправити: functools.wraps()або IPython? У будь-якому випадку, присвоєння вручну __signature__в кращому випадку - це рішення не є довгостроковим рішенням.
jfs

1
Схоже, inspect.getfullargspec()досі не повертається належний підпис functools.wrapsу python 3.4, і його потрібно використовувати inspect.signature()замість цього.
Tuukka Mustonen

16

Це вирішується стандартною бібліотекою Python functoolsта спеціально functools.wrapsфункцією, яка призначена для " оновлення функції обгортки, щоб виглядати як обгорнута функція ". Однак його поведінка залежить від версії Python, як показано нижче. Застосований до прикладу з питання, код виглядатиме так:

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 це призведе до наступного:

>>> 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 2, однак, він не оновлює список аргументів функції. При виконанні в Python 2 він створить:

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

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

Не впевнений, що це Sphinx, але це, здається, не працює, коли обмотана функція є методом класу. Сфінкс продовжує повідомляти про підпис декоратора.
alphabetasoup

9

Є модуль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)

Тоді підпис і допомога методу зберігаються:

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

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

EDIT: JF Себастьян зазначив, що я не змінював args_as_intsфункцію - вона тепер виправлена.



6

Другий варіант:

  1. Встановити модуль обгортання:

$ easy_install wrapt

wrapt мають бонус, зберігають підпис класу.


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

Як було сказано вище у відповіді jfs ; якщо ви переймаєтесь підписом з точки зору зовнішності ( helpта inspect.signature), то використовувати functools.wrapsце прекрасно.

Якщо ви переймаєтесь підписом з точки зору поведінки (зокрема, TypeErrorу випадку невідповідності аргументів), functools.wrapsце не зберігає. Ви скоріше скористаєтесь decoratorдля цього чи моїм узагальненням його основного двигуна 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)

Дивіться також цю публікацію проfunctools.wraps .


1
Також результат inspect.getfullargspecне зберігається за допомогою дзвінка functools.wraps.
laike9m

Дякуємо за корисний додатковий коментар @ laike9m!
smarie
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.