Лучший способ добавить атрибуты в функцию Python

возьмем простой случай функции Python, которая оценивает математическую функцию:

def func(x, a, b, c):
    """Return the value of the quadratic function, ax^2 + bx + c."""

    return a*x**2 + b*x + c

предположим, я хочу "прикрепить" некоторую дополнительную информацию в виде атрибута функции. Например, представление LaTeX. Я знаю это благодаря PEP232 Я могу сделать это вне определения функции:

def func(x, a, b, c):
    return a*x**2 + b*x + c
func.latex = r'$ax^2 + bx + c$'

но я хотел бы сделать это в само определение функции. Если я напишу

def func(x, a, b, c):
    func.latex = r'$ax^2 + bx + c$'
    return a*x**2 + b*x + c

это, конечно, работает, но только после я назвал func впервые (потому что Python "ленив" в выполнении функций (?))

является ли мой единственный вариант написать вызываемый класс?

class MyFunction:
     def __init__(self, func, latex):
         self.func = func
         self.latex = latex

     def __call__(self, *args, **kwargs):
         return self.func(*args, **kwargs)

func = MyFunction(lambda x,a,b,c: a*x**2+b*x+c, r'$ax^2 + bx + c$')

или есть особенность языка, который я пропускаю, чтобы сделать это аккуратно?

2 ответов


лучшим подходом для этого будет с использованием декораторов, для этого у вас есть два варианта:

функциональный декоратор:

вы можете создать декоратор на основе функций, который принимает в качестве аргумента представление latex и присоединяет его к функции, которую он украшает:

def latex_repr(r):
    def wrapper(f):
        f.latex = r
        return f
    return wrapper

затем вы можете использовать его при определении своей функции и предоставить соответствующее представление:

@latex_repr(r'$ax^2 + bx + c$')
def func(x, a, b, c):
    return a*x**2 + b*x + c

это означает кому:

func = latex_repr(r'$ax^2 + bx + c$')(func)

и составляет latex атрибут доступен сразу после определения функции:

print(func.latex)
'$ax^2 + bx + c$'

я сделал представление обязательным аргументом, вы можете определить разумное значение по умолчанию, если вы не хотите, чтобы представление всегда давалось.

декоратор на основе класса:

если классы являются предпочтения ваши, класс, декоратор на основе также может быть использован для аналогичного эффекта в более подходящие для Python способ, чем ваш оригинальная попытка:

class LatexRepr:
    def __init__(self, r):
        self.latex = r

    def __call__(self, f):
        f.latex = self.latex
        return f

вы использовать его таким же образом:

@LatexRepr(r'$ax^2 + bx + c$')
def func(x, a, b, c):
    return a*x**2 + b*x + c

print(func.latex)
'$ax^2 + bx + c$'

здесь LatexRepr(r'$ax^2 + bx + c$') инициализирует класс и возвращает вызываемый экземпляр (__call__ определено). Что это делает:

func = LatexRepr(r'$ax^2 + bx + c$')(func)
#                   __init__    
#                                  __call__

и делает то же самое wrapped делает.


поскольку они оба просто добавляют аргумент к функции, они просто возвращают его как есть. Они не заменяют его другим вызываемым.

хотя подход на основе классов делает трюк, основанный на функциях декоратор должен быть быстрее и легче.


дополнительно спросил:
"потому что Python "ленив" в выполнении функций": Python просто компилирует тело функции, он не выполняет никаких операторов внутри него; единственное, что он тут execute-значения аргументов по умолчанию (см. famous Q здесь). Вот почему вам сначала нужно вызвать функцию, чтобы она "получила" атрибут latex.

Дополнительным недостатком этого подхода является то, что вы выполнить это задание каждый раз вы вызываете функцию


поскольку вы рассматриваете свои функции как более сложные сущности, чем простые функции Python, конечно, имеет смысл представлять их как вызываемые экземпляры назначенного пользовательского класса, как вы предложили.

однако, более простой и распространенный способ сделать то, что вы хотите, используя декораторы:

def with_func_attrs(**attrs):
    def with_attrs(f):
        for k,v in attrs.items():
            setattr(f, k, v)
        return f
    return with_attrs

@with_func_attrs(latex = r'$ax^2 + bx + c$', foo = 'bar')
def func(...):
    return ...