Comparación y copia de objetos Python

He entrado en contacto con muchos ejemplos de comparación y copia de objetos de Python, como la siguiente declaración if para determinar si ayb son iguales:


if a == b:
    ...

Tomemos el segundo ejemplo nuevamente, donde l2 es una copia de l1.


l1 = [1, 2, 3]
l2 = list(l1)

Pero es posible que no sepa qué sucedió detrás de estas declaraciones. tal como,

  • ¿Es l2 una copia superficial o una copia profunda de l1?
  • ¿A == b compara los valores de dos objetos para la igualdad, o los dos objetos son exactamente iguales?

'==' VS 'es'

Igual a ( = = ==== ) Y son dos métodos de objetos de uso común en Python. En pocas palabras, el operador '==' compara si los valores entre los objetos son iguales. Por ejemplo, el siguiente ejemplo indica si los valores apuntados por las variables de comparación ayb son iguales.


a == b

El operador 'is' compara si la identidad del objeto es igual, es decir, si son el mismo objeto y si apuntan a la misma dirección de memoria.

En Python, la identidad de cada objeto se puede obtener a través de la función id (objeto). Por lo tanto, el operador 'es' equivale a comparar si los ID entre objetos son iguales. Veamos el siguiente ejemplo:


a = 10
b = 10

a == b
True

id(a)
4427562448

id(b)
4427562448

a is b
True

Aquí, primero, Python abrirá una parte de la memoria para el valor de 10, y luego las variables ayb apuntan a esta área de memoria al mismo tiempo, es decir, tanto a como b apuntan a la variable 10, por lo que los valores De a y b son iguales, y el id también es igual, a = = b y a es b, ambos devuelven Verdadero.

Sin embargo, debe tenerse en cuenta que para números enteros, la conclusión anterior de que a es b es Verdadero solo se aplica a números en el rango de -5 a 256. Por ejemplo, el siguiente ejemplo:


a = 257
b = 257

a == b
True

id(a)
4473417552

id(b)
4473417584

a is b
False

Aquí asignamos 257 a ayb al mismo tiempo, podemos ver que a == b todavía devuelve Verdadero, porque los valores apuntados por ayb son iguales. Pero lo extraño es que a es b devuelve falso y encontramos que los ID de ayb son diferentes.

De hecho, por consideraciones de optimización del rendimiento, Python mantiene internamente una matriz de números enteros de -5 a 256, que actúa como caché. De esta manera, cada vez que intente crear un número entero en el rango de -5 a 256, Python devolverá la referencia correspondiente de esta matriz en lugar de crear un nuevo espacio de memoria.

Sin embargo, si el número entero excede este rango, como el 257 en el ejemplo anterior, Python abrirá dos áreas de memoria para los dos 257, por lo que los ID de ayb son diferentes, y a es b devolverá False.

En términos generales, en el trabajo real, cuando comparamos variables, el número de veces que '==' se usa mucho más que 'es', porque generalmente nos preocupan más los valores de dos variables que sus direcciones de almacenamiento interno. Sin embargo, cuando comparamos una variable con un singleton, usualmente usamos 'es'. Un ejemplo típico es comprobar si una variable es None:


if a is None:
      ...

if a is not None:
      ...

Tenga en cuenta aquí que la velocidad y la eficiencia del operador de comparación 'es' suele ser mejor que la de '=='. Debido a que el operador 'is' no se puede sobrecargar, de esta manera, Python no necesita averiguar si el operador de comparación está sobrecargado en otra parte del programa y llamarlo. La ejecución del operador de comparación 'es' es solo para comparar los ID de dos variables.

Pero ' == ==== 'El operador es diferente. Ejecutar a == b es equivalente a ejecutar a.Eq(b), y la mayoría de los tipos de datos en Python sobrecargarán la función __eq__, y su procesamiento interno suele ser más complicado. Por ejemplo, para una lista, la función __eq__ recorrerá los elementos de la lista y comparará su orden y si sus valores son iguales.

Sin embargo, para las variables inmutables, si las comparamos con '==' o 'es' antes, ¿el resultado seguirá siendo el mismo?

La respuesta es, naturalmente, no. Veamos el siguiente ejemplo:


t1 = (1, 2, [3, 4])
t2 = (1, 2, [3, 4])
t1 == t2
True

t1[-1].append(5)
t1 == t2
False

Sabemos que las tuplas son inmutables, pero las tuplas se pueden anidar, los elementos en ella pueden ser tipos de lista y las listas son mutables, por lo que si modificamos un elemento variable en la tupla, entonces la tupla en sí también cambió, el resultado obtenido con Es posible que el operador 'es' o '==' anterior no sea aplicable.

Copia superficial y copia profunda

A continuación, echemos un vistazo a la copia superficial y la copia profunda en Python.

Para estas dos operaciones familiares, no quiero lanzar los conceptos primero y dejar que los memorices para distinguirlos. Comencemos con sus métodos de operación y entendamos la diferencia entre los dos a través del código.

Veamos primero la copia superficial. El método de copia superficial común es utilizar el constructor del tipo de datos en sí, como los siguientes dos ejemplos:


l1 = [1, 2, 3]
l2 = list(l1)

l2
[1, 2, 3]

l1 == l2
True

l1 is l2
False

s1 = set([1, 2, 3])
s2 = set(s1)

s2
{
    
    1, 2, 3}

s1 == s2
True

s1 is s2
False

Aquí, l2 es la copia superficial de l1 y s2 es la copia superficial de s1. Por supuesto, para las secuencias de variables, también podemos usar el operador de corte ':' para completar una copia superficial, como en el siguiente ejemplo de lista:


l1 = [1, 2, 3]
l2 = l1[:]

l1 == l2
True

l1 is l2
False

Por supuesto, Python también proporciona una función correspondiente copy.copy (), que es adecuada para cualquier tipo de datos:


import copy
l1 = [1, 2, 3]
l2 = copy.copy(l1)

Sin embargo, debe tenerse en cuenta que para las tuplas, el uso de tupla () o el operador de división ':' no creará una copia superficial, sino que devolverá una referencia a la misma tupla:


t1 = (1, 2, 3)
t2 = tuple(t1)

t1 == t2
True

t1 is t2
True

Aquí, la tupla (1, 2, 3) se crea solo una vez, y t1 y t2 apuntan a esta tupla al mismo tiempo.

En este punto, debe tener muy claro acerca de las copias superficiales. La copia superficial se refiere a la reasignación de una parte de la memoria para crear un nuevo objeto, y los elementos del interior son referencias a objetos secundarios en el objeto original. Por lo tanto, si el elemento en el objeto original es inmutable, no importa; pero si el elemento es mutable, la copia superficial generalmente trae algunos efectos secundarios, especialmente a los que debe prestar atención. Veamos el siguiente ejemplo:


l1 = [[1, 2], (30, 40)]
l2 = list(l1)
l1.append(100)
l1[0].append(3)

l1
[[1, 2, 3], (30, 40), 100]

l2
[[1, 2, 3], (30, 40)]

l1[1] += (50, 60)
l1
[[1, 2, 3], (30, 40, 50, 60), 100]

l2
[[1, 2, 3], (30, 40)]

En este ejemplo, primero inicializamos una lista l1, donde los elementos son una lista y una tupla; luego realizamos una copia superficial de l1 y asignamos l2. Debido a que el elemento en la copia superficial es una referencia al elemento del objeto original, el elemento en l2 y l1 apuntan al mismo objeto de lista y tupla.

Entonces mira hacia abajo. l1.append (100), lo que significa agregar el elemento 100 a la lista de l1. Esta operación no tendrá ningún efecto en l2, porque l2 y l1 en su conjunto son dos objetos diferentes y no comparten direcciones de memoria. Después de la operación, l2 permanece sin cambios y l1 cambiará:


[[1, 2, 3], (30, 40), 100]

Mire de nuevo, l1 [0] .append (3), aquí significa agregar el elemento 3 a la primera lista en l1. Como l2 es una copia superficial de l1, el primer elemento de l2 y el primer elemento de l1 apuntan a la misma lista, por lo que la primera lista de l2 también corresponderá al nuevo elemento 3. Tanto l1 como l2 cambiarán después de la operación:


l1: [[1, 2, 3], (30, 40), 100]
l2: [[1, 2, 3], (30, 40)]

El último es l1 [1] + = (50, 60), porque la tupla es inmutable, esto significa empalmar la segunda tupla en l1 y luego volver a crear una nueva tupla como la segunda en l1 Element, y no hay una nueva tupla se hace referencia en l2, por lo que l2 no se ve afectado. Después de la operación, l2 permanece sin cambios y l1 cambia:


l1: [[1, 2, 3], (30, 40, 50, 60), 100]

A través de este ejemplo, puede ver claramente los posibles efectos secundarios de usar una copia superficial. Por tanto, si queremos evitar este efecto secundario y copiar un objeto por completo, tenemos que utilizar la copia profunda.

La llamada copia profunda se refiere a la reasignación de una parte de la memoria, la creación de un nuevo objeto y la copia recursiva de los elementos del objeto original al nuevo objeto mediante la creación de nuevos subobjetos. Por lo tanto, el nuevo objeto no tiene relación con el objeto original.

Python usa copy.deepcopy () para implementar una copia profunda de los objetos. Por ejemplo, el ejemplo anterior está escrito en la siguiente forma, que es una copia profunda:


import copy
l1 = [[1, 2], (30, 40)]
l2 = copy.deepcopy(l1)
l1.append(100)
l1[0].append(3)

l1
[[1, 2, 3], (30, 40), 100]

l2 
[[1, 2], (30, 40)]

Podemos ver que no importa cómo cambie l1, l2 permanece igual. Porque en este momento l1 y l2 son completamente independientes y no tienen conexión.

Sin embargo, la copia profunda no es perfecta y, a menudo, trae una serie de problemas. Si hay una referencia a sí mismo en el objeto copiado, el programa puede caer fácilmente en un bucle infinito:


import copy
x = [1]
x.append(x)

x
[1, [...]]

y = copy.deepcopy(x)
y
[1, [...]]

En el ejemplo anterior, la lista x tiene una referencia a sí misma, por lo que x es una lista infinitamente anidada. Pero descubrimos que después de realizar una copia profunda de xay, el programa no mostraba un desbordamiento de pila. ¿Por qué es esto?

De hecho, esto se debe a que la función de copia profunda deepcopy mantiene un diccionario para registrar los objetos copiados y sus ID. Durante el proceso de copia, si el objeto a copiar ya está almacenado en el diccionario, será devuelto directamente desde el diccionario, lo podemos entender mirando el código fuente correspondiente:


def deepcopy(x, memo=None, _nil=[]):
    """Deep copy operation on arbitrary Python objects.
      
  See the module's __doc__ string for more info.
  """
  
    if memo is None:
        memo = {
    
    }
    d = id(x) # 查询被拷贝对象x的id
  y = memo.get(d, _nil) # 查询字典里是否已经存储了该对象
  if y is not _nil:
      return y # 如果字典里已经存储了将要拷贝的对象,则直接返回
        ...    

Supongo que te gusta

Origin blog.csdn.net/qq_41485273/article/details/114138854
Recomendado
Clasificación