web-dev-qa-db-fra.com

Pourquoi les raccourcis comme x + = y sont-ils considérés comme une bonne pratique?

Je n'ai aucune idée de leur nom, mais je les vois tout le temps. L'implémentation Python est quelque chose comme:

x += 5 comme notation abrégée pour x = x + 5.

Mais pourquoi est-ce considéré comme une bonne pratique? Je l'ai rencontré dans presque tous les livres ou tutoriels de programmation que j'ai lus pour Python, C, R ainsi de suite, etc. Je comprends que c'est pratique, en économisant trois frappes, y compris les espaces. Mais ils semblent toujours me trébucher quand je lis du code, et au moins dans mon esprit, le rendre moins lisible, pas plus.

Est-ce que je manque une raison claire et évidente pour laquelle ils sont utilisés partout?

309
Fomite

Ce n'est pas un raccourci.

Le symbole += Est apparu en langage C dans les années 1970, et - avec l'idée C de "l'assembleur intelligent" correspond à une instruction machine et un mode d'adressage clairement différents:

Des choses comme "i=i+1", "i+=1 "Et" ++i ", Bien qu'à un niveau abstrait produisent le même effet, correspondent à bas niveau à une manière différente de travailler processeur.

En particulier ces trois expressions, en supposant que la variable i réside dans l'adresse mémoire stockée dans un registre CPU (appelons-la D - pensez-y comme un "pointeur vers int") et la - ALU du processeur prend un paramètre et retourne un résultat dans un "accumulateur" (appelons-le A - pensez-y comme un int).

Avec ces contraintes (très courantes dans tous les microprocesseurs de cette période), la traduction sera très probablement

;i = i+1;
MOV A,(D); //Move in A the content of the memory whose address is in D
ADD A, 1;  //The addition of an inlined constant
MOV (D) A; //Move the result back to i (this is the '=' of the expression)

;i+=1;
ADD (D),1; //Add an inlined constant to a memory address stored value

;++i;
INC (D); //Just "tick" a memory located counter

La première façon de le faire est désoptimale, mais elle est plus générale lorsque vous utilisez des variables au lieu de constantes (ADD A, B Ou ADD A, (D+x)) ou lorsque vous traduisez des expressions plus complexes (elles se résument toutes dans Push opération de faible priorité dans une pile, appelez la haute priorité, pop et répétez jusqu'à ce que tous les arguments aient été éliminés).

La seconde est plus typique de la "machine à états": nous n'évaluons plus une expression ", mais" exploitons une valeur ": nous utilisons toujours l'ALU, mais évitons de déplacer les valeurs autour étant le résultat autorisé à remplacer le paramètre. Ce type d'instruction ne peut pas être utilisé lorsque des expressions plus complexes sont requises: i = 3*i + i-2 Ne peut pas être utilisé sur place, car i est requis plusieurs fois.

Le troisième - même plus simple - ne considère même pas l'idée d '"addition", mais utilise un circuit plus "primitif" (au sens du calcul) pour un compteur. L'instruction est raccourcie, se charge plus rapidement et s'exécute immédiatement, car le réseau combinatoire nécessaire pour moderniser un registre pour en faire un compteur est plus petit, et donc plus rapide que celui d'un additionneur complet.

Avec les compilateurs contemporains (voir C, maintenant), permettant l'optimisation du compilateur, la correspondance peut être permutée en fonction de la commodité, mais il y a toujours une différence conceptuelle dans la sémantique.

x += 5 Signifie

  • Trouvez l'endroit identifié par x
  • Ajoutez-y 5

Mais x = x + 5 Signifie:

  • Évaluer x + 5
    • Trouvez l'endroit identifié par x
    • Copiez x dans un accumulateur
    • Ajoutez 5 à l'accumulateur
  • Stockez le résultat dans x
    • Trouvez l'endroit identifié par x
    • Copiez l'accumulateur dessus

Bien sûr, l’optimisation peut

  • si "trouver x" n'a pas d'effets secondaires, les deux "trouver" peuvent être faites une fois (et x devient une adresse stockée dans un registre de pointeurs)
  • les deux copies peuvent être supprimées si l'ADD est appliqué à &x à la place à l'accumulateur

ce qui fait que le code optimisé coïncide avec celui de x += 5.

Mais cela ne peut être fait que si "trouver x" n'a pas d'effets secondaires, sinon

*(x()) = *(x()) + 5;

et

*(x()) += 5;

sont sémantiquement différents, car x() les effets secondaires (admettre x() est une fonction faisant des choses étranges et renvoyer un int*) seront produits deux ou une fois.

L'équivalence entre x = x + y Et x += y Est donc due au cas particulier où += Et = Sont appliqués à une valeur l directe.

Pour passer à Python, il a hérité de la syntaxe de C, mais comme il n'y a pas de traduction/optimisation AVANT l'exécution dans les langages interprétés, les choses ne sont pas nécessairement si intimement liées (car il y a une étape d'analyse en moins). Cependant, un interpréteur peut faire référence à différentes routines d'exécution pour les trois types d'expression, en tirant parti de différents codes machine en fonction de la façon dont l'expression est formée et du contexte d'évaluation.


Pour qui aime plus de détails ...

Chaque CPU a une ALU (unité arithmétique et logique) qui est, dans son essence même, un réseau combinatoire dont les entrées et les sorties sont "branchées" aux registres et/ou à la mémoire selon l'opcode de l'instruction.

Les opérations binaires sont généralement implémentées comme "modificateur d'un registre d'accumulateur avec une entrée prise" quelque part ", où quelque part peut être - à l'intérieur du flux d'instructions lui-même (typique pour le contenu manifeste: ADD A 5) - à l'intérieur d'un autre registre (typique pour le calcul d'expression avec temporaires: par exemple ADD AB) - à l'intérieur de la mémoire, à une adresse donnée par un registre (typique de l'extraction de données, par exemple: ADD A (H)) - H, dans ce cas, fonctionne comme un pointeur de déréférencement.

Avec ce pseudocode, x += 5 Est

ADD (X) 5

tandis que x = x+5 est

MOVE A (X)
ADD A 5
MOVE (X) A

Autrement dit, x + 5 donne un temporaire qui est ensuite attribué. x += 5 Fonctionne directement sur x.

L'implémentation réelle dépend du jeu d'instructions réel du processeur: S'il n'y a pas d'opcode ADD (.) c, le premier code devient le second: pas du tout.

S'il existe un tel opcode et que l'optimisation est activée, la deuxième expression, après avoir éliminé les mouvements inverses et ajusté l'opcode registres, devient la première.

603
Emilio Garavaglia

Selon la façon dont vous y pensez, c'est en fait plus facile à comprendre car c'est plus simple. Prends pour exemple:

x = x + 5 invoque le traitement mental de "prendre x, en ajouter cinq, puis attribuer cette nouvelle valeur à x"

x += 5 peut être considéré comme "augmenter x de 5"

Donc, ce n'est pas seulement un raccourci, il décrit en fait la fonctionnalité beaucoup plus directement. Lorsque vous lisez des morceaux de code, c'est beaucoup plus facile à saisir.

285
Eric King

Au moins dans Python , x += y et x = x + y peut faire des choses complètement différentes.

Par exemple, si nous le faisons

a = []
b = a

puis a += [3] aura pour résultat a == b == [3], tandis que a = a + [3] aura pour résultat a == [3] et b == []. C'est, += modifie l'objet sur place (eh bien, cela pourrait faire, vous pouvez définir le __iadd__ méthode pour faire à peu près tout ce que vous aimez), tandis que = crée un nouvel objet et lui lie la variable.

Ceci est très important lorsque vous effectuez un travail numérique avec NumPy , car vous vous retrouvez fréquemment avec plusieurs références à différentes parties d'un tableau, et il est important de vous assurer que vous ne modifiez pas par inadvertance une partie d'un tableau qu'il existe d'autres références à, ou copiez inutilement des tableaux (qui peuvent être très coûteux).

50
James

Cela s'appelle un idiome . Les idiomes de programmation sont utiles car ils constituent un moyen cohérent d'écrire une construction de programmation particulière.

Chaque fois que quelqu'un écrit x += y vous savez que x est incrémenté de y et pas d'une opération plus complexe (comme meilleure pratique, je ne mélangerais généralement pas des opérations plus compliquées et ces raccourcis de syntaxe). Cela est plus judicieux lors de l'incrémentation de 1.

43
Joe

Pour mettre le point de @ Pubby un peu plus clair, considérons someObj.foo.bar.func(x, y, z).baz += 5

Sans l'opérateur +=, Il y a deux façons de procéder:

  1. someObj.foo.bar.func(x, y, z).baz = someObj.foo.bar.func(x, y, z).baz + 5. Ce n'est pas seulement redondant et long, c'est aussi plus lent. Il faudrait donc
  2. Utilisez une variable temporaire: tmp := someObj.foo.bar.func(x, y, z); tmp.baz = tmp.bar + 5. C'est ok, mais c'est beaucoup de bruit pour une chose simple. C'est en fait très proche de ce qui se passe au moment de l'exécution, mais il est fastidieux d'écrire et le simple fait d'utiliser += Déplacera le travail vers le compilateur/interpréteur.

L'avantage de += Et d'autres opérateurs de ce type est indéniable, alors que s'y habituer n'est qu'une question de temps.

42
back2dos

Il est vrai qu'il est plus court et plus facile, et il est vrai qu'il a probablement été inspiré par le langage d'assemblage sous-jacent, mais la raison pour laquelle c'est meilleure pratique est qu'il empêche toute une classe d'erreurs, et il le rend plus facile pour revoir le code et être sûr de ce qu'il fait.

Avec

RidiculouslyComplexName += 1;

Puisqu'il n'y a qu'un seul nom de variable impliqué, vous êtes sûr de ce que fait l'instruction.

Avec RidiculouslyComplexName = RidiculosulyComplexName + 1;

Il y a toujours un doute que les deux côtés sont exactement les mêmes. Avez-vous vu le bug? Cela empire encore lorsque des indices et des qualificatifs sont présents.

21
Jamie Cox

Bien que la notation + = soit idiomatique et plus courte, ce ne sont pas les raisons pour lesquelles elle est plus facile à lire. La partie la plus importante de la lecture du code consiste à mapper la syntaxe au sens, et donc plus la syntaxe correspond aux processus de pensée du programmeur, plus elle sera lisible (c'est aussi la raison pour laquelle le code passe-partout est mauvais: il ne fait pas partie de la pensée mais toujours nécessaire pour faire fonctionner le code). Dans ce cas, la pensée est "d'incrémenter la variable x de 5", et non "soit x la valeur de x plus 5".

Il existe d'autres cas où une notation plus courte est mauvaise pour la lisibilité, par exemple lorsque vous utilisez un opérateur ternaire où une instruction if serait plus appropriée.

14
tdammers

Pour avoir un aperçu de la raison pour laquelle ces opérateurs sont dans les langages de "style C" pour commencer, il y a cet extrait de K&R 1st Edition (1978), il y a 34 ans:

Indépendamment de la concision, les opérateurs d'affectation ont l'avantage de mieux correspondre à la façon dont les gens pensent. Nous disons "ajouter 2 à i" ou "incrémenter i de 2", "pas" prendre i, ajouter 2, puis remettre le résultat dans i ". Donc i += 2. De plus, pour une expression compliquée comme

yyval[yypv[p3+p4] + yypv[p1+p2]] += 2

l'opérateur d'affectation rend le code plus facile à comprendre, car le lecteur n'a pas à vérifier minutieusement que deux expressions longues sont effectivement les mêmes, ou à se demander pourquoi elles ne le sont pas. Et un opérateur d'affectation peut même aider le compilateur à produire du code plus efficace.

Je pense qu'il est clair à partir de ce passage que Brian Kernighan et Dennis Ritchie (K&R), pensaient que les opérateurs d'affectation composée aidaient à la lisibilité du code.

Cela fait longtemps que K&R n'a pas écrit cela, et beaucoup de `` meilleures pratiques '' sur la façon dont les gens devraient écrire du code ont changé ou évolué depuis. Mais cette question programmers.stackexchange est la première fois que je me souviens d'une personne ayant exprimé une plainte concernant la lisibilité des affectations composées, alors je me demande si de nombreux programmeurs les trouvent problématiques? Là encore, lorsque je tape ceci, la question a 95 votes positifs, alors peut-être que les gens les trouvent discordants lors de la lecture du code.

12
Michael Burr

Outre la lisibilité, ils font en fait différentes choses: += n'a pas à évaluer son opérande gauche deux fois.

Par exemple, expr = expr + 5 évalue expr deux fois (en supposant que expr est impur).

9
Pubby

Outre les mérites évidents que d'autres ont très bien décrits, lorsque vous avez des noms très longs, il est plus compact.

  MyVeryVeryVeryVeryVeryLongName += 1;

ou

  MyVeryVeryVeryVeryVeryLongName =  MyVeryVeryVeryVeryVeryLongName + 1;
6
Andrey Rubshtein

C'est concis.

C'est beaucoup plus court à taper. Cela implique moins d'opérateurs. Il a moins de surface et moins de risques de confusion.

Il utilise un opérateur plus spécifique.

Ceci est un exemple artificiel, et je ne sais pas si les compilateurs réels implémentent cela. x + = y utilise en fait un argument et un opérateur et modifie x en place. x = x + y pourrait avoir une représentation intermédiaire de x = z où z est x + y. Ce dernier utilise deux opérateurs, addition et affectation, et une variable temporaire. L'opérateur unique indique très clairement que le côté valeur ne peut être autre chose que y et n'a pas besoin d'être interprété. Et il pourrait théoriquement y avoir un processeur sophistiqué ayant un opérateur plus-égal qui s'exécute plus rapidement qu'un opérateur plus et un opérateur d'affectation en série.

6
Mark Canlas

C'est un bel idiome. Que ce soit plus rapide ou non dépend de la langue. En C, c'est plus rapide car cela se traduit par une instruction pour augmenter la variable par le côté droit. Les langages modernes, y compris Python, Ruby, C, C++ et Java supportent tous la syntaxe op =. Elle est compacte et vous vous y habituez rapidement. Puisque vous la verrez beaucoup dans d'autres code populaire (OPC), vous pouvez aussi bien vous y habituer et l'utiliser. Voici ce qui se passe dans quelques autres langues.

En Python, en tapant x += 5 provoque toujours la création de l'objet entier 1 (bien qu'il puisse être tiré d'un pool) et l'orphelin de l'objet entier contenant 5.

En Java, il provoque une conversion tacite. Essayez de taper

int x = 4;
x = x + 5.2  // This causes a compiler error
x += 5.2     // This is not an error; an implicit cast is done.
5
ncmathsadist

Des opérateurs tels que += sont très utiles lorsque vous utilisez une variable comme accumulateur, c'est-à-dire un total cumulé:

x += 2;
x += 5;
x -= 3;

Est beaucoup plus facile à lire que:

x = x + 2;
x = x + 5;
x = x - 3;

Dans le premier cas, conceptuellement, vous modifiez la valeur dans x. Dans le second cas, vous calculez une nouvelle valeur et l'assignez à x à chaque fois. Et bien que vous n'écririez probablement jamais de code aussi simple, l'idée reste la même ... l'accent est mis sur ce que vous faites pour une valeur existante au lieu de créer une nouvelle valeur.

4
Caleb

Considère ceci

(some_object[index])->some_other_object[more] += 5

D0 tu veux vraiment écrire

(some_object[index])->some_other_object[more] = (some_object[index])->some_other_object[more] + 5
1
S.Lott

Dites-le une seule fois: dans x = x + 1, Je dis "x" deux fois.

Mais n'écrivez jamais "a = b + = 1" ou nous devrons tuer 10 chatons, 27 souris, un chien et un hamster.


Vous ne devez jamais modifier la valeur d'une variable, car cela permet de prouver plus facilement que le code est correct - voir programmation fonctionnelle. Cependant, si vous le faites, il vaut mieux ne pas dire les choses une seule fois.

1
ctrl-alt-delor

Les autres réponses ciblent les cas les plus courants, mais il y a une autre raison: dans certains langages de programmation, il peut être surchargé; par exemple. Scala .


Petit Scala leçon:

var j = 5 #Creates a variable
j += 4    #Compiles

val i = 5 #Creates a constant
i += 4    #Doesn’t compile

Si une classe définit uniquement le + opérateur, x+=y est en effet un raccourci de x=x+y.

Si une classe surcharge +=, cependant, ils ne sont pas:

var a = ""
a += "This works. a now points to a new String."

val b = ""
b += "This doesn’t compile, as b cannot be reassigned."

val c = StringBuffer() #implements +=
c += "This works, as StringBuffer implements “+=(c: String)”."

De plus, les opérateurs + et += sont deux opérateurs distincts (et pas seulement ceux-ci: + a, ++ a, a ++, a + b a + = b sont également des opérateurs différents); dans les langues où la surcharge de l'opérateur est disponible, cela peut créer des situations intéressantes. Tout comme décrit ci-dessus - si vous surchargez le + opérateur pour effectuer l'ajout, gardez à l'esprit que += devra également être surchargé.

1
flying sheep