python - ¿Por qué p[:] fue diseñado para funcionar de manera diferente en estas dos situaciones?
python-3.x slice (6)
p = [1,2,3]
print(p) # [1, 2, 3]
q=p[:] # supposed to do a shallow copy
q[0]=11
print(q) #[11, 2, 3]
print(p) #[1, 2, 3]
# above confirms that q is not p, and is a distinct copy
del p[:] # why is this not creating a copy and deleting that copy ?
print(p) # []
Lo anterior confirma que
p[:]
no funciona de la misma manera en estas 2 situaciones.
¿No es así?
Teniendo en cuenta que en el siguiente código, espero trabajar directamente con
p
y no con una copia de
p
,
p[0] = 111
p[1:3] = [222, 333]
print(p) # [111, 222, 333]
siento
del p[:]
es consistente con
p[:]
, todos ellos hacen referencia a la lista original pero
q=p[:]
es confuso (para los principiantes como yo) ya que
p[:]
en este caso da como resultado una nueva lista!
Mi expectativa de novato sería que
q=p[:]
debe ser el mismo que
q=p
¿Por qué los creadores permitieron que este comportamiento especial resultara en una copia?
Básicamente, la sintaxis de segmento se puede utilizar en 3 contextos diferentes:
-
Accediendo, es decir,
x = foo[:]
-
Configuración, es decir,
foo[:] = x
-
Eliminando, es decir,
del foo[:]
Y en estos contextos, los valores puestos entre corchetes solo seleccionan los ítems. Esto está diseñado para que la "porción" se use de manera consistente en cada uno de estos casos:
-
Entonces
x = foo[:]
obtiene todos los elementos enfoo
y los asigna ax
. Esto es básicamente una copia superficial. -
Pero
foo[:] = x
reemplazará todos los elementos enfoo
con los elementos enx
. -
Y al eliminar
del foo[:]
se eliminarán todos los elementos enfoo
.
Sin embargo, este comportamiento es personalizable como se explica en 3.3.7. Emulando tipos de contenedores :
object.__getitem__(self, key)
Llamado a implementar la evaluación de
self[key]
. Para los tipos de secuencia, las claves aceptadas deben ser enteros y objetos de sector. Tenga en cuenta que la interpretación especial de los índices negativos (si la clase desea emular un tipo de secuencia) depende del método__getitem__()
. Si la clave es de un tipo inapropiado,TypeError
puede ser levantado; si tiene un valor fuera del conjunto de índices para la secuencia (después de cualquier interpretación especial de valores negativos),IndexError
debe elevarse. Para los tipos de mapeo, si falta una clave (que no esté en el contenedor),KeyError
debe aparecer.Nota
for
bucles se espera que seIndexError
unIndexError
para índices ilegales para permitir la detección adecuada del final de la secuencia.
object.__setitem__(self, key, value)
Llamado para implementar la asignación a
self[key]
. La misma nota que para__getitem__()
. Esto solo debe implementarse para asignaciones si los objetos admiten cambios en los valores de las claves, o si se pueden agregar nuevas claves, o para secuencias si se pueden reemplazar elementos. Las mismas excepciones deben generarse para valores de clave incorrectos como para el método__getitem__()
.
object.__delitem__(self, key)
Llamado para implementar la eliminación de
self[key]
. La misma nota que para__getitem__()
. Esto solo debe implementarse para asignaciones si los objetos admiten la eliminación de claves, o para secuencias si se pueden eliminar elementos de la secuencia. Las mismas excepciones deben generarse para valores de clave incorrectos como para el método__getitem__()
.
(Énfasis mío)
Entonces, en teoría, cualquier tipo de contenedor podría implementar esto como quiera. Sin embargo, muchos tipos de contenedores siguen la lista de implementación.
Como han dicho otros;
p[:]
borra todos los elementos en
p
;
PERO no afectará a q.
Para entrar en más detalles, la lista de
docs
refiere a esto:
Todas las operaciones de división devuelven una nueva lista que contiene los elementos solicitados. Esto significa que la siguiente porción devuelve una copia nueva (superficial) de la lista:
>>> squares = [1, 4, 9, 16, 25] ... >>> squares[:] [1, 4, 9, 16, 25]
Entonces
q=p[:]
crea una copia
(superficial)
de
p
como una lista separada, pero luego de una inspección adicional apunta a una ubicación completamente separada en la memoria.
>>> p = [1,2,3]
>>> q=p[:]
>>> id(q)
139646232329032
>>> id(p)
139646232627080
Esto se explica mejor en el módulo de
copy
:
Una copia superficial construye un nuevo objeto compuesto y luego (en la medida de lo posible) inserta referencias en él a los objetos que se encuentran en el original.
Aunque la sentencia del se realiza recursivamente en listas / segmentos:
La eliminación de una lista de objetivos elimina recursivamente cada objetivo, de izquierda a derecha.
Entonces, si usamos
del p[:]
estamos eliminando el contenido de
p
mediante la iteración de cada elemento, mientras
q
no se altera como se indicó anteriormente, hace referencia a una lista separada aunque tiene los mismos elementos:
>>> del p[:]
>>> p
[]
>>> q
[1, 2, 3]
De hecho, esto también se menciona en los documentos de la lista y en el método
list.clear
:
lista. dupdo()
Devuelva una copia superficial de la lista. Equivalente a
a[:]
.lista. claro()
Eliminar todos los elementos de la lista. Equivalente a
del a[:]
.
Las tareas y las asignaciones se diseñan de forma coherente, simplemente no están diseñadas de la forma que usted esperaba. del nunca elimina objetos, elimina nombres / referencias (la eliminación de objetos solo ocurre de manera indirecta, es el recolector de refcount / basura que elimina los objetos); de manera similar, el operador de asignación nunca copia objetos, siempre está creando / actualizando nombres / referencias.
El operador del y del asignación toma una especificación de referencia (similar al concepto de un valor l en C, aunque los detalles difieren).
Esta especificación de referencia es un nombre de variable (identificador sin formato), una clave
__setitem__
(objeto entre corchetes) o
__setattr__
nombre de
__setattr__
(identificador después del punto).
Este valor de l no se evalúa como una expresión, ya que al hacerlo será imposible asignar o eliminar nada.
Considera la simetría entre:
p[:] = [1, 2, 3]
y
del p[:]
En ambos casos,
p[:]
funciona de manera idéntica porque ambos se evalúan como un valor l.
Por otro lado, en el siguiente código,
p[:]
es una expresión que se evalúa completamente en un objeto:
q = p[:]
No estoy seguro si quieres este tipo de respuesta. En palabras, para p [:], significa "iterar a través de todos los elementos de p". Si lo usas en
q=p[:]
Luego se puede leer como "iterar con todos los elementos de p y configurarlo en q". Por otro lado, utilizando
q=p
Simplemente significa "asignar la dirección de p a q" o "hacer qa puntero a p", lo cual es confuso si proviene de otros idiomas que manejan los punteros individualmente.
Por lo tanto, usándolo en del, como
del p[:]
Solo significa "borrar todos los elementos de p".
Espero que esto ayude.
Razones históricas, principalmente.
En las primeras versiones de Python, los iteradores y los generadores no eran realmente una cosa.
La mayoría de las formas de trabajar con secuencias solo devolvieron listas: el
range()
, por ejemplo, devolvió una lista completamente construida que contiene los números.
Por lo tanto, tenía sentido que los cortes, cuando se usan en el lado derecho de una expresión, devuelvan una lista.
a[i:j:s]
devolvió una nueva lista que contiene elementos seleccionados de
a
.
Y así,
a[:]
en el lado derecho de una tarea devolvería una nueva lista que contiene todos los elementos de
a
, es decir, una copia superficial: esta era perfectamente consistente en ese momento.
Por otro lado, los corchetes en el lado
izquierdo
de una expresión siempre modificaron la lista original: ese fue el precedente establecido por
a[i] = d
, y ese precedente fue seguido por
del a[i]
, y luego por
del a[i:j]
.
El tiempo transcurrido, y la copia de valores y la creación de instancias de nuevas listas en todo el lugar se consideró innecesario y costoso.
Hoy en día,
range()
devuelve un generador que produce cada número solo como se solicita, e iterar sobre una porción podría funcionar de la misma manera, pero el lenguaje de
copy = original[:]
está demasiado arraigado como un artefacto histórico.
Por cierto, en Numpy, este no es el caso:
ref = original[:]
hará una referencia en lugar de una copia superficial, lo cual es consistente con la forma en que funcionan el trabajo y la asignación a las matrices.
>>> a = np.array([1,2,3,4])
>>> b = a[:]
>>> a[1] = 7
>>> b
array([1, 7, 3, 4])
Python 4, si alguna vez sucede, puede seguir su ejemplo. Es, como has observado, mucho más consistente con otro comportamiento.
del
en iterador es solo una llamada a
__delitem__
con el índice como argumento.
Al igual que la llamada entre paréntesis [n] es una llamada al método
__getitem__
en una instancia de iterador con índice n.
Entonces, cuando llama a
p[:]
está creando una secuencia de elementos, y cuando llama a
del p[:]
, asigna esa del / __ delitem__ a cada elemento en esa secuencia.