¿Por qué los atajos como x + = y se consideran una buena práctica?


305

No tengo idea de cómo se llaman en realidad, pero los veo todo el tiempo. La implementación de Python es algo así como:

x += 5como una notación abreviada para x = x + 5.

Pero, ¿por qué se considera esto una buena práctica? Lo he encontrado en casi todos los libros o tutoriales de programación que he leído para Python, C, R , etc. Entiendo que es conveniente, ahorrando tres pulsaciones de teclas, incluidos los espacios. Pero siempre parecen hacerme tropezar cuando leo el código y, al menos en mi opinión, lo hacen menos legible, no más.

¿Me estoy perdiendo alguna razón clara y obvia por la que se usan en todo el lugar?


@EricLippert: ¿C # maneja esto de la misma manera que se describe la respuesta principal? ¿Es realmente más eficiente decir CLR x += 5que x = x + 5? ¿O es realmente solo azúcar sintáctico como sugieres?
carne

24
@blesh: Que pequeños detalles de cómo uno expresa una adición en el código fuente tienen un impacto en la eficiencia del código ejecutable resultante podría haber sido el caso en 1970; ciertamente no lo es ahora. Los compiladores de optimización son buenos, y tienes preocupaciones más grandes que un nanosegundo aquí o allá. La idea de que el operador + = se desarrolló "hace veinte años" es obviamente falsa; el fallecido Dennis Richie desarrolló C desde 1969 hasta 1973 en Bell Labs.
Eric Lippert

1
Ver blogs.msdn.com/b/ericlippert/archive/2011/03/29/… (ver también la segunda parte)
Fugas

55
La mayoría de los programadores funcionales considerarán esta mala práctica.
Pete Kirkham

Respuestas:


598

No es taquigrafía.

El +=símbolo apareció en el lenguaje C en la década de 1970 y, con la idea C de "ensamblador inteligente", corresponde a una instrucción de máquina y un modo de dirección claramente diferentes:

Cosas como " i=i+1", "i+=1"y" ++i", aunque en un nivel abstracto producen el mismo efecto, corresponden en un nivel bajo a una forma diferente de trabajar del procesador.

En particular, esas tres expresiones, suponiendo que la ivariable reside en la dirección de memoria almacenada en un registro de la CPU ( Dpor ejemplo, piense en ello como un "puntero a int") y la ALU del procesador toma un parámetro y devuelve un resultado en un "acumulador" (llamémoslo A - piense en ello como int).

Con estas restricciones (muy comunes en todos los microprocesadores de ese período), la traducción probablemente será

;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 primera forma de hacerlo es desóptima, pero es más general cuando se opera con variables en lugar de constantes ( ADD A, Bo ADD A, (D+x)) o cuando se traducen expresiones más complejas (todas se reducen al presionar la operación de baja prioridad en una pila, llame a la alta prioridad, pop y repita hasta que todos los argumentos hayan sido eliminados).

El segundo es más típico de la "máquina de estado": ya no estamos "evaluando una expresión", sino "operando un valor": todavía usamos la ALU, pero evitamos mover los valores como resultado permitido reemplazar el parámetro. Este tipo de instrucción no se puede usar donde se requieren expresiones más complicadas: i = 3*i + i-2no se puede operar en su lugar, ya que ise requiere más veces.

El tercero, incluso más simple, ni siquiera considera la idea de "suma", sino que usa un circuito más "primitivo" (en sentido computacional) para un contador. La instrucción se acorta, se carga más rápido y se ejecuta de inmediato, ya que la red combinatoria requerida para actualizar un registro para convertirlo en un contador es más pequeña y, por lo tanto, más rápida que la de un sumador completo.

Con los compiladores contemporáneos (consulte C, por ahora), que permiten la optimización del compilador, la correspondencia puede intercambiarse según la conveniencia, pero todavía hay una diferencia conceptual en la semántica.

x += 5 medio

  • Encuentra el lugar identificado por x
  • Añade 5

Pero x = x + 5significa:

  • Evaluar x + 5
    • Encuentra el lugar identificado por x
    • Copiar x en un acumulador
    • Añade 5 al acumulador
  • Almacenar el resultado en x
    • Encuentra el lugar identificado por x
    • Copia el acumulador

Por supuesto, la optimización puede

  • si "encontrar x" no tiene efectos secundarios, los dos "encontrar" se pueden hacer una vez (y x se convierte en una dirección almacenada en un registro de puntero)
  • las dos copias se pueden elidir si se aplica ADD &xal acumulador

haciendo que el código optimizado coincida con el x += 5uno.

Pero esto solo se puede hacer si "encontrar x" no tiene efectos secundarios, de lo contrario

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

y

*(x()) += 5;

son semánticamente diferentes, ya que x()los efectos secundarios (admitir x()es una función que hace cosas raras y devolver un int*) se producirán dos o una vez.

La equivalencia entre x = x + yy, por x += ylo tanto, se debe al caso particular donde +=y =se aplican a un valor l directo.

Para pasar a Python, heredó la sintaxis de C, pero como no hay traducción / optimización ANTES de la ejecución en lenguajes interpretados, las cosas no están necesariamente tan íntimamente relacionadas (ya que hay un paso de análisis menos). Sin embargo, un intérprete puede referirse a diferentes rutinas de ejecución para los tres tipos de expresión, aprovechando diferentes códigos de máquina dependiendo de cómo se forme la expresión y del contexto de evaluación.


Para quien le gusta más detalle ...

Cada CPU tiene una ALU (unidad aritmética-lógica) que es, en esencia, una red combinatoria cuyas entradas y salidas están "conectadas" a los registros y / o memoria dependiendo del código de operación de la instrucción.

Las operaciones binarias generalmente se implementan como "modificador de un registro de acumulador con una entrada tomada" en algún lugar ", donde en algún lugar puede estar, dentro del flujo de instrucciones en sí mismo (típico para el contenido manifiesto: ADD A 5), dentro de otro registro (típico para el cálculo de expresiones con temporales: por ejemplo, ADD AB): dentro de la memoria, en una dirección dada por un registro (típico de la obtención de datos, por ejemplo: ADD A (H)) - H, en este caso, funciona como un puntero de referencia.

Con este pseudocódigo, x += 5es

ADD (X) 5

mientras x = x+5es

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

Es decir, x + 5 da un temporal que luego se asigna. x += 5opera directamente en x.

La implementación real depende del conjunto de instrucciones reales del procesador: si no hay ADD (.) ccódigo de operación, el primer código se convierte en el segundo: de ninguna manera.

Si existe dicho código de operación, y la optimización está habilitada, la segunda expresión, después de eliminar los movimientos inversos y ajustar el código de operación de los registros, se convierte en la primera.


90
+1 para la única respuesta que explica que solía asignarse a un código de máquina diferente (y más eficiente) en los viejos tiempos.
Péter Török

11
@KeithThompson Eso es cierto, pero no se puede negar que el ensamblaje tuvo una gran influencia sobre el diseño del lenguaje C (y posteriormente todos los lenguajes de estilo C)
MattDavey

51
Erm, "+ =" no se asigna a "inc" (se asigna a "agregar"), "++" se asigna a "inc".
Brendan

11
Por "hace 20 años", creo que te refieres a "hace 30 años". Y, por cierto, COBOL había vencido a C por otros 20 años con: AGREGAR 5 A X.
JoelFan

10
Genial en teoría; mal en los hechos. El x86 ASM INC solo agrega 1, por lo que no afecta el operador "agregar y asignar" discutido aquí (sin embargo, esta sería una gran respuesta para "++" y "-").
Mark Brackett

281

Dependiendo de cómo lo piense, en realidad es más fácil de entender porque es más sencillo. Tomar como ejemplo:

x = x + 5 invoca el procesamiento mental de "tomar x, agregarle cinco y luego asignar ese nuevo valor a x"

x += 5 puede considerarse como "aumentar x en 5"

Entonces, no es solo una taquigrafía, sino que describe la funcionalidad mucho más directamente. Cuando se leen fragmentos de código, es mucho más fácil de entender.


32
+1, estoy totalmente de acuerdo. Empecé a programar cuando era niño, cuando mi mente era fácilmente maleable y x = x + 5todavía me preocupaba. Cuando me metí en las matemáticas a una edad posterior, me molestó aún más. El uso x += 5es significativamente más descriptivo y tiene mucho más sentido como expresión.
Polinómico

44
También está el caso donde la variable tiene un nombre largo: reallyreallyreallylongvariablename = reallyreallyreallylongvariablename + 1... ¡oh, no! un error tipográfico

99
@ Matt Fenwick: No tiene que ser un nombre de variable largo; podría ser una expresión de algún tipo. Es probable que sean aún más difíciles de verificar, y el lector debe prestar mucha atención para asegurarse de que sean iguales.
David Thornley

32
X = X + 5 es una especie de truco cuando tu objetivo es incrementar x en 5.
JeffO

1
@Polynomial Creo que me encontré con el concepto de x = x + 5 cuando era niño también. 9 años si no recuerdo mal, y tenía mucho sentido para mí y todavía tiene mucho sentido para mí ahora y lo prefiero a x + = 5. El primero es mucho más detallado y puedo imaginar el concepto mucho más claramente. la idea de que estoy asignando x para que sea el valor anterior de x (sea lo que sea) y luego agrego 5. Supongo que diferentes cerebros funcionan de diferentes maneras.
Chris Harrison

48

Al menos en Python , x += yy x = x + ypuede hacer cosas completamente diferentes.

Por ejemplo, si lo hacemos

a = []
b = a

entonces a += [3]dará como resultado a == b == [3], mientras que a = a + [3]dará como resultado a == [3]y b == []. Es decir, +=modifica el objeto en el lugar (bueno, podría hacerlo, puede definir el __iadd__método para hacer casi todo lo que quiera), mientras =crea un nuevo objeto y le vincula la variable.

Esto es muy importante cuando se realiza un trabajo numérico con NumPy , ya que con frecuencia termina con múltiples referencias a diferentes partes de una matriz, y es importante asegurarse de no modificar accidentalmente parte de una matriz a la que hay otras referencias, o copiar matrices innecesariamente (lo que puede ser muy costoso).


44
1 para __iadd__: hay lenguas donde se puede crear una referencia a una estructura de datos inmutables mutable, donde el operator +=se define, por ejemplo, Scala: val sb = StringBuffer(); lb += "mutable structure"vs var s = ""; s += "mutable variable". el más modifica el contenido de la estructura de datos mientras que el último hace que la variable apunte a una nueva.
ovejas voladoras

43

Se llama modismo . Los modismos de programación son útiles porque son una forma consistente de escribir una construcción de programación particular.

Cada vez que alguien escribe x += y, sabe que xse está incrementando yy no por una operación más compleja (como práctica recomendada, por lo general, no mezclaría operaciones más complicadas y estas sintaxis shorthands). Esto tiene más sentido cuando se incrementa en 1.


13
x ++ y ++ x son ligeramente diferentes a x + = 1 y entre sí.
Gary Willoughby

1
@Gary: ++xy x+=1son equivalentes en C y Java (tal vez también C #), aunque no necesariamente en C ++ debido a la compleja semántica del operador allí. La clave es que ambos evalúan xuna vez, incrementan la variable en uno y tienen un resultado que es el contenido de la variable después de la evaluación.
Donal Fellows

3
@Donal Fellows: la precedencia es diferente, así ++x + 3que no es lo mismo que x += 1 + 3. Paréntesis x += 1y es idéntico. Como declaraciones en sí mismas, son idénticas.
David Thornley

1
@DavidThornley: Es difícil decir "son idénticos" cuando ambos tienen un comportamiento indefinido :)
Lightness Races en órbita

1
Ninguna de las expresiones ++x + 3, x += 1 + 3o (x += 1) + 3tienen un comportamiento indefinido (suponiendo que el valor resultante "encaja").
John Hascall

42

Para poner el punto de @ Pubby un poco más claro, considere someObj.foo.bar.func(x, y, z).baz += 5

Sin el +=operador, hay dos formas de hacerlo:

  1. someObj.foo.bar.func(x, y, z).baz = someObj.foo.bar.func(x, y, z).baz + 5. Esto no solo es terriblemente redundante y largo, sino que también es más lento. Por lo tanto, uno tendría que
  2. Utilizar una variable temporal: tmp := someObj.foo.bar.func(x, y, z); tmp.baz = tmp.bar + 5. Esto está bien, pero es mucho ruido por algo simple. En realidad, esto está muy cerca de lo que sucede en tiempo de ejecución, pero es tedioso escribir y solo usarlo +=cambiará el trabajo al compilador / intérprete.

La ventaja de +=otros operadores similares es innegable, mientras que acostumbrarse a ellos es solo cuestión de tiempo.


Una vez que tenga 3 niveles de profundidad en la cadena de objetos, puede dejar de preocuparse por las pequeñas optimizaciones como 1 y el código ruidoso como 2. En cambio, piense en su diseño una vez más.
Dorus

44
@Dorus: La expresión que elegí es solo un representante arbitrario de "expresión compleja". Siéntase libre de reemplazarlo por algo en su cabeza, que no le molestará;)
back2dos

99
+1: Esta es la razón principal de esta optimización: siempre es correcta, no importa cuán compleja sea la expresión del lado izquierdo.
S.Lott

99
Poner un error tipográfico allí es una buena ilustración de lo que puede suceder.
David Thornley

21

Es cierto que es más corto y fácil, y es cierto que probablemente se inspiró en el lenguaje ensamblador subyacente, pero la mejor práctica es que evita toda una clase de errores y facilita la revisión del código y la seguridad Que hace.

Con

RidiculouslyComplexName += 1;

Como solo hay un nombre de variable involucrado, está seguro de lo que hace la declaración.

Con RidiculouslyComplexName = RidiculosulyComplexName + 1;

Siempre hay dudas de que las dos partes son exactamente iguales. ¿Viste el error? Se pone aún peor cuando hay subíndices y calificadores presentes.


55
Empeora aún más en los idiomas donde las declaraciones de asignación pueden crear variables implícitamente, especialmente si los idiomas distinguen entre mayúsculas y minúsculas.
supercat

14

Si bien la notación + = es idiomática y más corta, estas no son las razones por las que es más fácil de leer. La parte más importante de la lectura del código es asignar la sintaxis al significado, por lo que cuanto más se aproxime la sintaxis a los procesos de pensamiento del programador, más legible será (esta es también la razón por la cual el código repetitivo es malo: no es parte del pensamiento proceso, pero aún es necesario para que el código funcione). En este caso, el pensamiento es "incrementar la variable x en 5", no "dejar que x sea el valor de x más 5".

Hay otros casos en los que una notación más corta es mala para la legibilidad, por ejemplo, cuando usa un operador ternario donde una ifdeclaración sería más apropiada.


11

Para tener una idea de por qué estos operadores están en los lenguajes 'C-style', hay un extracto de K&R 1st Edition (1978), hace 34 años:

Aparte de la concisión, los operadores de asignación tienen la ventaja de que se corresponden mejor con la forma en que las personas piensan. Decimos "agregue 2 a i" o "incremente i en 2", no "tome i, agregue 2, luego coloque el resultado nuevamente en i". Por lo tanto i += 2. Además, para una expresión complicada como

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

el operador de asignación hace que el código sea más fácil de entender, ya que el lector no tiene que verificar minuciosamente que dos expresiones largas son realmente iguales, o preguntarse por qué no lo son. Y un operador de asignación puede incluso ayudar al compilador a producir código más eficiente.

Creo que está claro en este pasaje que Brian Kernighan y Dennis Ritchie (K&R) creían que los operadores de asignación compuesta ayudaron con la legibilidad del código.

Ha pasado mucho tiempo desde que K&R escribió eso, y muchas de las 'mejores prácticas' sobre cómo las personas deberían escribir código han cambiado o evolucionado desde entonces. Pero esta pregunta de programmers.stackexchange es la primera vez que recuerdo que alguien expresó una queja sobre la legibilidad de las tareas compuestas, por lo que me pregunto si muchos programadores consideran que son un problema. Por otra parte, mientras escribo esto, la pregunta tiene 95 votos a favor, por lo que tal vez las personas los encuentren discordantes al leer el código.


9

Además de la legibilidad, en realidad hacen cosas diferentes: +=no tiene que evaluar su operando izquierdo dos veces.

Por ejemplo, expr = expr + 5se evaluaría exprdos veces (suponiendo que exprsea ​​impuro).


Para todos menos los compiladores más extraños, no importa. La mayoría de los compiladores son lo suficientemente inteligentes como para generar el mismo binario para expr = expr + 5yexpr += 5
vsz

77
@vsz No si exprtiene efectos secundarios.
Pubby

66
@vsz: en C, si exprtiene efectos secundarios, expr = expr + 5 debe invocar esos efectos secundarios dos veces.
Keith Thompson

@Pubby: si tiene efectos secundarios, no hay problema sobre "conveniencia" y "legibilidad", que era el punto de la pregunta original.
vsz

2
Mejor tenga cuidado con esas declaraciones de "efectos secundarios". Las lecturas y las escrituras volatileson efectos secundarios x=x+5y x+=5tienen los mismos efectos secundarios cuando xesvolatile
MSalters

6

Además de los méritos obvios que otras personas describieron muy bien, cuando tienes nombres muy largos es más compacto.

  MyVeryVeryVeryVeryVeryLongName += 1;

o

  MyVeryVeryVeryVeryVeryLongName =  MyVeryVeryVeryVeryVeryLongName + 1;

6

Es conciso.

Es mucho más corto de escribir. Implica menos operadores. Tiene menos superficie y menos posibilidades de confusión.

Utiliza un operador más específico.

Este es un ejemplo artificial, y no estoy seguro de si los compiladores reales implementan esto. x + = y en realidad usa un argumento y un operador y modifica x en su lugar. x = x + y podría tener una representación intermedia de x = z donde z es x + y. Este último utiliza dos operadores, suma y asignación, y una variable temporal. El operador único deja muy claro que el lado del valor no puede ser otra cosa que y y no necesita ser interpretado. Y, en teoría, podría haber alguna CPU elegante que tenga un operador más-igual que funcione más rápido que un operador más y un operador de asignación en serie.


2
Teóricamente esmeoreticamente. Las ADDinstrucciones en muchas CPU tienen variantes que operan directamente en registros o memoria utilizando otros registros, memoria o constantes como el segundo agregado. No todas las combinaciones están disponibles (por ejemplo, agregar memoria a la memoria), pero hay suficientes para ser útiles. En cualquier caso, cualquier compilador con un optimizador decente sabrá generar el mismo código para el x = x + yque lo haría x += y.
Blrfl

Por lo tanto, "artificial".
Mark Canlas

5

Es un buen modismo. Si es más rápido o no depende del idioma. En C, es más rápido porque se traduce en una instrucción para aumentar la variable en el lado derecho. Los lenguajes modernos, incluidos Python, Ruby, C, C ++ y Java, admiten la sintaxis op =. Es compacto y te acostumbras rápidamente. Como lo verá mucho en el código de otras personas (OPC), también puede acostumbrarse y usarlo. Esto es lo que sucede en otros idiomas.

En Python, escribir x += 5aún causa la creación del objeto entero 1 (aunque puede extraerse de un grupo) y la huérfana del objeto entero que contiene 5.

En Java, hace que se produzca un lanzamiento tácito. Intenta escribir

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.

Lenguajes imperativos modernos . En lenguajes (puros) funcionales x+=5tiene tan poco significado como x=x+5; Por ejemplo, en Haskell, este último no causa xque se incremente en 5, sino que incurre en un bucle de recursión infinito. ¿Quién querría una taquigrafía para eso?
Leftaroundabout

No es necesariamente más rápido en absoluto con los compiladores modernos: incluso en C.
HörmannHH

La velocidad de +=no depende tanto del idioma como del procesador . Por ejemplo, X86 es una arquitectura de dos direcciones, solo es compatible de +=forma nativa. Se a = b + c;debe compilar una declaración como a = b; a += c;porque simplemente no hay instrucciones que puedan colocar el resultado de la adición en otro lugar que no sea el lugar de uno de los sumandos. La arquitectura Power, por el contrario, es una arquitectura de tres direcciones que no tiene comandos especiales para +=. En esta arquitectura, las declaraciones a += b;y a = a + b;siempre se compilan con el mismo código.
cmaster

4

Los operadores como +=son muy útiles cuando está utilizando una variable como acumulador , es decir, un total acumulado :

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

Es mucho más fácil de leer que:

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

En el primer caso, conceptualmente, estás modificando el valor en x. En el segundo caso, está calculando un nuevo valor y asignándolo a xcada vez. Y aunque probablemente nunca escribiría código que sea tan simple, la idea sigue siendo la misma ... el foco está en lo que está haciendo a un valor existente en lugar de crear un valor nuevo.


Para mí es exactamente lo contrario: la primera variante es como suciedad en la pantalla y la segunda es limpia y legible.
Mikhail V el

1
Diferentes estilos para diferentes personas, @MikhailV, pero si eres nuevo en la programación, puedes cambiar tu punto de vista después de un tiempo.
Caleb

No soy tan nuevo en programación, creo que simplemente te has acostumbrado a la notación + = durante mucho tiempo y, por lo tanto, puedes leerla. Lo que inicialmente no lo convierte en una sintaxis atractiva, por lo que un operador + rodeado de espacios es objetivamente más limpio que + = y amigos que son apenas distinguibles. No es que su respuesta sea incorrecta, pero no se debe confiar demasiado en el hábito propio de hacer consumos lo "fácil que es leer".
Mikhail V

Mi sensación es que se trata más de la diferencia conceptual entre acumular y almacenar nuevos valores que de hacer que la expresión sea más compacta. La mayoría de los lenguajes de programación imperativos tienen operadores similares, por lo que parece que no soy el único que lo encuentra útil. Pero como dije, un derrame cerebral diferente para diferentes personas. No lo uses si no te gusta. Si desea continuar la discusión, quizás el Software Engineering Chat sería más apropiado.
Caleb

1

Considera esto

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

D0 realmente quieres escribir

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

1

Las otras respuestas se dirigen a los casos más comunes, pero hay otra razón: en algunos lenguajes de programación, se puede sobrecargar; por ejemplo, Scala .


Lección de Scala pequeña:

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

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

Si una clase solo define el +operador, de x+=yhecho es un atajo de x=x+y.

Si una clase se sobrecarga +=, sin embargo, no son:

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)”."

Además, los operadores +y +=son dos operadores separados (y no solo estos: + a, ++ a, a ++, a + ba + = b también son operadores diferentes); en idiomas donde la sobrecarga del operador está disponible, esto podría crear situaciones interesantes. Tal como se describió anteriormente: si sobrecarga al +operador para realizar la adición, tenga en cuenta que +=también tendrá que sobrecargarse.


"Si una clase solo define el operador +, x + = y es de hecho un atajo de x = x + y". ¿Pero seguramente también funciona al revés? En C ++, es muy común definir primero +=y luego definir a+bcomo "hacer una copia a, ponerla busando +=, devolver la copia de a".
Leftaroundabout

1

Dilo una vez y solo una vez: en x = x + 1, digo 'x' dos veces.

Pero nunca escriba 'a = b + = 1' o tendremos que matar 10 gatitos, 27 ratones, un perro y un hámster.


Nunca debe cambiar el valor de una variable, ya que facilita probar que el código es correcto; consulte la programación funcional. Sin embargo, si lo hace, entonces es mejor no decir las cosas solo una vez.


"nunca cambiar el valor de una variable" ¿ paradigma funcional ?
Peter Mortensen
Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.