Search code examples
pythonpython-3.xfunctionintrospection

Best way to add attributes to a Python function


Take the simple case of a Python function which evaluates a mathematical function:

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

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

Suppose I want to "attach" some further information in the form of a function attribute. For example, the LaTeX representation. I know that thanks to PEP232 I can do this outside the function definition:

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

but I'd like to do it within the function definition itself. If I write

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

this certainly works, but only after I've called the func for the first time (because Python is "lazy" in executing functions(?))

Is my only option to write a callable class?

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$')

Or is there a feature of the language that I'm overlooking to do this neatly?


Solution

  • A better approach to accomplish this would be with the use of decorators, for this you have two options:

    Function-based Decorator:

    You can create a function-based decorator that accepts as an argument the latex representation and attaches it to the function it decorates:

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

    Then you can use it when defining your function and supply the appropriate representation:

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

    This translates to:

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

    and makes the latex attribute available immediately after defining the function:

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

    I've made the representation be a required argument, you could define a sensible default if you don't want to force the representation to always be given.

    Class-based Decorator:

    If classes are a preference of yours, a class-based decorator can also be used for a similar effect in a more Pythonic way than your original attempt:

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

    you use it in the same way:

    @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$'
    

    Here LatexRepr(r'$ax^2 + bx + c$') initializes the class and returns the callable instance (__call__ defined). What this does is:

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

    and does the same thing wrapped does.


    Since they both just add an argument to the function, they just return it as-is. They don't replace it with another callable.

    Although a class-based approach does the trick, the function-based decorator should be faster and more lightweight.


    You additionally asked:
    "because Python is "lazy" in executing functions": Python just compiles the function body, it doesn't execute any statements inside it; the only thing it does execute is default argument values (See famous Q here). That's why you first need to invoke the function for it to 'obtain' the attribute latex.

    The additional downside to this approach is that you execute that assignment everytime you invoke the function