python - tutorial - ¿Por qué se permiten expresiones de destino arbitrarias en for-loops?
the django project (4)
¿Hay alguna situación en la que esto sea realmente útil?
En efecto.
¿Alguna vez quisiste deshacerte de
itertools.combinations
?
def combinations (pool, repeat):
def combinations_recurse (acc, pool, index = 0):
if index < len(acc):
for acc[index] in pool:
yield from combinations_recurse(acc, pool, index + 1)
else:
yield acc
yield from combinations_recurse([pool[0]] * repeat, pool)
for comb in combinations([0, 1], 3):
print(comb)
Accidentalmente escribí un código como este:
foo = [42]
k = {''c'': ''d''}
for k[''z''] in foo: # Huh??
print k
Pero para mi sorpresa, esto no fue un error de sintaxis.
En su lugar, imprime
{''c'': ''d'', ''z'': 42}
.
Supongo que el código se traduce literalmente a algo como:
i = iter(foo)
while True:
try:
k[''z''] = i.next() # literally translated to assignment; modifies k!
print k
except StopIteration:
break
Pero ... ¿por qué el lenguaje lo permite? Esperaría que solo se permitieran identificadores individuales y tuplas de identificadores en la expresión de destino de for-stmt . ¿Hay alguna situación en la que esto sea realmente útil, no solo un problema extraño?
El bucle
for
sigue las reglas estándar de asignación, por lo que lo que funciona en el LHS de una asignación vainilla debería funcionar con
for
:
Cada elemento a su vez se asigna a la lista de objetivos utilizando las reglas estándar para las asignaciones
La construcción
for
simplemente invoca el mecanismo subyacente para asignar al objetivo que en el caso de su código de muestra es
STORE_SUBSCR
:
>>> foo = [42]
>>> k = {''c'': ''d''}
>>> dis.dis(''for k["e"] in foo: pass'')
1 0 SETUP_LOOP 16 (to 18)
2 LOAD_NAME 0 (foo)
4 GET_ITER
>> 6 FOR_ITER 8 (to 16)
8 LOAD_NAME 1 (k)
10 LOAD_CONST 0 (''e'')
12 STORE_SUBSCR <--------------------
14 JUMP_ABSOLUTE 6
>> 16 POP_BLOCK
>> 18 LOAD_CONST 1 (None)
20 RETURN_VALUE
Pero para mi sorpresa, esto no fue un error de sintaxis
Aparentemente, lo que sea que funcione en una tarea regular como la siguiente:
asignación de corte completa :
>>> for [][:] in []:
... pass
...
>>>
lista de suscripción
>>> for [2][0] in [42]:
... pass
...
>>>
la suscripción al diccionario, etc. serían objetivos candidatos válidos, con la única excepción de una asignación encadenada ; aunque, secretamente, creo que uno puede cocinar una sintaxis sucia para realizar el encadenamiento.
Esperaría solo identificadores únicos y tuplas de identificadores
No puedo pensar en un buen caso de uso para una clave de diccionario como objetivo.
Además, es más legible hacer la asignación de la clave del diccionario en el cuerpo del bucle que usarlo como un objetivo en la cláusula
for
.
Sin embargo, el desempaquetado extendido (Python 3) que es muy útil en las tareas regulares también es igualmente útil en un ciclo for:
>>> lst = [[1, '''', '''', 3], [3, '''', '''', 6]]
>>> for x, *y, z in lst:
... print(x,y,z)
...
1 ['''', ''''] 3
3 ['''', ''''] 6
Aquí también se invoca el mecanismo correspondiente para asignar a los diferentes objetivos;
múltiples
STORE_NAME
s:
>>> dis.dis(''for x, *y, z in lst: pass'')
1 0 SETUP_LOOP 20 (to 22)
2 LOAD_NAME 0 (lst)
4 GET_ITER
>> 6 FOR_ITER 12 (to 20)
8 EXTENDED_ARG 1
10 UNPACK_EX 257
12 STORE_NAME 1 (x) <-----
14 STORE_NAME 2 (y) <-----
16 STORE_NAME 3 (z) <-----
18 JUMP_ABSOLUTE 6
>> 20 POP_BLOCK
>> 22 LOAD_CONST 0 (None)
24 RETURN_VALUE
Demuestra que un
for
es apenas declaraciones de asignación simples ejecutadas sucesivamente.
El siguiente código tendría sentido, ¿verdad?
foo = [42]
for x in foo:
print x
El bucle
for
iteraría sobre la lista
foo
y asignaría cada objeto al nombre
x
en el espacio de nombres actual a su vez.
El resultado sería una única iteración y una impresión única de
42
.
En lugar de
x
en su código, tiene
k[''z'']
.
k[''z'']
es un nombre de almacenamiento válido.
Como
x
en mi ejemplo, todavía no existe.
Es, en efecto,
kz
en el espacio de nombres global.
El bucle crea
kz
o
k[''z'']
y le asigna los valores que encuentra en
foo
de la misma manera que crearía
x
y le asignaría los valores en mi ejemplo.
Si tuvieras más valores en foo ...
foo = [42, 51, "bill", "ted"]
k = {''c'': ''d''}
for k[''z''] in foo:
print k
daría como resultado:
{''c'': ''d'', ''z'': 42}
{''c'': ''d'', ''z'': 51}
{''c'': ''d'', ''z'': ''bill''}
{''c'': ''d'', ''z'': ''ted''}
Escribiste un código accidental perfectamente válido. Ni siquiera es un código extraño. Por lo general, no piensas en las entradas del diccionario como variables.
Incluso si el código no es extraño, ¿cómo puede ser útil permitir tal asignación?
key_list = [''home'', ''car'', ''bike'', ''locker'']
loc_list = [''under couch'', ''on counter'', ''in garage'', ''in locker'']
chain = {}
for index, chain[key_list[index]] in enumerate(loc_list):
pass
Probablemente no sea la mejor manera de hacerlo, pero reúne dos listas de igual longitud en un diccionario. Estoy seguro de que hay otras cosas en las que los programadores más experimentados han utilizado la asignación de teclas del diccionario para bucles. Tal vez...
Cada nombre es solo una clave de diccionario *.
for x in blah:
es precisamente
for vars()[''x''] in blah:
* (aunque ese diccionario no necesita implementarse como un objeto
dict
real, en el caso de algunas optimizaciones, como en los ámbitos de funciones).