Fortsetzung folgt. Beginnend in Python als ultimativer Fall von C ++. Teil 1/2 ".
Variablen und Datentypen
Nachdem wir endlich die Mathematik herausgefunden haben, wollen wir entscheiden, was Variablen in unserer Sprache bedeuten sollen.
In C ++ hat ein Programmierer die Wahl: Verwenden Sie automatische Variablen, die auf dem Stapel platziert sind, oder behalten Sie Werte im Programmdatenspeicher bei, indem Sie nur Zeiger auf diese Werte auf dem Stapel platzieren. Was ist, wenn wir nur eine dieser Optionen für Python auswählen?
Natürlich können wir nicht immer nur die Werte von Variablen verwenden, da große Datenstrukturen nicht auf den Stapel passen oder ihre ständige Bewegung auf dem Stapel zu Leistungsproblemen führt. Daher werden in Python nur Zeiger verwendet. Dies wird die Sprache konzeptionell vereinfachen.
Also der Ausdruck
a = 3
bedeutet, dass wir im Programmdatenspeicher ein Objekt „3“ erstellt haben (den sogenannten „Heap“) und den Namen „a“ zu einem Verweis darauf gemacht haben. Und der Ausdruck
b = a
In diesem Fall bedeutet dies, dass wir die Variable "b" gezwungen haben, auf dasselbe Objekt im Speicher zu verweisen, auf das sich "a" bezieht. Mit anderen Worten, wir haben den Zeiger kopiert.
Wenn alles ein Zeiger ist, wie viele Listentypen müssen wir dann in unserer Sprache implementieren? Natürlich ist nur einer eine Liste von Zeigern! Sie können damit Ganzzahlen, Zeichenfolgen und andere Listen speichern - schließlich handelt es sich hierbei um Zeiger.
Wie viele Arten von Hash-Tabellen müssen wir implementieren? (In Python wird dieser Typ als "Wörterbuch" bezeichnet.) Eins! Lassen Sie Zeiger auf Schlüssel mit Zeigern auf Werte verknüpfen.
Daher müssen wir keinen großen Teil der C ++ - Spezifikationsvorlagen in unserer Sprache implementieren, da wir alle Operationen an Objekten ausführen und auf Objekte immer per Zeiger zugegriffen werden kann. Natürlich müssen sich in Python geschriebene Programme nicht auf die Arbeit mit Zeigern beschränken: Es gibt Bibliotheken wie NumPy, mit denen Wissenschaftler wie in Fortran mit Datenfeldern im Speicher arbeiten können. Die Basis der Sprache - Ausdrücke wie „a = 3“ - funktioniert jedoch immer mit Zeigern.
Das Konzept „Alles ist ein Zeiger“ vereinfacht auch die Zusammensetzung von Typen bis an die Grenzen. Möchten Sie eine Liste mit Wörterbüchern? Erstellen Sie einfach eine Liste und legen Sie dort Wörterbücher ab! Sie müssen Python nicht um Erlaubnis bitten, Sie müssen keine zusätzlichen Typen deklarieren, alles funktioniert sofort.
Aber was ist, wenn wir zusammengesetzte Objekte als Schlüssel verwenden möchten? Der Schlüssel im Wörterbuch muss einen unveränderlichen Wert haben. Wie kann man sonst nach Werten suchen? Listen können sich ändern, daher können sie nicht in dieser Funktion verwendet werden. In solchen Situationen hat Python einen Datentyp, der wie eine Liste eine Folge von Objekten ist, aber im Gegensatz zu einer Liste ändert sich diese Reihenfolge nicht. Dieser Typ wird als Tupel oder tuple
(ausgesprochen „Tupel“ oder „Tupel“).
Tupel in Python lösen ein langjähriges Problem mit der Skriptsprache. Wenn Sie von dieser Funktion nicht beeindruckt sind, haben Sie wahrscheinlich noch nie versucht, Skriptsprachen für ernsthafte Arbeiten mit Daten zu verwenden, bei denen Sie nur Zeichenfolgen oder nur primitive Typen als Schlüssel in Hash-Tabellen verwenden können.
Eine andere Möglichkeit, die uns Tupel geben, besteht darin, mehrere Werte von einer Funktion zurückzugeben, ohne dafür zusätzliche Datentypen deklarieren zu müssen, wie dies in C und C ++ der Fall ist. Um die Verwendung dieser Funktion zu vereinfachen, konnte der Zuweisungsoperator außerdem Tupel automatisch in separate Variablen entpacken.
def get_address(): ... return host, port host, port = get_address()
Das Auspacken hat mehrere nützliche Nebenwirkungen. Beispielsweise kann der Austausch von Variablenwerten wie folgt geschrieben werden:
x, y = y, x
Alles ist ein Zeiger, dh Funktionen und Datentypen können als Daten verwendet werden. Wenn Sie mit dem Buch „Design Patterns“ von „The Gang of Four“ vertraut sind, müssen Sie sich daran erinnern, welche komplexen und verwirrenden Methoden es bietet, um die Auswahl des von Ihrem Programm zur Laufzeit erstellten Objekttyps zu parametrisieren. In vielen Programmiersprachen ist dies in der Tat schwierig! In Python verschwinden all diese Schwierigkeiten, weil wir wissen, dass eine Funktion einen Datentyp zurückgeben kann, dass sowohl Funktionen als auch Datentypen nur Verknüpfungen sind und Verknüpfungen beispielsweise in Wörterbüchern gespeichert werden können. Dies vereinfacht die Aufgabe bis an die Grenzen.
David Wheeler sagte: "Alle Programmierprobleme werden durch die Schaffung einer zusätzlichen Indirektionsebene gelöst." Die Verwendung von Links in Python ist die Indirektionsebene, die traditionell verwendet wurde, um viele Probleme in vielen Sprachen, einschließlich C ++, zu lösen. Wenn es dort jedoch explizit verwendet wird und dies die Programme kompliziert, wird es in Python implizit, einheitlich in Bezug auf Daten aller Art verwendet und ist benutzerfreundlich.
Aber wenn alles ein Link ist, worauf beziehen sich diese Links dann? Sprachen wie C ++ haben viele Typen. Lassen wir in Python nur einen Datentyp - ein Objekt! Spezialisten auf dem Gebiet der Typentheorie schütteln missbilligend den Kopf, aber ich glaube, dass ein Quelldatentyp, von dem alle anderen Typen in der Sprache abgeleitet sind, eine gute Idee ist, die die Einheitlichkeit der Sprache und ihre Benutzerfreundlichkeit gewährleistet.
Für bestimmte Speicherinhalte können verschiedene Python-Implementierungen (PyPy, Jython oder MicroPython) den Speicher auf unterschiedliche Weise verwalten. Um jedoch besser zu verstehen, wie die Einfachheit und Einheitlichkeit von Python implementiert wird, um das richtige mentale Modell zu bilden, ist es besser, sich an die Python-Referenzimplementierung in C mit dem Namen CPython zu wenden, die wir auf python.org herunterladen können .
struct { struct _typeobject *ob_type; }
Was wir im CPython-Quellcode sehen werden, ist eine Struktur, die aus einem Zeiger auf Informationen über den Typ einer bestimmten Variablen und einer Nutzlast besteht, die den spezifischen Wert der Variablen definiert.
Wie funktionieren Typinformationen? Lassen Sie uns noch einmal in den CPython-Quellcode eintauchen.
struct _typeobject { getattrfunc tp_getattr; setattrfunc tp_setattr; newfunc tp_new; freefunc tp_free; binaryfunc nb_add; binaryfunc nb_subtract; richcmpfunc tp_richcompare; }
Wir sehen Zeiger auf Funktionen, die alle Operationen bereitstellen, die für einen bestimmten Typ möglich sind: Addition, Subtraktion, Vergleich, Zugriff auf Attribute, Indizierung, Slicing usw. Diese Operationen wissen, wie sie mit der im Speicher befindlichen Nutzlast arbeiten Unterhalb eines Zeigers auf Typinformationen, sei es eine Ganzzahl, eine Zeichenfolge oder ein Objekt eines vom Benutzer erstellten Typs.
Dies unterscheidet sich grundlegend von C und C ++, bei denen Typinformationen mit Namen verknüpft sind, nicht mit Werten von Variablen. In Python sind alle Namen mit Links verknüpft. Der Referenzwert ist wiederum vom Typ. Dies ist die Essenz dynamischer Sprachen.
Um alle Merkmale der Sprache zu realisieren, reicht es aus, zwei Operationen für Links zu definieren. Eines der offensichtlichsten ist das Kopieren. Wenn wir einer Variablen, einem Slot in einem Wörterbuch oder einem Attribut eines Objekts einen Wert zuweisen, kopieren wir die Links. Dies ist eine einfache, schnelle und absolut sichere Operation: Durch das Kopieren von Links wird der Inhalt des Objekts nicht geändert.
Die zweite Operation ist ein Funktions- oder Methodenaufruf. Wie oben gezeigt, kann ein Python-Programm nur über in integrierten Objekten implementierte Methoden mit dem Speicher interagieren. Daher kann es keinen Fehler im Zusammenhang mit einem Speicherzugriff verursachen.
Möglicherweise haben Sie eine Frage: Wenn alle Variablen Referenzen enthalten, wie kann ich dann den Wert einer Variablen vor Änderungen schützen, indem ich ihn als Parameter an die Funktion übergebe?
n = 3 some_function(n)
Die Antwort ist, dass einfache Typen in Python unveränderlich sind: Sie implementieren einfach nicht die Methode, die für die Änderung ihres Werts verantwortlich ist. Das unveränderliche (unveränderliche) int
, float
, tuple
oder str
bietet in Sprachen wie "alles ist ein Zeiger" den gleichen semantischen Effekt wie automatische Variablen in C.
Einheitliche Typen und Methoden vereinfachen die Verwendung von generalisierter Programmierung oder Generika so weit wie möglich. Die Funktionen min()
, max()
, sum()
und dergleichen sind integriert, sie müssen nicht importiert werden. Und sie funktionieren mit allen Datentypen, in denen Vergleichsoperationen für min()
und max()
implementiert sind, Additionen für sum()
usw.
Objekte erstellen
Wir haben allgemein herausgefunden, wie sich Objekte verhalten sollen. Jetzt werden wir bestimmen, wie wir sie erstellen werden. Dies ist eine Frage der Sprachsyntax. C ++ unterstützt mindestens drei Möglichkeiten zum Erstellen eines Objekts:
- Automatisch durch Deklarieren einer Variablen dieser Klasse:
my_class c(arg);
- Verwenden des
new
Operators:
my_class *c = new my_class(arg);
- Factory durch Aufrufen einer beliebigen Funktion, die einen Zeiger zurückgibt:
my_class *c = my_factory(arg);
Wie Sie wahrscheinlich bereits vermutet haben, müssen wir, nachdem wir in den obigen Beispielen die Denkweise der Schöpfer von Python studiert haben, eine davon auswählen.
Aus demselben Buch, The Gangs of Four, haben wir gelernt, dass eine Fabrik die flexibelste und universellste Art ist, Objekte zu erstellen. Daher ist in Python nur diese Methode implementiert.
Zusätzlich zur Universalität ist diese Methode insofern gut, als Sie die Sprache nicht mit unnötiger Syntax überladen müssen, um dies sicherzustellen: Ein Funktionsaufruf ist bereits in unserer Sprache implementiert, und eine Factory ist nichts anderes als eine Funktion.
Eine weitere Regel zum Erstellen von Objekten in Python lautet: Jeder Datentyp ist eine eigene Factory. Natürlich können Sie eine beliebige Anzahl zusätzlicher benutzerdefinierter Fabriken schreiben (dies sind natürlich gewöhnliche Funktionen oder Methoden), aber die allgemeine Regel bleibt gültig:
Alle Typen werden als Objekte bezeichnet, und alle geben Werte ihres Typs zurück, die durch die im Aufruf übergebenen Argumente bestimmt werden.
Wenn Sie also nur die grundlegende Syntax der Sprache verwenden, können alle Manipulationen beim Erstellen von Objekten, wie z. B. die Muster "Arena" oder "Anpassung", gekapselt werden, da eine weitere großartige Idee aus C ++ darin besteht, dass der Typ selbst bestimmt, wie dies geschieht Laichen seiner Objekte, wie der new
Operator für ihn arbeitet.
Wie wäre es mit NULL?
Die Behandlung eines Nullzeigers erhöht die Komplexität des Programms, sodass wir NULL verbieten. Die Python-Syntax macht es unmöglich, einen Nullzeiger zu erstellen. Zwei elementare Operationen an Zeigern, über die wir bereits gesprochen haben, sind so definiert, dass jede Variable auf ein Objekt zeigt.
Infolgedessen kann der Benutzer Python nicht verwenden, um einen Fehler im Zusammenhang mit einem Speicherzugriff zu erstellen, z. B. einen Segmentierungsfehler oder außerhalb der Puffergrenzen. Mit anderen Worten, Python-Programme sind in den letzten 20 Jahren nicht von den beiden gefährlichsten Arten von Sicherheitslücken betroffen, die die Sicherheit des Internets gefährden.
Sie können fragen: "Wenn die Struktur von Operationen an Objekten unverändert ist, wie wir zuvor gesehen haben, wie werden Benutzer dann ihre eigenen Klassen mit Methoden und Attributen erstellen, die in dieser Struktur nicht aufgeführt sind?"
Die Magie liegt in der Tatsache, dass Python für benutzerdefinierte Klassen eine sehr einfache "Vorbereitung" mit einer kleinen Anzahl von implementierten Methoden hat. Hier sind die wichtigsten:
struct _typeobject { getattrfunc tr_getattr; setattrfunc tr_setattr; newfunc tp_new; }
tp_new()
erstellt eine Hash-Tabelle für die Benutzerklasse, genau wie für den dict
. tp_getattr()
extrahiert etwas aus dieser Hash-Tabelle, und tp_setattr()
im Gegenteil etwas dort ab. Somit wird die Fähigkeit beliebiger Klassen, Methoden und Attribute zu speichern, nicht auf der Ebene der C-Sprachstrukturen bereitgestellt, sondern auf einer höheren Ebene - einer Hash-Tabelle. (Natürlich mit Ausnahme einiger Fälle im Zusammenhang mit der Leistungsoptimierung.)
Zugriffsmodifikatoren
Was machen wir mit all den Regeln und Konzepten, die auf private
und protected
C ++ - Schlüsselwörtern basieren? Python, eine Skriptsprache, benötigt sie nicht. Wir haben bereits "geschützte" Teile der Sprache - dies sind Daten von eingebauten Typen. Unter keinen Umständen erlaubt Python einem Programm, beispielsweise die Bits einer Gleitkommazahl zu manipulieren! Diese Kapselungsstufe reicht aus, um die Integrität der Sprache selbst aufrechtzuerhalten. Wir, die Entwickler von Python, glauben, dass Sprachintegrität der einzig gute Vorwand ist, um Informationen zu verbergen. Alle anderen Strukturen und Benutzerprogrammdaten gelten als öffentlich.
Sie können einen Unterstrich ( _
) am Anfang eines Klassenattributnamens schreiben, um einen Kollegen zu warnen: Sie sollten sich nicht auf dieses Attribut verlassen. Aber der Rest von Python hat die Lehren aus den frühen 90ern gezogen: Dann glaubten viele, dass der Hauptgrund, warum wir aufgeblähte, unlesbare und fehlerhafte Programme schreiben, das Fehlen privater Variablen ist. Ich denke, die nächsten 20 Jahre haben alle in der Programmierbranche überzeugt: Private Variablen sind nicht die einzigen und bei weitem nicht das wirksamste Mittel gegen aufgeblähte und fehlerhafte Programme. Daher haben die Entwickler von Python beschlossen, sich nicht einmal um private Variablen zu kümmern, und wie Sie sehen, sind sie nicht gescheitert.
Speicherverwaltung
Was passiert mit unseren Objekten, Zahlen und Zeichenfolgen auf einer niedrigeren Ebene? Wie genau werden sie im Speicher gespeichert, wie bietet CPython ihnen gemeinsamen Zugriff, wann und unter welchen Bedingungen werden sie zerstört?
In diesem Fall haben wir die allgemeinste, vorhersehbarste und produktivste Art der Arbeit mit dem Speicher gewählt: Von der Seite des C-Programms sind alle unsere Objekte gemeinsame Zeiger .
Vor diesem Hintergrund sollten die Datenstrukturen, die wir zuvor im Abschnitt „Variablen und Datentypen“ untersucht haben, wie folgt ergänzt werden:
struct { Py_ssize_t ob_refcnt; struct { struct _typeobject *ob_type; } }
Jedes Objekt in Python (wir meinen natürlich die Implementierung von CPython) hat also einen eigenen Referenzzähler. Sobald es Null wird, kann das Objekt gelöscht werden.
Der Linkzählmechanismus beruht nicht auf zusätzlichen Berechnungen oder Hintergrundprozessen - ein Objekt kann sofort zerstört werden. Darüber hinaus bietet es eine hohe Datenlokalität: Oft wird der Speicher unmittelbar nach der Freigabe wieder verwendet. Das gerade zerstörte Objekt wurde höchstwahrscheinlich kürzlich verwendet, was bedeutet, dass es sich im Prozessor-Cache befand. Daher bleibt das neu erstellte Objekt im Cache. Diese beiden Faktoren - Einfachheit und Lokalität - machen das Zählen von Links zu einer sehr produktiven Methode der Speicherbereinigung.
(Aufgrund der Tatsache, dass Objekte in realen Programmen häufig aufeinander verweisen, kann der Referenzzähler in bestimmten Fällen nicht auf Null fallen, selbst wenn Objekte nicht mehr im Programm verwendet werden. Daher verfügt CPython auch über einen zweiten Speicherbereinigungsmechanismus - einen Hintergrundmechanismus, der auf basiert auf Generationen von Objekten. - ca. transl. )
Python-Entwicklerfehler
Wir haben versucht, eine Sprache zu entwickeln, die für Anfänger einfach, aber auch für Profis attraktiv genug ist. Gleichzeitig konnten wir Fehler beim Verstehen und Verwenden der von uns selbst erstellten Tools nicht vermeiden.
Python 2 versuchte aufgrund der Trägheit des Denkens in Verbindung mit Skriptsprachen, Zeichenfolgentypen zu konvertieren, wie es eine Sprache mit schwacher Typisierung tun würde. Wenn Sie versuchen, eine Bytezeichenfolge mit einer Zeichenfolge in Unicode zu kombinieren, konvertiert der Interpreter die Bytezeichenfolge implizit in Unicode unter Verwendung der auf dem System verfügbaren Codetabelle und zeigt das Ergebnis in Unicode an:
>>> 'byte string ' + u'unicode string' u'byte string unicode string'
Infolgedessen funktionierten einige Websites einwandfrei, während ihre Benutzer Englisch verwendeten, aber sie erzeugten kryptische Fehler, wenn sie Zeichen aus anderen Alphabeten verwendeten.
Dieser Sprachdesignfehler wurde in Python 3 behoben:
>>> b'byte string ' + u'unicode string' TypeError: can't concat bytes to str
Ein ähnlicher Fehler in Python 2 hing mit der „naiven“ Sortierung von Listen zusammen, die aus unvergleichlichen Elementen bestehen:
>>> sorted(['b', 1, 'a', 2]) [1, 2, 'a', 'b']
Python 3 macht dem Benutzer in diesem Fall klar, dass er versucht, etwas zu tun, das nicht sehr aussagekräftig ist:
>>> sorted(['b', 1, 'a', 2]) TypeError: unorderable types: int() < str()
Missbrauch
Benutzer missbrauchen gelegentlich die Dynamik der Python-Sprache, und in den 90er Jahren, als Best Practices noch nicht allgemein bekannt waren, geschah dies besonders häufig:
class Address(object): def __init__(self, host, port): self.host = host self.port = port
"Aber das ist nicht optimal!" - Einige sagten: - „Was ist, wenn der Port nicht vom Standardwert abweicht? Wie auch immer, wir geben ein ganzes Klassenattribut für seine Speicherung aus! “ Und das Ergebnis ist so etwas wie
class Address(object): def __init__(self, host, port=None): self.host = host if port is not None:
Es erscheinen also Objekte des gleichen Typs im Programm, die jedoch nicht einheitlich bedient werden können, da einige von ihnen ein bestimmtes Attribut haben, andere nicht! Und wir können dieses Attribut nicht berühren, ohne vorher seine Anwesenheit zu überprüfen:
Derzeit ist die Fülle an hasattr()
, isinstance()
und anderer Selbstbeobachtung ein sicheres Zeichen für schlechten Code, und es wird als bewährte isinstance()
, Attribute immer im Objekt vorhanden zu machen. Dies bietet eine einfachere Syntax beim Zugriff darauf:
Die frühen Experimente mit dynamisch hinzugefügten und gelöschten Attributen endeten also, und jetzt betrachten wir Klassen in Python ähnlich wie in C ++.
Eine andere schlechte Angewohnheit des frühen Python war die Verwendung von Funktionen, bei denen ein Argument völlig unterschiedliche Typen haben kann. Sie könnten beispielsweise denken, dass es für den Benutzer zu schwierig sein könnte, jedes Mal eine Liste von Spaltennamen zu erstellen, und Sie sollten ihm erlauben, diese auch als einzelne Zeile zu übergeben, wobei die Namen einzelner Spalten beispielsweise durch ein Komma getrennt sind:
class Dataframe(object): def __init__(self, columns): if isinstance(columns, str): columns = columns.split(',') self.columns = columns
Dieser Ansatz kann jedoch zu Problemen führen. Was ist zum Beispiel, wenn ein Benutzer uns versehentlich eine Zeile gibt, die nicht als Liste von Spaltennamen verwendet werden soll? Oder ob der Spaltenname ein Komma enthalten soll?
Außerdem ist es schwieriger, einen solchen Code zu warten, zu debuggen und insbesondere zu testen: In Tests kann nur einer der beiden von uns unterstützten Typen überprüft werden, die Abdeckung beträgt jedoch weiterhin 100%, und der andere Typ wird nicht getestet.
Als Ergebnis kamen wir zu dem Schluss, dass Python es dem Benutzer ermöglicht, Argumente eines beliebigen Typs an Funktionen zu übergeben, aber die meisten von ihnen verwenden in den meisten Situationen eine Funktion auf dieselbe Weise wie in C: Übergeben Sie ein Argument desselben Typs an sie.
Die Notwendigkeit, eval()
in einem Programm zu verwenden, wird als explizite Fehlkalkulation der Architektur angesehen. Höchstwahrscheinlich haben Sie einfach nicht herausgefunden, wie Sie dasselbe auf normale Weise tun können. − , Jupyter notebook - − eval()
, Python ! , C++ .
, ( getattr()
, hasattr()
, isinstance()
) . , , , , : , , , !
: , . 20 , C++ Python. , , . .
, shared_ptr
TensorFlow 2016 2018 .
TensorFlow − C++-, Python- ( C++ − TensorFlow, ).

TensorFlow, shared_ptr
, . , .
C++? . , ? , , C++ Python!