web-dev-qa-db-fra.com

Meilleure façon d'ajouter des attributs à une fonction Python

Prenons le cas simple d'une fonction Python qui évalue une fonction mathématique:

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

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

Supposons que je veuille "attacher" quelques informations supplémentaires sous la forme d'un attribut de fonction. Par exemple, la représentation LaTeX. Je sais que grâce à PEP232 je peux le faire en dehors de la définition de la fonction:

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

mais je voudrais le faire dans la définition de la fonction elle-même. Si j'écris

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

cela fonctionne certainement, mais seulement après j'ai appelé le func pour la première fois (parce que Python est "paresseux") "dans l'exécution des fonctions (?))

Est-ce ma seule option pour écrire une classe appelable?

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

Ou y a-t-il une caractéristique du langage que je néglige pour le faire proprement?

20
xnx

Une meilleure approche pour y parvenir serait d'utiliser des décorateurs, pour cela vous avez deux options:

Décorateur basé sur les fonctions:

Vous pouvez créer un décorateur basé sur une fonction qui accepte comme argument la représentation en latex et l'attache à la fonction qu'il décore:

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

Ensuite, vous pouvez l'utiliser pour définir votre fonction et fournir la représentation appropriée:

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

Cela se traduit par:

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

et rend l'attribut latex disponible immédiatement après la définition de la fonction:

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

J'ai fait de la représentation un argument obligatoire, vous pouvez définir un défaut sensible si vous ne voulez pas forcer la représentation à toujours être donnée.

Décorateur en classe:

Si les classes sont votre préférence, un décorateur basé sur les classes peut également être utilisé pour un effet similaire d'une manière plus Pythonic que votre tentative d'origine:

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

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

vous l'utilisez de la même manière:

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

Ici, LatexRepr(r'$ax^2 + bx + c$') initialise la classe et retourne l'instance appelable (__call__ Défini). Ce que cela signifie:

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

et fait la même chose que wrapped.


Puisqu'ils ajoutent tous deux un argument à la fonction, ils le renvoient tel quel. Ils ne le remplacent pas par un autre appelable.

Bien qu'une approche basée sur les classes fasse l'affaire, le décorateur basé sur les fonctions devrait être plus rapide et plus léger.


Vous avez également demandé:
"parce que Python est" paresseux "dans l'exécution des fonctions": Python compile simplement le corps de la fonction, il n'exécute aucune instruction à l'intérieur il; la seule chose qu'il fait exécuter est des valeurs d'argument par défaut (Voir le fameux Q ici ). C'est pourquoi vous devez d'abord appeler la fonction pour qu'elle "obtienne" l'attribut latex.

L'inconvénient supplémentaire de cette approche est que vous exécutez cette affectation à chaque fois vous appelez la fonction

21

Puisque vous traitez vos fonctions comme des entités plus complexes que les fonctions simples Python, il est certainement très logique de les représenter comme des instances appelables d'une classe définie par l'utilisateur désignée, comme vous l'avez suggéré.

Cependant, une façon plus simple et courante de faire ce que vous voulez est d'utiliser des décorateurs:

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 ...
6
shx2