J'ai une classe qui représente un objet. Et j'ai un tas de méthodes qui modifient cet état d'objet sans retour évident ou évidemment sans retour. En C #, je déclarerais toutes ces méthodes comme void
et ne verrais aucune alternative. Mais en Python je suis sur le point de faire toutes les méthodes return self
pour me donner la possibilité d'écrire de superbes lignes comme ceci:
classname().method1().method2().method3()
Est-ce Pythonic, ou autrement acceptable en Python?
Voici un mail de Guido van Rossum (l'auteur du langage de programmation Python) sur ce sujet: https://mail.python.org/pipermail/python-dev/ 2003-octobre/038855.html
Je voudrais expliquer une fois de plus pourquoi je suis si catégorique que sort () ne devrait pas retourner "moi".
Cela vient d'un style de codage (populaire dans divers autres langages, je pense que LISP en fait particulièrement partie) où une série d'effets secondaires sur un seul objet peut être enchaînée comme ceci:
x.compress (). chop (y) .sort (z)
ce serait la même chose que
x.compress () x.chop (y) x.sort (z)
Je trouve que l'enchaînement forme une menace pour la lisibilité; cela nécessite que le lecteur soit intimement familiarisé avec chacune des méthodes. Le deuxième formulaire indique clairement que chacun de ces appels agit sur le même objet, et donc même si vous ne connaissez pas très bien la classe et ses méthodes, vous pouvez comprendre que les deuxième et troisième appels sont appliqués à x (et que tous les appels sont faits pour leurs effets secondaires), et pas pour autre chose.
Je voudrais réserver le chaînage aux opérations qui renvoient de nouvelles valeurs, comme les opérations de traitement de chaîne:
y = x.rstrip ("\ n"). split (":"). lower ()
Il existe quelques modules de bibliothèque standard qui encouragent le chaînage des appels d'effets secondaires (pstat me vient à l'esprit). Il ne devrait pas y en avoir de nouveaux; pstat a glissé à travers mon filtre quand il était faible.
C'est une excellente idée pour les API où vous créez un état à l'aide de méthodes. SQLAlchemy utilise cela avec grand effet par exemple:
>>> from sqlalchemy.orm import aliased
>>> adalias1 = aliased(Address)
>>> adalias2 = aliased(Address)
>>> for username, email1, email2 in \
... session.query(User.name, adalias1.email_address, adalias2.email_address).\
... join(adalias1, User.addresses).\
... join(adalias2, User.addresses).\
... filter(adalias1.email_address=='[email protected]').\
... filter(adalias2.email_address=='[email protected]'):
... print(username, email1, email2)
Notez qu'il ne retourne pas self
dans de nombreux cas; il renverra un clone de l'objet courant avec un certain aspect modifié. De cette façon, vous pouvez créer des chaînes divergentes basées sur une base partagée; base = instance.method1().method2()
, puis foo = base.method3()
et bar = base.method4()
.
Dans l'exemple ci-dessus, l'objet Query
renvoyé par un appel Query.join()
ou Query.filter()
n'est pas la même instance, mais une nouvelle instance avec le filtre ou la jointure qui lui est appliquée .
Il utilise une Generative
classe de base pour s'appuyer sur; donc plutôt que return self
, le modèle utilisé est:
def method(self):
clone = self._generate()
clone.foo = 'bar'
return clone
que SQLAlchemy a encore simplifié en utilisant n décorateur :
def _generative(func):
@wraps(func)
def decorator(self, *args, **kw):
new_self = self._generate()
func(new_self, *args, **kw)
return new_self
return decorator
class FooBar(GenerativeBase):
@_generative
def method(self):
self.foo = 'bar'
Tout ce que la méthode décorée @_generative
Doit faire est de faire les modifications sur la copie, le décorateur se charge de produire la copie, de lier la méthode à la copie plutôt qu'à l'original et de la renvoyer à l'appelant pour vous. .
Voici un exemple (idiot) qui montre un scénario quand c'est une bonne technique
class A:
def __init__(self, x):
self.x = x
def add(self, y):
self.x += y
return self
def multiply(self, y)
self.x *= y
return self
def get(self):
return self.x
a = A(0)
print a.add(5).mulitply(2).get()
Dans ce cas, vous pouvez créer un objet dans lequel l'ordre dans lequel les opérations sont effectuées est strictement déterminé par l'ordre de l'appel de fonction, ce qui pourrait rendre le code plus lisible (mais aussi plus long)
Si vous le souhaitez, vous pouvez utiliser un décorateur ici. Il se démarquera de quelqu'un qui parcourt votre code pour voir l'interface, et vous n'avez pas à le faire explicitement return self
de chaque fonction (ce qui peut être gênant si vous avez plusieurs points de sortie).
import functools
def fluent(func):
@functools.wraps(func)
def wrapped(*args, **kwargs):
# Assume it's a method.
self = args[0]
func(*args, **kwargs)
return self
return wrapped
class Foo(object):
@fluent
def bar(self):
print("bar")
@fluent
def baz(self, value):
print("baz: {}".format(value))
foo = Foo()
foo.bar().baz(10)
Tirages:
bar
baz: 10