Attributs de la fonction Python - utilisations et abus [fermé]

195

Peu de gens connaissent cette fonctionnalité, mais les fonctions (et méthodes) de Python peuvent avoir des attributs . Voir:

>>> def foo(x):
...     pass
...     
>>> foo.score = 10
>>> dir(foo)
['__call__', '__class__', '__delattr__', '__dict__', '__doc__', '__get__', '__getattribute__', '__hash__', '__init__', '__module__', '__name__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__str__', 'func_closure', 'func_code', 'func_defaults', 'func_dict', 'func_doc', 'func_globals', 'func_name', 'score']
>>> foo.score
10
>>> foo.score += 1
>>> foo.score
11

Quels sont les usages et abus possibles de cette fonctionnalité en Python? Une bonne utilisation à ma connaissance est l'utilisation par PLY de la docstring pour associer une règle de syntaxe à une méthode. Mais qu'en est-il des attributs personnalisés? Y a-t-il de bonnes raisons de les utiliser?

Eli Bendersky
la source
3
Découvrez PEP 232 .
user140352
2
Est-ce très surprenant? En général, les objets Python prennent en charge les attributs ad hoc. Bien sûr, certains ne le font pas, en particulier ceux avec un type intégré. Pour moi, ceux qui ne soutiennent pas cela semblent être les exceptions, pas la règle.
allyourcode
3
Une application dans Django: Personnalisez la liste des modifications administratives
Grijesh Chauhan
2
@GrijeshChauhan Je suis venu à cette question après avoir vu ces documents!
Alexander Suraphel
5
Dommage que ce soit fermé, je voulais ajouter que vous pouvez attacher toutes les exceptions personnalisées que la fonction pourrait déclencher, pour fournir un accès facile lors de la capture dans le code appelant. Je fournirais un exemple illustratif, mais c'est mieux fait dans une réponse.
Will Hardy

Réponses:

153

J'utilise généralement des attributs de fonction comme stockage pour les annotations. Supposons que je veuille écrire, dans le style de C # (indiquant qu'une certaine méthode devrait faire partie de l'interface de service Web)

class Foo(WebService):
    @webmethod
    def bar(self, arg1, arg2):
         ...

alors je peux définir

def webmethod(func):
    func.is_webmethod = True
    return func

Ensuite, lorsqu'un appel de service Web arrive, je recherche la méthode, vérifie si la fonction sous-jacente a l'attribut is_webmethod (la valeur réelle n'est pas pertinente) et refuse le service si la méthode est absente ou non destinée à être appelée sur le Web.

Martin c. Löwis
la source
2
Pensez-vous qu'il y a des inconvénients à cela? Par exemple, que se passe-t-il si deux bibliothèques tentent d'écrire le même attribut ad hoc?
allyourcode
18
Je pensais faire exactement cela. Puis je me suis arrêté. "Est-ce une mauvaise idée?" Je me demandais. Ensuite, j'ai erré vers SO. Après quelques marmonnements, j'ai trouvé cette question / réponse. Je ne sais toujours pas si c'est une bonne idée.
allyourcode
7
C'est certainement l'utilisation la plus légitime des attributs de fonction de toutes les réponses (en novembre 2012). La plupart (sinon la totalité) des autres réponses utilisent des attributs de fonction en remplacement des variables globales; cependant, ils ne suppriment PAS l'état global, ce qui est exactement le problème avec les variables globales. C'est différent, car une fois la valeur définie, elle ne change pas; c'est constant. Une bonne conséquence de cela est que vous ne rencontrez pas de problèmes de synchronisation, qui sont inhérents aux variables globales. Oui, vous pouvez fournir votre propre synchronisation, mais c'est le point: ce n'est pas sûr automatiquement.
allyourcode
En effet, je dis, tant que l'attribut ne change pas le comportement de la fonction en question, c'est bien. Comparer avec.__doc__
Dima Tisnek
Cette approche peut également être utilisée pour attacher une description de sortie à la fonction décorée, qui manque dans python 2. *.
Juh_
125

Je les ai utilisés comme variables statiques pour une fonction. Par exemple, étant donné le code C suivant:

int fn(int i)
{
    static f = 1;
    f += i;
    return f;
}

Je peux implémenter la fonction de manière similaire en Python:

def fn(i):
    fn.f += i
    return fn.f
fn.f = 1

Cela tomberait certainement dans la partie "abus" du spectre.

mipadi
la source
2
Intéressant. Existe-t-il d'autres façons d'implémenter des variables statiques en python?
Eli Bendersky
4
-1, cela serait implémenté avec un générateur en python.
124
C'est une assez mauvaise raison de voter contre cette réponse, qui démontre une analogie entre C et Python, ne préconisant pas la meilleure façon possible d'écrire cette fonction particulière.
Robert Rossney
3
@RobertRossney Mais si les générateurs sont le chemin à parcourir, alors c'est une mauvaise utilisation des attributs de fonction. Si oui, alors c'est un abus. Je ne sais pas s'il faut voter contre les abus, car la question le demande aussi: P
allyourcode
1
Semble définitivement un abus, par PEP 232, merci @ user140352, et le commentaire de hop et cette réponse SO
plaques de cuisson
52

Vous pouvez faire des objets de manière JavaScript ... Cela n'a aucun sens mais cela fonctionne;)

>>> def FakeObject():
...   def test():
...     print "foo"
...   FakeObject.test = test
...   return FakeObject
>>> x = FakeObject()
>>> x.test()
foo
defnull
la source
36
+1 Un bel exemple d'abus de cette fonctionnalité, qui est l'une des choses que la question demandait.
Michael Dunn
1
En quoi est-ce différent de la réponse de mipadi? Semble être la même chose, sauf qu'au lieu d'un int, la valeur d'attribut est une fonction.
allyourcode
est def test()vraiment nécessaire?
Keerthana Prabhakaran
15

Je les utilise avec parcimonie, mais ils peuvent être assez pratiques:

def log(msg):
   log.logfile.write(msg)

Maintenant, je peux utiliser logtout au long de mon module et rediriger la sortie simplement en définissant log.logfile. Il existe de nombreuses autres façons d'accomplir cela, mais celui-ci est léger et simple. Et même si ça sentait drôle la première fois que je l'ai fait, j'ai fini par croire que ça sent mieux que d'avoir une logfilevariable globale .

Robert Rossney
la source
7
re smell: Cela ne supprime cependant pas le fichier journal global. Il l'écarte simplement dans un autre global, la fonction log.
allyourcode
2
@allyourcode: Mais cela peut aider à éviter les conflits de noms si vous devez avoir un tas de fichiers journaux globaux pour différentes fonctions dans le même module.
firegurafiku
11

Les attributs de fonction peuvent être utilisés pour écrire des fermetures légères qui enveloppent le code et les données associées ensemble:

#!/usr/bin/env python

SW_DELTA = 0
SW_MARK  = 1
SW_BASE  = 2

def stopwatch():
   import time

   def _sw( action = SW_DELTA ):

      if action == SW_DELTA:
         return time.time() - _sw._time

      elif action == SW_MARK:
         _sw._time = time.time()
         return _sw._time

      elif action == SW_BASE:
         return _sw._time

      else:
         raise NotImplementedError

   _sw._time = time.time() # time of creation

   return _sw

# test code
sw=stopwatch()
sw2=stopwatch()
import os
os.system("sleep 1")
print sw() # defaults to "SW_DELTA"
sw( SW_MARK )
os.system("sleep 2")
print sw()
print sw2()

1.00934004784

2.00644397736

3.01593494415

Kevin Little
la source
3
Pourquoi pousser des fonctions quand nous avons des classes à portée de main? Et n'oublions pas que les classes peuvent émuler une fonction.
muhuk
1
time.sleep(1)est aussi mieux queos.system('sleep 1')
Boris Gorelik
3
@bgbg True, bien que cet exemple ne concerne pas le sommeil.
allyourcode
C'est définitivement un abus; l'utilisation des fonctions ici est totalement gratuite. muhuk a parfaitement raison: les classes sont une meilleure solution.
allyourcode
1
Je demanderais également: "Quel est l'avantage de cela sur une classe?" pour contrer l'inconvénient de ce qui n'est pas aussi évident pour de nombreux programmeurs Python.
cjs
6

J'ai créé ce décorateur d'aide pour définir facilement les attributs de fonction:

def with_attrs(**func_attrs):
    """Set attributes in the decorated function, at definition time.
    Only accepts keyword arguments.
    E.g.:
        @with_attrs(counter=0, something='boing')
        def count_it():
            count_it.counter += 1
        print count_it.counter
        print count_it.something
        # Out:
        # >>> 0
        # >>> 'boing'
    """
    def attr_decorator(fn):
        @wraps(fn)
        def wrapper(*args, **kwargs):
            return fn(*args, **kwargs)

        for attr, value in func_attrs.iteritems():
            setattr(wrapper, attr, value)

        return wrapper

    return attr_decorator

Un cas d'utilisation consiste à créer une collection d'usines et à interroger le type de données qu'elles peuvent créer au niveau méta d'une fonction.
Par exemple (très stupide):

@with_attrs(datatype=list)
def factory1():
    return [1, 2, 3]

@with_attrs(datatype=SomeClass)
def factory2():
    return SomeClass()

factories = [factory1, factory2]

def create(datatype):
    for f in factories:
        if f.datatype == datatype:
            return f()
    return None
DiogoNeves
la source
Comment le décorateur peut-il aider? Pourquoi ne pas simplement vous placer factory1.datatype=listjuste en dessous de la déclaration comme des décorateurs à l'ancienne?
Ceasar Bautista
2
2 différences principales: style, plusieurs attributs sont plus faciles à définir. Vous pouvez certainement définir en tant qu'attribut, mais je suis verbeux avec plusieurs attributs à mon avis et vous avez également la possibilité d'étendre le décorateur pour un traitement ultérieur (comme avoir des valeurs par défaut définies à un endroit au lieu de tous les endroits qui utilisent la fonction ou devoir appeler une fonction supplémentaire après la définition des attributs). Il y a d'autres façons d'obtenir tous ces résultats, je trouve que c'est plus propre, mais heureux de changer d'avis;)
DiogoNeves
Mise à jour rapide: avec Python 3, vous devez utiliser à la items()place de iteritems().
Scott signifie
4

Parfois, j'utilise un attribut d'une fonction pour mettre en cache des valeurs déjà calculées. Vous pouvez également avoir un décorateur générique qui généralise cette approche. Soyez conscient des problèmes de concurrence et des effets secondaires de ces fonctions!


la source
J'aime cette idée! Une astuce plus courante pour la mise en cache des valeurs calculées consiste à utiliser un dict comme valeur par défaut d'un attribut que l'appelant n'est jamais censé fournir - puisque Python n'évalue cela qu'une seule fois lors de la définition de la fonction, vous pouvez y stocker des données et les faire coller autour. Bien que l'utilisation d'attributs de fonction puisse être moins évidente, cela me semble beaucoup moins hacky.
Soren Bjornstad
1

J'ai toujours pensé que la seule raison pour laquelle cela était possible était qu'il y avait un endroit logique pour mettre une chaîne de doc ou autre. Je sais que si je l'utilisais pour n'importe quel code de production, cela perturberait la plupart des lecteurs.

Dale Reidy
la source
1
Je suis d'accord avec votre point principal à propos de ce qui est très probablement déroutant, mais concernant les docstrings: Oui, mais pourquoi les fonctions ont-elles des attributs AD-HOC? Il pourrait y avoir un ensemble fixe d'attributs, un pour contenir la docstring.
allyourcode
@allyourcode Le fait d'avoir le cas général plutôt que des cas ad hoc spécifiques conçus dans le langage rend les choses plus simples et augmente la compatibilité avec les anciennes versions de Python. (Par exemple, le code qui définit / manipule les docstrings fonctionnera toujours avec une version de Python qui ne fait pas de docstrings, tant qu'il gère le cas où l'attribut n'existe pas.)
cjs