[Python] Wird es nicht kopiert, obwohl "copy ()" fertig ist? Überzeugungen und Misserfolge über tiefes Kopieren

Gepostet: 2020/9/13

Einführung

In diesem Artikel geht es um Referenzzuweisung, flache Kopie und tiefe Kopie. Es gibt bereits mehrere Artikel, aber dieser Artikel enthält die Situation, in der ich den Fehler und andere neue Erkenntnisse während meiner Recherche bemerkt habe.

Ich habe flache und tiefe Kopien nicht verstanden und dachte, dass Referenzzuweisung = flache Kopie, .copy () = tiefe Kopie. Nachdem ich diesen Fehler untersucht hatte, stellte ich jedoch fest, dass es drei Arten der Substitution gibt.

3 Substitutionen

Referenzzuordnung

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

Wenn Sie dann "b" neu schreiben, wird auch "a" neu geschrieben. Dies ist ** Referenzzuordnung ** [^ d1]. Da sich a und b auf dasselbe (Objekt) beziehen, sieht es so aus, als ob das andere ebenfalls neu geschrieben wird, wenn Sie eines umschreiben.

Überprüfen wir die Objekt-ID mit id ().

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

Die ID ist die gleiche. Sie können sehen, dass a und b gleich sind. [^ d1]: Ich habe es als Referenzaufgabe geschrieben, aber in Python konnte ich einen solchen Satz im Internet nicht finden. "Übergeben als Referenz" ist ein Begriff, der in Funktionsargumenten verwendet wird, und ich konnte keine andere gute Möglichkeit finden, ihn auszudrücken. Deshalb habe ich beschlossen, ihn als Referenz zuzuweisen.

Flache Kopie

Was ist, wenn Sie "b" als ein von "a" getrenntes Objekt behandeln möchten? Normalerweise benutze ich .copy ().

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

Das a und b sind richtig getrennt. Dies ist eine ** flache Kopie **. Es gibt andere Möglichkeiten, eine flache Kopie zu erstellen.

Eine flache Kopie der Liste


#Eine flache Kopie der Liste
import copy
a = [1,2]

b = a.copy()  # .copy()Anwendungsbeispiel
b = copy.copy(a)  #Beispiel für die Verwendung des Kopiermoduls
b = a[:]  #Beispiel für die Verwendung von Scheiben
b = [x for x in a]  #Beispiel für die Verwendung der Listeneinschlussnotation
b = list(a)  # list()Anwendungsbeispiel

Eine flache Kopie des Wörterbuchs


#Eine flache Kopie des Wörterbuchs
import copy
a = {"hoge":1, "piyo":2}

b = a.copy()  # .copy()Anwendungsbeispiel
b = copy.copy(a)  #Beispiel für die Verwendung des Kopiermoduls
b = dict(a.items())  # items()Ein Beispiel für die Konvertierung dessen, was herausgenommen wurde

Tiefe Kopie?

Jetzt machen wir eine ** tiefe Kopie **.

import copy

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

Das Ergebnis entspricht einer flachen Kopie.

"Copy ()" aber nicht kopiert

Aber was ist mit dem folgenden Beispiel?

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

Ich habe "a" in der ersten Zeile zu einer zweidimensionalen Liste gemacht. Ich hätte eine Kopie machen sollen, aber das "a" wurde auch umgeschrieben. Was ist der Unterschied zum vorherigen Beispiel?

Veränderliches Objekt

Python hat veränderbare (veränderbare) Objekte und unveränderliche (unveränderliche) Objekte. Wenn klassifiziert,

Veränderlich: list, dict, numpy.ndarray [^ m1] usw. Unveränderlich: int, str, tuple usw.

Es ist wie [^ m2] [^ m3]. Im obigen Beispiel wird die Liste in die Liste aufgenommen. Mit anderen Worten, ich habe das veränderbare Objekt in das veränderbare Objekt eingefügt. Überprüfen wir nun, ob es sich um dasselbe Objekt handelt.

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

Die äußere Liste "a" "b" ist unterschiedlich, aber die innere Liste "a [0]" b [0] "ist dieselbe. Mit anderen Worten, wenn Sie b [0] neu schreiben, wird auch a [0] neu geschrieben.

Dieses Verhalten ist also auf die Verwendung einer flachen Kopie zurückzuführen, obwohl sich im Objekt ein veränderliches Objekt befindet [^ m4]. Und in einem solchen Fall wird ** Deep Copy ** verwendet.

[^ m1]: Es scheint, dass ndarray unveränderlich gemacht werden kann (https://note.nkmk.me/python-numpy-ndarray-immutable-read-only/) [^ m2]: https://hibiki-press.tech/python/data_type/1763 (Major eingebaut veränderlich, unveränderlich, iterierbar) [^ m3]: https://gammasoft.jp/blog/python-built-in-types/ (In Python integrierte Klassentabelle für Datentypen (veränderbar usw.))

[^ m4]: Python-Dokumentation gibt an, dass "zusammengesetzte Objekte (einschließlich anderer Objekte wie Listen und Klasseninstanzen)" Objekt) "ist geschrieben. Um genau zu sein, liegt es an einer flachen Kopie des zusammengesetzten Objekts. Wie ich in einem anderen Abschnitt geschrieben habe, passiert dasselbe, selbst wenn ich die Liste in den unveränderlichen Taple stecke.

Lösungstiefe Kopie

1. Verwenden Sie eine tiefe Kopie

Verwenden Sie ** tiefe Kopie **.

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]]

Mal sehen, ob sie das gleiche Objekt sind.

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

Das ist am Ende auch so. b [0] [1] ist ein unveränderliches Objekt int, und es gibt kein Problem, da beim Neuzuweisen von [^ k1] automatisch ein anderes Objekt erstellt wird.

Davon abgesehen haben die veränderlichen Objekte unterschiedliche IDs, sodass Sie sehen können, dass sie kopiert wurden.

[^ k1]: https://atsuoishimoto.hatenablog.com/entry/20110414/1302750443 (Geheimnis des Operators) Python scheint unveränderliche Objekte zu verwenden, um den Speicher zu reduzieren.

2. Verwenden Sie numpy.ndarray

Diesmal unterscheidet es sich ein wenig vom Inhalt, und es ist schwieriger, also habe ich es heruntergebracht. Siehe den Abschnitt "[Lösung 2 Make numpy.ndarray](# Lösung 2 Make it numpyndarray)".

Code, als ich einen Fehler bemerkte

Ich werde fast den gleichen Code posten, als ich den Fehler bemerkt habe. Ich habe die folgenden Daten erstellt.

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
}

Auf diese Weise habe ich Daten mit verschachtelten veränderlichen Objekten erstellt, z. B. einer Liste in einem Wörterbuch, einem Wörterbuch darin und einem Bild (ndarray). Dann habe ich ein weiteres Wörterbuch für den JSON-Export erstellt, wobei nur "img" weggelassen wurde.

Als ich danach versuchte, "img" aus dem ursprünglichen Wörterbuch abzurufen, bekam ich einen "KeyError". Ich fragte mich, warum ich es für eine Weile hätte kopieren sollen, und erkannte, dass die Verweise auf die Objekte im Wörterbuch dieselben sein könnten.

#Der Code, der das Problem verursacht hat
data = a["data"].copy()  #Das ist falsch
for i in range(len(data)):
    del data[i]["img"]  #Entfernen Sie img aus dem Wörterbuch
b = {"data":data, "total_size":a["total_size"]}  #Neues Wörterbuch

img_0 = a["data"][0]["img"]  #KeyError, obwohl ich a nicht berührt habe
# KeyError: 'img'

Die einfachste Lösung besteht darin, es in eine tiefe Kopie wie "data = copy.deepcopy (a [" data "])" zu ändern. In diesem Fall müssen Sie jedoch das Bild kopieren, das Sie später löschen möchten. Dies kann sich auf den Speicher und die Ausführungsgeschwindigkeit auswirken.

Daher denke ich, dass es besser ist, in Form des Extrahierens der erforderlichen Daten zu schreiben, als die unnötigen Daten aus den Originaldaten zu löschen.

#Code neu geschrieben, um die erforderlichen Daten abzurufen
data = []
for d in a["data"]:
    new_dict = {}
    for k in d.keys():
        if(k=="img"):  #Fügen Sie nicht nur img hinzu
            continue
        new_dict[k] = d[k]  #Beachten Sie, dass es sich nicht um eine Kopie handelt
    data.append(new_dict)
b = {"data":data, "total_size":a["total_size"]}  #Neues Wörterbuch

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

Ich habe es verwendet, um die kopierten Daten im JSON-Format zu exportieren, daher ist der obige Code in Ordnung. Wenn ich die kopierten Daten jedoch neu schreiben möchte, muss ich Deepcopy verwenden (wenn es veränderbare Objekte enthält). ).

Flache Kopie und tiefe Kopie

Wie Sie dem obigen Beispiel entnehmen können

Flache Kopie: Nur das Zielobjekt Tiefe Kopie: Zielobjekt + Alle veränderlichen Objekte, die im Zielobjekt enthalten sind

Wird kopiert. Weitere Informationen finden Sie in der Python-Dokumentation (Kopie) (https://docs.python.org/ja/3/library/copy.html). Ich denke, es ist eine gute Idee, es einmal zu lesen.

Überprüfung der Ausführungsgeschwindigkeit

Wir haben ein Wörterbuch "a" mit Text erstellt und die Ausführungsgeschwindigkeit von flachen und tiefen Kopien getestet.

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)

Ergebnis

wird bearbeitet durchschnittlich(ms) Minimum(ms) maximal(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

Es ist eine ordnungsgemäße Überprüfung, daher nicht sehr zuverlässig, aber Sie können sehen, dass der Unterschied zwischen flacher und tiefer Kopie groß ist. Daher scheint es besser, die Daten zu verwenden, die nicht entsprechend den zu verwendenden Daten und der Verwendungsmethode neu geschrieben wurden, z. B. eine flache Kopie.

Andere Überprüfung usw.

Kopie der Homebrew-Klasse

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

hoge = Hoge()
# hoge_copy = hoge.copy() #Fehler, da keine Kopiermethode vorhanden ist
hoge_copy = copy.copy(hoge)  #Flache Kopie
hoge_copy.a[1] = 10000
hoge_copy.b = 100
print(hoge.a)  # [1, 10000, 3](Umgeschrieben)
print(hoge.b)  #3 (nicht umgeschrieben)

Selbst für Ihre eigene Klasse reicht eine flache Kopie nicht aus, wenn die Mitgliedsvariable ein veränderbares Objekt ist.

Kopie von Taple

Selbst wenn es als Taple bezeichnet wird, ist es ein Fall, in dem ein veränderliches Objekt in den Taple gelegt wird.

import copy
a = ([1,2],[3,4])
b = copy.copy(a)  #Flache Kopie
print(a)  # ([1, 2], [3, 4])
b[0][0] = 100  #Das kann gemacht werden
print(a)  # ([100, 2], [3, 4])(Umgeschrieben)
b[0] = [100,2]  #Kann nicht mit Typfehler umgeschrieben werden

Da der Taple unveränderlich ist, kann der Wert nicht umgeschrieben werden, aber das im Taple enthaltene veränderbare Objekt kann umgeschrieben werden. Auch in diesem Fall kopiert die flache Kopie die darin enthaltenen Objekte nicht.

Über .copy () in der Liste

Ich habe mich gefragt, wie das Kopieren der Liste "b = a.copy ()" abläuft, also habe ich mir den Python-Quellcode angesehen.

cpytnon / Objects / listobject.c Zeile 812 (zitiert aus dem Hauptzweig vom 11.9.2020) Quelllink (Position möglicherweise geändert)

/*[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));
}

Wie Sie in den Kommentaren sehen können

Return a shallow copy of the list.

Und es steht geschrieben, dass es sich um eine flache Kopie handelt. Die Implementierung darunter wird auch als "list_slice" geschrieben, so dass es so aussieht, als ob sie nur wie "b = a [0: len (a)]" geschnitten wird.

Lösung # 2-numpy.ndarray

Es unterscheidet sich ein wenig von dieser Geschichte, aber wenn Sie mit mehrdimensionalen Arrays arbeiten, können Sie anstelle von Listen auch NumPys ndarray verwenden. Seien Sie jedoch vorsichtig.

import numpy as np
import copy

a = [[1,2],[3,4]]
a = np.array(a)  #In ndarray konvertieren
b = copy.copy(a)
# b = a.copy()  #Dies ist auch möglich
b[0][0] = 100
print(a)
# [[1 2]
# [3 4]]
print(b)
# [[100   2]
# [  3   4]]

Wie Sie sehen können, ist die Verwendung von "copy.copy ()" oder ".copy ()" in Ordnung, aber die Verwendung von Slices schreibt das ursprüngliche Array wie eine Liste neu. Dies ist auf den Unterschied zwischen NumPy-Kopie und -Ansicht zurückzuführen.

Referenz: https://deepage.net/features/numpy-copyview.html (Erläuterung der NumPy-Kopie und -Ansicht auf leicht verständliche Weise)

#Bei Verwendung von Scheiben
import numpy as np

a = [[1,2], [3,4]]
a = np.array(a)
b = a[:]  #Scheibe(=Ansicht erstellen)
# b = a[:,:]  #Das ist das gleiche
# b = a.view()  #Gleich wie das
b[0][0] = 100
print(a)
# [[100   2]
# [  3   4]]
print(b)
# [[100   2]
# [  3   4]]

Auch in diesem Fall stimmt das Vergleichsergebnis mit "is" nicht mit der Liste überein.

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]))

#Beim Schneiden der Liste
a = [[1,2],[3,4]]
b = a[:]
check(a,b)
# 1778721130184 1778721130184
# True True

#Wenn ndarray in Scheiben geschnitten wird (Ansicht wird erstellt)
a = np.array([[1,2],[3,4]])
b = a[:]
check(a,b)
# 1778722507712 1778722507712
# False True

Wie Sie in der letzten Zeile sehen können, ist die ID dieselbe, aber das Vergleichsergebnis von ist False. Daher müssen Sie vorsichtig sein, da der Operator "is" verwendet werden kann, um die Identität des Objekts zu überprüfen, und selbst wenn es "False" ist, kann es neu geschrieben werden.

Wenn Sie sich den Operator "is" ansehen, wird dieser zurückgegeben, wenn die IDs identisch sind ^ n1 ^ n2 [^ n3]. Dies ist jedoch nicht der Fall. Ist Numpy etwas Besonderes? Es ist nicht gut verstanden.

Die Ausführungsumgebung ist Python 3.7.4 & numpy1.16.5 + mkl.

[^ n3]: https://docs.python.org/ja/3/reference/expressions.html#is (6.10.3. Vergleich der Identität) In der offiziellen Referenz lautet die Funktion "Objektidentität ist id ()" Es wird mit beurteilt. "

abschließend

Ich hatte bisher noch nie ein Problem mit ".copy ()", daher war mir das Kopieren überhaupt nicht wichtig. Es ist sehr beängstigend zu glauben, dass ein Teil des Codes, den ich bisher geschrieben habe, unerwartet neu geschrieben wurde.

Das Problem mit veränderlichen Objekten in Python betrifft auch Funktionsargumentargumente. Wenn Sie dies nicht wissen, generieren Sie unbeabsichtigte Daten, ohne es zu bemerken. Überprüfen Sie sie daher, wenn Sie sie nicht kennen.   http://amacbee.hatenablog.com/entry/2016/12/07/004510 (Übergabe als Wert und Übergabe als Referenz in Python) https://qiita.com/yuku_t/items/fd517a4c3d13f6f3de40 (Der Standardwert des Arguments sollte unveränderlich sein.)

#Es ist besser, kein veränderbares Objekt als Standardargument anzugeben

def hoge(data=[1,2]): #schlechtes Beispiel
def hoge(data=None): #Gutes Beispiel 1
def hoge(data=(1,2)): #Gutes Beispiel 2
#Das passiert auch
a = [[0,1]] * 3
print(a)  # [[0, 1], [0, 1], [0, 1]]
a[0][0] = 3
print(a)  # [[3, 1], [3, 1], [3, 1]](Allewurdenumgeschrieben)

Referenz

[1] https://qiita.com/Kaz_K/items/a3d619b9e670e689b6db (Informationen zu Python-Kopie und Deepcopy) [2] https://www.python.ambitious-engineer.com/archives/661 (Kopiermodul flache Kopie und tiefe Kopie) [3] https://snowtree-injune.com/2019/09/16/shallow-copy/ (Python ♪ Als nächstes erinnern wir uns aus gutem Grund an "Pass by Reference", "flache Kopie", "tiefe Kopie"). [4] https://docs.python.org/ja/3/library/copy.html (Kopieren --- flache und tiefe Kopiervorgänge)

Recommended Posts

[Python] Wird es nicht kopiert, obwohl "copy ()" fertig ist? Überzeugungen und Misserfolge über tiefes Kopieren
Flache Python-Kopie und tiefe Kopie
Flache Python-Kopie und tiefe Kopie
Python # Über Referenz und Kopie
[Python] Süß Ist es süß? Über Suiten und Ausdrücke in offiziellen Dokumenten
Importfehler, obwohl ich Python installiert habe
Über den Unterschied zwischen "==" und "is" in Python
Über flache und tiefe Kopien von Python / Ruby