232 votes

Appeler explicitement return dans une fonction ou non

Il y a quelque temps J'ai été réprimandé par Simon Urbanek de l'équipe centrale de R (je crois) pour recommander à un utilisateur d'appeler explicitement return à la fin d'une fonction (son commentaire a toutefois été supprimé) :

foo = function() {
  return(value)
}

Il a plutôt recommandé :

foo = function() {
  value
}

Probablement dans une situation comme celle-ci, c'est nécessaire :

foo = function() {
 if(a) {
   return(a)
 } else {
   return(b)
 }
}

Son commentaire nous éclaire sur les raisons de ne pas appeler return à moins d'être strictement nécessaire est une bonne chose, mais cela a été supprimé.

Ma question est la suivante : pourquoi ne pas appeler return plus rapide ou meilleur, et donc préférable ?

13 votes

return est inutile, même dans le dernier exemple. Suppression de return peut le rendre un peu plus rapide, mais à mon avis, c'est parce que R est dit être un langage de programmation fonctionnel.

5 votes

@kohske Pourriez-vous développer votre commentaire dans une réponse, en incluant plus de détails sur la raison pour laquelle c'est plus rapide, et comment cela est lié au fait que R est un langage de programmation fonctionnel ?

2 votes

return induit un saut non-local, et le saut non-local explicite est inhabituel pour FP. En fait, par exemple, le schéma n'a pas return . Je pense que mes commentaires sont trop courts (et peut-être incorrects) comme réponse.

151voto

Petr Matousu Points 828

La question était : Pourquoi ne pas appeler (explicitement) le retour est-il plus rapide ou meilleur, et donc préférable ?

Il n'y a pas de déclaration dans la documentation R faisant une telle supposition.
La page principale ? "fonction" dit :

function( arglist ) expr
return(value)

Est-ce plus rapide sans retour d'appel ?

Ambos function() y return() sont des fonctions primitives et les function() renvoie lui-même la dernière valeur évaluée, même sans inclure return() fonction.

Appel à return() comme .Primitive('return') avec cette dernière valeur comme argument fera le même travail mais nécessite un appel de plus. De sorte que cet appel (souvent) inutile .Primitive('return') peut attirer des ressources supplémentaires. Une mesure simple montre cependant que la différence résultante est très faible et ne peut donc pas être la raison de ne pas utiliser le retour explicite. Le graphique suivant est créé à partir de données sélectionnées de cette façon :

bench_nor2 <- function(x,repeats) { system.time(rep(
# without explicit return
(function(x) vector(length=x,mode="numeric"))(x)
,repeats)) }

bench_ret2 <- function(x,repeats) { system.time(rep(
# with explicit return
(function(x) return(vector(length=x,mode="numeric")))(x)
,repeats)) }

maxlen <- 1000
reps <- 10000
along <- seq(from=1,to=maxlen,by=5)
ret <- sapply(along,FUN=bench_ret2,repeats=reps)
nor <- sapply(along,FUN=bench_nor2,repeats=reps)
res <- data.frame(N=along,ELAPSED_RET=ret["elapsed",],ELAPSED_NOR=nor["elapsed",])

# res object is then visualized
# R version 2.15

Function elapsed time comparison

L'image ci-dessus peut différer légèrement selon votre plateforme. Sur la base des données mesurées, la taille de l'objet retourné ne fait aucune différence, le nombre de répétitions (même si elles sont mises à l'échelle) ne fait qu'une très petite différence, qui, dans la réalité, avec des données réelles et un algorithme réel, ne pourrait pas être comptée ou rendre votre script plus rapide.

Est-ce mieux sans retour d'appel ?

Return est un bon outil pour concevoir clairement des "feuilles" de code où la routine doit se terminer, sortir de la fonction et retourner la valeur.

# here without calling .Primitive('return')
> (function() {10;20;30;40})()
[1] 40
# here with .Primitive('return')
> (function() {10;20;30;40;return(40)})()
[1] 40
# here return terminates flow
> (function() {10;20;return();30;40})()
NULL
> (function() {10;20;return(25);30;40})()
[1] 25
> 

Cela dépend de la stratégie et du style de programmation du programmeur, il peut utiliser la fonction no return() car elle n'est pas nécessaire.

Les programmeurs de base de R utilisent les deux approches, c'est-à-dire avec et sans return() explicite, comme il est possible de le trouver dans les sources des fonctions de base.

Souvent, seule la fonction return() est utilisée (sans argument), renvoyant NULL dans les cas où il faut arrêter la fonction de manière conditionnelle.

Il n'est pas clair s'il est meilleur ou non car l'utilisateur standard ou l'analyste utilisant R ne peut pas voir la vraie différence.

Mon opinion est que la question devrait être : Y a-t-il un danger à utiliser un retour explicite provenant de l'implémentation de R ?

Ou, peut-être mieux, l'utilisateur qui écrit le code de la fonction devrait toujours demander : Quel est l'effet dans no en utilisant un retour explicite (ou en plaçant l'objet à retourner comme dernière feuille de la branche de code) dans le code de la fonction ?

6 votes

Merci pour cette très bonne réponse. Je pense qu'il n'y a aucun danger à utiliser return et c'est le programmeur qui décide de l'utiliser ou non.

53 votes

La vitesse de return est vraiment la dernière chose dont vous devriez vous soucier.

2 votes

Je pense que c'est une mauvaise réponse. Les raisons se résument à un désaccord fondamental sur la valeur de l'utilisation de produits inutiles. return appels de fonction. La question que vous debe demander n'est pas celui que vous proposez à la fin. Au lieu de cela, c'est : "pourquoi debe J'utilise une redondance return ? Quel avantage procure-t-il ?" Il s'avère que la réponse est "pas grand-chose", voire "aucun". Votre réponse n'en tient pas compte.

114voto

flodel Points 41487

Si tout le monde est d'accord pour dire que

  1. return n'est pas nécessaire à la fin du corps d'une fonction
  2. ne pas utiliser return est légèrement plus rapide (selon le test d'@Alan, 4,3 microsecondes contre 5,1)

devrions-nous tous arrêter d'utiliser return à la fin d'une fonction ? Je ne le ferai certainement pas, et j'aimerais expliquer pourquoi. J'espère savoir si d'autres personnes partagent mon opinion. Et je m'excuse si ce n'est pas une réponse directe à l'OP, mais plutôt un long commentaire subjectif.

Le principal problème que je rencontre en n'utilisant pas return est que, comme Paul l'a souligné, il y a d'autres endroits dans le corps d'une fonction où vous pouvez en avoir besoin. Et si vous êtes obligé d'utiliser return quelque part au milieu de votre fonction, pourquoi ne pas faire en sorte que tout return déclarations explicites ? Je déteste être incohérent. Je pense aussi que le code se lit mieux ; on peut parcourir la fonction et voir facilement tous les points de sortie et les valeurs.

Paul a utilisé cet exemple :

foo = function() {
 if(a) {
   return(a)
 } else {
   return(b)
 }
}

Malheureusement, on pourrait faire remarquer qu'il peut facilement être réécrit comme :

foo = function() {
 if(a) {
   output <- a
 } else {
   output <- b
 }
output
}

Cette dernière version est même conforme à certaines normes de codage de programmation qui préconisent une déclaration de retour par fonction. Je pense qu'un meilleur exemple aurait pu être :

bar <- function() {
   while (a) {
      do_stuff
      for (b) {
         do_stuff
         if (c) return(1)
         for (d) {
            do_stuff
            if (e) return(2)
         }
      }
   }
   return(3)
}

Cela serait beaucoup plus difficile à réécrire en utilisant une seule déclaration de retour : il faudrait plusieurs déclarations de retour. break et un système complexe de variables booléennes pour les propager. Tout cela pour dire que la règle du retour unique ne fonctionne pas bien avec R. Donc, si vous avez besoin d'utiliser la fonction return à certains endroits du corps de votre fonction, pourquoi ne pas être cohérent et l'utiliser partout ?

Je ne pense pas que l'argument de la vitesse soit valable. Une différence de 0,8 microseconde n'est rien quand on commence à regarder les fonctions qui font vraiment quelque chose. La dernière chose que je vois, c'est qu'il faut moins taper, mais bon, je ne suis pas paresseux.

8 votes

+1, il existe un besoin évident pour le return dans certains cas, comme l'a montré @flodel. Par ailleurs, il existe des situations où il est préférable d'omettre une déclaration de retour, par exemple dans le cas de nombreux appels de petites fonctions. Dans tous les autres cas, disons 95%, il n'y a pas vraiment d'importance si l'on utilise return ou non, c'est une question de préférence. J'aime utiliser return car il est plus explicite dans ce que vous voulez dire, donc plus lisible. Peut-être que cette discussion s'apparente à <- vs = ?

8 votes

Cela revient à traiter R comme un langage de programmation impératif, ce qu'il n'est pas : c'est un langage de programmation fonctionnel. La programmation fonctionnelle fonctionne simplement différemment, et utiliser return pour retourner une valeur est absurde, au même titre que d'écrire if (x == TRUE) au lieu de if (x) .

5 votes

Vous réécrivez aussi foo comme foo <- function(x) if (a) a else b (avec des retours à la ligne si nécessaire). Pas besoin de retour explicite ou de valeur intermédiaire.

30voto

nograpes Points 11530

Il s'agit d'une discussion intéressante. Je pense que l'exemple de @flodel est excellent. Cependant, je pense qu'il illustre mon point de vue (et @koshke le mentionne dans un commentaire) selon lequel return a un sens lorsque vous utilisez un impératif au lieu d'un fonctionnel style de codage .

Je ne veux pas insister sur le point, mais j'aurais réécrit foo comme ça :

foo = function() ifelse(a,a,b)

Un style fonctionnel évite les changements d'état, comme le stockage de la valeur de output . Dans ce style, return n'est pas à sa place ; foo ressemble plus à une fonction mathématique.

Je suis d'accord avec @flodel : l'utilisation d'un système complexe de variables booléennes en bar serait moins claire, et inutile lorsque vous avez return . Ce qui fait bar si favorable à return est qu'il est écrit dans un style impératif. En effet, les variables booléennes représentent les changements d'"état" évités dans un style fonctionnel.

Il est vraiment difficile de réécrire bar en style fonctionnel, parce que c'est juste du pseudocode, mais l'idée est quelque chose comme ça :

e_func <- function() do_stuff
d_func <- function() ifelse(any(sapply(seq(d),e_func)),2,3)
b_func <- function() {
  do_stuff
  ifelse(c,1,sapply(seq(b),d_func))
}

bar <- function () {
   do_stuff
   sapply(seq(a),b_func) # Not exactly correct, but illustrates the idea.
}

El while serait la plus difficile à réécrire, parce qu'elle est contrôlée par les changements d'état de la boucle a .

La perte de vitesse causée par un appel à return est négligeable, mais l'efficacité gagnée en évitant return et la réécriture dans un style fonctionnel est souvent énorme. Dire aux nouveaux utilisateurs d'arrêter d'utiliser return ne sera probablement pas utile, mais les guider vers un style fonctionnel sera payant.


@Paul return est nécessaire dans le style impératif parce que vous voulez souvent quitter la fonction à différents points d'une boucle. Un style fonctionnel n'utilise pas de boucles, et n'a donc pas besoin de return . Dans un style purement fonctionnel, l'appel final est presque toujours la valeur de retour souhaitée.

En Python, les fonctions nécessitent un return déclaration. Toutefois, si vous avez programmé votre fonction dans un style fonctionnel, vous n'aurez probablement qu'une seule instruction return déclaration : à la fin de votre fonction.

En utilisant un exemple tiré d'un autre billet de StackOverflow, disons que nous voulons une fonction qui renvoie TRUE si toutes les valeurs d'un x avait une longueur étrange. On pourrait utiliser deux styles :

# Procedural / Imperative
allOdd = function(x) {
  for (i in x) if (length(i) %% 2 == 0) return (FALSE)
  return (TRUE)
}

# Functional
allOdd = function(x) 
  all(length(x) %% 2 == 1)

Dans un style fonctionnel, la valeur à retourner tombe naturellement aux extrémités de la fonction. Là encore, cela ressemble davantage à une fonction mathématique.

@GSee Les avertissements soulignés dans ?ifelse sont certainement intéressants, mais je ne pense pas qu'ils essaient de dissuader l'utilisation de la fonction. En fait, ifelse a l'avantage de vectoriser automatiquement les fonctions. Par exemple, considérons une version légèrement modifiée de foo :

foo = function(a) { # Note that it now has an argument
 if(a) {
   return(a)
 } else {
   return(b)
 }
}

Cette fonction fonctionne bien lorsque length(a) est 1. Mais si vous réécrivez foo avec un ifelse

foo = function (a) ifelse(a,a,b)

Maintenant foo fonctionne sur toute longueur de a . En fait, ça marcherait même quand a est une matrice. En retournant une valeur de la même forme que test est une fonctionnalité qui aide à la vectorisation, pas un problème.

0 votes

Je ne comprends pas bien pourquoi return ne correspond pas à un style de programmation fonctionnel. Que l'on programme de manière impérative ou fonctionnelle, à un moment donné, une fonction ou une sous-routine doit retourner quelque chose. Par exemple, la programmation fonctionnelle en python nécessite toujours un retour return déclaration. Pourriez-vous nous en dire plus sur ce point ?

3 votes

Dans cette situation, l'utilisation de ifelse(a,a,b) est une de mes bêtes noires. Il semble que chaque ligne dans ?ifelse crie, "ne m'utilisez pas au lieu de if (a) {a} else b ...", par exemple, "... renvoie une valeur ayant la même forme que test ", "si yes o no sont trop courts, leurs éléments sont recyclés.", "le mode de résultat peut dépendre de la valeur de test ", "l'attribut de classe du résultat est tiré de test et peut être inapproprié pour les valeurs choisies parmi les suivantes yes y no "

0 votes

Au second regard, foo n'a pas beaucoup de sens ; elle renverra toujours VRAI ou b . Utilisation de ifelse il retournera 1 ou plusieurs VRAIS, et/ou 1 ou plusieurs b s. Initialement, je pensais que l'intention de la fonction était de dire "si une certaine déclaration est VRAIE, retourner quelque chose, sinon, retourner autre chose". Je ne pense pas que cela devrait être vectorisé, parce que cela deviendrait alors "retourne les éléments d'un objet qui sont VRAIS, et pour tous les éléments qui ne sont pas VRAIS, retourne b .

22voto

Alan Points 876

Il semble que sans return() c'est plus rapide...

library(rbenchmark)
x <- 1
foo <- function(value) {
  return(value)
}
fuu <- function(value) {
  value
}
benchmark(foo(x),fuu(x),replications=1e7)
    test replications elapsed relative user.self sys.self user.child sys.child
1 foo(x)     10000000   51.36 1.185322     51.11     0.11          0         0
2 fuu(x)     10000000   43.33 1.000000     42.97     0.05          0         0

____ EDIT __ _ __ _ __ _ __ _ __ _ ___

Je procède à d'autres repères ( benchmark(fuu(x),foo(x),replications=1e7) ) et le résultat est inversé... Je vais essayer sur un serveur.

0 votes

Pourriez-vous expliquer la raison de cette différence ?

4 votes

@PaulHiemstra La réponse de Petr couvre l'une des principales raisons de cette situation ; deux appels lors de l'utilisation de return() et un autre si vous ne le faites pas. Il est totalement redondant à la fin d'une fonction comme function() retourne sa dernière valeur. Vous ne le remarquerez que dans le cas de nombreuses répétitions d'une fonction où peu de choses sont faites en interne, de sorte que le coût de l'opération est inférieur à celui de l'opération. return() devient une grande partie du temps total de calcul de la fonction.

14voto

Hugh Perkins Points 1050

Un problème lié au fait de ne pas mettre explicitement "return" à la fin est que si l'on ajoute des déclarations supplémentaires à la fin de la méthode, la valeur de retour est soudainement fausse :

foo <- function() {
    dosomething()
}

Cela renvoie la valeur de dosomething() .

Maintenant, nous arrivons le lendemain et ajoutons une nouvelle ligne :

foo <- function() {
    dosomething()
    dosomething2()
}

Nous voulions que notre code renvoie la valeur de dosomething() mais ce n'est plus le cas.

Avec un retour explicite, cela devient vraiment évident :

foo <- function() {
    return( dosomething() )
    dosomething2()
}

Nous pouvons voir qu'il y a quelque chose d'étrange dans ce code, et le corriger :

foo <- function() {
    dosomething2()
    return( dosomething() )
}

1 votes

Oui, en fait, je trouve qu'un return() explicite est utile lors du débogage ; une fois que le code est nettoyé, son besoin est moins impérieux et je préfère l'élégance de ne pas l'avoir ...

0 votes

Mais ce n'est pas un problème dans le code réel, c'est purement théorique. Un code qui pourrait souffre de cela a un problème bien plus important : un flux de code obscur et fragile, si peu évident que de simples ajouts le brisent.

0 votes

@KonradRudolph Je pense que vous êtes en train de faire une sorte de No-True Scotsman là-dessus ;-) "Si c'est un problème dans votre code, vous êtes un mauvais programmeur !". Je ne suis pas vraiment d'accord. Je pense que si vous pouvez vous en sortir en prenant des raccourcis sur de petits morceaux de code, où vous connaissez chaque ligne par cœur, cela vous reviendra en pleine figure lorsque votre code deviendra plus gros.

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