[Python] N'est-il pas copié même si "copy ()" est fait? Croyances et échecs concernant la copie profonde

Publié: 2020/9/13

introduction

Cet article concerne l'attribution de références, la copie superficielle et la copie complète. Il existe déjà plusieurs articles, mais cet article inclut la situation où j'ai remarqué l'erreur et d'autres nouvelles découvertes au cours de mes recherches.

Je ne comprenais pas les copies superficielles et profondes et je pensais que l'attribution de référence = copie superficielle, .copy () = copie profonde. Cependant, après avoir enquêté sur cet échec, j'ai constaté qu'il existe trois types de substitution.

3 substitutions

Affectation de référence

a = [1,2]
b = a
b[0] = 100
print(a)  # [100, 2]
print(b)  # [100, 2]

Ensuite, si vous réécrivez «b», «a» sera également réécrit. Ceci est une ** affectation de référence ** [^ d1]. Puisque «a» et «b» font référence à la même chose (objet), si vous en réécrivez un, il semble que l'autre soit également réécrit.

Vérifions l'ID de l'objet avec ʻid () `.

print(id(a))  # 2639401210440
print(id(b))  # 2639401210440
print(a is b)  # True

L'identifiant est le même. Vous pouvez voir que «a» et «b» sont identiques. [^ d1]: Je l'ai écrit comme un devoir de référence, mais je n'ai pas pu trouver une telle phrase sur Internet en Python. «Passer par référence» est un terme utilisé dans les arguments de fonction, et je n'ai pas trouvé d'autre bon moyen de le dire, j'ai donc décidé de l'attribuer par référence.

Copie superficielle

Alors que faire si vous voulez traiter b comme un objet séparé de ʻa? J'utilise généralement .copy ()`.

a = [1,2]
b = a.copy()  #Copie superficielle
b[0] = 100
print(a, id(a))  # [1, 2] 1566893363784
print(b, id(b))  # [100, 2] 1566893364296
print(a is b)  # False

«A» et «b» sont correctement séparés. Ceci est une ** copie superficielle **. Il existe d'autres moyens de faire une copie superficielle.

Une copie superficielle de la liste


#Une copie superficielle de la liste
import copy
a = [1,2]

b = a.copy()  # .copy()Exemple d'utilisation
b = copy.copy(a)  #Exemple d'utilisation du module de copie
b = a[:]  #Exemple d'utilisation de tranches
b = [x for x in a]  #Exemple d'utilisation de la notation d'inclusion de liste
b = list(a)  # list()Exemple d'utilisation

Une copie superficielle du dictionnaire


#Une copie superficielle du dictionnaire
import copy
a = {"hoge":1, "piyo":2}

b = a.copy()  # .copy()Exemple d'utilisation
b = copy.copy(a)  #Exemple d'utilisation du module de copie
b = dict(a.items())  # items()Un exemple de conversion de ce qui a été retiré en

Copie profonde?

Maintenant, faisons une ** copie complète **.

import copy

a = [1,2]
b = copy.deepcopy(a)  #Copie profonde
b[0] = 100
print(a, id(a))  # [1, 2] 2401980169416
print(b, id(b))  # [100, 2] 2401977616520
print(a is b)  # False

Le résultat est le même qu'une copie superficielle.

"Copier ()" mais pas copié

Mais qu'en est-il de l'exemple suivant?

a = [[1,2], [3,4]]  #Changement
b = a.copy()
b[0][0] = 100
print(a)  # [[100, 2], [3, 4]]
print(b)  # [[100, 2], [3, 4]]

J'ai fait «a» sur la première ligne une liste à deux dimensions. J'aurais dû en faire une copie, mais «a» a également été réécrit. Quelle est la différence avec l'exemple précédent?

Objet mutable

Python a des objets mutables (modifiables) et des objets immuables (immuables). Une fois classé,

Mutable: liste, dict, numpy.ndarray [^ m1], etc. Immuable: int, str, tuple, etc.

C'est comme [^ m2] [^ m3]. Dans l'exemple ci-dessus, la liste est placée dans la liste. En d'autres termes, je place l'objet mutable à l'intérieur de l'objet mutable. Voyons maintenant s'il s'agit du même objet.

print(a is b, id(a), id(b))
# False 2460506990792 2460504457096

print(a[0] is b[0], id(a[0]), id(b[0]))
# True 2460503003720 2460503003720

La liste extérieure ʻab` est différente, mais la liste intérieure ʻa [0] b [0] `est la même. En d'autres termes, si vous réécrivez b [0], un [0] sera également réécrit.

Donc ce comportement est dû à l'utilisation d'une copie superficielle même si l'objet contient un objet mutable [^ m4]. Et la ** copie profonde ** est utilisée dans un tel cas.

[^ m1]: Il semble que ndarray puisse être rendu immuable (https://note.nkmk.me/python-numpy-ndarray-immutable-read-only/) [^ m2]: https://hibiki-press.tech/python/data_type/1763 (Major intégré mutable, immuable, itérable) [^ m3]: https://gammasoft.jp/blog/python-built-in-types/ (table de classification des types de données intégrée à Python (mutable, etc.))

[^ m4]: Documentation Python indique que "les objets composites (y compris d'autres objets tels que les listes et les instances de classe)" Object) "est écrit. Donc, pour être précis, c'est à cause d'une copie superficielle de l'objet composite. Comme je l'ai écrit dans une autre section, la même chose se produit même si je mets la liste dans le taple immuable.

Copie en profondeur de la solution

1. Utilisez une copie complète

Utilisez ** deep copy **.

import copy

a = [[1,2], [3,4]]
b = copy.deepcopy(a)
b[0][0] = 100
print(a)  # [[1, 2], [3, 4]]
print(b)  # [[100, 2], [3, 4]]

Voyons s'il s'agit du même objet.

print(a is b, id(a), id(b))
# False 2197556646152 2197556610760

print(a[0] is b[0], id(a[0]), id(b[0]))
# False 2197556617864 2197556805320

print(a[0][1] is b[0][1], id(a[0][1]), id(b[0][1]))
# True 140736164557088 140736164557088

C'est la même chose à la fin. b [0] [1] est un objet immuable ʻint`, et il n'y a pas de problème car un autre objet est automatiquement créé lors de la réaffectation de [^ k1].

En dehors de cela, les objets mutables ont des identifiants différents, vous pouvez donc voir qu'ils ont été copiés.

[^ k1]: https://atsuoishimoto.hatenablog.com/entry/20110414/1302750443 (Mystère de l'opérateur is) Python semble utiliser des objets immuables pour réduire la mémoire.

2. Utilisez numpy.ndarray

C'est un peu différent du contenu cette fois-ci, et c'est plus difficile, alors je l'ai réduit. Voir la section "[Solution 2 Make numpy.ndarray](# Solution 2 Make it numpyndarray)".

Code quand j'ai remarqué une erreur

Je posterai presque le même code que lorsque j'ai remarqué l'erreur. J'ai créé les données suivantes.

import numpy as np

a = {"data":[
        {"name": "img_0.jpg ", "size":"100x200", "img": np.zeros((100,200))},
        {"name": "img_1.jpg ", "size":"100x100", "img": np.zeros((100,100))},
        {"name": "img_2.jpg ", "size":"150x100", "img": np.zeros((150,100))}],
    "total_size": 5000
}

De cette façon, j'ai créé des données avec des objets mutables imbriqués, comme une liste dans un dictionnaire, un dictionnaire et une image (ndarray). Ensuite, j'ai créé un autre dictionnaire pour l'exportation json, en omettant uniquement ʻimg`.

Après cela, quand j'ai essayé de récupérer ʻimgdu dictionnaire original, j'ai eu unKeyError`. Je me demandais pourquoi j'aurais dû le copier pendant un moment et je me suis rendu compte que les références aux objets dans le dictionnaire pouvaient être les mêmes.

#Le code qui a causé le problème
data = a["data"].copy()  #C'est faux
for i in range(len(data)):
    del data[i]["img"]  #Supprimer l'img du dictionnaire
b = {"data":data, "total_size":a["total_size"]}  #Nouveau dictionnaire

img_0 = a["data"][0]["img"]  #KeyError même si je n'ai pas touché à un
# KeyError: 'img'

La solution la plus simple est de passer à une copie profonde comme data = copy.deepcopy (a [" data "]), mais dans ce cas, vous devez prendre la peine de copier l'image que vous souhaitez effacer plus tard. Cela peut affecter la mémoire et la vitesse d'exécution.

Par conséquent, je pense qu'il est préférable d'écrire sous la forme d'extraction des données nécessaires au lieu d'effacer les données inutiles des données d'origine.

#Code réécrit pour récupérer les données nécessaires
data = []
for d in a["data"]:
    new_dict = {}
    for k in d.keys():
        if(k=="img"):  #Ne pas inclure uniquement img
            continue
        new_dict[k] = d[k]  #Notez que ce n'est pas une copie
    data.append(new_dict)
b = {"data":data, "total_size":a["total_size"]}  #Nouveau dictionnaire

img_0 = a["data"][0]["img"]  #Fonctionner

Je l'ai utilisé pour exporter les données copiées au format json, donc le code ci-dessus convient, mais si je veux réécrire les données copiées, je dois utiliser deepcopy (s'il contient des objets mutables) ).

Copie superficielle et copie profonde

Comme vous pouvez le voir dans l'exemple ci-dessus

Copie superficielle: uniquement l'objet cible Copie profonde: objet cible + tous les objets mutables contenus dans l'objet cible

Sera copié. Pour plus d'informations, consultez la documentation Python (copie) (https://docs.python.org/ja/3/library/copy.html). Je pense que c'est une bonne idée de le lire une fois.

Vérification de la vitesse d'exécution

Nous avons créé un dictionnaire «a» contenant du texte et testé la vitesse d'exécution des copies superficielles et profondes.

import copy
import time
import numpy as np

def test1(a):
    start = time.time()
    # b = a
    # b = a.copy()
    # b = copy.copy(a)
    b = copy.deepcopy(a)
    process_time = time.time()-start
    return process_time

a = {i:"hogehoge"*100 for i in range(10000)}
res = []
for i in range(100):
    res.append(test1(a))
print(np.average(res)*1000, np.min(res)*1000, np.max(res)*1000)

résultat

En traitement moyenne(ms) le minimum(ms) maximum(ms)
b=a 0.0 0.0 0.0
a.copy() 0.240 0.0 1.00
copy.copy(a) 0.230 0.0 1.00
copy.deepcopy(a) 118 78.0 414

C'est une vérification appropriée, donc ce n'est pas très fiable, mais vous pouvez voir que la différence entre la copie superficielle et la copie profonde est grande. Par conséquent, il semble préférable d'utiliser les données qui ne sont pas réécrites en fonction des données à utiliser et de la méthode d'utilisation, comme l'utilisation d'une copie superficielle.

Autre vérification, etc.

Copie de la classe homebrew

import copy
class Hoge:
    def __init__(self):
        self.a = [1,2,3]
        self.b = 3

hoge = Hoge()
# hoge_copy = hoge.copy() #Erreur car il n'y a pas de méthode de copie
hoge_copy = copy.copy(hoge)  #Copie superficielle
hoge_copy.a[1] = 10000
hoge_copy.b = 100
print(hoge.a)  # [1, 10000, 3](Réécrit)
print(hoge.b)  #3 (non réécrit)

Même pour votre propre classe, si la variable membre est un objet mutable, une copie superficielle ne suffit pas.

Copie de taple

Même s'il s'appelle un taple, c'est un cas où un objet mutable est placé dans le tapple.

import copy
a = ([1,2],[3,4])
b = copy.copy(a)  #Copie superficielle
print(a)  # ([1, 2], [3, 4])
b[0][0] = 100  #Ceci peut être fait
print(a)  # ([100, 2], [3, 4])(Réécrit)
b[0] = [100,2]  #Ne peut pas être réécrit avec une erreur de type

Puisque le taple est immuable, la valeur ne peut pas être réécrite, mais l'objet mutable inclus dans le taple peut être réécrit. Dans ce cas également, la copie superficielle ne copie pas les objets à l'intérieur.

À propos de .copy () dans la liste

Je me demandais quel était le processus de copie de la liste b = a.copy (), alors j'ai jeté un coup d'œil au code source Python.

cpytnon / Objects / listobject.c Ligne 812 (cité de la branche master au 11/09/2020) Lien source (La position peut avoir changé)

/*[clinic input]
list.copy
Return a shallow copy of the list.
[clinic start generated code]*/

static PyObject *
list_copy_impl(PyListObject *self)
/*[clinic end generated code: output=ec6b72d6209d418e input=6453ab159e84771f]*/
{
    return list_slice(self, 0, Py_SIZE(self));
}

Comme vous pouvez le voir dans les commentaires

Return a shallow copy of the list.

Et il est écrit que c'est une copie superficielle. L'implémentation ci-dessous est également écrite comme list_slice, il semble donc qu'elle soit simplement découpée comme b = a [0: len (a)].

Solution n ° 2-numpy.ndarray

C'est un peu différent de cette histoire, mais si vous avez affaire à des tableaux multidimensionnels, vous pouvez également utiliser le ndarray de NumPy au lieu de listes. Attention cependant.

import numpy as np
import copy

a = [[1,2],[3,4]]
a = np.array(a)  #Convertir en ndarray
b = copy.copy(a)
# b = a.copy()  #C'est également possible
b[0][0] = 100
print(a)
# [[1 2]
# [3 4]]
print(b)
# [[100   2]
# [  3   4]]

Comme vous pouvez le voir, utiliser copy.copy () ou .copy () est bien, mais utiliser des tranches réécrira le tableau d'origine, tout comme une liste. Cela est dû à la différence entre la copie et la vue NumPy.

Référence: https://deepage.net/features/numpy-copyview.html (Explication de la copie et de l'affichage NumPy d'une manière facile à comprendre)

#Lors de l'utilisation de tranches
import numpy as np

a = [[1,2], [3,4]]
a = np.array(a)
b = a[:]  #tranche(=Créer une vue)
# b = a[:,:]  #C'est pareil
# b = a.view()  #Identique à ça
b[0][0] = 100
print(a)
# [[100   2]
# [  3   4]]
print(b)
# [[100   2]
# [  3   4]]

De plus, dans ce cas, le résultat de la comparaison par ʻis` ne sera pas le même que la liste.

import numpy as np

def check(a, b):
    print(id(a[0]), id(b[0]))
    print(a[0] is b[0], id(a[0])==id(b[0]))

#Lors du découpage de la liste
a = [[1,2],[3,4]]
b = a[:]
check(a,b)
# 1778721130184 1778721130184
# True True

#Lorsque ndarray est découpé (la vue est créée)
a = np.array([[1,2],[3,4]])
b = a[:]
check(a,b)
# 1778722507712 1778722507712
# False True

Comme vous pouvez le voir sur la dernière ligne, l'identifiant est le même, mais le résultat de la comparaison est False. Par conséquent, il convient de noter que l'identité de l'objet peut être confirmée par l'opérateur «is», et même si elle est False, elle peut être réécrite.

Quand je recherche l'opérateur ʻis`, il dit qu'il renvoie si les identifiants sont les mêmes, mais ^ n1 ^ n2 [^ n3], mais ce n'est pas le cas. Est-ce que numpy est spécial? Ce n'est pas bien compris.

L'environnement d'exécution est Python 3.7.4 & numpy1.16.5 + mkl.

[^ n3]: https://docs.python.org/ja/3/reference/expressions.html#is (6.10.3. Comparaison d'identité) Dans la référence officielle, "L'identité d'objet est la fonction id () Il est jugé en utilisant. "

en conclusion

Je n'ai jamais eu de problème avec .copy () jusqu'ici, donc je ne me souciais pas du tout de copier. Il est très effrayant de penser qu'une partie du code que j'ai écrit jusqu'à présent peut avoir été réécrit de manière inattendue.

Le problème avec les objets mutables en Python parle également des arguments par défaut des fonctions. Si vous ne le savez pas, vous générerez des données involontaires sans vous en rendre compte, veuillez donc les vérifier si vous ne les connaissez pas.   http://amacbee.hatenablog.com/entry/2016/12/07/004510 (Passage par valeur et passage par référence en Python) https://qiita.com/yuku_t/items/fd517a4c3d13f6f3de40 (La valeur par défaut de l'argument doit être immuable)

#Il vaut mieux ne pas spécifier un objet mutable comme argument par défaut

def hoge(data=[1,2]): #mauvais exemple
def hoge(data=None): #Bon exemple 1
def hoge(data=(1,2)): #Bon exemple 2
#Cela arrive aussi
a = [[0,1]] * 3
print(a)  # [[0, 1], [0, 1], [0, 1]]
a[0][0] = 3
print(a)  # [[3, 1], [3, 1], [3, 1]](Tousontétéréécrits)

référence

[1] https://qiita.com/Kaz_K/items/a3d619b9e670e689b6db (À propos de la copie et du deepcopy Python) [2] https://www.python.ambitious-engineer.com/archives/661 (copie superficielle du module et copie profonde) [3] https://snowtree-injune.com/2019/09/16/shallow-copy/ (Python ♪ Ensuite, rappelons-nous par raison "passer par référence" "copie superficielle" "copie profonde") [4] https://docs.python.org/ja/3/library/copy.html (copie --- opérations de copie superficielle et de copie profonde)

Recommended Posts

[Python] N'est-il pas copié même si "copy ()" est fait? Croyances et échecs concernant la copie profonde
Copie superficielle Python et copie profonde
Copie superficielle Python et copie profonde
Python # À propos de la référence et de la copie
[Python] Doux Est-ce doux? À propos des suites et des expressions dans les documents officiels
Erreur d'importation même si Python est installé
À propos de la différence entre "==" et "is" en python
À propos des copies superficielles et profondes de Python / Ruby