¿El desempaquetado de argumentos usa iteración o obtención de elementos?

Estoy usando Python 2.7.3.

Considere una clase ficticia con iteración personalizada (aunque sea mala) y comportamiento de obtención de elementos:

class FooList(list): def __iter__(self): return iter(self) def next(self): return 3 def __getitem__(self, idx): return 3 

Haz un ejemplo y observa el extraño comportamiento:

 >>> zz = FooList([1,2,3]) >>> [x for x in zz] # Hangs because of the self-reference in `__iter__`. >>> zz[0] 3 >>> zz[1] 3 

Pero ahora, hagamos una función y luego analicemos el desempaquetado en zz :

 def add3(a, b, c): return a + b + c >>> add3(*zz) 6 # I expected either 9 or for the interpreter to hang like the comprehension! 

Por lo tanto, el desempaquetado de argumentos es de alguna manera obtener los datos del elemento de zz pero no iterando sobre el objeto con su iterador implementado y tampoco haciendo el iterador de un hombre pobre y llamando a __getitem__ por tantos elementos como el objeto.

Entonces la pregunta es: ¿cómo add3(*zz) la syntax add3(*zz) los miembros de datos de zz si no es por estos métodos? ¿Me estoy perdiendo otro patrón común para obtener miembros de datos de un tipo como este?

Mi objective es ver si puedo escribir una clase que implemente iteración o obtención de elementos de tal manera que cambie lo que significa el argumento de desempaquetado de syntax para esa clase. Después de probar los dos ejemplos anteriores, ahora me pregunto cómo el desempaque de argumentos llega a los datos subyacentes y si el progtwigdor puede influir en ese comportamiento. Google para esto solo devolvió un mar de resultados que explican el uso básico de la syntax *args .

No tengo un caso de uso por la necesidad de hacer esto y no estoy diciendo que sea una buena idea. Solo quiero ver cómo hacerlo por curiosidad.

Adicional

Dado que los tipos incorporados se tratan de manera especial, aquí hay un ejemplo con un object donde solo mantengo un objeto de lista e implemento mi propio comportamiento de obtención y configuración para emular la lista.

 class FooList(object): def __init__(self, lst): self.lst = lst def __iter__(self): raise ValueError def next(self): return 3 def __getitem__(self, idx): return self.lst.__getitem__(idx) def __setitem__(self, idx, itm): self.lst.__setitem__(idx, itm) 

En este caso,

 In [234]: zz = FooList([1,2,3]) In [235]: [x for x in zz] --------------------------------------------------------------------------- ValueError Traceback (most recent call last)  in () ----> 1 [x for x in zz]  in __iter__(self) 2 def __init__(self, lst): 3 self.lst = lst ----> 4 def __iter__(self): raise ValueError 5 def next(self): return 3 6 def __getitem__(self, idx): return self.lst.__getitem__(idx) ValueError: In [236]: add_3(*zz) --------------------------------------------------------------------------- ValueError Traceback (most recent call last)  in () ----> 1 add_3(*zz)  in __iter__(self) 2 def __init__(self, lst): 3 self.lst = lst ----> 4 def __iter__(self): raise ValueError 5 def next(self): return 3 6 def __getitem__(self, idx): return self.lst.__getitem__(idx) ValueError: 

Pero en cambio, si me aseguro de que la iteración se detenga y siempre devuelva 3, puedo hacer que jueguen con lo que estaba fotografiando en el primer caso:

 class FooList(object): def __init__(self, lst): self.lst = lst self.iter_loc = -1 def __iter__(self): return self def next(self): if self.iter_loc < len(self.lst)-1: self.iter_loc += 1 return 3 else: self.iter_loc = -1 raise StopIteration def __getitem__(self, idx): return self.lst.__getitem__(idx) def __setitem__(self, idx, itm): self.lst.__setitem__(idx, itm) 

Entonces veo esto, que es lo que originalmente esperaba:

 In [247]: zz = FooList([1,2,3]) In [248]: ix = iter(zz) In [249]: ix.next() Out[249]: 3 In [250]: ix.next() Out[250]: 3 In [251]: ix.next() Out[251]: 3 In [252]: ix.next() --------------------------------------------------------------------------- StopIteration Traceback (most recent call last)  in () ----> 1 ix.next()  in next(self) 10 else: 11 self.iter_loc = -1 ---> 12 raise StopIteration 13 def __getitem__(self, idx): return self.lst.__getitem__(idx) 14 def __setitem__(self, idx, itm): self.lst.__setitem__(idx, itm) StopIteration: In [253]: ix = iter(zz) In [254]: ix.next() Out[254]: 3 In [255]: ix.next() Out[255]: 3 In [256]: ix.next() Out[256]: 3 In [257]: ix.next() --------------------------------------------------------------------------- StopIteration Traceback (most recent call last)  in () ----> 1 ix.next()  in next(self) 10 else: 11 self.iter_loc = -1 ---> 12 raise StopIteration 13 def __getitem__(self, idx): return self.lst.__getitem__(idx) 14 def __setitem__(self, idx, itm): self.lst.__setitem__(idx, itm) StopIteration: In [258]: add_3(*zz) Out[258]: 9 In [259]: zz[0] Out[259]: 1 In [260]: zz[1] Out[260]: 2 In [261]: zz[2] Out[261]: 3 In [262]: [x for x in zz] Out[262]: [3, 3, 3] 

Resumen

  1. La syntax *args basa únicamente en la iteración. Para los tipos incorporados, esto sucede de una manera que no se puede anular directamente en las clases que heredan del tipo incorporado.

  2. Estos dos son funcionalmente equivalentes:

    foo(*[x for x in args])

    foo(*args)

  3. Estos no son equivalentes incluso para estructuras de datos finitos.

    foo(*args)

    foo(*[args[i] for i in range(len(args))])

Has sido mordido por una de las verrugas más irritantes de Python: los tipos incorporados y las subclases de ellas se tratan mágicamente en algunos lugares.

Desde que escribe las subclases de la list , Python mágicamente alcanza su interior para descomprimirlo. No utiliza la API de iterador real en absoluto. Si inserta declaraciones print dentro de su next y __getitem__ , verá que no se está llamando a ninguna. Este comportamiento no puede ser anulado; en su lugar, tendría que escribir su propia clase que reimplementa los tipos incorporados. Puedes intentar usar UserList ; No he comprobado si eso funcionaría.

La respuesta a su pregunta es que el desempaquetado de argumentos utiliza la iteración. Sin embargo, la iteración en sí misma puede usar __getitem__ si no hay un __iter__ explícito definido. No puede crear una clase que defina un comportamiento de desempaquetado de argumentos que sea diferente del comportamiento de iteración normal.

No se debe asumir que el protocolo de iterador (básicamente “cómo funciona __iter__ “) se aplica a los tipos que subclase en tipos integrados como list . Si crea una subclase de una incorporada, su subclase puede comportarse mágicamente como la incorporada subyacente en ciertas situaciones, sin hacer uso de sus métodos mágicos de personalización (como __iter__ ). Si desea personalizar el comportamiento de forma completa y confiable, no puede crear subclases de tipos incorporados (excepto, por supuesto, object ).