PEP 572 (expresiones de asignación en python 3.8)

Hola Habr Esta vez veremos PEP 572, que habla sobre expresiones de asignación. Si todavía es escéptico sobre el operador ": =" o no comprende completamente las reglas para su uso, entonces este artículo es para usted. Aquí encontrará muchos ejemplos y respuestas a la pregunta: "¿Por qué es así?" Este artículo resultó ser lo más completo posible, y si tienes poco tiempo, mira la sección que escribí. Al principio, las principales "tesis" se recopilan para un trabajo cómodo con expresiones de asignación. Perdóname por adelantado si encuentras errores (escríbemelos, lo arreglaré). Empecemos:

PEP 572 - Expresiones de asignación

Energía572
Título:Expresiones de asignación
AutoresChris Angelico <rosuav en gmail.com>, Tim Peters <tim.peters en gmail.com>, Guido van Rossum <guido en python.org>
Discusión:doc-sig en python.org
Estado:Aceptado
Un tipo:Estándar
Creado:28-feb-2018
Versión de Python:3.8
Publicar historia:28-feb-2018, 02-mar-2018, 23-mar-2018, 04-abr-2018, 17-abr-2018, 25-abr-2018, 09-jul-2018, 05-ago-2019
Permiso para adoptar la norma:mail.python.org/pipermail/python-dev/2018-July/154601.html (con VPN durante mucho tiempo, pero se carga)
Contenido


anotación


Esta convención hablará sobre la posibilidad de asignación dentro de expresiones, usando la nueva notación NAME: = expr.

Como parte de las innovaciones, se ha actualizado el procedimiento para calcular generadores de diccionario (comprensión del diccionario). Esto garantiza que la expresión clave se evalúe antes que la expresión de valor (esto le permite vincular la clave a una variable y luego reutilizar la variable creada en el cálculo del valor correspondiente a la clave).

Durante una discusión de este PEP, este operador se hizo conocido extraoficialmente como el operador de la morsa. El nombre formal de la construcción es "Expresión de asignación" (de acuerdo con el título PEP: Expresiones de asignación), pero puede denominarse "Expresiones con nombre". Por ejemplo, la implementación de referencia en CPython usa este mismo nombre.

Justificación


El nombramiento es una parte importante de la programación que le permite usar un nombre "descriptivo" en lugar de una expresión más larga, y también facilita la reutilización de valores. Actualmente, esto solo se puede hacer en forma de instrucciones, lo que hace que esta operación no esté disponible al generar listas (comprensión de listas), así como en otras expresiones.

Además, nombrar partes de una expresión grande puede ayudar con la depuración interactiva al proporcionar herramientas para mostrar mensajes y resultados intermedios. Sin la capacidad de capturar los resultados de expresiones anidadas, necesitará cambiar el código fuente, pero usando las expresiones de asignación solo necesita insertar algunos "marcadores" de la forma "nombre: = expresión". Esto elimina la refactorización innecesaria y, por lo tanto, reduce la probabilidad de cambios involuntarios de código durante la depuración (una causa común de Heisenbugs son los errores que cambian las propiedades del código durante la depuración y pueden aparecer inesperadamente en la producción]), y este código será más comprensible para otro al programador

La importancia del código real


Durante el desarrollo de esta PEP, muchas personas (tanto proponentes como críticos) se centraron demasiado en los ejemplos de juguetes, por un lado, y en los ejemplos demasiado complejos, por el otro.

El peligro de los ejemplos de juguetes es doble: a menudo son demasiado abstractos para hacer que alguien diga "oh, esto es irresistible", y también son fácilmente rechazados con las palabras "Nunca escribiría eso". El peligro de los ejemplos demasiado complejos es que proporcionan un entorno conveniente para los críticos que sugieren que se elimine esta funcionalidad ("Esto es demasiado confuso", dicen esas personas).

Sin embargo, hay un buen uso de tales ejemplos: ayudan a aclarar la semántica prevista. Por lo tanto, daremos algunos de ellos a continuación. Sin embargo, para ser convincentes , los ejemplos deben basarse encódigo real que fue escrito sin pensar en este PEP. Es decir, el código que forma parte de una aplicación realmente útil (no hay diferencia: ya sea grande o pequeña). Tim Peters nos ayudó mucho mirando sus repositorios personales y eligiendo ejemplos del código que escribió, que (en su opinión) sería más comprensible si se reescribieran (sin fanatismo) usando expresiones de asignación. Su conclusión es la siguiente: los cambios actuales traerían una mejora modesta pero obvia en algunos bits de su código.

Otro ejemplo de código real es la observación indirecta de cómo los programadores valoran la compacidad. Guido van Rossum revisó la base de código de Dropbox y encontró alguna evidencia de que los programadores prefieren escribir menos líneas de código que usar algunas expresiones pequeñas.

Caso en cuestión: Guido encontró varios puntos ilustrativos cuando un programador repite una subexpresión (lo que ralentiza el programa), pero guarda una línea adicional de código. Por ejemplo, en lugar de escribir:

match = re.match(data)
group = match.group(1) if match else None

Los programadores prefirieron esta opción:

group = re.match(data).group(1) if re.match(data) else None

Aquí hay otro ejemplo que muestra que los programadores a veces están dispuestos a hacer más trabajo para mantener el "nivel anterior" de sangría:

match1 = pattern1.match(data)
match2 = pattern2.match(data)
if match1:
    result = match1.group(1)
elif match2:
    result = match2.group(2)
else:
    result = None

Este código calcula el patrón2, incluso si el patrón1 ya coincide (en este caso, la segunda subcondición nunca se cumplirá). Por lo tanto, la siguiente solución es más efectiva, pero menos atractiva:

match1 = pattern1.match(data)
if match1:
    result = match1.group(1)
else:
    match2 = pattern2.match(data)
    if match2:
        result = match2.group(2)
    else:
        result = None

Sintaxis y Semántica


En la mayoría de los casos en los que Python usa expresiones arbitrarias, ahora puede usar expresiones de asignación. Tienen la forma NOMBRE: = expr, donde expr es cualquier expresión válida de Python, excepto la tupla sin paréntesis, y NAME es el identificador. El valor de tal expresión coincide con el original, pero un efecto adicional es la asignación de un valor al objeto de destino:

# Handle a matched regex
if (match := pattern.search(data)) is not None:
    # Do something with match

# A loop that can't be trivially rewritten using 2-arg iter()
while chunk := file.read(8192):
   process(chunk)

# Reuse a value that's expensive to compute
[y := f(x), y**2, y**3]

# Share a subexpression between a comprehension filter clause and its output
filtered_data = [y for x in data if (y := f(x)) is not None]

Casos excepcionales


Hay varios lugares donde las expresiones de asignación no están permitidas para evitar ambigüedades o confusión entre los usuarios:

  • Las expresiones de asignación no incluidas entre paréntesis están prohibidas en el nivel "superior":

    y := f(x)  # 
    (y := f(x))  # ,   

    Esta regla facilitará que el programador elija entre un operador de asignación y una expresión de asignación; no habrá una situación sintáctica en la que ambas opciones sean equivalentes.
  • . :

    y0 = y1 := f(x)  # 
    y0 = (y1 := f(x))  # ,   

    . :

    foo(x = y := f(x))  # 
    foo(x=(y := f(x)))  # ,     

    , .
  • . :

    def foo(answer = p := 42):  # 
        ...
    def foo(answer=(p := 42)):  # Valid, though not great style
        ...

    , (. , «» ).
  • , . :

    def foo(answer: p := 42 = 5):  # 
        ...
    def foo(answer: (p := 42) = 5):  # ,  
        ...

    : , "=" ":=" .
  • -. :

    (lambda: x := 1) # 
    lambda: (x := 1) # ,  
    (x := lambda: 1) # 
    lambda line: (m := re.match(pattern, line)) and m.group(1) # Valid

    - , ":=". . , , () , .
  • f- . :

    >>> f'{(x:=10)}'  # ,  
    '10'
    >>> x = 10
    >>> f'{x:=10}'    # ,  ,  '=10'
    '        10'

    , , f-, . f- ":" . , f- . , .


Una expresión de asignación no introduce un nuevo alcance. En la mayoría de los casos, el alcance en el que se creará la variable no requiere explicación: será actual. Si la variable usó las palabras clave no locales o globales antes, entonces la expresión de asignación tendrá esto en cuenta. Solo lambda (que es una definición anónima de una función) se considera un ámbito separado para estos fines.

Hay un caso especial: una expresión de asignación que se produce en los generadores de listas, conjuntos, diccionarios o en las "expresiones de los generadores" mismos (en adelante denominados colectivamente "generadores" (comprensiones)) vincula la variable al alcance que contiene el generador, observando el modificador globab o no global, si existe.

La justificación de este caso especial es doble. En primer lugar, nos permite capturar convenientemente el "miembro" en las expresiones any () y all (), por ejemplo:

if any((comment := line).startswith('#') for line in lines):
    print("First comment:", comment)
else:
    print("There are no comments")

if all((nonblank := line).strip() == '' for line in lines):
    print("All lines are blank")
else:
    print("First non-blank line:", nonblank)

En segundo lugar, proporciona una forma compacta de actualizar una variable desde un generador, por ejemplo:

# Compute partial sums in a list comprehension
total = 0
partial_sums = [total := total + v for v in values]
print("Total:", total)

Sin embargo, el nombre de la variable de la expresión de asignación no puede coincidir con el nombre ya utilizado en los generadores por el ciclo for para iterar. Los apellidos son locales para el generador en el que aparecen. Sería inconsistente si las expresiones de asignación también se refirieran al alcance dentro del generador.

Por ejemplo, [i: = i + 1 para i en el rango (5)] no es válido: el bucle for determina que i es local para el generador, pero la parte "i: = i + 1" insiste en que i es una variable desde el externo alcance Por la misma razón, los siguientes ejemplos no funcionarán:


[[(j := j) for i in range(5)] for j in range(5)] # 
[i := 0 for i, j in stuff]                       # 
[i+1 for i in (i := stuff)]                      # 

Aunque técnicamente es posible asignar una semántica consistente para tales casos, es difícil determinar si la forma en que entendemos esta semántica funcionará en su código real. Es por eso que la implementación de referencia garantiza que tales casos generen SyntaxError en lugar de ejecutarse con un comportamiento indefinido, dependiendo de la implementación particular del hardware. Esta restricción se aplica incluso si una expresión de asignación nunca se ejecuta:

[False and (i := 0) for i, j in stuff]     # 
[i for i, j in stuff if True or (j := 1)]  # 

# [.  . - ""   
# ,       
# ,    ,   ]

Para el cuerpo del generador (la parte anterior a la primera palabra clave "for") y la expresión de filtro (la parte posterior al "if" y antes de cualquier "for" anidado), esta restricción se aplica exclusivamente a los nombres de variables que se utilizan simultáneamente como variables iterativas. Como ya dijimos, las expresiones Lambda introducen un nuevo alcance explícito de la función y, por lo tanto, pueden usarse en expresiones de generadores sin restricciones adicionales. [aprox. nuevamente, excepto en tales casos: [i para i en el rango (2, (lambda: (s: = 2) ()))]]

Debido a las limitaciones de diseño en la implementación de referencia (el analizador de la tabla de símbolos no puede reconocer si los nombres de la parte izquierda del generador se usan en la parte restante donde se encuentra la expresión iterable), por lo tanto, las expresiones de asignación están completamente prohibidas como parte de iterable (en la parte después de cada "in" y antes de cualquier palabra clave posterior "si" o "para"). Es decir, todos estos casos son inaceptables:

[i+1 for i in (j := stuff)]                    # 
[i+1 for i in range(2) for j in (k := stuff)]  # 
[i+1 for i in [j for j in (k := stuff)]]       # 
[i+1 for i in (lambda: (j := stuff))()]        # 

Otra excepción ocurre cuando se usa una expresión de asignación en generadores que están dentro del alcance de una clase. Si, al usar las reglas anteriores, se produce la creación de una clase medida nuevamente en el alcance, entonces dicha expresión de asignación no es válida y dará como resultado un SyntaxError:

class Example:
    [(j := i) for i in range(5)]  # 

(El motivo de la última excepción es el alcance implícito de la función creada por el generador; actualmente no existe un mecanismo de tiempo de ejecución para que las funciones hagan referencia a una variable ubicada en el alcance de la clase, y no queremos agregar dicho mecanismo. Si este problema se resuelve alguna vez, entonces este caso especial (posiblemente) se eliminará de la especificación de las expresiones de asignación. Tenga en cuenta que este problema ocurrirá incluso si creó una variable en el alcance de la clase anteriormente e intente cambiarla con una expresión de asignación del generador).

Consulte el Apéndice B para ver ejemplos de Las expresiones de asignación que se encuentran en los generadores se convierten en código equivalente.

Prioridad relativa: =


El operador: = se agrupa más fuerte que la coma en todas las posiciones sintácticas donde sea posible, pero más débil que todos los demás operadores, incluyendo o, y, no, y expresiones condicionales (A si C más B). Como se deduce de la sección "Casos excepcionales" anterior, las expresiones de asignación nunca funcionan en el mismo "nivel" que la asignación clásica =. Si se requiere un orden diferente de operaciones, use paréntesis.

El operador: = se puede usar directamente cuando se llama al argumento posicional de una función. Sin embargo, esto no funcionará directamente en el argumento. Algunos ejemplos que aclaran lo que está técnicamente permitido y lo que no es posible:

x := 0 # 

(x := 0) #  

x = y := 0 # 

x = (y := 0) #  

len(lines := f.readlines()) # 

foo(x := 3, cat='vector') # 

foo(cat=category := 'vector') # 

foo(cat=(category := 'vector')) #  

La mayoría de los ejemplos "válidos" anteriores no se recomiendan para su uso en la práctica, ya que las personas que escanean rápidamente su código fuente pueden no entender correctamente su significado. Pero en casos simples esto está permitido:

# Valid
if any(len(longline := line) >= 100 for line in lines):
    print("Extremely long line:", longline)

Esta PEP recomienda que siempre ponga espacios alrededor: =, similar a la recomendación de PEP 8 para = para la asignación clásica. (La diferencia de la última recomendación es que prohíbe espacios alrededor de =, que se utiliza para pasar argumentos clave a la función).

Cambiar el orden de los cálculos.


Para tener una semántica bien definida, este acuerdo requiere que el procedimiento de evaluación esté claramente definido. Técnicamente, este no es un requisito nuevo. Python ya tiene una regla de que las subexpresiones generalmente se evalúan de izquierda a derecha. Sin embargo, las expresiones de asignación hacen que estos "efectos secundarios" sean más notorios, y proponemos un cambio en el orden de cálculo actual:

  • En los generadores de diccionario {X: Y para ...}, Y se evalúa actualmente antes que X. Sugerimos cambiar esto para que X se calcule antes que Y. (En un dict clásico como {X: Y}, así como en dict ((X, Y) para ...) esto ya se ha implementado. Por lo tanto, los generadores de diccionario deben cumplir con este mecanismo)


Diferencias entre expresiones de asignación e instrucciones de asignación.


Lo más importante es que ": =" es una expresión , lo que significa que se puede usar en casos donde las instrucciones no son válidas, incluidas las funciones y generadores lambda. Por el contrario, las expresiones de asignación no admiten la funcionalidad extendida que se puede usar en las instrucciones de asignación:

  • La asignación en cascada no se admite directamente

    x = y = z = 0  # Equivalent: (z := (y := (x := 0)))
  • No se admiten "objetivos" separados, excepto el nombre de variable simple NAME:

    # No equivalent
    a[i] = x
    self.rest = []
  • La funcionalidad y las comas de prioridad "alrededor" son diferentes:

    x = 1, 2  # Sets x to (1, 2)
    (x := 1, 2)  # Sets x to 1
  • Los valores de desempaque y desempaque no tienen equivalencia "pura" o no son compatibles

    # Equivalent needs extra parentheses
    loc = x, y  # Use (loc := (x, y))
    info = name, phone, *rest  # Use (info := (name, phone, *rest))
    
    # No equivalent
    px, py, pz = position
    name, phone, email, *other_info = contact
  • Las anotaciones de tipo en línea no son compatibles:

    # Closest equivalent is "p: Optional[int]" as a separate declaration
    p: Optional[int] = None
  • No hay una forma abreviada de operaciones:

    total += tax  # Equivalent: (total := total + tax)

La especificación cambia durante la implementación


Los siguientes cambios se realizaron en base a nuestra experiencia y análisis adicional después de la primera redacción de este PEP y antes del lanzamiento de Python 3.8:

  • Para garantizar la coherencia con otras excepciones similares, y no introducir un nuevo nombre que puede no ser conveniente para los usuarios finales, la subclase propuesta originalmente de TargetScopeError para SyntaxError se ha eliminado y reducido al SyntaxError habitual. [3]
  • Debido a las limitaciones en el análisis de la tabla de caracteres CPython, la implementación de referencia de la expresión de asignación genera un SyntaxError para todos los usos dentro de los iteradores. Anteriormente, esta excepción ocurría solo si el nombre de la variable que se estaba creando coincidía con el que ya se usaba en la expresión iterativa. Esto puede revisarse si hay ejemplos suficientemente convincentes, pero la complejidad adicional parece inapropiada para casos de uso puramente "hipotéticos".

Ejemplos


Ejemplos de la biblioteca estándar de Python


site.py


env_base se usa solo en una condición, por lo que la asignación se puede colocar en if, como el "encabezado" de un bloque lógico.

  • Código actual:
    env_base = os.environ.get("PYTHONUSERBASE", None)
    if env_base:
        return env_base
  • Código mejorado:
    if env_base := os.environ.get("PYTHONUSERBASE", None):
        return env_base

_pydecimal.py


Puede evitar ifs anidados, eliminando así un nivel de sangría.

  • Código actual:
    if self._is_special:
        ans = self._check_nans(context=context)
        if ans:
            return ans
  • Código mejorado:
    if self._is_special and (ans := self._check_nans(context=context)):
        return ans

copy.py


El código parece más clásico y también evita el anidamiento múltiple de sentencias condicionales. (Consulte el Apéndice A para obtener más información sobre el origen de este ejemplo).

  • Código actual:
    reductor = dispatch_table.get(cls)
    if reductor:
        rv = reductor(x)
    else:
        reductor = getattr(x, "__reduce_ex__", None)
        if reductor:
            rv = reductor(4)
        else:
            reductor = getattr(x, "__reduce__", None)
            if reductor:
                rv = reductor()
            else:
                raise Error(
                    "un(deep)copyable object of type %s" % cls)
  • Código mejorado:

    if reductor := dispatch_table.get(cls):
        rv = reductor(x)
    elif reductor := getattr(x, "__reduce_ex__", None):
        rv = reductor(4)
    elif reductor := getattr(x, "__reduce__", None):
        rv = reductor()
    else:
        raise Error("un(deep)copyable object of type %s" % cls)

datetime.py


tz se usa solo para s + = tz. Moverlo hacia adentro si ayuda a mostrar su área lógica de uso.

  • Código actual:

    s = _format_time(self._hour, self._minute,
                     self._second, self._microsecond,
                     timespec)
    tz = self._tzstr()
    if tz:
        s += tz
    return s
  • Código mejorado:

    s = _format_time(self._hour, self._minute,
                     self._second, self._microsecond,
                     timespec)
    if tz := self._tzstr():
        s += tz
    return s

sysconfig.py


Llamar a fp.readline () como una "condición" en el ciclo while (así como llamar al método .match ()) en la condición if hace que el código sea más compacto sin complicar su comprensión.

  • Código actual:

    while True:
        line = fp.readline()
        if not line:
            break
        m = define_rx.match(line)
        if m:
            n, v = m.group(1, 2)
            try:
                v = int(v)
            except ValueError:
                pass
            vars[n] = v
        else:
            m = undef_rx.match(line)
            if m:
                vars[m.group(1)] = 0
  • Código mejorado:

    while line := fp.readline():
        if m := define_rx.match(line):
            n, v = m.group(1, 2)
            try:
                v = int(v)
            except ValueError:
                pass
            vars[n] = v
        elif m := undef_rx.match(line):
            vars[m.group(1)] = 0

Simplificar generadores de listas


Ahora el generador de listas se puede filtrar efectivamente "capturando" la condición:

results = [(x, y, x/y) for x in input_data if (y := f(x)) > 0]

Después de eso, la variable se puede reutilizar en otra expresión:

stuff = [[y := f(x), x/y] for x in range(5)]

Tenga en cuenta nuevamente que en ambos casos la variable y está en el mismo alcance que el resultado de las variables y demás.

Capturar valores en condiciones


Las expresiones de asignación se pueden usar efectivamente en las condiciones de una declaración if o while:

# Loop-and-a-half
while (command := input("> ")) != "quit":
    print("You entered:", command)

# Capturing regular expression match objects
# See, for instance, Lib/pydoc.py, which uses a multiline spelling
# of this effect
if match := re.search(pat, text):
    print("Found:", match.group(0))
# The same syntax chains nicely into 'elif' statements, unlike the
# equivalent using assignment statements.
elif match := re.search(otherpat, text):
    print("Alternate found:", match.group(0))
elif match := re.search(third, text):
    print("Fallback found:", match.group(0))

# Reading socket data until an empty string is returned
while data := sock.recv(8192):
    print("Received data:", data)

En particular, este enfoque puede eliminar la necesidad de crear un bucle infinito, asignación y verificación de condición. También le permite dibujar un paralelo suave entre un ciclo que usa una llamada de función como su condición, así como un ciclo que no solo verifica la condición, sino que también usa el valor real devuelto por la función en el futuro.

Tenedor


Un ejemplo del mundo de bajo nivel de UNIX: [aprox. Fork () es una llamada al sistema en sistemas operativos tipo Unix que crea un nuevo subproceso en relación con el padre.]

if pid := os.fork():
    # Parent code
else:
    # Child code

Alternativas rechazadas


En general, sugerencias similares son bastante comunes en la comunidad python. A continuación hay una serie de sintaxis alternativas para expresiones de asignación que son demasiado específicas para comprender y que han sido rechazadas a favor de lo anterior.

Cambiar el alcance de los generadores


En una versión anterior de este PEP, se propuso realizar cambios sutiles en las reglas de alcance de los generadores para hacerlos más adecuados para su uso en el alcance de las clases. Sin embargo, estas propuestas conducirían a una incompatibilidad hacia atrás y, por lo tanto, fueron rechazadas. Por lo tanto, este PEP pudo enfocarse completamente solo en expresiones de asignación.

Ortografía alternativa


En general, las expresiones de asignación propuestas tienen la misma semántica, pero están escritas de manera diferente.

  1. EXPR como NOMBRE:

    stuff = [[f(x) as y, x/y] for x in range(5)]

    EXPR as NAME import, except with, (, ).

    ( , «with EXPR as VAR» EXPR VAR, EXPR.__enter__() VAR.)

    , ":=" :
    • , if f(x) as y , ​​ if f x blah-blah, if f(x) and y.
    • , as , , :
      • import foo as bar
      • except Exc as var
      • with ctxmgr() as var

      , as if while , as « » .
    • «»
      • NAME = EXPR
      • if NAME := EXPR

      .
  2. EXPR -> NAME

    stuff = [[f(x) -> y, x/y] for x in range(5)]

    , R Haskell, . ( , - y < — f (x) Python, - .) «as» , import, except with, . Python ( ), ":=" ( Algol-58) .
  3. «»

    stuff = [[(f(x) as .y), x/.y] for x in range(5)] # with "as"
    stuff = [[(.y := f(x)), x/.y] for x in range(5)] # with ":="

    . Python, , .
  4. where: :

    value = x**2 + 2*x where:
        x = spam(1, 4, 7, q)

    ( , «»). , «» ( with:). . PEP 3150, ( given: ).
  5. TARGET from EXPR:

    stuff = [[y from f(x), x/y] for x in range(5)]

    Es menos probable que esta sintaxis entre en conflicto con otros que como (a menos que cuente las construcciones elevar Exc de Exc), pero de lo contrario sea comparable a ellos. En lugar de un paralelo con expr como objetivo: (que puede ser útil, pero también puede ser confuso), esta opción no tiene paralelos con nada en absoluto, pero sorprendentemente se recuerda mejor.


Casos especiales en declaraciones condicionales


Uno de los casos de uso más comunes para las expresiones de asignación son las declaraciones if y while. En lugar de una solución más general, el uso de as mejora la sintaxis de estas dos declaraciones al agregar un medio de capturar el valor a comparar:

if re.search(pat, text) as match:
    print("Found:", match.group(0))

Esto funciona bien, pero SOLO cuando la condición deseada se basa en la "corrección" del valor de retorno. Por lo tanto, este método es efectivo para casos específicos (verificar expresiones regulares, leer sockets, devolver una cadena vacía cuando finaliza la ejecución), y es completamente inútil en casos más complejos (por ejemplo, cuando la condición es f (x) <0, y desea guardar el valor de f (x)). Además, esto no tiene sentido en los generadores de listas.

Ventajas : sin ambigüedades sintácticas. Desventajas : incluso si lo usa solo en declaraciones if / while, solo funciona bien en algunos casos.

Casos especiales en generadores


Otro caso de uso común para las expresiones de asignación son los generadores (list / set / dict y genexps). Como se indicó anteriormente, se hicieron sugerencias para soluciones específicas.

  1. donde, dejar o dado:

    stuff = [(y, x/y) where y = f(x) for x in range(5)]
    stuff = [(y, x/y) let y = f(x) for x in range(5)]
    stuff = [(y, x/y) given y = f(x) for x in range(5)]

    Este método da como resultado una subexpresión entre el bucle for y la expresión principal. También introduce una palabra clave de idioma adicional, que puede crear conflictos. De las tres opciones, dónde es la más limpia y legible, pero aún existen conflictos potenciales (por ejemplo, SQLAlchemy y numpy tienen sus métodos where, así como tkinter.dnd.Icon en la biblioteca estándar).
  2. con NAME = EXPR:

    stuff = [(y, x/y) with y = f(x) for x in range(5)]

    , , with. . , «» for. C, , . : « «with NAME = EXPR:» , ?»
  3. with EXPR as NAME:

    stuff = [(y, x/y) with f(x) as y for x in range(5)]

    , as, . , for. with

Independientemente del método elegido, se introducirá una gran diferencia semántica entre los generadores y sus versiones implementadas a través de un bucle for. Sería imposible envolver el ciclo en un generador sin procesar la etapa de creación de las variables. La única palabra clave que podría reorientarse para esta tarea es la palabra con . Pero esto le dará una semántica diferente en diferentes partes del código, lo que significa que necesita crear una nueva palabra clave, pero esto implica muchos costos.

Baja prioridad del operador


El operador = = tiene dos prioridades lógicas. O debería tener la menor prioridad posible (a la par con el operador de asignación). O debería tener mayor prioridad que los operadores de comparación. Colocar su prioridad entre los operadores de comparación y las operaciones aritméticas (para ser precisos: un poco más bajo que OR a nivel de bits) le permitirá prescindir de paréntesis en la mayoría de los casos cuando y mientras lo usa, ya que es más probable que desee mantener el valor de algo antes cómo se realizará la comparación en él:

pos = -1
while pos := buffer.find(search_term, pos + 1) >= 0:
    ...

Tan pronto como find () devuelve -1, el ciclo termina. Si: = une los operandos tan libremente como =, entonces el resultado de find () primero será "capturado" en el operador de comparación y generalmente devolverá Verdadero o Falso, lo que es menos útil.

Aunque este comportamiento sería conveniente en la práctica en muchas situaciones, sería más difícil de explicar. Y entonces podemos decir que "el operador: = se comporta igual que el operador de asignación habitual". Es decir, la prioridad para: = se eligió lo más cerca posible del operador = (excepto que: = tiene una prioridad más alta que la coma).

Da comas a la derecha


Algunos críticos argumentan que las expresiones de asignación deben reconocer tuplas sin la adición de corchetes para que las dos entradas sean equivalentes:

(point := (x, y))
(point := x, y)

(En la versión actual del estándar, el último registro será equivalente a la expresión ((punto: = x), y).)

Pero es lógico que en esta situación, cuando se usa la expresión de asignación en la llamada a la función, también tenga una prioridad menor que la coma, por lo que obtuvimos sería la siguiente equivalencia confusa:

foo (x: = 1, y)
foo (x: = (1, y))

Y obtenemos la única salida menos confusa: hacer que el operador: = sea una prioridad más baja que la coma.

Siempre requiere soportes


Siempre se ha propuesto poner entre corchetes las expresiones de asignación. Esto nos ahorraría muchas ambigüedades. De hecho, a menudo se necesitarán paréntesis para extraer el valor deseado. Pero en los siguientes casos, la presencia de paréntesis nos pareció claramente superflua:

# Top level in if
if match := pattern.match(line):
    return match.group(1)

# Short call
len(lines := f.readlines())

Objeciones frecuentes


¿Por qué no simplemente convertir las declaraciones de asignación en expresiones?


C y lenguajes similares definen el operador = como una expresión, no una instrucción, como lo hace Python. Esto permite la asignación en muchas situaciones, incluidos los lugares donde se comparan las variables. Las similitudes sintácticas entre if (x == y) y if (x = y) contradicen su semántica muy diferente. Por lo tanto, esta PEP presenta al operador: = para aclarar sus diferencias.

¿Por qué molestarse con las expresiones de asignación si existen instrucciones de asignación?


Estas dos formas tienen diferentes flexibilidades. El operador: = puede usarse dentro de una expresión más grande, y en el operador = puede ser usado por la "familia de mini-operadores" del tipo "+ =". También = le permite asignar valores por atributos e índices.

¿Por qué no usar el alcance local y prevenir la contaminación del espacio de nombres?


Las versiones anteriores de este estándar incluían un alcance local real (limitado a una declaración) para expresiones de asignación, evitando la fuga de nombres y la contaminación del espacio de nombres. A pesar del hecho de que en algunas situaciones esto dio una cierta ventaja, en muchas otras complica la tarea, y los beneficios no están justificados por las ventajas del enfoque existente. Esto se hace en interés de la simplicidad del lenguaje. ¿Ya no necesitas esta variable? Hay una solución: elimine la variable usando la palabra clave del o agregue un guión bajo a su nombre.

(El autor desea agradecer a Guido van Rossum y Christophe Groth por sus sugerencias para avanzar el estándar PEP en esta dirección. [2])

Recomendaciones de estilo


Dado que las expresiones de asignación a veces se pueden usar a la par con un operador de asignación, surge la pregunta, ¿qué se prefiere? De acuerdo con otras convenciones de estilo (como PEP 8), hay dos recomendaciones:

  1. Si puede usar ambas opciones de asignación, entonces dé preferencia a los operadores. Expresan más claramente sus intenciones.
  2. Si el uso de expresiones de asignación genera ambigüedad en el orden de ejecución, reescriba el código con el operador clásico.

Gracias


Los autores de esta norma desean agradecer a Nick Coghlan y Steven D'Aprano por sus importantes contribuciones a este PEP, así como a los miembros de Python Core Mentorship por su ayuda para implementar esto.

Apéndice A: Conclusiones de Tim Peters


Aquí hay un breve ensayo que Tim Peters escribió sobre este tema.

No me gusta el código "confundido", y tampoco me gusta poner lógica conceptualmente no relacionada en una línea. Entonces, por ejemplo, en lugar de:

i = j = count = nerrors = 0

Prefiero escribir:

i = j = 0
count = 0
nerrors = 0

Por lo tanto, creo que encontraré varios lugares donde quiero usar expresiones de asignación. Ni siquiera quiero hablar sobre su uso en expresiones que ya están extendidas a la mitad de la pantalla. En otros casos, comportamientos como:

mylast = mylast[1]
yield mylast[0]

Significativamente mejor que esto:

yield (mylast := mylast[1])[0]

Estos dos códigos tienen conceptos completamente diferentes y mezclarlos sería una locura. En otros casos, la combinación de expresiones lógicas complica la comprensión del código. Por ejemplo, reescribiendo:

while True:
    old = total
    total += term
    if old == total:
        return total
    term *= mx2 / (i*(i+1))
    i += 2

En una forma más corta, hemos perdido la "lógica". Debe comprender cómo funciona este código. Mi cerebro no quiere hacer esto:

while total != (total := total + term):
    term *= mx2 / (i*(i+1))
    i += 2
return total

Pero tales casos son raros. La tarea de preservar el resultado es muy común, y "disperso es mejor que denso" no significa que "casi vacío es mejor que disperso" [aprox. una referencia a Zen Python]. Por ejemplo, tengo muchas funciones que devuelven Ninguna o 0 para decir "No tengo nada útil, pero como esto sucede a menudo, no quiero molestarlo con excepciones". De hecho, este mecanismo también se usa en expresiones regulares que devuelven None cuando no hay coincidencias. Por lo tanto, en este ejemplo, mucho código:

result = solution(xs, n)
if result:
    # use result

La siguiente opción me parece más comprensible y, por supuesto, más legible:

if result := solution(xs, n):
    # use result

Al principio no le di mucha importancia a esto, pero una construcción tan corta apareció tan a menudo que pronto comenzó a molestarme que no podía usarla. ¡Me sorprendió! [aprox. aparentemente esto fue escrito antes de que Python 3.8 fuera lanzado oficialmente.]

Hay otros casos donde las expresiones de asignación realmente "disparan". En lugar de hurgar en mi código nuevamente, Kirill Balunov dio un buen ejemplo de la función copy () de la biblioteca estándar copy.py:

reductor = dispatch_table.get(cls)
if reductor:
    rv = reductor(x)
else:
    reductor = getattr(x, "__reduce_ex__", None)
    if reductor:
        rv = reductor(4)
    else:
        reductor = getattr(x, "__reduce__", None)
        if reductor:
            rv = reductor()
        else:
            raise Error("un(shallow)copyable object of type %s" % cls)

La sangría cada vez mayor es engañosa: después de todo, la lógica es plana: la primera prueba exitosa "gana":

if reductor := dispatch_table.get(cls):
    rv = reductor(x)
elif reductor := getattr(x, "__reduce_ex__", None):
    rv = reductor(4)
elif reductor := getattr(x, "__reduce__", None):
    rv = reductor()
else:
    raise Error("un(shallow)copyable object of type %s" % cls)

El uso simple de expresiones de asignación permite que la estructura visual del código enfatice el "plano" de la lógica. Pero la sangría cada vez mayor lo hace implícito.

Aquí hay otro pequeño ejemplo de mi código, que me hizo muy feliz porque me permitió poner lógica internamente relacionada en una línea y eliminar el molesto nivel de sangría "artificial". Esto es exactamente lo que quiero de la declaración if y facilita la lectura. El siguiente código:

diff = x - x_base
if diff:
    g = gcd(diff, n)
    if g > 1:
        return g

Convertido en:

if (diff := x - x_base) and (g := gcd(diff, n)) > 1:
    return g

Entonces, en la mayoría de las líneas donde ocurre la asignación variable, no usaría expresiones de asignación. Pero este diseño es tan frecuente que todavía hay muchos lugares donde aprovecharía esta oportunidad. En los casos más recientes, gané un poco, ya que a menudo aparecían. En la subparte restante, esto condujo a mejoras medianas o grandes. Por lo tanto, usaría expresiones de asignación con mucha más frecuencia que un triple si, pero con mucha menos frecuencia que la asignación aumentada [aprox. opciones cortas: * =, / =, + =, etc.].

Ejemplo numérico


Tengo otro ejemplo que me llamó la atención antes.

Si todas las variables son números enteros positivos, y la variable a es mayor que la enésima raíz de x, entonces este algoritmo devuelve el redondeo "inferior" de la enésima raíz de x (y aproximadamente duplica el número de bits exactos por iteración):

while a > (d := x // a**(n-1)):
    a = ((n-1)*a + d) // n
return a

No está claro por qué, pero tal variante del algoritmo es menos obvia que un bucle infinito con un salto de rama condicional (bucle y medio). También es difícil demostrar la corrección de esta implementación sin depender de una declaración matemática ("media aritmética - desigualdad de la media geométrica") y sin saber algunas cosas no triviales sobre cómo las funciones de redondeo anidado se comportan hacia abajo. Pero aquí el problema ya está en las matemáticas, y no en la programación.

Y si sabe todo esto, entonces la opción que usa expresiones de asignación se lee muy fácilmente, como una oración simple: "Verifique la" suposición "actual y si es demasiado grande, reduzca" y la condición le permite guardar inmediatamente el valor intermedio de la condición del bucle. En mi opinión, la forma clásica es más difícil de entender:

while True:
    d = x // a**(n-1)
    if a <= d:
        break
    a = ((n-1)*a + d) // n
return a

Apéndice B: Un intérprete de código aproximado para generadores


Este apéndice intenta aclarar (aunque no especificar) las reglas mediante las cuales se debe crear una variable en expresiones generadoras. Para una serie de ejemplos ilustrativos, mostramos el código fuente donde el generador es reemplazado por una función equivalente en combinación con algunos "andamios".

Como [x para ...] es equivalente a list (x para ...), los ejemplos no pierden su generalidad. Y dado que estos ejemplos están destinados solo a aclarar las reglas generales, no pretenden ser realistas.

Nota: los generadores ahora se implementan mediante la creación de funciones de generador anidadas (similares a las que se proporcionan en este apéndice). Los ejemplos muestran la nueva parte, que agrega la funcionalidad adecuada para trabajar con el alcance de las expresiones de asignación (como el alcance si la asignación se realizó en un bloque que contiene el generador más externo). Para simplificar la "inferencia de tipos", estos ejemplos ilustrativos no tienen en cuenta que las expresiones de asignación son opcionales (pero tienen en cuenta el alcance de la variable creada dentro del generador).

Primero recordemos qué código se genera "bajo el capó" para generadores sin expresiones de asignación:

  • Código fuente (EXPR usa con mayor frecuencia la variable VAR):

    def f():
        a = [EXPR for VAR in ITERABLE]
  • El código convertido (no nos preocupemos por los conflictos de nombres):

    def f():
        def genexpr(iterator):
            for VAR in iterator:
                yield EXPR
        a = list(genexpr(iter(ITERABLE)))


Agreguemos una expresión de asignación simple.

  • Fuente:

    def f():
        a = [TARGET := EXPR for VAR in ITERABLE]
    
  • Código convertido:

    def f():
        if False:
            TARGET = None  # Dead code to ensure TARGET is a local variable
        def genexpr(iterator):
            nonlocal TARGET
            for VAR in iterator:
                TARGET = EXPR
                yield TARGET
        a = list(genexpr(iter(ITERABLE)))

Ahora agreguemos la declaración TARGET global a la declaración de la función f ().

  • Fuente:

    def f():
        global TARGET
        a = [TARGET := EXPR for VAR in ITERABLE]
    
  • Código convertido:

    def f():
        global TARGET
        def genexpr(iterator):
            global TARGET
            for VAR in iterator:
                TARGET = EXPR
                yield TARGET
        a = list(genexpr(iter(ITERABLE)))

O viceversa, agreguemos TARGET no local a la declaración de la función f ().

  • Fuente:

    def g():
        TARGET = ...
        def f():
            nonlocal TARGET
            a = [TARGET := EXPR for VAR in ITERABLE]
    
  • Código convertido:

    def g():
        TARGET = ...
        def f():
            nonlocal TARGET
            def genexpr(iterator):
                nonlocal TARGET
                for VAR in iterator:
                    TARGET = EXPR
                    yield TARGET
            a = list(genexpr(iter(ITERABLE)))

Finalmente, pongamos dos generadores.

  • Fuente:

    def f():
        a = [[TARGET := i for i in range(3)] for j in range(2)]
        # I.e., a = [[0, 1, 2], [0, 1, 2]]
        print(TARGET)  # prints 2
    
  • Código convertido:

    def f():
        if False:
            TARGET = None
        def outer_genexpr(outer_iterator):
            nonlocal TARGET
            def inner_generator(inner_iterator):
                nonlocal TARGET
                for i in inner_iterator:
                    TARGET = i
                    yield i
            for j in outer_iterator:
                yield list(inner_generator(range(3)))
        a = list(outer_genexpr(range(2)))
        print(TARGET)

Apéndice C: Sin cambios en la semántica del alcance


Tenga en cuenta que en Python la semántica del alcance no ha cambiado. El alcance de las funciones locales aún se determina en el momento de la compilación y tiene una extensión de tiempo indefinida en el tiempo de ejecución (cierre). Ejemplo:

a = 42
def f():
    # `a` is local to `f`, but remains unbound
    # until the caller executes this genexp:
    yield ((a := i) for i in range(3))
    yield lambda: a + 100
    print("done")
    try:
        print(f"`a` is bound to {a}")
        assert False
    except UnboundLocalError:
        print("`a` is not yet bound")

Entonces:

>>> results = list(f()) # [genexp, lambda]
done
`a` is not yet bound
# The execution frame for f no longer exists in CPython,
# but f's locals live so long as they can still be referenced.
>>> list(map(type, results))
[<class 'generator'>, <class 'function'>]
>>> list(results[0])
[0, 1, 2]
>>> results[1]()
102
>>> a
42

Referencias


  1. Prueba de implementación del concepto
  2. Discusión de la semántica de las expresiones de asignación (VPN está apretada pero cargada)
  3. Discusión de TargetScopeError en PEP 572 (cargado de manera similar a la anterior)

Derechos de autor


Este documento se ha hecho público.

Fuente: github.com/python/peps/blob/master/pep-0572.rst

Mi parte


Para comenzar, resumamos:
  • Para que las personas no intenten eliminar la dualidad semántica, en muchos lugares "clásicos" en los que podría usar "=" y ": =" existen restricciones, por lo tanto, el operador :: = debe estar entre corchetes. Estos casos deberán revisarse en la sección que describe el uso básico.
  • La prioridad de las expresiones de asignación es ligeramente mayor que la de una coma. Debido a esto, las tuplas no se forman durante la asignación. También hace posible usar el operador: = al pasar argumentos a una función.
  • , , , . . lambda , «» .
  • : ,
  • , .
  • / .
  • , .

Al final, quiero decir que me gustó el nuevo operador. Le permite escribir código más plano en condiciones, listas de "filtro" y también (finalmente) eliminar la "línea solitaria" misma antes de if. Si las personas usan expresiones de asignación para su propósito previsto, esta será una herramienta muy conveniente que aumentará la legibilidad y la belleza del código (aunque, esto se puede decir sobre cualquier lenguaje funcional ...)

All Articles