Header Ads Widget

Ticker

6/recent/ticker-posts

La controversia detrás del operador de morsa en Python

Si no lo ha oído, Python 3.8 presenta un nuevo operador bastante controvertido llamado operador de morsa. En este artículo, compartiré algunas de mis primeras impresiones, así como las vistas desde todos los lados. No dudes en compartir algunos de tus pensamientos también en los comentarios.

Entendiendo al operador de morsa

Recientemente, estaba navegando por dev.to y encontré un artículo realmente genial de Jason McDonald que cubría una nueva característica en Python 3.8, el operador de morsa. Si usted no ha visto el operador, que tiene este aspecto: :=.

En este artículo, Jason afirma que el nuevo operador "le permite almacenar y probar un valor en la misma línea". En otras palabras, podemos comprimir esto:

1
2
3
4
nums = [87, 71, 58]
max_range = max(nums) - min(nums)
if max_range > 30:
  # do something

Dentro de esto:

1
2
3
nums = [87, 71, 58]
if (max_range := max(nums) - min(nums)) > 30:
  # do something

En este ejemplo, guardamos una línea porque movimos la asignación a la condición usando el operador de morsa. Específicamente, el operador de morsa realiza la asignación al mismo tiempo que devuelve el valor almacenado .

En este caso, max_rangealmacenaremos 29, para que podamos usarlo más tarde. Por ejemplo, podríamos tener algunas condiciones adicionales que aprovechan max_range:

1
2
3
4
5
nums = [87, 71, 58]
if (max_range := max(nums) - min(nums)) > 30:
  # do something
elif max_range < 20:
  # do something else

Por supuesto, si eres como yo, realmente no ves la ventaja. Por eso decidí investigar un poco.

Primeras impresiones

Cuando vi esta sintaxis por primera vez, pensé de inmediato "wow, esto no parece una sintaxis que encaje bien con el Zen de Python". De hecho, después de volver a visitar el Zen de Python , creo que hay varios puntos que esta nueva sintaxis pierde.

Lo bello es mejor que lo feo

Si bien la belleza está en el ojo del espectador, debes admitir que una declaración de asignación en medio de una expresión es algo fea. En el ejemplo anterior, que tenía que añadir un conjunto adicional de paréntesis para hacer la expresión más explícita izquierda. Desafortunadamente, los paréntesis adicionales reducen bastante la belleza.

Escaso es mejor que denso

Si la intención del operador de morsa es comprimir dos líneas en una, entonces eso contradice directamente "lo disperso es mejor que lo denso". En el ejemplo que compartí anteriormente, la primera condición es bastante densa; hay mucho que desempacar. ¿No tendría siempre más sentido colocar la tarea en una línea separada?

Si está buscando un buen ejemplo de una función que comprime código, eche un vistazo a la lista de comprensión . No solo reduce el anidamiento, sino que también simplifica mucho el proceso de generar una lista. Para ser honesto, no entiendo esa vibra con el operador de morsa. La asignación ya es bastante fácil de hacer.

Frente a la ambigüedad, rechace la tentación de adivinar.

En el ejemplo anterior, introduje paréntesis para hacer la condición más explícita. Si hubiera omitido los paréntesis, se vuelve un poco más difícil de analizar:

1
if range := max(nums) - min(nums) > 30:

En este caso, tenemos varios operadores en una sola línea, por lo que no está claro qué operadores tienen prioridad. Resulta que la aritmética es lo primero. Después de eso, el entero resultante se compara con 30. Finalmente, el resultado de esa comparación ( False) se almacena en rango y se devuelve. ¿Lo habrías adivinado al mirar esta línea?

Para empeorar las cosas, el operador de morsa hace que la asignación sea ambigua. En resumen, el operador de morsa parece una declaración, pero se comporta como una expresión con efectos secundarios. Si no está seguro de por qué eso podría ser un problema, consulte mi artículo sobre la diferencia entre declaraciones y expresiones .

Debe haber una, y preferiblemente sólo una, forma obvia de hacerlo.

Una de las cosas interesantes de este operador es que ahora presenta una forma completamente nueva de realizar asignaciones. En otras palabras, viola directamente la regla de "debe haber una única forma de hacerlo".

Dicho esto, estoy un poco en la cerca con este porque el nuevo operador es más explícito. En otras palabras, diferencia la intención detrás de :==Además, reduce los posibles errores relacionados con la confusión =y las ==condiciones.

Del mismo modo, por lo que puedo decir, no se puede usar el operador de morsa en los mismos lugares en los que usaría la asignación. De hecho, son operadores completamente diferentes. Desafortunadamente, nada te impide hacer algo como esto:

1
(x := 5)

No sé por qué harías esto, pero ahora es un código muy legal. Afortunadamente, PEP 572 lo prohíbe . Por supuesto, eso no impide que un código como este aparezca en la naturaleza. De hecho, la documentación enumera varias formas en que se puede abusar de la nueva sintaxis. ¡Eso no es una buena señal!

Si la implementación es difícil de explicar, es una mala idea

En este punto, tracé la línea con esta nueva característica. Mientras buscaba para leer las opiniones de otros sobre el tema, encontré la siguiente pepita de oro:

Esto se siente muy Perl-y en el ejemplo dado, ya que requiere que sepa lo que significa otro operador para leer el código que lo usa. Dado que se supone que Python es un "pseudocódigo ejecutable" (aproximadamente), este tipo de operador nuevo podría aumentar la cantidad de aprendizaje que un principiante tiene que hacer para leer el código de otros. Espero que esta decisión no allane el camino para más similares, porque haría que el código Python fuera mucho menos legible para alguien que aún no haya estudiado los nuevos operadores. snazz , 2019

Fue entonces cuando me di cuenta de por qué amo tanto a Python. Es tan fácil de leer. En este punto, realmente siento que la adición de este operador fue un error.

Contrapunto

Como con todo, odio formarme una opinión sin profundizar realmente en el tema, así que decidí escuchar a las personas que estaban entusiasmadas con esta función. Para mi sorpresa, encontré muchos ejemplos interesantes.

Las actualizaciones de variables de bucle son fáciles

De lejos, el caso más fuerte para el nuevo operador de morsa está en los bucles while. Específicamente, me gustó el ejemplo de Dustin Ingram que aprovechó el operador para eliminar líneas de código duplicadas. Por ejemplo, podemos convertir esto ( fuente ):

1
2
3
4
chunk = file.read(8192)
while chunk:
  process(chunk)
  chunk = file.read(8192)

Dentro de esto:

1
2
while chunk := file.read(8192):
  process(chunk)

Al introducir el operador de morsa, eliminamos una línea de código duplicada. Ahora, cada vez que el ciclo itera, lo actualizamos automáticamente chunksin tener que inicializarlo o actualizarlo explícitamente.

Ver este ejemplo me basta para ver el valor en el operador de morsa. De hecho, estoy tan impresionado con este ejemplo que me hizo preguntarme dónde más podría usarse para mejorar el código existente.

Dicho esto, investigué un poco, y algunas personas todavía sentían que este era un mal ejemplo. Después de todo, ¿no debería admitir la lectura de archivos un iterable? De esa manera, podríamos usar un bucle for, y esto no sería un problema en absoluto. En otras palabras, ¿no está el operador de morsa simplemente encubriendo un mal diseño de biblioteca? Quizás.

Comprensión de listas Obtenga una nueva herramienta

Como ávido entusiasta de la comprensión de listas, descubrí que el operador de morsa puede mejorar la eficiencia al permitirnos reutilizar los cálculos. Por ejemplo, podríamos tener una comprensión que se ve así:

1
[determinant(m) for m in matrices if determinant(m) > 0]

En este ejemplo, construimos una lista de determinantes a partir de una lista de matrices. Por supuesto, solo queremos incluir matrices cuyos determinantes sean mayores que cero.

Desafortunadamente, el cálculo del determinante puede resultar caro. Además, si tenemos muchas matrices, calcular el determinante dos veces por matriz podría resultar costoso. Afortunadamente, el operador de morsa está aquí para ayudar:

1
[d for m in matrices if (d := determinant(m)) > 0]

Ahora, solo calculamos el determinante una vez para cada matriz. ¿Qué tan hábil es eso?

Diverso

Más allá de los dos ejemplos anteriores, he visto algunos otros ejemplos que incluyen la coincidencia de patrones, pero realmente no lo aprecio. Honestamente, los otros ejemplos parecen una especie de nicho.

Por ejemplo, PEP 572 establece que el operador de morsa ayuda a ahorrar costosos cálculos. Por supuesto, el ejemplo que brindan es la construcción de una lista:

1
[y := f(x), y**2, y**3]

Aquí, tenemos una lista que se ve así:

1
[y, y**2, y**3]

En otras palabras, ¿qué nos impide declarar y en una línea separada?

1
2
y = f(x)
[y, y**2, y**3]

En el ejemplo de comprensión de la lista anterior, lo entiendo, pero aquí no. Quizás haya un ejemplo más detallado que explique por qué necesitaríamos incrustar una declaración de asignación en la creación de listas. Si tiene uno, no dude en compartirlo en los comentarios.

Evaluación

Ahora que he tenido la oportunidad de mirar al nuevo operador de morsas de manera más o menos objetiva, debo decir que creo que mis primeras impresiones aún se mantienen, pero estoy dispuesto a que me convenzan de lo contrario.

Después de ver algunos ejemplos sólidos, todavía era realmente escéptico, por lo que decidí echar un vistazo a la razón de ser del operador en PEP 572 . Si tienes la oportunidad, échale un vistazo a ese documento porque es enorme. Claramente, esta decisión fue bien pensada. Mi único temor es que los autores hayan sido persuadidos de incluir la característica mediante la falacia del costo hundido por cizallamiento , pero quién sabe.

Si lee PEP 572, verá 79 bloques de código en toda la página. Para mí, esa es solo una cantidad ridícula de ejemplos. Para empeorar las cosas, una gran parte de los ejemplos muestran casos extremos en los que el operador no trabajará o no sería ideal en lugar de proporcionar una ventaja. Por ejemplo, eche un vistazo a algunos de estos ejemplos:

1
x = y = z = 0  # Equivalent: (z := (y := (x := 0)))
1
2
x = 1, 2  # Sets x to (1, 2)
(x := 1, 2)  # Sets x to 1
1
total += tax  # Equivalent: (total := total + tax)

Dicho esto, los autores llegaron a proporcionar algunos ejemplos de su biblioteca estándar reelaborada . Por supuesto, estos ejemplos son mucho más extensos, así que no los compartiré aquí. Sin embargo, puedes echar un vistazo.

Personalmente, creo que los ejemplos vinculados anteriormente ilustran la ventaja del operador de morsa mucho mejor que algunos de los casos que compartí en la sección de contrapunto. Específicamente, cada vez que el operador de morsa elimina el código duplicado o anidado, estoy contento con eso. De lo contrario, parece tener muy pocos casos de uso obvios.

Mi preocupación es que agregar un nuevo operador agrega una complejidad innecesaria al lenguaje, y no estoy convencido de que los pros superen los contras. De todos modos, confío en la decisión del equipo que lo armó y estoy emocionado de ver cómo la comunidad lo usa.

Publicar un comentario

0 Comentarios