Comment renvoyer plusieurs valeurs d'une fonction? [fermé]

1067

La manière canonique de renvoyer plusieurs valeurs dans les langues qui la prennent en charge est souvent le couplage .

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 à mesure que le nombre de valeurs renvoyées augmente. Et si vous souhaitez renvoyer quatre ou cinq valeurs? Bien sûr, vous pouvez continuer à les multiplier, mais il est facile d'oublier quelle valeur est où. Il est également assez moche de les déballer où vous voulez les recevoir.

Option: utiliser un dictionnaire

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

Considérer ce qui suit:

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

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

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

result['y0']

Option: utiliser une classe

Cependant, il existe 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 langages. En effet, si vous travailliez en C, cela pourrait très bien être votre seule option. Voici:

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, ils { y0, y1, y2 }finissent par être des entrées à l'intérieur __dict__du ReturnValue.

Il y a une fonctionnalité supplémentaire fournie par Python pour les petits objets, l' __slots__attribut. 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

Dans le manuel de référence Python :

La __slots__déclaration 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. L'espace est enregistré car il __dict__n'est pas créé pour chaque instance.

Option: utilisation d'une classe de données (Python 3.7+)

En utilisant les nouvelles classes de données de Python 3.7, renvoyez une classe avec des méthodes spéciales, des saisies et d'autres outils utiles ajoutés automatiquement:

@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 ignoré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 la moins préférée. Je suppose que je suis entaché par l'exposition à Haskell, mais l'idée de listes de types mixtes m'a toujours fait mal à l'aise. Dans cet exemple particulier, la liste est de type non-mixte, mais elle pourrait l'être.

Pour autant que je sache, une liste utilisée de cette manière n'a vraiment rien à voir avec le tuple. La seule vraie différence entre les listes et les tuples en Python est que les listes sont mutables , contrairement aux tuples.

Personnellement, j'ai tendance à reprendre les conventions de la programmation fonctionnelle: utiliser des listes pour n'importe quel nombre 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 le long préambule, vient la question inévitable. Quelle méthode (pensez-vous) est la meilleure?

saffsd
la source
10
En vous d' excellents exemples que vous utilisez variables y3, mais à moins que Y3 est déclarée mondiale, ce céderais NameError: global name 'y3' is not definedpeut - être juste utiliser 3?
hetepeperfan
11
Beaucoup de bonnes questions avec d'excellentes réponses sont fermées car le mot-clé «opinion» apparaît. On pourrait dire que l'ensemble de l'OS est basé sur une opinion, mais c'est une opinion éclairée par des faits, des références et une expertise spécifique. Ce n'est pas parce que quelqu'un demande «qui vous semble le mieux» qu'il demande des opinions personnelles abstraites de faits réels, de références et d'expertise spécifiques. Ils demandent presque certainement précisément ce genre d'opinion, celle basée en profondeur sur, et documentée avec, les faits, les références et l'expertise spécifique que la personne a utilisée pour former l'opinion.
NeilG
@hetepeperfan pas besoin de changer 3, et ni l'un ni l'autre ne définit y3 dans global, vous pouvez également utiliser un nom local y3, qui fera le même travail aussi.
okie

Réponses:

637

Des tuples nommés ont été ajoutés dans 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 (3.6+, je pense), la nouvelle typingbibliothèque a obtenu la NamedTupleclasse pour rendre les tuples nommés plus faciles à créer et plus puissants. L'héritage de typing.NamedTuplevous permet d'utiliser des docstrings, des valeurs par défaut et des annotations de type.

Exemple (extrait des documents):

class Employee(NamedTuple):  # inherit from typing.NamedTuple
    name: str
    id: int = 3  # default value

employee = Employee('Guido')
assert employee.id == 3
A. Coady
la source
68
Ce n'est que la bonne réponse car c'est la seule structure canonique que l'OP n'a pas prise en compte et parce qu'elle résout son problème de gestion des tuples longs. Doit être marqué comme accepté.
frappe aérienne le
7
Eh bien, la logique de conception namedtupleest d'avoir une empreinte mémoire plus petite pour les résultats de masse (longues listes de tuples, telles que les résultats des requêtes DB). 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 couples nommés sont également une solution agréable / plus agréable dans ce cas.
Lutz Prechelt
8
@wom: Ne fais pas ça. Python ne fait aucun effort pour unifier les namedtupledéfinitions (chaque appel en crée une nouvelle), la création de la namedtupleclasse est relativement coûteuse en CPU et en mémoire, et toutes les définitions de classe impliquent intrinsèquement des références cycliques (ainsi sur CPython, vous attendez une exécution GC cyclique leur libération). Cela le rend également impossible pour picklela classe (et donc, impossible d'utiliser des instances avec multiprocessingdans la plupart des cas). Chaque création de la classe sur mon 3.6.4 x64 consomme ~ 0,337 ms et occupe un peu moins de 1 Ko de mémoire, tuant ainsi toute économie d'instance.
ShadowRanger
3
Je noterai que Python 3.7 a amélioré la vitesse de création de nouvelles namedtupleclasses ; les coûts du processeur baissent d'environ un facteur 4x , mais ils sont toujours environ 1000 fois plus élevés que le coût de création d'une instance, et le coût de la mémoire pour chaque classe reste élevé (j'avais tort dans mon dernier commentaire sur "moins de 1 Ko" pour la classe, _sourceelle est généralement de 1,5 Ko; elle _sourceest supprimée dans la version 3.7, elle est donc probablement plus proche de la revendication initiale d'un peu moins de 1 Ko par création de classe).
ShadowRanger
4
@SergeStroobandt - Cela fait partie de la bibliothèque standard, mais pas une fonction intégrée. Vous n'avez pas à vous soucier du fait qu'il pourrait ne pas être installé sur un autre système avec Python> = 2.6. Ou vous opposez-vous simplement à la ligne de code supplémentaire?
Justin
234

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

De retour un dictionnaire avec les touches "y0", "y1", "y2", etc. n'offre aucun avantage sur tuples. De retour un ReturnValueexemple avec des propriétés .y0, .y1, .y2, etc. n'offre aucun avantage sur tuples non plus . Vous devez commencer à nommer les choses si vous voulez aller n'importe où, et vous pouvez quand même le faire en utilisant des tuples:

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

size, type, dimensions = get_image_data(x)

À mon humble avis, la seule bonne technique au-delà des tuples est de renvoyer des objets réels avec des méthodes et des propriétés appropriées, comme vous obtenez de re.match()ou open(file).

trop de php
la source
6
Question - y a-t-il une différence entre size, type, dimensions = getImageData(x)et (size, type, dimensions) = getImageData(x)? C'est-à-dire, le fait d'habiller le côté gauche d'une affectation tuplée fait-il une différence?
Reb.Cabin
11
@ Reb.Cabin Il n'y a pas de différence. Tuple est identifié par une virgule et l'utilisation de parenthèses consiste simplement à regrouper les choses. Par exemple, (1)est un entier int (1,)ou 1,est un tuple.
phil
19
Re "renvoyer un dictionnaire avec les clés y0, y1, y2, etc. n'offre aucun avantage sur les tuples": le dictionnaire a l'avantage de pouvoir ajouter des champs au dictionnaire renvoyé sans casser le code existant.
ostrokach
Re "renvoyer un dictionnaire avec les clés y0, y1, y2, etc. n'offre aucun avantage sur les tuples": il est également plus lisible et moins sujet aux erreurs lorsque vous accédez aux données en fonction de son nom plutôt que de sa position.
Denis Dollfus
204

De nombreuses réponses suggèrent que vous devez renvoyer une collection quelconque, comme un dictionnaire ou une liste. Vous pouvez laisser la syntaxe supplémentaire et simplement écrire les valeurs de retour, séparées par des virgules. Remarque: cela renvoie techniquement un tuple.

def f():
    return True, False
x, y = f()
print(x)
print(y)

donne:

True
False
Joseph Hansen
la source
24
Vous retournez toujours une collection. C'est un tuple. Je préfère les parenthèses pour le rendre plus explicite. Essayez ceci: type(f())retourne <class 'tuple'>.
Igor
20
@Igor: Il n'y a aucune raison de rendre l' tupleaspect explicite; ce n'est pas vraiment important que vous retourniez un tuple, c'est l'idiome pour retourner une période de valeurs multiples. Même raison pour laquelle vous omettez les parens avec l'idiome de swap x, y = y, x, l'initialisation multiple x, y = 0, 1, etc .; bien sûr, cela fait du tuples sous le capot, mais il n'y a aucune raison de le rendre explicite, car le tuples n'est pas du tout le point. Le didacticiel Python introduit plusieurs affectations bien avant qu'il ne touche même à l' tupleart.
ShadowRanger
@ShadowRanger toute séquence de valeurs séparées par une virgule à droite de =est un tuple en Python avec ou sans parenthèses autour d'eux. Il n'y a donc en fait ni explicite ni implicite ici. a, b, c est autant un tuple que (a, b, c). Il n'y a pas non plus de création de tuple "sous le capot" lorsque vous retournez de telles valeurs, car il s'agit simplement d'un tuple simple. L'OP a déjà mentionné des tuples donc il n'y a en fait aucune différence entre ce qu'il a mentionné et ce que cette réponse montre. Aucun
Ken4scholars
2
C'est littéralement la première option suggérée dans la question
endolith
1
@endolith Les deux fois le gars pose une question ( « Comment puis-je retourner plusieurs valeurs? » et « Comment vous retournez plusieurs valeurs? ») sont répondu par cette réponse. Le texte de la question a parfois changé. Et c'est une question d'opinion.
Joseph Hansen
74

Je vote pour le dictionnaire.

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

De plus, l'introduction d'une structure «spéciale» rend votre code plus difficile à suivre. (Quelqu'un d'autre devra chercher dans le code pour savoir de quoi il s'agit)

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

def g(x):
  y0 = x + 1
  y1 = x * 3
  y2 = y0 ** y3
  return {'y0':y0, 'y1':y1 ,'y2':y2 }
monkut
la source
5
après de nombreuses années de programmation, je tends vers ce que jamais la structure des données et des fonctions est requise. Fonctionnez d'abord, vous pouvez toujours refactoriser au besoin.
monkut
Comment pourrions-nous obtenir les valeurs dans le dictionnaire sans appeler la fonction plusieurs fois? Par exemple, si je veux utiliser y1 et y3 dans une fonction différente?
Matt
3
affecter les résultats à une variable distincte. result = g(x); other_function(result)
monkut
1
@monkut oui. Cette méthode permet également de passer le résultat à plusieurs fonctions, qui prennent des arguments différents du résultat, sans avoir à référencer spécifiquement des parties particulières du résultat à chaque fois.
Gnudiff
38

Une autre option serait d'utiliser des générateurs:

>>> def f(x):
        y0 = x + 1
        yield y0
        yield x * 3
        yield y0 ** 4


>>> a, b, c = f(5)
>>> a
6
>>> b
15
>>> c
1296

Bien que les tuples IMHO soient généralement les meilleurs, sauf dans les cas où les valeurs renvoyées sont candidates à l'encapsulation dans une classe.

rlms
la source
1
Cela semble être la solution la plus propre et a une syntaxe propre. Y a-t-il des inconvénients à cela? Si vous n'utilisez pas tous les rendements, y a-t-il des rendements «non dépensés» qui attendent de vous blesser?
Jiminion
24
Cela peut être "propre", mais cela ne semble pas du tout intuitif. Comment quelqu'un qui n'a jamais rencontré ce modèle avant peut-il savoir que le déballage automatique des tuples déclencherait chacun yield?
coredumperror
1
@CoreDumpError, les générateurs ne sont que… des générateurs. Il n'y a pas de différence externe entre def f(x): …; yield b; yield a; yield rvs (g for g in [b, a, r])et les deux seront facilement convertis en listes ou en tuples, et en tant que tels prendront en charge le décompactage de tuple. La forme du générateur de tuple suit une approche fonctionnelle, tandis que la forme de la fonction est impérative et permettra le contrôle de flux et l'affectation des variables.
sleblanc le
30

Je préfère utiliser des tuples chaque fois qu'un tuple semble "naturel"; les coordonnées 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 nuire à la signification du groupe, je ne devrais probablement pas utiliser de tuple.

J'utilise les dictionnaires comme valeur de retour uniquement lorsque les objets groupés ne sont pas toujours les mêmes. Pensez aux en-têtes de courrier électronique facultatifs.

Pour le reste des cas, lorsque les objets groupés ont une signification inhérente à l'intérieur du groupe ou qu'un objet à part entière avec ses propres méthodes est nécessaire, j'utilise une classe.

tzot
la source
29

Je préfère:

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

Il semble que tout le reste ne soit qu'un code supplémentaire pour faire la même chose.

UnkwnTech
la source
22
Les tuples sont plus faciles à déballer: y0, y1, y2 = g () avec un dict que vous devez faire: result = g () y0, y1, y2 = result.get ('y0'), result.get ('y1' ), result.get ('y2') qui est un peu moche. Chaque solution a ses «avantages» et ses «inconvénients».
Oli
27
>>> def func():
...    return [1,2,3]
...
>>> a,b,c = func()
>>> a
1
>>> b
2
>>> c
3
WebQube
la source
@edouard Non, il ne renvoie pas un tuple et non une liste.
Simon Hibbs
1
la déstructuration est l' argument du retour des listes à mon avis
semiomant
21

Généralement, la "structure spécialisée" EST en fait un état actuel sensible d'un objet, avec ses propres méthodes.

class Some3SpaceThing(object):
  def __init__(self,x):
    self.g(x)
  def g(self,x):
    self.y0 = x + 1
    self.y1 = x * 3
    self.y2 = y0 ** y3

r = Some3SpaceThing( x )
r.y0
r.y1
r.y2

J'aime trouver des noms de structures anonymes dans la mesure du possible. Les noms significatifs rendent les choses plus claires.

S.Lott
la source
20

Les tuples, dict et objets de Python offrent au programmeur un compromis harmonieux entre formalité et commodité pour les petites structures de données («choses»). Pour moi, le choix de la façon de représenter une chose est dicté principalement par la façon dont je vais utiliser la structure. En C ++, c'est une convention courante à utiliser structpour les éléments de données uniquement et classpour les objets avec des méthodes, même si vous pouvez légalement placer des méthodes sur un struct; mon habitude est similaire en Python, avec dictet tupleà la place de struct.

Pour les jeux de coordonnées, j'utiliserai tupleplutôt qu'un point classou un dict(et notez que vous pouvez utiliser un tuplecomme clé de dictionnaire, donc dictfaites de grands tableaux multidimensionnels clairsemés).

Si je vais parcourir une liste de choses, je préfère décompresser les tuples sur l'itération:

for score,id,name in scoreAllTheThings():
    if score > goodScoreThreshold:
        print "%6.3f #%6d %s"%(score,id,name)

... car la version de l'objet est plus encombrée à lire:

for entry in scoreAllTheThings():
    if entry.score > goodScoreThreshold:
        print "%6.3f #%6d %s"%(entry.score,entry.id,entry.name)

... et encore moins le dict.

for entry in scoreAllTheThings():
    if entry['score'] > goodScoreThreshold:
        print "%6.3f #%6d %s"%(entry['score'],entry['id'],entry['name'])

Si la chose est largement utilisée et que vous vous trouvez à effectuer des opérations similaires non triviales à plusieurs endroits dans le code, il vaut généralement la peine d'en faire un objet de classe avec des méthodes appropriées.

Enfin, si je vais échanger des données avec des composants système non Python, je les garderai le plus souvent dans un dictcar c'est le mieux adapté à la sérialisation JSON.

Russell Borogove
la source
19

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

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

John Fouhy
la source
4

Dans des langages comme Python, j'utilisais généralement un dictionnaire car il implique moins de surcharge que la création d'une nouvelle classe.

Cependant, si je me retrouve constamment à renvoyer le même ensemble de variables, cela implique probablement une nouvelle classe que je factoriserai.

peluches
la source
4

J'utiliserais un dict pour passer et renvoyer des valeurs d'une fonction:

Utilisez un formulaire variable tel que défini dans le formulaire .

form = {
    'level': 0,
    'points': 0,
    'game': {
        'name': ''
    }
}


def test(form):
    form['game']['name'] = 'My game!'
    form['level'] = 2

    return form

>>> print(test(form))
{u'game': {u'name': u'My game!'}, u'points': 0, u'level': 2}

C'est le moyen le plus efficace pour moi et pour l'unité de traitement.

Vous devez passer un seul pointeur et renvoyer un seul pointeur.

Vous n'avez pas à modifier les arguments des fonctions (des milliers d'entre eux) chaque fois que vous apportez une modification à votre code.

Elis Byberi
la source
Les dits sont mutables. Si vous transmettez un dict à une fonction et que cette fonction modifie le dict, les modifications seront reflétées en dehors de la portée de cette fonction. Le fait que la fonction renvoie le dict à la fin peut impliquer que la fonction n'a pas d'effets secondaires, par conséquent, la valeur ne doit pas être renvoyée, ce qui indique clairement que testcela modifiera directement la valeur. Comparez cela avec dict.update, qui ne renvoie pas de valeur.
sleblanc
@sleblanc "Le fait que la fonction renvoie le dict à la fin peut impliquer que la fonction n'a pas d'effets secondaires". Cela n'implique pas cela parce que, comme vous l'avez dit, dict est mutable. Cependant, le retour formne nuit pas à la lisibilité ni aux performances. Dans les cas où vous devrez peut-être le reformater form, le renvoyer [formulaire] garantit que le dernier formest renvoyé car vous ne garderez aucune trace des modifications du formulaire.
Elis Byberi
3

«Meilleur» est une décision partiellement subjective. Utilisez des tuples pour les petits jeux de retour dans le cas général où une immuable est acceptable. Un tuple est toujours préférable à une liste lorsque la mutabilité n'est pas une exigence.

Pour des valeurs de retour plus complexes, ou dans le cas où la formalité est précieuse (c'est-à-dire un code de valeur élevée), un tuple nommé est préférable. Pour le cas le plus complexe, un objet est généralement préférable. Cependant, c'est vraiment la situation qui compte. S'il est judicieux de renvoyer un objet parce que c'est ce que vous avez naturellement à la fin de la fonction (par exemple, motif d'usine), renvoyez l'objet.

Comme l'a dit le sage:

L'optimisation prématurée est la racine de tout mal (ou du moins la plupart) dans la programmation.

joel3000
la source
1
Donald Knuth l'a dit (en 1974).
Peter Mortensen