Zeiger in Python: Was ist der Sinn?

Zeiger in Python: Worum geht es?

Wenn Sie jemals mit untergeordneten Sprachen wie C oder C ++ gearbeitet haben, haben Sie wahrscheinlich von Zeigern gehört. Mit Zeigern können Sie in Teilen Ihres Codes eine hohe Effizienz erzielen. Sie sorgen auch bei Anfängern für Verwirrung und können selbst bei Experten zu verschiedenen Fehlern bei der Speicherverwaltung führen. Wo befinden sie sich in Python und wie können Sie Zeiger in Python simulieren?

Zeiger werden häufig in C und C ++ verwendet. Im Wesentlichen handelt es sich um Variablen, die die Speicheradresse einer anderen Variablen enthalten. Für eine Auffrischung der Zeiger können Sie dieseoverview on C Pointers. auschecken

In diesem Artikel erhalten Sie ein besseres Verständnis des Python-Objektmodells und erfahren, warum Zeiger in Python nicht wirklich vorhanden sind. In den Fällen, in denen Sie das Zeigerverhalten nachahmen müssen, lernen Sie, wie Sie Zeiger in Python ohne den Alptraum der Speicherverwaltung simulieren können.

In diesem Artikel werden Sie:

  • Erfahren Sie, warum Zeiger in Python nicht vorhanden sind

  • Untersuchen Sie den Unterschied zwischen C-Variablen und Python-Namen

  • Simulieren Sie Zeiger in Python

  • Experimentieren Sie mit realen Zeigern mitctypes

Note: In diesem Artikel bezieht sich „Python“ auf die Referenzimplementierung von Python in C, auch bekannt als CPython. Da der Artikel einige Interna der Sprache beschreibt, gelten diese Hinweise für CPython 3.7, möglicherweise jedoch nicht für zukünftige oder frühere Iterationen der Sprache.

Free Bonus:Click here to get access to a chapter from Python Tricks: The Book zeigt Ihnen die Best Practices von Python anhand einfacher Beispiele, die Sie sofort anwenden können, um schöneren + Pythonic-Code zu schreiben.

Warum hat Python keine Zeiger?

Die Wahrheit ist, dass ich es nicht weiß. Könnten Zeiger in Python nativ existieren? Wahrscheinlich, aber Zeiger scheinen gegen dieZen of Python zu gehen. Zeiger fördern eher implizite als explizite Änderungen. Besonders für Anfänger sind sie oft komplex statt einfach. Schlimmer noch, sie bitten um Möglichkeiten, sich in den Fuß zu schießen oder etwas wirklich Gefährliches zu tun, wie aus einem Teil der Erinnerung zu lesen, den Sie eigentlich nicht sollten.

Python neigt dazu, Implementierungsdetails wie Speicheradressen von seinen Benutzern zu abstrahieren. Python konzentriert sich oft auf Benutzerfreundlichkeit statt auf Geschwindigkeit. Daher sind Zeiger in Python nicht wirklich sinnvoll. Um nicht zu befürchten, bietet Python Ihnen standardmäßig einige der Vorteile der Verwendung von Zeigern.

Das Verständnis von Zeigern in Python erfordert einen kurzen Abstecher in die Implementierungsdetails von Python. Insbesondere müssen Sie Folgendes verstehen:

  1. Unveränderliche gegen veränderbare Objekte

  2. Python-Variablen / Namen

Halten Sie Ihre Speicheradressen fest und legen Sie los.

Objekte in Python

In Python ist alles ein Objekt. Zum Beweis können Sie eine REPL öffnen und mitisinstance() erkunden:

>>>

>>> isinstance(1, object)
True
>>> isinstance(list(), object)
True
>>> isinstance(True, object)
True
>>> def foo():
...     pass
...
>>> isinstance(foo, object)
True

Dieser Code zeigt Ihnen, dass alles in Python tatsächlich ein Objekt ist. Jedes Objekt enthält mindestens drei Daten:

  • Referenzanzahl

  • Type

  • Wert

Dasreference count dient zur Speicherverwaltung. Um einen detaillierten Überblick über die Interna der Speicherverwaltung in Python zu erhalten, können SieMemory Management in Python lesen.

Der Typ wird auf der CPython-Ebene verwendet, um die Typensicherheit zur Laufzeit zu gewährleisten. Schließlich gibt es den Wert, der der tatsächliche Wert ist, der dem Objekt zugeordnet ist.

Allerdings sind nicht alle Objekte gleich. Es gibt noch einen weiteren wichtigen Unterschied, den Sie verstehen müssen: unveränderliche und veränderbare Objekte. Das Verständnis des Unterschieds zwischen den Objekttypen hilft wirklich dabei, die erste Schicht der Zwiebel zu verdeutlichen, die Zeiger in Python sind.

Unveränderliche vs veränderliche Objekte

In Python gibt es zwei Arten von Objekten:

  1. Immutable objects können nicht geändert werden.

  2. Mutable objects kann geändert werden.

Das Verständnis dieses Unterschieds ist der erste Schlüssel zum Navigieren in der Zeigerlandschaft in Python. Hier ist eine Aufschlüsselung der gängigen Typen und ob sie veränderlich oder unveränderlich sind oder nicht:

Type Unveränderlich?

int

Yes

float

Yes

bool

Yes

complex

Yes

tuple

Yes

frozenset

Yes

str

Yes

list

No

set

No

dict

No

Wie Sie sehen können, sind viele häufig verwendete primitive Typen unveränderlich. Sie können dies selbst beweisen, indem Sie Python schreiben. Sie benötigen einige Tools aus der Python-Standardbibliothek:

  1. id() gibt die Speicheradresse des Objekts zurück.

  2. is gibt genau dannTrue zurück, wenn zwei Objekte dieselbe Speicheradresse haben.

Sie können diese wieder in einer REPL-Umgebung verwenden:

>>>

>>> x = 5
>>> id(x)
94529957049376

Im obigen Code haben Siex den Wert5 zugewiesen. Wenn Sie versucht haben, diesen Wert durch Hinzufügen zu ändern, erhalten Sie ein neues Objekt:

>>>

>>> x += 1
>>> x
6
>>> id(x)
94529957049408

Obwohl der obige Code den Wert vonx zu ändern scheint, erhalten Sie als Antwort einnew-Objekt.

Der Typstr ist ebenfalls unveränderlich:

>>>

>>> s = "real_python"
>>> id(s)
140637819584048
>>> s += "_rocks"
>>> s
'real_python_rocks'
>>> id(s)
140637819609424

Wiederum erhälts nach der Operation von+= eine Speicheradresse vondifferent.

Bonus: Der Operator+= übersetzt in verschiedene Methodenaufrufe.

Bei einigen Objekten wielist wird+= in__iadd__() übersetzt (direktes Hinzufügen). Dadurch werdenself geändert und dieselbe ID zurückgegeben. str undint verfügen jedoch nicht über diese Methoden und führen zu__add__() Aufrufen anstelle von__iadd__().

Weitere Informationen finden Sie in Pythonhttps://docs.python.org/3/reference/datamodel.html#object.iadd [Datenmodelldokumente.]

Der Versuch, die Zeichenfolges direkt zu mutieren, führt zu einem Fehler:

>>>

>>> s[0] = "R"
Traceback (most recent call last):
  File "", line 1, in 
TypeError: 'str' object does not support item assignment

Der obige Code schlägt fehl und Python gibt an, dassstr diese Mutation nicht unterstützt. Dies entspricht der Definition, dass der Typstrunveränderlich ist.

Vergleichen Sie das mit einem veränderlichen Objekt wielist:

>>>

>>> my_list = [1, 2, 3]
>>> id(my_list)
140637819575368
>>> my_list.append(4)
>>> my_list
[1, 2, 3, 4]
>>> id(my_list)
140637819575368

Dieser Code zeigt einen großen Unterschied zwischen den beiden Objekttypen. my_list hat ursprünglich eine ID. Selbst nachdem4 an die Liste angehängt wurde, hatmy_list die IDsame. Dies liegt daran, dass der Typlistveränderlich ist.

Eine andere Möglichkeit zu demonstrieren, dass die Liste veränderlich ist, ist die Zuweisung:

>>>

>>> my_list[0] = 0
>>> my_list
[0, 2, 3, 4]
>>> id(my_list)
140637819575368

In diesem Code mutieren Siemy_list und setzen das erste Element auf0. Die ID bleibt jedoch auch nach dieser Zuweisung erhalten. Da veränderbare und unveränderliche Objekte nicht im Weg sind, besteht der nächste Schritt auf Ihrem Weg zuPython enlightenmentdarin, das variable Ökosystem von Python zu verstehen.

Variablen verstehen

Python-Variablen unterscheiden sich grundlegend von Variablen in C oder C ++. Tatsächlich hat Python nicht einmal Variablen. Python has names, not variables.

Dies mag pedantisch erscheinen und ist es größtenteils. Meistens ist es durchaus akzeptabel, sich Python-Namen als Variablen vorzustellen, aber es ist wichtig, den Unterschied zu verstehen. Dies gilt insbesondere dann, wenn Sie in Python durch das schwierige Thema der Zeiger navigieren.

Um den Unterschied besser zu verstehen, können Sie sich ansehen, wie Variablen in C funktionieren, was sie darstellen, und dies dann mit der Funktionsweise von Namen in Python vergleichen.

Variablen in C.

Angenommen, Sie hatten den folgenden Code, der die Variablex definiert:

int x = 2337;

Diese eine Codezeile weist bei der Ausführung mehrere unterschiedliche Schritte auf:

  1. Weisen Sie genügend Speicher für eine Ganzzahl zu

  2. Weisen Sie diesem Speicherort den Wert2337 zu

  3. Geben Sie an, dassx auf diesen Wert zeigt

In einer vereinfachten Ansicht des Gedächtnisses könnte es so aussehen:

In-Memory representation of X (2337)

Hier sehen Sie, dass die Variablex einen falschen Speicherort von0x7f1 und den Wert2337 hat. Wenn Sie später im Programm den Wert vonx ändern möchten, können Sie Folgendes tun:

x = 2338;

Der obige Code weist der Variablenx einen neuen Wert (2338) zu, wodurchoverwriting der vorherige Wert ist. Das bedeutet, dass die Variablexmutable ist. Das aktualisierte Speicherlayout zeigt den neuen Wert:

New In-Memory representation of X (2338)

Beachten Sie, dass sich die Position vonxnicht geändert hat, sondern nur der Wert selbst. Dies ist ein wichtiger Punkt. Dies bedeutet, dassxis the memory location, nicht nur ein Name dafür.

Eine andere Art, dieses Konzept zu betrachten, ist das Eigentum. In gewisser Hinsicht besitztx den Speicherort. x ist zunächst ein leeres Feld, das genau zu einer Ganzzahl passt, in der Ganzzahlwerte gespeichert werden können.

Wenn Siex einen Wert zuweisen, geben Sie einen Wert in das Feld ein, dasx besitzt. Wenn Sie eine neue Variable (y) einführen möchten, können Sie diese Codezeile hinzufügen:

int y = x;

Dieser Code erstellt einenew-Box mit dem Nameny und kopiert den Wert vonx in die Box. Jetzt sieht das Speicherlayout folgendermaßen aus:

In-Memory representation of X (2338) and Y (2338)

Beachten Sie die neue Position0x7f5 vony. Obwohl der Wert vonx nachy kopiert wurde, besitzt die Variabley eine neue Adresse im Speicher. Daher können Sie den Wert vony überschreiben, ohnex zu beeinflussen:

y = 2339;

Jetzt sieht das Speicherlayout folgendermaßen aus:

Updated representation of Y (2339)

Auch hier haben Sie den Wert beiy geändert, abernot an seiner Position. Darüber hinaus haben Sie die ursprüngliche Variablexüberhaupt nicht beeinflusst. Dies steht in krassem Gegensatz zu der Funktionsweise von Python-Namen.

Namen in Python

Python hat keine Variablen. Es hat Namen. Ja, dies ist ein pedantischer Punkt, und Sie können den Begriff Variablen durchaus so oft verwenden, wie Sie möchten. Es ist wichtig zu wissen, dass es einen Unterschied zwischen Variablen und Namen gibt.

Nehmen wir den entsprechenden Code aus dem obigen C-Beispiel und schreiben ihn in Python:

>>>

>>> x = 2337

Ähnlich wie in C wird der obige Code während der Ausführung in mehrere verschiedene Schritte unterteilt:

  1. Erstellen Sie einPyObject

  2. Setzen Sie den Typcode fürPyObject auf Integer

  3. Setzen Sie den Wert fürPyObject auf2337

  4. Erstellen Sie einen Namen mit dem Namenx

  5. Zeigen Siex auf die neuenPyObject

  6. Erhöhen Sie die Anzahl derPyObject um 1

Note:PyObject ist nicht dasselbe wie Pythonsobject. Es ist spezifisch für CPython und stellt die Basisstruktur für alle Python-Objekte dar.

PyObject ist als C-Struktur definiert. Wenn Sie sich also fragen, warum Sietypecode oderrefcount nicht direkt aufrufen können, liegt dies daran, dass Sie keinen direkten Zugriff auf die Strukturen haben. Methodenaufrufe wiesys.getrefcount() können dabei helfen, einige Interna zu erhalten.

Im Gedächtnis könnte es ungefähr so ​​aussehen:

Python In-Memory representation of X (2337)

Sie können sehen, dass sich das Speicherlayout stark vom vorherigen C-Layout unterscheidet. Anstattx den Speicherblock zu besitzen, in dem sich der Wert2337 befindet, besitzt das neu erstellte Python-Objekt den Speicher, in dem2337 lebt. Der Python-Namexbesitzt nicht direkt die Speicheradresse vonany, so wie die C-Variablexeinen statischen Speicherplatz im Speicher besaß.

Wenn Sie versuchen,x einen neuen Wert zuzuweisen, können Sie Folgendes versuchen:

>>>

>>> x = 2338

Was hier passiert, unterscheidet sich vom C-Äquivalent, aber nicht zu stark von der ursprünglichen Bindung in Python.

Dieser Code:

  • Erstellt ein neuesPyObject

  • Setzt den Typcode fürPyObject auf Integer

  • Setzt den Wert fürPyObject auf2338

  • Zeigtx auf die neuenPyObject

  • Erhöht die Anzahl der neuenPyObject um 1

  • Verringert die Anzahl der altenPyObject um 1

Jetzt in Erinnerung würde es ungefähr so ​​aussehen:

Python Name Pointing to new object (2338)

Dieses Diagramm veranschaulicht, dassx auf eine Referenz auf ein Objekt verweist und den Speicherplatz nicht wie zuvor besitzt. Es zeigt auch, dass der Befehlx = 2338 keine Zuweisung ist, sondern den Namenx an eine Referenz bindet.

Außerdem befindet sich das vorherige Objekt (das den Wert2337 enthielt) jetzt mit einer Referenzanzahl von 0 im Speicher und wird vongarbage collector bereinigt.

Sie können der Mischung einen neuen Namen,y, wie im C-Beispiel hinzufügen:

>>>

>>> y = x

Im Speicher hätten Sie einen neuen Namen, aber nicht unbedingt ein neues Objekt:

X and Y Names pointing to 2338

Jetzt können Sie sehen, dass für ein neues Python-Objektnot erstellt wurde, nur ein neuer Name, der auf dasselbe Objekt verweist. Außerdem wurde die Nachzählung des Objekts um eins erhöht. Sie können die Gleichheit der Objektidentität überprüfen, um zu bestätigen, dass sie identisch sind:

>>>

>>> y is x
True

Der obige Code zeigt an, dassx undy dasselbe Objekt sind. Machen Sie aber keinen Fehler:y ist immer noch unveränderlich.

Sie können beispielsweisey addieren:

>>>

>>> y += 1
>>> y is x
False

Nach dem Additionsaufruf erhalten Sie ein neues Python-Objekt zurück. Nun sieht die Erinnerung so aus:

x name and y name different objects

Ein neues Objekt wurde erstellt undy zeigt jetzt auf das neue Objekt. Interessanterweise ist dies der gleiche Endzustand, wenn Siey direkt an2339 gebunden haben:

>>>

>>> y = 2339

Die obige Anweisung führt zu demselben Endspeicherzustand wie die Addition. Zusammenfassend lässt sich sagen, dass Sie in Python keine Variablen zuweisen. Stattdessen binden Sie Namen an Referenzen.

Ein Hinweis zu internen Objekten in Python

Nachdem Sie nun verstanden haben, wie Python-Objekte erstellt und Namen an diese Objekte gebunden werden, ist es an der Zeit, einen Schraubenschlüssel in die Maschinerie zu werfen. Dieser Schraubenschlüssel heißt internierte Objekte.

Angenommen, Sie haben den folgenden Python-Code:

>>>

>>> x = 1000
>>> y = 1000
>>> x is y
True

Wie oben sindx undy beide Namen, die auf dasselbe Python-Objekt verweisen. Es ist jedoch nicht immer garantiert, dass das Python-Objekt, das den Wert1000 enthält, dieselbe Speicheradresse hat. Wenn Sie beispielsweise zwei Zahlen addieren, um1000 zu erhalten, erhalten Sie eine andere Speicheradresse:

>>>

>>> x = 1000
>>> y = 499 + 501
>>> x is y
False

Dieses Mal gibt die Zeilex is yFalse zurück. Wenn dies verwirrend ist, machen Sie sich keine Sorgen. Hier sind die Schritte, die auftreten, wenn dieser Code ausgeführt wird:

  1. Python-Objekt erstellen (1000)

  2. Weisen Sie diesem Objekt den Namenx zu

  3. Python-Objekt erstellen (499)

  4. Python-Objekt erstellen (501)

  5. Addieren Sie diese beiden Objekte

  6. Erstellen Sie ein neues Python-Objekt (1000)

  7. Weisen Sie diesem Objekt den Nameny zu

Technical Note: Die obigen Schritte werden nur ausgeführt, wenn dieser Code in einer REPL ausgeführt wird. Wenn Sie das obige Beispiel nehmen, es in eine Datei einfügen und die Datei ausführen würden, würden Sie feststellen, dass die Zeilex is yTrue zurückgibt.

Dies liegt daran, dass Compiler intelligent sind. Der CPython-Compiler versucht, Optimierungen mit dem Namenpeephole optimizations vorzunehmen, um Ausführungsschritte nach Möglichkeit zu speichern. Ausführliche Informationen finden Sie unterCPython’s peephole optimizer source code.

Ist das nicht verschwenderisch? Ja, das ist es, aber das ist der Preis, den Sie für all die großartigen Vorteile von Python zahlen. Sie müssen sich nie um die Reinigung dieser Zwischenobjekte kümmern oder müssen sogar wissen, dass sie existieren! Die Freude ist, dass diese Operationen relativ schnell sind und Sie bis jetzt keine dieser Details kennen mussten.

Die Kernentwickler von Python bemerkten nach ihrer Weisheit auch diese Verschwendung und beschlossen, einige Optimierungen vorzunehmen. Diese Optimierungen führen zu Verhaltensweisen, die für Neulinge überraschend sein können:

>>>

>>> x = 20
>>> y = 19 + 1
>>> x is y
True

In diesem Beispiel sehen Sie fast den gleichen Code wie zuvor, außer dass diesmal das ErgebnisTrue ist. Dies ist das Ergebnis von internierten Objekten. Python erstellt eine bestimmte Teilmenge von Objekten im Speicher vorab und speichert sie für den täglichen Gebrauch im globalen Namespace.

Welche Objekte von der Implementierung von Python abhängen. CPython 3.7 praktiziert Folgendes:

  1. Ganzzahlige Zahlen zwischen-5 und256

  2. Zeichenfolgen, die nur ASCII-Buchstaben, Ziffern oder Unterstriche enthalten

Der Grund dafür ist, dass diese Variablen höchstwahrscheinlich in vielen Programmen verwendet werden. Durch das Internieren dieser Objekte verhindert Python Speicherzuweisungsaufrufe für konsistent verwendete Objekte.

Zeichenfolgen mit weniger als 20 Zeichen, die ASCII-Buchstaben, Ziffern oder Unterstriche enthalten, werden interniert. Der Grund dafür ist, dass davon ausgegangen wird, dass es sich um eine Art Identität handelt:

>>>

>>> s1 = "realpython"
>>> id(s1)
140696485006960
>>> s2 = "realpython"
>>> id(s2)
140696485006960
>>> s1 is s2
True

Hier sehen Sie, dasss1 unds2 beide auf dieselbe Adresse im Speicher zeigen. Wenn Sie einen Nicht-ASCII-Buchstaben, eine Ziffer oder einen Unterstrich einführen würden, würden Sie ein anderes Ergebnis erhalten:

>>>

>>> s1 = "Real Python!"
>>> s2 = "Real Python!"
>>> s1 is s2
False

Da dieses Beispiel ein Ausrufezeichen (!) enthält, werden diese Zeichenfolgen nicht interniert und sind unterschiedliche Objekte im Speicher.

Bonus: Wenn Sie wirklich möchten, dass diese Objekte auf dasselbe interne Objekt verweisen, sollten Siesys.intern() auschecken. Einer der Anwendungsfälle für diese Funktion ist in der Dokumentation beschrieben:

Das Internieren von Zeichenfolgen ist nützlich, um ein wenig Leistung bei der Wörterbuchsuche zu erzielen. Wenn die Schlüssel in einem Wörterbuch interniert sind und der Suchschlüssel interniert ist, können die Schlüsselvergleiche (nach dem Hashing) durch einen Zeigervergleich anstelle eines Zeichenfolgenvergleichs durchgeführt werden. (Source)

Internierte Objekte sorgen oft für Verwirrung. Denken Sie daran, wenn Sie jemals Zweifel haben, dass Sie immerid() undis verwenden können, um die Objektgleichheit zu bestimmen.

Zeiger in Python simulieren

Nur weil Zeiger in Python nicht nativ vorhanden sind, bedeutet dies nicht, dass Sie die Vorteile der Verwendung von Zeigern nicht nutzen können. Tatsächlich gibt es in Python mehrere Möglichkeiten, Zeiger zu simulieren. In diesem Abschnitt lernen Sie zwei davon:

  1. Verwenden veränderlicher Typen als Zeiger

  2. Verwenden von benutzerdefinierten Python-Objekten

Okay, kommen wir zum Punkt.

Verwenden veränderlicher Typen als Zeiger

Sie haben bereits etwas über veränderbare Typen gelernt. Da diese Objekte veränderbar sind, können Sie sie so behandeln, als wären sie Zeiger, um das Zeigerverhalten zu simulieren. Angenommen, Sie möchten den folgenden c-Code replizieren:

void add_one(int *x) {
    *x += 1;
}

Dieser Code nimmt einen Zeiger auf eine Ganzzahl (*x) und erhöht dann den Wert um eins. Hier ist eine Hauptfunktion zum Ausüben des Codes:

#include 

int main(void) {
    int y = 2337;
    printf("y = %d\n", y);
    add_one(&y);
    printf("y = %d\n", y);
    return 0;
}

Im obigen Code weisen Sie2337y zu, drucken den aktuellen Wert aus, erhöhen den Wert um eins und drucken dann den geänderten Wert aus. Die Ausgabe der Ausführung dieses Codes wäre die folgende:

y = 2337
y = 2338

Eine Möglichkeit, diese Art von Verhalten in Python zu replizieren, ist die Verwendung eines veränderlichen Typs. Verwenden Sie eine Liste und ändern Sie das erste Element:

>>>

>>> def add_one(x):
...     x[0] += 1
...
>>> y = [2337]
>>> add_one(y)
>>> y[0]
2338

Hier greiftadd_one(x) auf das erste Element zu und erhöht seinen Wert um eins. Die Verwendung vonlist bedeutet, dass das Endergebnis offenbar den Wert verändert hat. Es gibt also Zeiger in Python? Nun, nein. Dies ist nur möglich, weillist ein veränderlicher Typ ist. Wenn Sie versuchen,tuple zu verwenden, wird folgende Fehlermeldung angezeigt:

>>>

>>> z = (2337,)
>>> add_one(z)
Traceback (most recent call last):
  File "", line 1, in 
  File "", line 2, in add_one
TypeError: 'tuple' object does not support item assignment

Der obige Code zeigt, dasstuple unveränderlich ist. Daher wird die Artikelzuweisung nicht unterstützt. list ist nicht der einzige veränderbare Typ. Ein weiterer gängiger Ansatz zur Nachahmung von Zeigern in Python ist die Verwendung vondict.

Angenommen, Sie hatten eine Anwendung, in der Sie jedes Mal, wenn ein interessantes Ereignis eintrat, den Überblick behalten wollten. Eine Möglichkeit, dies zu erreichen, besteht darin, eindictzu erstellen und eines der Elemente als Zähler zu verwenden:

>>>

>>> counters = {"func_calls": 0}
>>> def bar():
...     counters["func_calls"] += 1
...
>>> def foo():
...     counters["func_calls"] += 1
...     bar()
...
>>> foo()
>>> counters["func_calls"]
2

In diesem Beispiel wird das Wörterbuchcountersverwendet, um die Anzahl der Funktionsaufrufe zu verfolgen. Nachdem Siefoo() aufgerufen haben, hat sich der Zähler erwartungsgemäß auf2 erhöht. Alles nur, weildict veränderlich ist.

Beachten Sie, dass dies nur das Zeigerverhalten vonsimulatesist und nicht direkt echten Zeigern in C oder C zugeordnet wird. Das heißt, diese Operationen sind teurer als in C oder C.

Verwenden von Python-Objekten

Die Optiondict ist eine großartige Möglichkeit, Zeiger in Python zu emulieren, aber manchmal wird es mühsam, sich den von Ihnen verwendeten Schlüsselnamen zu merken. Dies gilt insbesondere dann, wenn Sie das Wörterbuch in verschiedenen Teilen Ihrer Anwendung verwenden. Hier kann eine benutzerdefinierte Python-Klasse wirklich helfen.

Um auf dem letzten Beispiel aufzubauen, nehmen Sie an, dass Sie Metriken in Ihrer Anwendung verfolgen möchten. Das Erstellen einer Klasse ist eine großartige Möglichkeit, die lästigen Details zu abstrahieren:

class Metrics(object):
    def __init__(self):
        self._metrics = {
            "func_calls": 0,
            "cat_pictures_served": 0,
        }

Dieser Code definiert die KlasseMetrics. Diese Klasse verwendet immer noch eindict, um die tatsächlichen Daten zu speichern, die sich in der Mitgliedsvariablen_metricsbefinden. Dies gibt Ihnen die Veränderlichkeit, die Sie benötigen. Jetzt müssen Sie nur noch auf diese Werte zugreifen können. Ein guter Weg, dies zu tun, ist mit Eigenschaften:

class Metrics(object):
    # ...

    @property
    def func_calls(self):
        return self._metrics["func_calls"]

    @property
    def cat_pictures_served(self):
        return self._metrics["cat_pictures_served"]

Dieser Code verwendet@property. Wenn Sie mit Dekorateuren nicht vertraut sind, können Sie diesePrimer on Python Decorators überprüfen. Mit dem Dekorator@property können Sie auffunc_calls undcat_pictures_served zugreifen, als wären sie Attribute:

>>>

>>> metrics = Metrics()
>>> metrics.func_calls
0
>>> metrics.cat_pictures_served
0

Die Tatsache, dass Sie auf diese Namen als Attribute zugreifen können, bedeutet, dass Sie die Tatsache abstrahiert haben, dass diese Werte indict angegeben sind. Sie machen es auch expliziter, wie die Namen der Attribute lauten. Natürlich müssen Sie in der Lage sein, diese Werte zu erhöhen:

class Metrics(object):
    # ...

    def inc_func_calls(self):
        self._metrics["func_calls"] += 1

    def inc_cat_pics(self):
        self._metrics["cat_pictures_served"] += 1

Sie haben zwei neue Methoden eingeführt:

  1. inc_func_calls()

  2. inc_cat_pics()

Diese Methoden ändern die Werte in den Metrikendict. Sie haben jetzt eine Klasse, die Sie so ändern, als würden Sie einen Zeiger ändern:

>>>

>>> metrics = Metrics()
>>> metrics.inc_func_calls()
>>> metrics.inc_func_calls()
>>> metrics.func_calls
2

Hier können Sie an verschiedenen Stellen in Ihren Anwendungen auffunc_calls zugreifen undinc_func_calls() aufrufen und Zeiger in Python simulieren. Dies ist nützlich, wenn Sie über Metriken verfügen, die in verschiedenen Teilen Ihrer Anwendungen häufig verwendet und aktualisiert werden müssen.

Note: Insbesondere in dieser Klasse verhindert das explizite Festlegen voninc_func_calls() undinc_cat_pics() anstelle von@property.setter, dass Benutzer diese Werte auf einen beliebigenint oder einen ungültigen Wert setzen wie eindict.

Hier ist die vollständige Quelle für die KlasseMetrics:

class Metrics(object):
    def __init__(self):
        self._metrics = {
            "func_calls": 0,
            "cat_pictures_served": 0,
        }

    @property
    def func_calls(self):
        return self._metrics["func_calls"]

    @property
    def cat_pictures_served(self):
        return self._metrics["cat_pictures_served"]

    def inc_func_calls(self):
        self._metrics["func_calls"] += 1

    def inc_cat_pics(self):
        self._metrics["cat_pictures_served"] += 1

Echte Zeiger mitctypes

Okay, vielleicht gibt es in Python Zeiger, speziell in CPython. Mit dem eingebautenctypes Modul können Sie echte C-Stil-Zeiger in Python erstellen. Wenn Sie mitctypes nicht vertraut sind, können Sie sichExtending Python With C Libraries and the “ctypes” Module ansehen.

Der wahre Grund, warum Sie dies verwenden würden, ist, wenn Sie einen Funktionsaufruf für eine C-Bibliothek durchführen müssen, für die ein Zeiger erforderlich ist. Kehren wir zur vorherigen C-Funktion vonadd_one()zurück:

void add_one(int *x) {
    *x += 1;
}

Auch hier erhöht dieser Code den Wert vonx um eins. Um dies zu verwenden, kompilieren Sie es zuerst in ein freigegebenes Objekt. Angenommen, die obige Datei ist inadd.c gespeichert, können Sie dies mitgcc erreichen:

$ gcc -c -Wall -Werror -fpic add.c
$ gcc -shared -o libadd1.so add.o

Der erste Befehl kompiliert die C-Quelldatei in ein Objekt namensadd.o. Der zweite Befehl verwendet diese nicht verknüpfte Objektdatei und erstellt ein freigegebenes Objekt mit dem Namenlibadd1.so.

libadd1.so sollte sich in Ihrem aktuellen Verzeichnis befinden. Sie können es mitctypes in Python laden:

>>>

>>> import ctypes
>>> add_lib = ctypes.CDLL("./libadd1.so")
>>> add_lib.add_one
<_FuncPtr object at 0x7f9f3b8852a0>

Der Code vonctypes.CDLLgibt ein Objekt zurück, das das gemeinsam genutzte Objekt vonlibadd1darstellt. Da Sie in diesem freigegebenen Objektadd_one() definiert haben, können Sie darauf zugreifen, als wäre es ein anderes Python-Objekt. Bevor Sie die Funktion aufrufen, sollten Sie die Funktionssignatur angeben. Auf diese Weise stellt Python sicher, dass Sie den richtigen Typ an die Funktion übergeben.

In diesem Fall ist die Funktionssignatur ein Zeiger auf eine Ganzzahl. Mitctypes können Sie dies mit dem folgenden Code angeben:

>>>

>>> add_one = add_lib.add_one
>>> add_one.argtypes = [ctypes.POINTER(ctypes.c_int)]

In diesem Code stellen Sie die Funktionssignatur so ein, dass sie den Erwartungen von C entspricht. Wenn Sie nun versuchen würden, diesen Code mit dem falschen Typ aufzurufen, erhalten Sie eine nette Warnung anstelle eines undefinierten Verhaltens:

>>>

>>> add_one(1)
Traceback (most recent call last):
  File "", line 1, in 
ctypes.ArgumentError: argument 1: : \
expected LP_c_int instance instead of int

Python gibt einen Fehler aus und erklärt, dassadd_one() einen Zeiger anstelle einer ganzen Zahl möchte. Glücklicherweise hatctypes die Möglichkeit, Zeiger auf diese Funktionen zu übergeben. Deklarieren Sie zunächst eine Ganzzahl im C-Stil:

>>>

>>> x = ctypes.c_int()
>>> x
c_int(0)

Der obige Code erstellt eine Ganzzahlx im C-Stil mit einem Wert von0. ctypes bietet die praktischenbyref(), mit denen eine Variable als Referenz übergeben werden kann.

Note: Der Ausdruckby reference widerspricht der Übergabe einer Variablenby value.

Wenn Sie als Referenz übergeben, übergeben Sie die Referenz an die ursprüngliche Variable. Änderungen werden daher in der ursprünglichen Variablen berücksichtigt. Das Übergeben eines Werts führt zu einer Kopie der ursprünglichen Variablen, und Änderungen werden im Original nicht berücksichtigt.

Sie können dies verwenden, umadd_one() aufzurufen:

>>>

>>> add_one(ctypes.byref(x))
998793640
>>> x
c_int(1)

Nett! Ihre Ganzzahl wurde um eins erhöht. Herzlichen Glückwunsch, Sie haben in Python erfolgreich echte Zeiger verwendet.

Fazit

Sie haben jetzt ein besseres Verständnis für den Schnittpunkt zwischen Python-Objekten und Zeigern. Obwohl einige der Unterscheidungen zwischen Namen und Variablen pedantisch erscheinen, erweitert das grundlegende Verständnis dieser Schlüsselbegriffe Ihr Verständnis dafür, wie Python mit Variablen umgeht.

Sie haben auch einige hervorragende Möglichkeiten zum Simulieren von Zeigern in Python kennengelernt:

  • Verwendung veränderbarer Objekte als Zeiger mit geringem Overhead

  • Erstellen von benutzerdefinierten Python-Objekten zur Vereinfachung der Verwendung

  • Entsperren von echten Zeigern mit dem Modulctypes

Mit diesen Methoden können Sie Zeiger in Python simulieren, ohne die von Python bereitgestellte Speichersicherheit zu beeinträchtigen.

Danke fürs Lesen. Wenn Sie noch Fragen haben, können Sie sich entweder im Kommentarbereich oder auf Twitter an uns wenden.