1214 votes

Alternatives for returning multiple values from a Python function

Le moyen canonique de retourner plusieurs valeurs dans les langues qui le supportent est souvent l'utilisation des tuples.

Option : Utiliser un tuple

Considérez cet exemple trivial:

def f(x):
  y0 = x + 1
  y1 = x * 3
  y2 = y0 ** y3
  return (y0, y1, y2)

Cependant, cela devient rapidement problématique lorsque le nombre de valeurs retournées augmente. Et si vous voulez retourner quatre ou cinq valeurs ? Bien sûr, vous pourriez continuer à les mettre dans des tuples, mais il est facile d'oublier quelle valeur est où. C'est aussi plutôt laid de les déballer partout où vous voulez les recevoir.

Option : Utiliser un dictionnaire

La prochaine étape logique semble être d'introduire une sorte de 'notation de record'. En Python, la manière évidente de le faire est au moyen d'un dict.

Considérez ce qui suit:

def g(x):
  y0 = x + 1
  y1 = x * 3
  y2 = y0 ** y3
  return {'y0': y0, 'y1': y1 ,'y2': y2}

(Juste pour être clair, y0, y1 et y2 ne sont que des identifiants abstraits. Comme mentionné, en pratique vous utiliseriez des identifiants significatifs.)

Maintenant, nous avons un mécanisme permettant de projeter un membre particulier de l'objet retourné. Par exemple,

result['y0']

Option : Utiliser une classe

Cependant, il y a une autre option. Nous pourrions plutôt retourner une structure spécialisée. J'ai encadré cela dans le contexte de Python, mais je suis sûr que cela s'applique également à d'autres langues. En effet, si vous travailliez en C, cela pourrait bien être votre seule option. Voici comment procéder:

class ReturnValue:
  def __init__(self, y0, y1, y2):
     self.y0 = y0
     self.y1 = y1
     self.y2 = y2

def g(x):
  y0 = x + 1
  y1 = x * 3
  y2 = y0 ** y3
  return ReturnValue(y0, y1, y2)

En Python, les deux précédents sont peut-être très similaires en termes de plomberie - après tout { y0, y1, y2 } finissent simplement par être des entrées dans le __dict__ interne du ReturnValue.

Python offre cependant une fonctionnalité supplémentaire pour les petits objets, l'attribut __slots__. La classe pourrait être exprimée comme suit:

class ReturnValue(object):
  __slots__ = ["y0", "y1", "y2"]
  def __init__(self, y0, y1, y2):
     self.y0 = y0
     self.y1 = y1
     self.y2 = y2

D'après le Manuel de référence Python:

La déclaration __slots__ prend une séquence de variables d'instance et réserve juste assez d'espace dans chaque instance pour contenir une valeur pour chaque variable. De l'espace est économisé car __dict__ n'est pas créé pour chaque instance.

Option : Utiliser un dataclass (Python 3.7+)

En utilisant les nouvelles dataclasses de Python 3.7, retournez une classe avec des méthodes spéciales ajoutées automatiquement, du typage et d'autres outils utiles:

@dataclass
class Returnvalue:
    y0: int
    y1: float
    y3: int

def total_cost(x):
    y0 = x + 1
    y1 = x * 3
    y2 = y0 ** y3
    return ReturnValue(y0, y1, y2)

Option : Utiliser une liste

Une autre suggestion que j'avais négligée vient de Bill the Lizard:

def h(x):
  result = [x + 1]
  result.append(x * 3)
  result.append(y0 ** y3)
  return result

C'est ma méthode préférée la moins. Je suppose que je suis influencé par mon exposition à Haskell, mais l'idée de listes à types mixtes m'a toujours mis mal à l'aise. Dans cet exemple particulier, la liste n'est pas de type mixte, mais elle pourrait l'être concevablement.

Une liste utilisée de cette manière ne gagne vraiment rien par rapport au tuple autant que je sache. La seule vraie différence entre les listes et les tuples en Python est que les listes sont mutables, tandis que les tuples ne le sont pas.

Je tends personnellement à transmettre les conventions de la programmation fonctionnelle: utiliser des listes pour un nombre quelconque d'éléments du même type, et des tuples pour un nombre fixe d'éléments de types prédéterminés.

Question

Après cette longue introduction, vient la question inévitable. Quelle méthode (selon vous) est la meilleure ?

10 votes

Dans vos exemples excellents, vous utilisez la variable y3, mais à moins que y3 ne soit déclaré global, cela entraînerait une NameError: global name 'y3' is not defined. Peut-être devriez-vous simplement utiliser 3 ?

32 votes

De nombreuses grandes questions avec de grandes réponses sont fermées parce que le mot-clé 'opinion' apparaît. Vous pourriez argumenter que l'ensemble de SO est basé sur l'opinion, mais c'est une opinion informée par des faits, des références et une expertise spécifique. Juste parce que quelqu'un demande "lequel pensez-vous est le meilleur" ne signifie pas qu'ils demandent des opinions personnelles abstraites des faits réels, des références et de l'expertise spécifique. Ils demandent presque certainement ce genre d'opinion, basée complètement sur, et documentée avec, les faits, les références et l'expertise spécifique que la personne a utilisés pour former son opinion.

0 votes

@hetepeperfan pas besoin de changer 3, et définir y3 en global non plus, vous pouvez également utiliser un nom local y3, cela fera également l'affaire.

690voto

Coady Points 11374

Les tuples nommés ont été ajoutés en 2.6 à cet effet. Voir également os.stat pour un exemple intégré similaire.

>>> import collections
>>> Point = collections.namedtuple('Point', ['x', 'y'])
>>> p = Point(1, y=2)
>>> p.x, p.y
1 2
>>> p[0], p[1]
1 2

Dans les versions récentes de Python 3 (à partir de 3.6, je crois), la nouvelle bibliothèque typing a introduit la classe NamedTuple pour rendre la création de tuples nommés plus facile et plus puissante. L'héritage de typing.NamedTuple vous permet d'utiliser des docstrings, des valeurs par défaut et des annotations de type.

Exemple (provenant de la documentation) :

class Employee(NamedTuple):  # hériter de typing.NamedTuple
    name: str
    id: int = 3  # valeur par défaut

employee = Employee('Guido')
assert employee.id == 3

78 votes

C'est la seule réponse correcte car c'est la seule structure canonique que l'OP n'a pas prise en compte et car elle répond à son problème de gestion des longues tuples. Devrait être marqué comme accepté.

7 votes

Eh bien, la rationale de conception pour namedtuple est d'avoir une empreinte mémoire plus petite pour les résultats en masse (longues listes de tuples, tels que les résultats de requêtes de base de données). Pour les éléments individuels (si la fonction en question n'est pas souvent appelée), les dictionnaires et les classes sont également très bien. Mais les namedtuples sont aussi une solution agréable/plus agréable dans ce cas.

2 votes

Meilleure réponse je pense. Une chose que je n'ai pas réalisée immédiatement - vous n'avez pas besoin de déclarer le namedtuple dans la portée externe; votre fonction peut elle-même définir le conteneur et le renvoyer.

264voto

too much php Points 27983

Pour les petits projets, je trouve qu'il est plus facile de travailler avec des tuples. Quand cela devient trop difficile à gérer (et pas avant), je commence à regrouper les choses dans des structures logiques, cependant je pense que votre utilisation suggérée de dictionnaires et d'objets ReturnValue est incorrecte (ou trop simpliste).

Retourner un dictionnaire avec les clés "y0", "y1", "y2", etc. n'offre aucun avantage par rapport aux tuples. Retourner une instance de ReturnValue avec les propriétés .y0, .y1, .y2, etc. n'offre pas non plus d'avantage par rapport aux tuples. Vous devez commencer à nommer les choses si vous voulez avancer, et vous pouvez le faire en utilisant des tuples de toute façon :

def get_image_data(filename):
    [snip]
    return size, (format, version, compression), (width,height)

size, type, dimensions = get_image_data(x)

À mon avis, la seule bonne technique au-delà des tuples est de retourner de vrais objets avec des méthodes et des propriétés appropriées, comme on le fait avec re.match() ou open(file).

6 votes

Question - y a-t-il une différence entre size, type, dimensions = getImageData(x) et (size, type, dimensions) = getImageData(x)? Autrement dit, est-ce que l'emballage du côté gauche d'une assignation en tuple fait une différence?

12 votes

@Reb.Cabin Il n'y a pas de différence. Un tuple est identifié par une virgule et l'utilisation de parenthèses sert simplement à regrouper les éléments. Par exemple (1) est un int tandis que (1,) ou 1, est un tuple.

27 votes

Re "retourner un dictionnaire avec des clés y0, y1, y2 etc ne présente pas d'avantage par rapport aux tuples" : le dictionnaire a l'avantage de vous permettre d'ajouter des champs au dictionnaire retourné sans casser le code existant.

82voto

monkut Points 14549

Je vote pour le dictionnaire.

Je trouve que si je crée une fonction qui retourne plus de 2-3 variables, je les regroupe dans un dictionnaire. Sinon, j'ai tendance à oublier l'ordre et le contenu de ce que je retourne.

De plus, introduire une structure 'spéciale' rend votre code plus difficile à suivre. (Quelqu'un d'autre devra parcourir le code pour savoir ce que c'est)

Si vous êtes préoccupé par la recherche de type, utilisez des clés de dictionnaire descriptives, par exemple, 'liste des valeurs x'.

def g(x):
  y0 = x + 1
  y1 = x * 3
  y2 = y0 ** y3
  return {'y0':y0, 'y1':y1 ,'y2':y2 }

6 votes

Après de nombreuses années de programmation, j'ai tendance à faire ce que la structure des données et des fonctions nécessite. La fonction d'abord, vous pouvez toujours refacturer si nécessaire.

0 votes

Comment pourrions-nous obtenir les valeurs à l'intérieur du dictionnaire sans appeler la fonction plusieurs fois? Par exemple, si je veux utiliser y1 et y3 dans une fonction différente?

4 votes

Affecter les résultats à une variable séparée. résultat = g(x); other_function(result)

31voto

tzot Points 32224

Je préfère utiliser des tuples chaque fois qu'un tuple semble "naturel"; les coordonnées en sont un exemple typique, où les objets séparés peuvent se tenir seuls, par exemple dans des calculs de mise à l'échelle sur un seul axe, et l'ordre est important. Remarque : si je peux trier ou mélanger les éléments sans effet négatif sur le sens du groupe, alors je ne devrais probablement pas utiliser de tuple.

J'utilise des dictionnaires en tant que valeur de retour uniquement lorsque les objets regroupés ne sont pas toujours les mêmes. Pensez aux en-têtes de courrier électronique optionnels.

Pour tous les autres cas, où les objets regroupés ont un sens inhérent à l'intérieur du groupe ou qu'un objet complet avec ses propres méthodes est nécessaire, j'utilise une classe.

20voto

John Fouhy Points 14700

+1 sur la suggestion de S.Lott d'une classe de conteneur nommée.

Pour Python 2.6 et plus, un tuple nommé fournit un moyen utile de créer facilement ces classes de conteneurs, et les résultats sont "légers et ne nécessitent pas plus de mémoire que les tuples réguliers".

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X