27

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?

Dimitris Fasarakis Hilliard
  • 150,925
  • 31
  • 268
  • 253
xnx
  • 24,509
  • 11
  • 70
  • 109

2 Answers2

31

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

Dimitris Fasarakis Hilliard
  • 150,925
  • 31
  • 268
  • 253
  • I think you have some leftovers from your edits. Currently this bit "That's why you first need to invoke the function for it to 'obtain' the attribute latex." and this bit: "The additional downside to this approach is that you execute that assignment everytime you invoke the function" are incorrect -- the decorator is applied during function definition, and the assignment happens once, at that time. – Marius Gedminas Nov 07 '17 at 17:04
  • @MariusGedminas Those aren't referring to decorators, they are referring to OPs method of assigning the attribute inside the function. If you see his question he additionally asks "because Python is "lazy" in executing functions(?)". I see how it is confusing, though, I'll add it to the end of the answer. Thanks for the comment :-) – Dimitris Fasarakis Hilliard Nov 07 '17 at 17:07
9

Since you treat your functions as more complex entities than plain Python functions, it certainly makes a lot of sense to represent them as callable instances of a designated user-defined class, like you suggested.

However, a simpler and common way to do what you want is using decorators:

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 ...
Dimitris Fasarakis Hilliard
  • 150,925
  • 31
  • 268
  • 253
shx2
  • 61,779
  • 13
  • 130
  • 153