Warum erstellt Python nur beim Durchlaufen einer Liste eine Kopie des einzelnen Elements?


31

Mir ist gerade aufgefallen, dass in Python, wenn man schreibt

for i in a:
    i += 1

Die Elemente der ursprünglichen Liste asind eigentlich überhaupt nicht betroffen, da isich herausstellt, dass die Variable nur eine Kopie des ursprünglichen Elements in ist a.

Um das ursprüngliche Element zu ändern,

for index, i in enumerate(a):
    a[index] += 1

würde gebraucht werden.

Dieses Verhalten hat mich wirklich überrascht. Dies scheint sehr eingängig zu sein, scheint sich von anderen Sprachen zu unterscheiden und hat zu Fehlern in meinem Code geführt, die ich heute lange Zeit debuggen musste.

Ich habe das Python-Tutorial schon einmal gelesen. Nur um sicher zu gehen, habe ich das Buch gerade noch einmal durchgesehen und es erwähnt dieses Verhalten überhaupt nicht.

Was ist die Begründung für dieses Design? Wird erwartet, dass es eine Standardübung in vielen Sprachen ist, sodass das Tutorial davon ausgeht, dass die Leser es auf natürliche Weise verstehen sollten? In welchen anderen Sprachen ist das gleiche Iterationsverhalten vorhanden, auf das ich in Zukunft achten sollte?


19
Das ist nur wahr, wenn iunveränderlich ist oder Sie eine nicht mutierende Operation ausführen. Bei einer verschachtelten Liste for i in a: a.append(1)würde sich das Verhalten ändern. Python nicht die verschachtelten Listen kopieren. Ganzzahlen sind jedoch unveränderlich und Addition gibt ein neues Objekt zurück, es ändert das alte nicht.
Jonrsharpe

10
Das ist überhaupt nicht überraschend. Ich kann mir keine Sprache vorstellen, die für ein Array von Grundtypen wie Integer nicht genau gleich ist. Versuchen Sie es zum Beispiel mit Javascript a=[1,2,3];a.forEach(i => i+=1);alert(a). Gleiche in C #
edc65

7
Würden Sie damit rechnen i = i + 1zu beeinflussen a?
deltaB

7
Beachten Sie, dass dieses Verhalten in anderen Sprachen nicht anders ist. C, Javascript, Java usw. verhalten sich so.
Slebetman

1
@jonrsharpe für Listen "+ =" ändert die alte Liste, während "+" eine neue erstellt
Vasily Alexeev

Antworten:


68

Ich habe kürzlich bereits eine ähnliche Frage beantwortet und es ist sehr wichtig zu wissen, dass +=dies unterschiedliche Bedeutungen haben kann:

  • Wenn der Datentyp In-Place-Addition implementiert (dh eine ordnungsgemäß funktionierende __iadd__Funktion hat), werden die Daten, iauf die verwiesen wird, aktualisiert (egal, ob sie in einer Liste oder an einem anderen Ort enthalten sind).

  • Wenn der Datentyp keine __iadd__Methode implementiert , ist die i += xAnweisung nur ein syntaktischer Zucker i = i + x. Daher wird ein neuer Wert erstellt und dem Variablennamen zugewiesen i.

  • Wenn der Datentyp implementiert __iadd__, aber etwas Seltsames tut. Möglicherweise wird es aktualisiert ... oder nicht - das hängt davon ab, was dort implementiert ist.

Python-Ganzzahlen, Floats und Strings werden nicht implementiert, __iadd__sodass sie nicht direkt aktualisiert werden. Andere Datentypen wie numpy.arrayoder listimplementieren es jedoch und verhalten sich so, wie Sie es erwartet haben. Es ist also keine Frage des Kopierens oder Nicht-Kopierens beim Iterieren (normalerweise werden keine Kopien für lists und tuples erstellt - aber das hängt auch von der Implementierung der Container __iter__und der __getitem__Methode ab!) - es ist eher eine Frage des Datentyps Sie haben in Ihrem gespeichert a.


2
Dies ist die richtige Erklärung für das in der Frage beschriebene Verhalten.
Pabouk

19

Klarstellung - Terminologie

Python unterscheidet nicht zwischen den Konzepten Referenz und Zeiger . Sie verwenden normalerweise nur den Begriff Referenz , aber wenn Sie mit Sprachen wie C ++ vergleichen, die diese Unterscheidung haben - es ist viel näher an einem Zeiger .

Da der Fragesteller eindeutig aus dem C ++ - Hintergrund stammt und diese Unterscheidung - die für die Erklärung erforderlich ist - in Python nicht vorhanden ist , habe ich mich für die Verwendung der C ++ - Terminologie entschieden:

  • Wert : Aktuelle Daten, die sich im Speicher befinden. void foo(int x);ist eine Signatur einer Funktion, die eine Ganzzahl nach Wert empfängt .
  • Zeiger : Eine Speicheradresse, die als Wert behandelt wird. Kann zurückgestellt werden, um auf den Speicher zuzugreifen, auf den er verweist. void foo(int* x);ist eine Signatur einer Funktion, die eine Ganzzahl per Zeiger empfängt .
  • Hinweis : Zucker um Zeiger. Es gibt einen Zeiger hinter den Kulissen, aber Sie können nur auf den zurückgestellten Wert zugreifen und die Adresse, auf die er verweist, nicht ändern. void foo(int& x);ist eine Signatur einer Funktion, die eine Ganzzahl als Referenz erhält .

Was meinst du mit "anders als andere Sprachen"? Die meisten mir bekannten Sprachen, die für jede Schleife Unterstützung bieten, kopieren das Element, sofern nicht ausdrücklich anders angegeben.

Speziell für Python (obwohl viele dieser Gründe auf andere Sprachen mit ähnlichen architektonischen oder philosophischen Konzepten zutreffen können):

  1. Dieses Verhalten kann bei Personen, die sich dessen nicht bewusst sind, zu Fehlern führen. Das alternative Verhalten kann jedoch auch bei Personen, die sich dessen bewusst sind, zu Fehlern führen . Wenn Sie eine Variable zuweisen ( i), halten Sie normalerweise nicht an und berücksichtigen alle anderen Variablen, die aufgrund dieser Variable geändert würden ( a). Die Einschränkung des Bereichs, an dem Sie arbeiten, ist ein wichtiger Faktor für die Verhinderung von Spaghetti-Code. Daher ist die Iteration nach Kopie in der Regel die Standardeinstellung, auch in Sprachen, die die Iteration nach Referenz unterstützen.

  2. Python-Variablen sind immer einzelne Zeiger, daher ist es günstig, sie kopiert zu iterieren - günstiger als referenziert zu iterieren. Dies würde bei jedem Zugriff auf den Wert eine zusätzliche Verzögerung erfordern.

  3. Python kennt keine Referenzvariablen wie zum Beispiel C ++. Das heißt, alle Variablen in Python sind Verweise, aber in dem Sinne, dass sie Zeiger sind - keine Verweise hinter den Kulissen wie C ++ - type& nameArgumente. Da dieses Konzept in Python nicht vorhanden ist, sollten Sie die Iteration nach Referenz implementieren - geschweige denn zum Standard machen! - erfordert mehr Komplexität für den Bytecode.

  4. Pythons forAussage funktioniert nicht nur bei Arrays, sondern auch bei einem allgemeineren Konzept von Generatoren. Hinter den Kulissen ruft Python iterIhre Arrays auf, um ein Objekt zu erhalten, das - wenn Sie es aufrufen next- entweder das nächste Element oder raisesa zurückgibt StopIteration. Es gibt verschiedene Möglichkeiten, Generatoren in Python zu implementieren, und es wäre viel schwieriger gewesen, sie für die Iteration nach Referenz zu implementieren.


Danke für die Antwort. Scheint, dass mein Verständnis für Iteratoren dann immer noch nicht solide genug ist. Sind Iteratoren nicht standardmäßig in C ++ enthalten? Wenn Sie den Iterator dereferenzieren, können Sie den Wert des Elements des Originalcontainers immer sofort ändern.
Xji

4
Python tut iterieren durch Verweis (gut, durch einen Wert, aber der Wert ist eine Referenz). Wenn Sie dies mit einer Liste veränderlicher Objekte versuchen, wird schnell gezeigt, dass kein Kopieren stattfindet.
Jonrsharpe

Iteratoren in C ++ sind eigentlich Objekte, die zurückgestellt werden können, um auf den Wert im Array zuzugreifen. Um das ursprüngliche Element zu ändern, verwenden Sie *it = ...- aber diese Art von Syntax zeigt bereits an, dass Sie etwas an einem anderen Ort ändern -, was Grund Nr. 1 weniger problematisch macht. Die Gründe 2 und 3 treffen auch nicht zu, da das Kopieren in C ++ teuer ist und das Konzept der Referenzvariablen existiert. Was Grund 4 betrifft - die Möglichkeit, eine Referenz zurückzugeben, ermöglicht eine einfache Implementierung für alle Fälle.
Idan Arye

1
@jonrsharpe Ja, es wird als Referenz bezeichnet, aber in jeder Sprache, in der zwischen Zeigern und Referenzen unterschieden wird, ist diese Art der Iteration eine Iteration nach Zeigern (und da Zeiger Werte sind - Iteration nach Wert). Ich werde eine Klarstellung hinzufügen.
Idan Arye

20
Ihr allererster Absatz besagt, dass Python, wie auch diese anderen Sprachen, das Element in eine for-Schleife kopiert. Das tut es nicht. Der Umfang der Änderungen, die Sie an diesem Element vornehmen, wird dadurch nicht eingeschränkt. Das OP sieht dieses Verhalten nur, weil ihre Elemente unveränderlich sind. Ohne diese Unterscheidung zu erwähnen, ist Ihre Antwort bestenfalls unvollständig und im schlimmsten Fall irreführend.
Jonrsharpe

11

Keine der hier aufgeführten Antworten gibt Ihnen Code, mit dem Sie wirklich veranschaulichen können, warum dies im Python-Land passiert. Und es macht Spaß, dies in einem tieferen Sinne zu betrachten, und so geht es weiter.

Der Hauptgrund dafür, dass dies nicht wie erwartet funktioniert, ist, dass Sie in Python Folgendes schreiben:

i += 1

es tut nicht das, was du denkst. Ganzzahlen sind unveränderlich. Dies kann man sehen, wenn man sich anschaut, was das Objekt tatsächlich in Python ist:

a = 0
print('ID of the first integer:', id(a))
a += 1
print('ID of the first integer +=1:', id(a))

Die ID-Funktion repräsentiert einen eindeutigen und konstanten Wert für ein Objekt in seiner Lebensdauer. Konzeptionell ist es einer Speicheradresse in C / C ++ lose zugeordnet. Den obigen Code ausführen:

ID of the first integer: 140444342529056
ID of the first integer +=1: 140444342529088

Dies bedeutet, dass die erste anicht mehr mit der zweiten identisch ist a, da ihre IDs unterschiedlich sind. Tatsächlich befinden sie sich an verschiedenen Stellen im Speicher.

Bei einem Objekt verhält es sich jedoch anders. Ich habe überschreibt die +=Betreiber hier:

class CustomInt:
  def __iadd__(self, other):
    # Override += 1 for this class
    self.value = self.value + other.value
    return self

  def __init__(self, v):
    self.value = v

ints = []
for i in range(5):
  int = CustomInt(i)
  print('ID={}, value={}'.format(id(int), i))
  ints.append(int)


for i in ints:
  i += CustomInt(i.value)

print("######")
for i in ints:
  print('ID={}, value={}'.format(id(i), i.value))

Das Ausführen dieses Befehls führt zu folgender Ausgabe:

ID=140444284275400, value=0
ID=140444284275120, value=1
ID=140444284275064, value=2
ID=140444284310752, value=3
ID=140444284310864, value=4
######
ID=140444284275400, value=0
ID=140444284275120, value=2
ID=140444284275064, value=4
ID=140444284310752, value=6
ID=140444284310864, value=8

Beachten Sie, dass das id-Attribut in diesem Fall für beide Iterationen identisch ist , auch wenn der Wert des Objekts unterschiedlich ist (Sie können auch idden int-Wert des Objekts ermitteln, der sich aufgrund von Ganzzahlen ändern würde, wenn er sich ändert) unveränderlich sind).

Vergleichen Sie dies damit, wenn Sie dieselbe Übung mit einem unveränderlichen Objekt ausführen:

ints_primitives = []
for i in range(5):
  int = i
  ints_primitives.append(int)
  print('ID={}, value={}'.format(id(int), i))

print("######")
for i in ints_primitives:
  i += 1
  print('ID={}, value={}'.format(id(int), i))


print("######")
for i in ints_primitives:
  print('ID={}, value={}'.format(id(i), i))

Dies gibt aus:

ID=140023258889248, value=0
ID=140023258889280, value=1
ID=140023258889312, value=2
ID=140023258889344, value=3
ID=140023258889376, value=4
######
ID=140023258889280, value=1
ID=140023258889312, value=2
ID=140023258889344, value=3
ID=140023258889376, value=4
ID=140023258889408, value=5
######
ID=140023258889248, value=0
ID=140023258889280, value=1
ID=140023258889312, value=2
ID=140023258889344, value=3
ID=140023258889376, value=4

Ein paar Dinge hier zu beachten. Erstens +=fügen Sie in der Schleife mit dem nicht mehr zum ursprünglichen Objekt hinzu. In diesem Fall verwendet Python eine andere ID , da ints zu den unveränderlichen Typen in Python gehört. Interessant ist auch, dass Python denselben Basiswert idfür mehrere Variablen mit demselben unveränderlichen Wert verwendet:

a = 1999
b = 1999
c = 1999

print('id a:', id(a))
print('id b:', id(b))
print('id c:', id(c))

id a: 139846953372048
id b: 139846953372048
id c: 139846953372048

tl; dr - Python hat eine Handvoll unveränderlicher Typen, die das Verhalten verursachen, das Sie sehen. Für alle veränderlichen Typen ist Ihre Erwartung richtig.


6

@ Idans Antwort ist eine gute Erklärung dafür, warum Python die Schleifenvariable nicht wie in C als Zeiger behandelt, aber es lohnt sich, ausführlicher zu erklären, wie die Code-Snippets entpackt werden, wie in Python viele einfach erscheinende Bits Code wird tatsächlich Aufrufe von eingebauten Methoden sein . Um dein erstes Beispiel zu nehmen

for i in a:
    i += 1

Es gibt zwei Dinge zu entpacken: die for _ in _:Syntax und die _ += _Syntax. Um die for-Schleife zuerst zu nehmen, hat Python wie andere Sprachen eine for-eachSchleife, die im Wesentlichen die Syntax sugar für ein Iteratormuster ist. In Python ist ein Iterator ein Objekt, das eine .__next__(self)Methode definiert , die das aktuelle Element in der Sequenz zurückgibt, mit dem nächsten fortfährt und ein auslöst, StopIterationwenn die Sequenz keine Elemente mehr enthält. Ein Iterable ist ein Objekt, das eine .__iter__(self)Methode definiert , die einen Iterator zurückgibt.

(Hinweis: ein Iteratorist auch ein Iterableund kehrt von seiner .__iter__(self)Methode zurück.)

Python verfügt normalerweise über eine eingebaute Funktion, die an die benutzerdefinierte Methode mit doppeltem Unterstrich delegiert. Also hat es iter(o)was auflöst o.__iter__()und next(o)das auflöst o.__next__(). Beachten Sie, dass diese integrierten Funktionen häufig eine angemessene Standarddefinition verwenden, wenn die Methode, an die sie delegieren würden, nicht definiert ist. Zum Beispiel len(o)löst in der Regel zu , o.__len__()aber wenn das Verfahren nicht definiert ist , wird es dann versuchen iter(o).__len__().

Ein for - Schleife ist im Wesentlichen in Bezug auf dem definiert next(), iter()und basische Kontrollstrukturen. Im Allgemeinen der Code

for i in %EXPR%:
    %LOOP%

werde zu sowas ausgepackt

_a_iter = iter(%EXPR%)
while True:
    try:
        i = next(_a_iter)
    except StopIteration:
        break
    %LOOP%

Also in diesem Fall

for i in a:
    i += 1

wird ausgepackt

_a_iter = iter(a) # = a.__iter__()
while True:
    try: 
        i = next(_a_iter) # = _a_iter.__next__()
    except StopIteration:
        break
    i += 1

Die andere Hälfte davon ist i += 1. In der Regel %ASSIGN% += %EXPR%wird ausgepackt %ASSIGN% = %ASSIGN%.__iadd__(%EXPR%). Hier __iadd__(self, other)tut sich inplace add und kehrt zurück.

(Hinweis: Dies ist ein weiterer Fall, in dem Python eine Alternative auswählt, wenn die Hauptmethode nicht definiert ist. Wenn das Objekt nicht implementiert wird __iadd__, wird auf diese zurückgegriffen __add__. Dies geschieht in diesem Fall tatsächlich, da intes nicht implementiert wird __iadd__- was sinnvoll ist, weil sie sind unveränderlich und können daher nicht geändert werden.)

Ihr Code hier sieht also so aus

_a_iter = iter(a)
while True:
    try:
        i = next(_a_iter)
    except StopIteration:
        break
    i = iadd(i,1)

wo wir definieren können

def iadd(o, v):
    try:
        return o.__iadd__(v)
    except AttributeError:
        return o.__add__(v)

In Ihrem zweiten Teil des Codes ist noch ein bisschen mehr los. Die zwei neuen Dinge, die wir wissen müssen, sind die, in die %ARG%[%KEY%] = %VALUE%entpackt (%ARG%).__setitem__(%KEY%, %VALUE%)und in die %ARG%[%KEY%]entpackt wird (%ARG%).__getitem__(%KEY%). Wenn wir dieses Wissen zusammenstellen, werden wir a[ix] += 1entpackt a.__setitem__(ix, a.__getitem__(ix).__add__(1))(wieder: __add__anstatt __iadd__weil __iadd__es nicht von ints implementiert wird). Unser endgültiger Code sieht folgendermaßen aus:

_a_iter = iter(enumerate(a))
while True:
    try:
        index, i = next(_a_iter)
    except StopIteration:
        break
    a.__setitem__(index, iadd(a.__getitem__(index), 1))

Zur Beantwortung tatsächlich Ihre Frage, warum die erste ist die Liste nicht ändern , während der zweite Fall ist, in unserem ersten Schnipsel bekommen wir iaus next(_a_iter), was bedeutet , iwird eine sein int. Da int's nicht an Ort und Stelle geändert werden kann, i += 1ändert sich nichts an der Liste. In unserem zweiten Fall ändern wir wieder nicht intdie Liste, sondern ändern sie durch einen Aufruf __setitem__.

Der Grund für diese ganze aufwändige Übung ist, dass sie meiner Meinung nach die folgende Lektion über Python lehrt:

  1. Der Preis für die Lesbarkeit von Python liegt darin, dass es ständig diese magischen Double-Score-Methoden aufruft.
  2. Um wirklich einen Teil des Python-Codes verstehen zu können, müssen Sie diese Übersetzungen verstehen.

Die Methoden mit doppeltem Unterstrich sind eine Hürde für den Einstieg, aber für die Sicherung von Pythons Ruf als "lauffähiger Pseudocode" von entscheidender Bedeutung. Ein anständiger Python-Programmierer hat ein gründliches Verständnis für diese Methoden und wie sie aufgerufen werden, und definiert sie, wo immer dies sinnvoll ist.

Edit : @deltab korrigiert meine nachlässige Verwendung des Begriffs "Sammlung".


2
"iteratoren sind auch sammlungen" ist nicht ganz richtig: sie sind auch wiederholbar, aber sammlungen haben auch __len__and__contains__
deltab 30.01.17

2

+=Funktioniert unterschiedlich, je nachdem, ob der aktuelle Wert veränderlich oder unveränderlich ist . Dies war der Hauptgrund dafür, dass die Implementierung in Python lange auf sich warten ließ, da Python-Entwickler befürchteten, dass dies verwirrend sein könnte.

Wenn ies sich um ein int handelt, kann es nicht geändert werden, da ints unveränderlich sind. Wenn sich daher der Wert von ichanges ändert, muss es notwendigerweise auf ein anderes Objekt verweisen:

>>> i=3
>>> id(i)
14336296
>>> i+=1
>>> id(i)
14336272   # Other object

Wenn jedoch die linke Seite veränderlich ist , kann + = sie tatsächlich ändern. wie wenn es eine Liste ist:

>>> i=[]
>>> id(i)
140257231883944
>>> i+=[1]
>>> id(i)
140257231883944  # Still the same object!

iBezieht sich in Ihrer for-Schleife auf jedes Element der aReihe nach. Wenn dies ganze Zahlen sind, gilt der erste Fall, und das Ergebnis von i += 1muss sein, dass es sich auf ein anderes ganzzahliges Objekt bezieht. Die Liste hat anatürlich immer noch die gleichen Elemente, die sie immer hatte.


Ich verstehe nicht , diese Unterscheidung zwischen wandelbar und unveränderlichen Objekten: Wenn i = 1Sätze izu einem unveränderlichen Integer - Objekt, dann i = []setzen sollten iauf ein unveränderliche Liste Objekt. Mit anderen Worten, warum sind ganzzahlige Objekte unveränderlich und Listenobjekte veränderlich? Dahinter sehe ich keine Logik.
Giorgio

@Giorgio: Die Objekte stammen aus verschiedenen Klassen, listimplementiert Methoden, die ihren Inhalt ändern, intnicht. [] ist Objekt ein veränderliches Liste und i = []kann iauf dieses Objekt beziehen.
RemcoGerlich

@Giorgio In Python gibt es keine unveränderliche Liste. Listen sind veränderlich. Ganzzahlen sind nicht. Wenn Sie etwas wie eine Liste, aber unveränderlich wollen, betrachten Sie ein Tupel. Es ist nicht klar, auf welchem ​​Level das beantwortet werden soll.
Jonrsharpe

@RemcoGerlich: Ich verstehe, dass sich verschiedene Klassen unterschiedlich verhalten, ich verstehe nicht, warum sie auf diese Weise implementiert wurden, dh ich verstehe die Logik hinter dieser Wahl nicht. Ich hätte den +=Operator / die Methode so implementiert , dass sie sich für beide Typen ähnlich verhält (Prinzip der geringsten Überraschung): entweder das ursprüngliche Objekt ändern oder eine geänderte Kopie für Ganzzahlen und Listen zurückgeben.
Giorgio

1
@ Giorgio: Es ist absolut richtig, dass +=es in Python überraschend ist, aber es wurde der Eindruck erweckt , dass die anderen Optionen, die Sie erwähnen, auch überraschend oder zumindest weniger praktisch gewesen wären (das Ändern des ursprünglichen Objekts kann nicht mit dem gebräuchlichsten Werttyp durchgeführt werden) Sie verwenden + = mit, ints. Und das Kopieren einer ganzen Liste ist viel teurer als das Mutieren. Python kopiert keine Dinge wie Listen und Wörterbücher, es sei denn, dies wird ausdrücklich dazu aufgefordert. Es war damals eine große Debatte.
RemcoGerlich

1

Die Schleife hier ist irgendwie irrelevant. Ähnlich wie bei Funktionsparametern oder -argumenten ist das Einrichten einer for-Schleife im Wesentlichen nur eine ausgefallene Zuweisung.

Ganzzahlen sind unveränderlich. Sie können sie nur ändern, indem Sie eine neue Ganzzahl erstellen und sie demselben Namen wie das Original zuweisen.

Die Semantik von Python für die Zuweisung wird direkt auf Cs abgebildet (nicht überraschend bei CPythons PyObject * -Pointern). Die einzige Einschränkung besteht darin, dass alles ein Zeiger ist und Sie keine doppelten Zeiger haben dürfen. Betrachten Sie den folgenden Code:

a = 1
b = a
b += 1
print(a)

Was geschieht? Es druckt 1. Warum? Es entspricht ungefähr dem folgenden C-Code:

i64* a = malloc(sizeof(i64));
*a = 1;
i64* b = a;
i64* tmp = malloc(sizeof(i64));
tmp = *b + 1;
b = tmp;
printf("%d\n", *a);

Im C-Code ist es offensichtlich, dass der Wert von überhaupt nicht abetroffen ist.

Was den Grund betrifft, warum Listen zu funktionieren scheinen, lautet die Antwort im Grunde nur, dass Sie denselben Namen zuweisen. Listen sind veränderlich. Die Identität des benannten Objekts a[0]ändert sich, es a[0]handelt sich jedoch weiterhin um einen gültigen Namen. Sie können dies mit dem folgenden Code überprüfen:

x = 1
a = [x]
print(a[0] is x)
a[0] += 1
print(a[0] is x)

Dies ist jedoch nichts Besonderes für Listen. Ersetzen Sie a[0]in diesem Code mit yund Sie erhalten genau das gleiche Ergebnis.

Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.