web-dev-qa-db-fra.com

Retourne moi-même dans python

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?

26
Philip B

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.

26
Querenker

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

14
Martijn Pieters

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)

12
Banach Tarski

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
4
Waleed Khan