Comprensión profunda de la copia de objetos y el diseño de la memoria de Python

prefacio

En este artículo, presentaré principalmente el problema de la copia en python. Sin más preámbulos, veamos el código directamente. ¿Conoce los resultados de salida de los siguientes fragmentos de programa?

a = [1, 2, 3, 4]
b = a
print(f"{a = } \t|\t {b = }")
a[0] = 100
print(f"{a = } \t|\t {b = }")
复制代码
a = [1, 2, 3, 4]
b = a.copy()
print(f"{a = } \t|\t {b = }")
a[0] = 100
print(f"{a = } \t|\t {b = }")
复制代码
a = [[1, 2, 3], 2, 3, 4]
b = a.copy()
print(f"{a = } \t|\t {b = }")
a[0][0] = 100
print(f"{a = } \t|\t {b = }")
复制代码
a = [[1, 2, 3], 2, 3, 4]
b = copy.copy(a)
print(f"{a = } \t|\t {b = }")
a[0][0] = 100
print(f"{a = } \t|\t {b = }")
复制代码
a = [[1, 2, 3], 2, 3, 4]
b = copy.deepcopy(a)
print(f"{a = } \t|\t {b = }")
a[0][0] = 100
print(f"{a = } \t|\t {b = }")
复制代码

En este artículo, analizaremos el programa anterior en detalle.

Diseño de memoria de objetos de Python

Primero, presentemos un sitio web útil sobre la distribución lógica de datos en la memoria, pythontutor.com/visualize.h…

Ejecutamos el primer código en este sitio: 

De los resultados de salida anteriores, a y b apuntan a objetos de datos en la misma memoria. Así que la salida del primer código es la misma. ¿Cómo debemos determinar la dirección de memoria de un objeto? En Python, nos proporciona una función integrada id() para obtener la dirección de memoria de un objeto:

a = [1, 2, 3, 4]
b = a
print(f"{a = } \t|\t {b = }")
a[0] = 100
print(f"{a = } \t|\t {b = }")
print(f"{id(a) = } \t|\t {id(b) = }")
# 输出结果
# a = [1, 2, 3, 4] 	|	 b = [1, 2, 3, 4]
# a = [100, 2, 3, 4] 	|	 b = [100, 2, 3, 4]
# id(a) = 4393578112 	|	 id(b) = 4393578112
复制代码

De hecho, el diseño de la memoria de objetos anterior tiene algunos problemas, o no es lo suficientemente preciso, pero también puede mostrar la relación entre varios objetos. Echémosle un vistazo más profundo ahora. En Cpython, puede pensar que cada variable puede considerarse como un puntero, que apunta a los datos que se van a representar, y este puntero almacena la dirección de memoria del objeto de Python.

En Python, la lista en realidad contiene punteros a cada objeto de Python, no a los datos reales. Por lo tanto, el pequeño fragmento de código anterior puede usar el siguiente diagrama para representar el diseño del objeto en la memoria: 

La variable a apunta a la lista en la memoria  [1, 2, 3, 4], y hay 4 datos en la lista, y estos cuatro datos son punteros, y estos cuatro punteros apuntan a los cuatro datos de 1, 2, 3 y 4 en la memoria. Puede que tengas dudas, ¿no es esto un problema? Todos son datos enteros, ¿por qué no almacenar directamente los datos enteros en la lista, por qué agregar un puntero y luego apuntar a estos datos?

De hecho, en Python, cualquier objeto de Python se puede almacenar en la lista.Por ejemplo, el siguiente programa es legal:

data = [1, {1:2, 3:4}, {'a', 1, 2, 25.0}, (1, 2, 3), "hello world"]
复制代码

Los tipos de datos del primero al último datos en la lista anterior son: datos enteros, diccionario, conjunto, tupla y cadena Ahora, para realizar esta característica de Python, ¿la característica del puntero cumple con los requisitos? La memoria ocupada por cada puntero es la misma, por lo que puede usar una matriz para almacenar punteros a objetos de Python y luego apuntar los punteros a objetos reales de Python.

pequeña prueba

Después del análisis anterior, echemos un vistazo al siguiente código, cuál es su diseño de memoria:

data = [[1, 2, 3], 4, 5, 6]
data_assign = data
data_copy = data.copy()
复制代码

  • data_assign = data, Hemos hablado antes sobre el diseño de la memoria de esta declaración de asignación, pero también la estamos revisando. El significado de esta declaración de asignación es que los datos a los que apunta data_assign y data son los mismos datos, es decir, la misma lista.
  • data_copy = data.copy(), el significado de esta declaración de asignación es hacer una copia superficial de los datos a los que apuntan los datos y luego dejar que data_copy apunte a los datos copiados. El significado de la copia superficial aquí es copiar cada puntero en la lista en lugar del puntero en la lista se copian los datos. En el diagrama de distribución de la memoria del objeto anterior, podemos ver que copia_datos apunta a una nueva lista, pero los datos a los que apunta el puntero en la lista son los mismos que los datos a los que apunta el puntero en la lista de datos, donde copia_datos se representa con una flecha verde, y los datos se indican con una flecha negra.

Ver la dirección de memoria del objeto

En el artículo anterior, analizamos principalmente el diseño de la memoria del objeto. En esta sección, usamos python para proporcionarnos una herramienta muy efectiva para verificar esto. En python, podemos usar id() para ver la dirección de memoria del objeto, e id(a) es para ver la dirección de memoria del objeto al que apunta el objeto a.

  • Mira la salida del siguiente programa:
a = [1, 2, 3]
b = a
print(f"{id(a) = } {id(b) = }")
for i in range(len(a)):
    print(f"{i = } {id(a[i]) = } {id(b[i]) = }")
复制代码

De acuerdo con nuestro análisis anterior, a y b apuntan al mismo bloque de memoria, es decir, las dos variables apuntan al mismo objeto de Python, por lo que los resultados de id de salida anteriores a y b son los mismos, el resultado de salida anterior como sigue:

id(a) = 4392953984 id(b) = 4392953984
i = 0 id(a[i]) = 4312613104 id(b[i]) = 4312613104
i = 1 id(a[i]) = 4312613136 id(b[i]) = 4312613136
i = 2 id(a[i]) = 4312613168 id(b[i]) = 4312613168
复制代码
  • Eche un vistazo a la dirección de memoria de la copia superficial:
a = [[1, 2, 3], 4, 5]
b = a.copy()
print(f"{id(a) = } {id(b) = }")
for i in range(len(a)):
    print(f"{i = } {id(a[i]) = } {id(b[i]) = }")
复制代码

De acuerdo con nuestro análisis anterior, el método de copia para llamar a la lista en sí es hacer una copia superficial de la lista, solo se copian los datos del puntero de la lista, y los datos reales a los que apunta el puntero en la lista no se copian, por lo tanto, si recorremos los datos en la lista para obtener la dirección del objeto señalado, los resultados devueltos por la lista a y la lista b son los mismos, pero la diferencia con el ejemplo anterior es que las direcciones de las listas señaladas por a y b son diferentes (debido a que los datos se copian, puede consultar Los resultados de la copia superficial a continuación se entienden). 

Se puede entender combinando los siguientes resultados de salida con el texto anterior:

id(a) = 4392953984 id(b) = 4393050112 # 两个对象的输出结果不相等
i = 0 id(a[i]) = 4393045632 id(b[i]) = 4393045632 # 指向的是同一个内存对象因此内存地址相等 下同
i = 1 id(a[i]) = 4312613200 id(b[i]) = 4312613200
i = 2 id(a[i]) = 4312613232 id(b[i]) = 4312613232
复制代码

módulo de copia

Hay una copia de paquete integrada en python, que se utiliza principalmente para copiar objetos. En este módulo, hay principalmente dos métodos copy.copy(x) y copy.deepcopy().

  • El método copy.copy(x) se utiliza principalmente para la copia superficial. El significado de este método es el mismo para la lista que el método x.copy() de la lista misma, que es realizar una copia superficial. Este método construirá un nuevo objeto python y copiará todas las referencias de datos (punteros) en el objeto x. 

  • copy.deepcopy(x) Este método es principalmente para hacer una copia profunda del objeto x. El significado de la copia profunda aquí es construir un nuevo objeto y ver recursivamente cada objeto en el objeto x. Si el objeto visto recursivamente es un Los objetos inmutables no se copiarán.Si el objeto visto es un objeto mutable, se volverá a abrir un nuevo espacio de memoria y los datos originales en el objeto x se copiarán en la nueva memoria. (Analizaremos objetos mutables e inmutables en la siguiente sección)

  • De acuerdo con el análisis anterior, podemos saber que el costo de la copia profunda es mayor que el de la copia superficial, especialmente cuando hay muchos subobjetos en un objeto, tomará mucho tiempo y espacio de memoria.

  • Para los objetos de Python, la diferencia entre la copia profunda y la copia superficial se encuentra principalmente en los objetos compuestos (hay subobjetos en el objeto, como listas, ancestros, instancias de clases, etc.). Este punto está relacionado principalmente con los objetos mutables e inmutables de la siguiente sección.

Objetos mutables e inmutables y copia de objetos.

Hay dos tipos principales de objetos en Python, objetos mutables y objetos inmutables. El llamado objeto mutable significa que el contenido del objeto se puede cambiar, y el objeto inmutable significa que el contenido del objeto no se puede cambiar.

  • Objetos mutables: como listas (list), diccionarios (dict), colecciones (set), matrices de bytes (bytearray) y objetos de instancia de clases.
  • Objetos inmutables: entero (int), coma flotante (float), complejo (complejo), cadena, tupla, colección inmutable (congelado), bytes (bytes).

Al ver esto, es posible que tenga dudas, ¿no se pueden modificar enteros y cadenas?

a = 10
a = 100
a = "hello"
a = "world"
复制代码

Por ejemplo, el siguiente código es correcto y no se producirá ningún error, pero de hecho, el objeto al que apunta a ha cambiado.Cuando el primer objeto apunta a un entero o una cadena, si se reasigna un entero nuevo y diferente o una cadena object, python creará un nuevo objeto, podemos usar el siguiente código para verificar:

a = 10
print(f"{id(a) = }")
a = 100
print(f"{id(a) = }")
a = "hello"
print(f"{id(a) = }")
a = "world"
print(f"{id(a) = }")
复制代码

La salida del programa anterior es la siguiente:

id(a) = 4365566480
id(a) = 4365569360
id(a) = 4424109232
id(a) = 4616350128
复制代码

Se puede ver que el objeto de memoria apuntado por la variable ha cambiado después de la reasignación (porque ha cambiado la dirección de memoria), el cual es un objeto inmutable, aunque la variable puede ser reasignada, el objeto obtenido no se modifica sobre el objeto original. !

Ahora echemos un vistazo a cómo cambia la dirección de memoria después de modificar la lista de objetos mutables:

data = []
print(f"{id(data) = }")
data.append(1)
print(f"{id(data) = }")
data.append(1)
print(f"{id(data) = }")
data.append(1)
print(f"{id(data) = }")
data.append(1)
print(f"{id(data) = }")
复制代码

La salida del código anterior es la siguiente:

id(data) = 4614905664
id(data) = 4614905664
id(data) = 4614905664
id(data) = 4614905664
id(data) = 4614905664
复制代码

De los resultados de salida anteriores, podemos saber que cuando agregamos nuevos datos a la lista (modificamos la lista), la dirección de la lista en sí no cambia, que es un objeto mutable.

Hablamos de copia profunda y copia superficial antes, analicemos el siguiente código ahora:

data = [1, 2, 3]
data_copy = copy.copy(data)
data_deep = copy.deepcopy(data)
print(f"{id(data ) = } | {id(data_copy) = } | {id(data_deep) = }")
print(f"{id(data[0]) = } | {id(data_copy[0]) = } | {id(data_deep[0]) = }")
print(f"{id(data[1]) = } | {id(data_copy[1]) = } | {id(data_deep[1]) = }")
print(f"{id(data[2]) = } | {id(data_copy[2]) = } | {id(data_deep[2]) = }")
复制代码

La salida del código anterior es la siguiente:

id(data ) = 4620333952 | id(data_copy) = 4619860736 | id(data_deep) = 4621137024
id(data[0]) = 4365566192 | id(data_copy[0]) = 4365566192 | id(data_deep[0]) = 4365566192
id(data[1]) = 4365566224 | id(data_copy[1]) = 4365566224 | id(data_deep[1]) = 4365566224
id(data[2]) = 4365566256 | id(data_copy[2]) = 4365566256 | id(data_deep[2]) = 4365566256
复制代码

Al ver esto, definitivamente estará muy confundido, ¿por qué los objetos de memoria señalados por copia profunda y copia superficial son iguales? En la sección anterior, podemos entender que debido a que la copia superficial copia las referencias, los objetos a los que apuntan son los mismos, pero ¿por qué el objeto de memoria señalado después de la copia profunda es igual que la copia superficial? Esto se debe precisamente a que los datos en la lista son datos enteros, que es un objeto inmutable. Si se modifica el objeto al que apunta data o data_copy, apuntará a un nuevo objeto y no modificará directamente el objeto original. Por lo tanto, para De hecho, los objetos inmutables no necesitan abrir un nuevo espacio de memoria para su reasignación, porque los objetos en esta memoria no cambiarán.

Veamos un objeto copiable:

data = [[1], [2], [3]]
data_copy = copy.copy(data)
data_deep = copy.deepcopy(data)
print(f"{id(data ) = } | {id(data_copy) = } | {id(data_deep) = }")
print(f"{id(data[0]) = } | {id(data_copy[0]) = } | {id(data_deep[0]) = }")
print(f"{id(data[1]) = } | {id(data_copy[1]) = } | {id(data_deep[1]) = }")
print(f"{id(data[2]) = } | {id(data_copy[2]) = } | {id(data_deep[2]) = }")
复制代码

La salida del código anterior es la siguiente:

id(data ) = 4619403712 | id(data_copy) = 4617239424 | id(data_deep) = 4620032640
id(data[0]) = 4620112640 | id(data_copy[0]) = 4620112640 | id(data_deep[0]) = 4620333952
id(data[1]) = 4619848128 | id(data_copy[1]) = 4619848128 | id(data_deep[1]) = 4621272448
id(data[2]) = 4620473280 | id(data_copy[2]) = 4620473280 | id(data_deep[2]) = 4621275840
复制代码

Del resultado del programa anterior, podemos ver que cuando un objeto mutable se almacena en la lista, si realizamos una copia profunda, se creará un objeto nuevo (la dirección de memoria del objeto de la copia profunda es diferente de la de la copia superficial).

Análisis de fragmentos de código

Después del estudio anterior, la pregunta planteada al comienzo de este artículo debería ser muy simple para usted. Analicemos ahora estos fragmentos de código:

a = [1, 2, 3, 4]
b = a
print(f"{a = } \t|\t {b = }")
a[0] = 100
print(f"{a = } \t|\t {b = }")
复制代码

Esto es muy simple. Las diferentes variables de a y b apuntan a la misma lista. Si los datos en a cambian, los datos en b también cambiarán. El resultado es el siguiente:

a = [1, 2, 3, 4] 	|	 b = [1, 2, 3, 4]
a = [100, 2, 3, 4] 	|	 b = [100, 2, 3, 4]
id(a) = 4614458816 	|	 id(b) = 4614458816
复制代码

Echemos un vistazo al segundo fragmento de código.

a = [1, 2, 3, 4]
b = a.copy()
print(f"{a = } \t|\t {b = }")
a[0] = 100
print(f"{a = } \t|\t {b = }")
复制代码

Debido a que b es una copia superficial de a, a y b apuntan a listas diferentes, pero los datos en las listas apuntan a lo mismo, pero dado que los datos enteros son inmutables, cuando a[0] cambia, los datos originales no se modificarán, pero se creará un nuevo dato entero en la memoria, por lo que el contenido de la lista b no cambiará. Entonces, la salida del código anterior se ve así:

a = [1, 2, 3, 4] 	|	 b = [1, 2, 3, 4]
a = [100, 2, 3, 4] 	|	 b = [1, 2, 3, 4]
复制代码

Echemos un vistazo al tercer fragmento:

a = [[1, 2, 3], 2, 3, 4]
b = a.copy()
print(f"{a = } \t|\t {b = }")
a[0][0] = 100
print(f"{a = } \t|\t {b = }")
复制代码

Esto es similar al análisis del segundo fragmento, pero a[0] es un objeto variable, por lo que cuando se modifican los datos, el punto de a[0] no cambia, por lo que el contenido modificado de a afectará a b.

a = [[1, 2, 3], 2, 3, 4] 	|	 b = [[1, 2, 3], 2, 3, 4]
a = [[100, 2, 3], 2, 3, 4] 	|	 b = [[100, 2, 3], 2, 3, 4]
复制代码

El último fragmento:

a = [[1, 2, 3], 2, 3, 4]
b = copy.deepcopy(a)
print(f"{a = } \t|\t {b = }")
a[0][0] = 100
print(f"{a = } \t|\t {b = }")
复制代码

La copia profunda volverá a crear un objeto idéntico a a[0] en la memoria, y permitirá que b[0] apunte a este objeto, por lo que modificar a[0] no afectará a b[0], por lo que el resultado es el siguiente:

a = [[1, 2, 3], 2, 3, 4] 	|	 b = [[1, 2, 3], 2, 3, 4]
a = [[100, 2, 3], 2, 3, 4] 	|	 b = [[1, 2, 3], 2, 3, 4]
复制代码

Desmitificando los objetos de Python

Echemos un breve vistazo a cómo Cpython implementa la estructura de datos de la lista y qué se define en la lista:

typedef struct {
    PyObject_VAR_HEAD
    /* Vector of pointers to list elements.  list[0] is ob_item[0], etc. */
    PyObject **ob_item;

    /* ob_item contains space for 'allocated' elements.  The number
     * currently in use is ob_size.
     * Invariants:
     *     0 <= ob_size <= allocated
     *     len(list) == ob_size
     *     ob_item == NULL implies ob_size == allocated == 0
     * list.sort() temporarily sets allocated to -1 to detect mutations.
     *
     * Items must normally not be NULL, except during construction when
     * the list is not yet visible outside the function that builds it.
     */
    Py_ssize_t allocated;
} PyListObject;
复制代码

Entre las estructuras definidas anteriormente:

  • asignado indica la cantidad de espacio de memoria asignado, es decir, el número de punteros que se pueden almacenar. Cuando todo el espacio se agota, el espacio de memoria debe aplicarse nuevamente.
  • ob_item apunta a la matriz en la memoria que realmente almacena punteros a objetos de python. Por ejemplo, si queremos obtener el puntero al primer objeto de la lista, es list->ob_item[0]. datos reales, es *(lista->ob_item[ 0]).
  • PyObject_VAR_HEAD es una macro que define una subestructura en la estructura.La definición de esta subestructura es la siguiente:
typedef struct {
    PyObject ob_base;
    Py_ssize_t ob_size; /* Number of items in variable part */
} PyVarObject;
复制代码
  • Aquí no hablaremos del objeto PyObject, pero principalmente hablaremos de ob_size, que indica cuántos datos están almacenados en la lista. Esto es diferente de asignado. asignado indica cuánto espacio tiene la matriz a la que apunta ob_item, y ob_size indica cuánto muchos elementos se almacenan en la matriz Data ob_size <= asignado.

Después de comprender la estructura de la lista, ahora deberíamos poder comprender el diseño de memoria anterior.Todas las listas no almacenan datos reales, pero almacenan punteros a estos datos.

Resumir

Este artículo presenta principalmente la copia y el diseño de la memoria de los objetos en python, así como la verificación de las direcciones de memoria de los objetos y, finalmente, presenta la estructura de la implementación de la lista interna de cpython para ayudarlo a comprender el diseño de la memoria de los objetos de la lista.


Lo anterior es todo el contenido de este artículo, soy un matón , ¡nos vemos en el próximo número! ! !

Supongo que te gusta

Origin blog.csdn.net/weixin_73136678/article/details/128571590
Recomendado
Clasificación