¿Por qué la asignación después del final de una lista a través de un sector no genera un IndexError?

Estoy trabajando en una implementación de lista dispersa y recientemente implementé una asignación a través de un sector. Esto me llevó a descubrir algún comportamiento en la implementación de la list incorporada de Python que me parece sorprendente .

Dada una list vacía y una asignación a través de una porción:

 >>> l = [] >>> l[100:] = ['foo'] 

Habría esperado un IndexError de la list aquí porque la forma en que se implementa esto significa que no se puede recuperar un elemento del índice especificado:

 >>> l[100] Traceback (most recent call last): File "", line 1, in  IndexError: list index out of range 

'foo' ni siquiera se puede recuperar de la porción especificada:

 >>> l = [] >>> l[100:] = ['foo'] >>> l[100:] [] 

l[100:] = ['foo'] agrega a la list (es decir, l == ['foo'] después de esta asignación) y parece haberse comportado de esta manera desde la versión inicial del BDFL . No puedo encontrar esta funcionalidad documentada en ninguna parte (*) pero tanto CPython como PyPy se comportan de esta manera.

La asignación por índice genera un error:

 >>> l[100] = 'bar' Traceback (most recent call last): File "", line 1, in  IndexError: list assignment index out of range 

Entonces, ¿por qué asignar el final de una list través de un sector no IndexError un IndexError (o algún otro error, supongo)?


Para aclarar después de los dos primeros comentarios, esta pregunta se refiere específicamente a la asignación , no a la recuperación ( consulte ¿Por qué el índice de corte de subcadenas fuera de rango funciona en Python? ).

Dando la tentación de adivinar y asignando 'foo' a l en el índice 0 cuando había especificado explícitamente el índice 100 no sigue el Zen habitual de Python.

Considere el caso donde la asignación sucede lejos de la inicialización y el índice es una variable . La persona que llama ya no puede recuperar sus datos de la ubicación especificada.

Asignar a un sector antes del final de una list comporta de manera diferente al ejemplo anterior:

 >>> l = [None, None, None, None] >>> l[3:] = ['bar'] >>> l[3:] ['bar'] 

(*) Este comportamiento se define en la Nota 4 de 5.6. Tipos de secuencia en la documentación oficial (gracias a Eethan ) pero no se explica por qué se consideraría deseable en la asignación.


Nota: entiendo cómo funciona la recuperación y puedo ver cómo puede ser deseable ser coherente con esto para la asignación, pero estoy buscando una razón citada de por qué la asignación a una porción se comportaría de esta manera. l[100:] devolver [] inmediatamente después de l[100:] = ['foo'] pero l[3:] devolver ['bar'] después de l[3:] = ['bar'] es sorprendente si tiene no hay conocimiento de len(l) , especialmente si estás siguiendo el lenguaje EAFP de Python.

Vamos a ver qué está pasando realmente:

 >>> l = [] >>> l[100:] = ['foo'] >>> l[100:] [] >>> l ['foo'] 

Así que la asignación fue realmente exitosa, y el elemento se colocó en la lista, como el primer elemento.

Esto se debe a que 100: en la posición de indexación se convierte en un objeto de sector: slice(100, None, None) :

 >>> class Foo: ... def __getitem__(self, i): ... return i ... >>> Foo()[100:] slice(100, None, None) 

Ahora, la clase de slice tiene un indices métodos (no puedo encontrar su documentación de Python en línea, sin embargo) que, cuando se le da una longitud de una secuencia, dará (start, stop, stride) que se ajusta para la longitud de esa secuencia. secuencia.

 >>> slice(100, None, None).indices(0) (0, 0, 1) 

Por lo tanto, cuando esta división se aplica a una secuencia de longitud 0, se comporta exactamente igual que una división slice(0, 0, 1) para las recuperaciones de división , por ejemplo, en lugar de que foo[100:] arroje un error cuando foo es una secuencia vacía, se comporta como si foo[0:0:1] fuera solicitado – esto resultará en una porción vacía en la recuperación.

Ahora el código de establecimiento debería funcionar correctamente cuando se usó l[100:] cuando l es una secuencia que tiene más de 100 elementos . Para que funcione allí, lo más fácil es no reinventar la rueda, y simplemente usar el mecanismo de indices anterior. Como inconveniente, ahora se verá un poco peculiar en los casos de borde, pero las asignaciones de segmento a segmentos que están “fuera de límites” se colocarán al final de la secuencia actual. (Sin embargo, resulta que hay poca reutilización de código en el código CPython; list_ass_slice duplica esencialmente todo este manejo de índice, aunque también estaría disponible a través de la API C del objeto de sector ).

Por lo tanto: si el índice de inicio de un segmento es mayor o igual que la longitud de una secuencia, el segmento resultante se comporta como si fuera un segmento de ancho cero que comienza desde el final de la secuencia . Es decir: si a >= len(l) , l[a:] comporta como l[len(l):len(l)] en los tipos incorporados. Esto es cierto para cada una de la asignación, recuperación y eliminación.

Lo deseable de esto es que no necesita ninguna excepción . El método slice.indices no necesita manejar ninguna excepción; para una secuencia de longitud l , slice.indices(l) siempre dará como resultado (start, end, stride) de índices que se pueden usar para cualquier asignación, recuperación y eliminación, y se garantiza que tanto el start como el end son 0 <= v <= len(l) .

Para la indexación, se debe generar un error si el índice dado está fuera de los límites, porque no hay un valor predeterminado aceptable que pueda devolverse. (No es aceptable devolver None , porque None podría ser un elemento válido de la secuencia).

Por el contrario, para segmentar, no es necesario generar un error si alguno de los índices está fuera de los límites, porque es aceptable devolver una secuencia vacía como valor predeterminado. Y también es deseable hacer esto, porque proporciona una manera consistente de referirse a las subsecuencias tanto entre elementos como más allá de los extremos de la secuencia (permitiendo así las inserciones).

Como se indica en las Notas de tipos de secuencia , si el valor inicial o final de un sector es mayor que len(seq) , entonces se usa len(seq) .

Por lo tanto, dados a = [4, 5, 6] , las expresiones a[3:] y a[100:] apuntan a la subsecuencia vacía que sigue al último elemento de la lista. Sin embargo, después de una asignación de división que utiliza estas expresiones, es posible que ya no se refieran a la misma cosa, ya que la longitud de la lista puede haber cambiado.

Por lo tanto, después de la asignación a[3:] = [7] , la división a[3:] devolverá [7] . Pero después de la asignación a[100:] = [8] , la división a[100:] seguirá devolviendo [] , porque len(a) sigue siendo inferior a 100 . Y dado todo lo anterior, esto es exactamente lo que se debe esperar si se mantiene la coherencia entre la asignación de división y la recuperación de división.