Vom ÜbersetzerBrandon Rhodes ist eine sehr bescheidene Person, die sich auf Twitter als "Python-Programmierer präsentiert, der der Community einen Kredit in Form von Berichten oder Aufsätzen zurückzahlt". Die Anzahl dieser „Berichte und Aufsätze“ ist beeindruckend, ebenso wie die Anzahl der kostenlosen Projekte, zu denen Brandon beigetragen hat oder beigetragen hat. Und Brandon hat zwei Bücher veröffentlicht und schreibt ein drittes.
In Kommentaren zu Habré finde ich sehr oft ein grundlegendes Missverständnis oder eine Ablehnung dynamischer Sprachen, dynamischer Typisierung, verallgemeinerter Programmierung und anderer Paradigmen. Ich veröffentliche diese autorisierte (abgekürzte) Übersetzung (Transkription) eines von Brandons Berichten in der Hoffnung, dass sie Programmierern, die in den Paradigmen statischer Sprachen existieren, helfen wird, dynamische Sprachen, insbesondere Python, besser zu verstehen.
Wie bei mir üblich, bitte ich Sie, mich per PM über meine Fehler und Tippfehler zu informieren.
Was bedeutet der Ausdruck "Randfall" im Titel meines Berichts? Der Grenzfall tritt auf, wenn Sie eine Folge von Optionen durchlaufen, bis Sie den Extremwert erreichen. Zum Beispiel ein n-seitiges Polygon. Wenn n = 3 ist, ist dies ein Dreieck, n = 4 ist ein Viereck, n = 5 ist ein Fünfeck usw. Wenn n gegen unendlich geht, werden die Seiten kleiner und größer und der Umriss des Polygons wird wie ein Kreis. Somit ist der Kreis der Grenzfall für reguläre Polygone. Dies passiert, wenn eine bestimmte Idee an ihre Grenzen stößt.
Ich möchte über Python als Extremfall für C ++ sprechen. Wenn Sie alle guten Ideen aus C ++ übernehmen und bis zu ihrer logischen Schlussfolgerung bereinigen, werden Sie sicher so natürlich mit Python enden, wie eine Reihe von Polygonen zu einem Kreis kommt.
Nicht zum Kerngeschäft gehörende Vermögenswerte
In den 90er Jahren interessierte ich mich für Python: Es war eine Zeit in meinem Leben, in der ich "Nicht-Kern-Assets", wie ich es nenne, loswurde. Viele Dinge langweilten mich. Zum Beispiel Unterbrechungen. Erinnern Sie sich, einmal gab es auf vielen Computerplatinen solche Kontakte mit Jumpern? Und Sie setzen diese Jumper in den Handbüchern so, dass die Grafikkarte einen Interrupt mit höherer Priorität erhält, damit Ihr Spiel schneller läuft? Ich hatte es also satt, Speicher mit malloc()
und free()
malloc()
und malloc()
ungefähr zur gleichen Zeit, als ich aufhörte, die Leistung meines Computers mit Jumpern anzupassen. Es war 1997 oder so.
Ich meine, wenn wir einen Prozess untersuchen, bemühen wir uns normalerweise, die vollständige Kontrolle darüber zu erlangen und alle möglichen Hebel und Knöpfe zur Hand zu haben. Dann sind einige Leute immer noch fasziniert von dieser Möglichkeit der Kontrolle. Aber mein Charakter ist, dass ich, sobald ich mich an das Management gewöhnt habe und verstehe, was was ist, sofort nach der Möglichkeit suche, einige meiner Kräfte aufzugeben, Hebel und Knöpfe auf eine Maschine zu übertragen, damit sie mir Unterbrechungen zuweist.
Daher suchte ich Ende der 90er Jahre nach einer Programmiersprache, mit der ich mich auf den Themenbereich und die Aufgabenmodellierung konzentrieren konnte, anstatt mir Gedanken darüber zu machen, in welchem Bereich des Computerspeichers meine Daten gespeichert sind. Wie können wir C ++ vereinfachen, ohne die Sünden berühmter Skriptsprachen zu wiederholen?
Zum Beispiel konnte ich Perl nicht verwenden, und Sie wissen warum? Dieses Dollarzeichen! Er machte sofort klar, dass der Schöpfer von Perl nicht verstand, wie Programmiersprachen funktionieren. Sie verwenden den Dollar in Bash, um Variablennamen vom Rest der Zeichenfolge zu trennen, da ein Bash-Programm aus buchstäblich wahrgenommenen Befehlen und deren Parametern besteht. Nachdem Sie diese Programmiersprachen kennengelernt haben, in denen Zeichenfolgen zwischen Paaren kleiner Zeichen, die als Anführungszeichen bezeichnet werden, und nicht im gesamten Programmtext platziert werden, werden Sie $
als visuellen Müll wahrnehmen. Das Dollarzeichen ist nutzlos, es ist hässlich, es muss gehen! Wenn Sie eine Sprache für ernsthafte Programmierung entwerfen möchten, sollten Sie keine Sonderzeichen verwenden, um Variablen anzugeben.
Syntax
Was ist mit der Syntax? Nehmen Sie C als Basis! Es funktioniert ziemlich gut. Die Zuordnung sei mit einem Gleichheitszeichen gekennzeichnet. Diese Bezeichnung wird nicht in allen Sprachen akzeptiert, aber auf die eine oder andere Weise sind viele daran gewöhnt. Aber machen wir die Zuweisung nicht zu einem Ausdruck. Die Benutzer unserer Sprache sind nicht nur professionelle Programmierer, sondern auch Schüler, Wissenschaftler oder Datenwissenschaftler (wenn Sie nicht wissen, welche dieser Benutzerkategorien den schlechtesten Code schreibt, werde ich darauf hinweisen, dass dies keine Schüler sind). Wir geben Benutzern nicht die Möglichkeit, den Status von Variablen an unerwarteten Stellen zu ändern, und machen die Zuweisung zu einem Operator.
Was sollte dann verwendet werden, um Gleichheit zu bezeichnen, wenn das Gleichheitszeichen bereits für die Zuweisung verwendet wurde? Natürlich doppelte Zuordnung, wie in C! Viele sind schon daran gewöhnt. Wir werden auch die Notation für alle arithmetischen und bitweisen Operationen von C ausleihen, da diese Notationen funktionieren und viele mit ihnen sehr zufrieden sind.
Natürlich können wir etwas verbessern. Was denken Sie, wenn Sie das Prozentzeichen im Programmtext sehen? Natürlich über String-Interpolation! Obwohl %
in erster Linie ein Modulerfassungsoperator ist, war es für Zeichenfolgen einfach undefiniert. Und wenn ja, warum nicht wiederverwenden?
Numerische und String-Literale, die Sequenzen mit Backslashes steuern - all dies sieht in C aus.
Ausführungsflusskontrolle? Das Gleiche if
, if
while
break
und continue
. Natürlich werden wir ein wenig Spaß hinzufügen, indem wir das gute Alte kooptieren, um Datenstrukturen und Wertebereiche zu durchlaufen. Dies wird später in C ++ 11 vorgeschlagen, aber in Python hat der for
Operator zunächst alle Operationen zum Berechnen von Größen, Durchlaufen von Links, Inkrementieren des Zählers usw. gekapselt, dh alles getan, was erforderlich war, um dem Benutzer ein Element der Datenstruktur bereitzustellen. Welche Art von Strukturen? Es spielt keine Rolle, geben Sie es einfach weiter, es wird es herausfinden.
Wir werden auch Ausnahmen von C ++ ausleihen, aber wir werden sie im Hinblick auf den Ressourcenverbrauch so billig machen, dass sie nicht nur zur Behandlung von Fehlern, sondern auch zur Steuerung des Ausführungsflusses verwendet werden können. Wir werden die Indizierung durch Hinzufügen von Slicing interessanter machen - die Fähigkeit, nicht nur einzelne Elemente sequentieller Datenstrukturen, sondern auch deren Bereiche zu indizieren.
Oh ja! Wir werden den ursprünglichen Designfehler in C beheben - fügen Sie ein baumelndes Komma hinzu!
Diese Geschichte begann mit Pascal, einer schrecklichen Sprache, in der ein Semikolon als Ausdrucksbegrenzer verwendet wird. Dies bedeutet, dass der Benutzer am Ende jedes Ausdrucks im Block mit Ausnahme des letzten ein Semikolon einfügen muss. Daher besteht jedes Mal, wenn Sie die Reihenfolge der Ausdrücke in einem Programm in Pascal ändern, die Gefahr eines Syntaxfehlers, wenn Sie nicht sicherstellen, dass das Semikolon aus der letzten Zeile entfernt und am Ende der Zeile hinzugefügt wird, die früher die letzte war.
If (n = 0) then begin writeln('N is now zero'); func := 1 end
Kernigan und Ritchie haben das Richtige getan, als sie das Semikolon in C als Terminator des Ausdrucks und nicht als Trennzeichen definiert haben. Diese wunderbare Symmetrie entsteht, wenn jede Zeile im Programm, einschließlich der letzten, gleich endet und frei ausgetauscht werden kann. Leider änderte sich in Zukunft das Gefühl der Harmonie für sie und sie machten das Komma zu einem Trennzeichen in statischen Initialisierern. Dies sieht gut aus, wenn der Ausdruck in eine Zeile passt:
int a[] = {4, 5, 6};
Wenn Ihr Initialisierer jedoch länger wird und Sie ihn vertikal anordnen, erhalten Sie dieselbe unangenehme Asymmetrie wie in Pascal:
int a[] = { 4, 5, 6 };
In einem frühen Stadium seiner Entwicklung hat Python das hängende Komma in Datenstrukturen völlig optional gemacht, unabhängig davon, wie die Elemente dieser Struktur angeordnet sind: horizontal oder vertikal. Dies ist übrigens sehr praktisch für die automatische Codegenerierung: Sie müssen das letzte Element nicht als Sonderfall behandeln.
Später haben die Standards C99 und C ++ 11 auch das anfängliche Missverständnis korrigiert, sodass Sie nach dem letzten Literal im Initialisierer ein Komma einfügen können.
Namespaces
Wir müssen auch Namespaces oder Namespaces in unsere Programmiersprache implementieren. Dies ist ein kritischer Teil der Sprache, der uns vor Fehlern wie Namenskonflikten bewahren sollte. Wir werden es einfacher machen als C ++: Anstatt dem Benutzer die Möglichkeit zu geben, den Namespace willkürlich zu benennen, erstellen wir einen Namespace pro Modul (Datei) und kennzeichnen sie mit Dateinamen. Wenn Sie beispielsweise das Modul foo.py
erstellen, wird ihm der Namespace foo
zugewiesen.
Um mit einem solchen vereinfachten Modell von Namespaces arbeiten zu können, benötigt ein Benutzer nur einen Operator.
Erstellen Sie das Verzeichnis my_package
, legen Sie die Datei my_module.py
und deklarieren Sie die Klasse in der Datei:
class C(object): READ = 1 WRITE = 2
Der Zugriff auf die Klassenattribute erfolgt dann wie folgt:
import my_package.my_module my_package.my_module.C.READ
Keine Sorge, wir werden den Benutzer nicht zwingen, jedes Mal den vollständigen Namen auszudrucken. Wir werden ihm die Möglichkeit geben, mehrere Versionen der import
Anweisung zu verwenden, um den Grad der "Nähe" des Namespace zu variieren:
import my_package.my_module my_package.my_module.C.READ from my_package import my_module my_module.C.READ from my_package.my_module import C C.READ
Daher werden dieselben Namen, die in verschiedenen Paketen angegeben sind, niemals in Konflikt geraten:
import json j = json.load(file) import pickle p = pickle.load(file)
Die Tatsache, dass jedes Modul einen eigenen Namespace hat, bedeutet auch, dass wir keinen static
Modifikator benötigen. Wir erinnern uns jedoch an eine Funktion, die static
ausgeführt wurde - die Kapselung interner Variablen. Um Kollegen zu zeigen, dass ein bestimmter Name (Variable, Klasse oder Modul) nicht öffentlich ist, beginnen wir ihn mit einem Unterstrich, z. B. _ignore_this
. Es kann auch ein Signal für die IDE sein, diesen Namen bei der automatischen Vervollständigung nicht zu verwenden.
Funktionsüberlastung
Wir werden keine Funktionsüberladung in unserer Sprache implementieren. Der Überlastungsmechanismus ist zu kompliziert. Stattdessen verwenden wir optionale Argumente mit Standardwerten, die im Aufruf weggelassen werden können, sowie benannte Argumente, um über optionale Argumente mit gültigen Standardwerten zu „springen“ und nur die Werte anzugeben, die sich von den Standardwerten unterscheiden. Wichtig ist, dass wir aufgrund der fehlenden Überlastung nicht feststellen müssen, welche Funktion aus dem Satz überladener Funktionen gerade aufgerufen wurde und wie der Anrufmanager funktioniert: Die Funktion ist in diesem Modul immer eine, sie ist leicht anhand des Namens zu finden.
System-APIs
Wir gewähren dem Benutzer vollen Zugriff auf viele System-APIs, einschließlich Sockets. Ich verstehe nicht, warum Autoren von Skriptsprachen immer ihre eigenen genialen Möglichkeiten anbieten, einen Socket zu öffnen. Sie realisieren jedoch nie die vollständige Unix Socket-API. Sie implementieren 5-6 Funktionen, die sie verstehen, und werfen alles andere weg. Im Gegensatz zu ihnen verfügt Python über Standardmodule für die Interaktion mit dem Betriebssystem, die jeden Standardsystemaufruf implementieren. Das heißt, Sie können jetzt Stevens 'Buch öffnen und mit dem Schreiben von Code beginnen. Und alle Ihre Steckdosen, Prozesse und Gabeln funktionieren genau so, wie es heißt. Ja, es ist möglich, dass Guido oder die frühen Python-Mitarbeiter genau das getan haben, weil sie zu faul waren, um ihre Implementierung von Systembibliotheken zu schreiben, zu faul, um den Benutzern erneut zu erklären, wie Sockets funktionieren. Infolgedessen haben sie jedoch einen wunderbaren Effekt erzielt: Sie können Ihr gesamtes in C und C ++ erworbenes UNIX-Wissen auf die Python-Umgebung übertragen.
Daher haben wir uns entschieden, welche Funktionen wir von C ++ „ausleihen“, um unsere einfache Skriptsprache zu erstellen. Jetzt müssen wir entscheiden, was wir reparieren wollen.
Undefiniertes Verhalten
Unbekanntes Verhalten, undefiniertes Verhalten, durch Implementierung definiertes Verhalten ... Dies sind alles schlechte Ideen für die Sprache, die von Schulkindern, Wissenschaftlern und Datenwissenschaftlern verwendet wird. Und der Leistungsgewinn, für den solche Dinge zulässig sind, ist im Vergleich zu Unannehmlichkeiten oft vernachlässigbar. Stattdessen werden wir bekannt geben, dass jedes syntaktisch korrekte Programm auf jeder Plattform das gleiche Ergebnis liefert. Wir werden den Sprachstandard mit Phrasen wie "Python wertet alle Ausdrücke von links nach rechts aus" beschreiben, anstatt zu versuchen, die Berechnungen je nach Prozessor, Betriebssystem oder Mondphase neu zu ordnen. Wenn der Benutzer sicher ist, dass die Reihenfolge der Berechnungen wichtig ist, hat er das Recht, den Code ordnungsgemäß umzuschreiben: Am Ende ist der Benutzer der Hauptcode.
Betriebsprioritäten
Sie müssen ähnliche Fehler festgestellt haben: Ausdruck
oflags & 0x80 == nflags & 0x80
gibt immer 0 zurück, da Vergleiche in C Vorrang vor bitweisen Operationen haben. Mit anderen Worten, dieser Ausdruck wird zu ausgewertet
oflags & (0x80 == nflags) & 0x80
Oh, das C!
Wir werden die mögliche Ursache solcher Fehler in unserer einfachen Skriptsprache beseitigen und die Priorität von Vergleichsoperationen hinter Arithmetik und Bitmanipulation stellen, damit der Ausdruck aus unserem Beispiel intuitiver berechnet wird:
(oflags & 0x80) == (nflags & 0x80)
Weitere Verbesserungen
Die Lesbarkeit des Codes ist uns wichtig. Wenn die arithmetischen Operationen der C-Sprache dem Benutzer auch durch Schularithmetik bekannt sind, ist die Verwechslung zwischen logischen und bitweisen Operationen eine eindeutige Fehlerquelle. Wir werden das doppelte kaufmännische Und durch das Wort and
und die doppelte vertikale Linie durch das Wort or
ersetzen, damit unsere Sprache eher der menschlichen Sprache als der Streikposten der „Computer“ -Zeichen ähnelt.
Wir werden die Möglichkeit einer verkürzten Berechnung unseren logischen Operatoren ( https://en.wikipedia.org/wiki/Short-circuit_evaluation ) überlassen, ihnen aber auch die Möglichkeit geben, den Endwert eines beliebigen Typs zurückzugeben, nicht nur Boolesch. Dann Ausdrücke wie
s = error.message or 'Error'
In diesem Beispiel wird die Variable auf error.message
gesetzt, wenn sie nicht leer ist, andernfalls die Zeichenfolge 'Error'.
Wir erweitern die Idee von C, dass 0 gleichbedeutend mit false ist, auf andere Objekte als Ganzzahlen. Zum Beispiel auf leeren Zeilen und Containern.
Wir werden den ganzzahligen Überlauf zerstören. Unsere Sprache wird konsistent implementiert und einfach zu bedienen sein, sodass sich unsere Benutzer nicht an einen Sonderwert erinnern müssen, der verdächtig nahe bei zwei Milliarden liegt. Danach ändert das Ganze, um eins erhöht, plötzlich das Vorzeichen. Wir implementieren solche Ganzzahlen, die sich wie Ganzzahlen verhalten, bis sie den gesamten verfügbaren Speicher erschöpfen.
Strenge gegen schwache Eingabe
Ein weiteres wichtiges Thema bei der Gestaltung der Skriptsprache: die Genauigkeit der Eingabe. Viele im Publikum sind mit JavaScript vertraut? Was passiert, wenn die Zahl 3 von der Zeichenfolge '4' abgezogen wird?
js> '4' - 3 1
Großartig! Und wenn Sie die Zahl 3 zur Zeichenfolge '4' hinzufügen?
js> '4' + 3 "43"
Dies wird als laxe (oder schwache) Eingabe bezeichnet. Dies ist so etwas wie ein Minderwertigkeitskomplex, wenn eine Programmiersprache glaubt, dass ein Programmierer ihn verurteilen wird, wenn er das Ergebnis eines nicht einmal offensichtlich bedeutungslosen Ausdrucks durch wiederholtes Casting von Typen zurückgeben kann. Das Problem ist, dass die Typkonvertierung, die eine schwach typisierte Sprache automatisch erzeugt, sehr selten zu einem aussagekräftigen Ergebnis führt. Versuchen wir etwas komplexere Konvertierungen:
js> [] + [] "" js> [] + {} "[object Object]"
Wir erwarten, dass die Additionsoperation kommutativ ist, aber was passiert, wenn wir die Begriffe im letzteren Fall ändern?
js> {} + [] 0
JavaScript ist in seinen Problemen nicht allein. Perl versucht in einer ähnlichen Situation auch, zumindest etwas zurückzugeben:
perl> "3" + 1 4
Und awk wird so etwas tun:
$ echo | awk '{print "3" + 1}' 4
Die Entwickler von Skriptsprachen haben traditionell geglaubt, dass loses Tippen praktisch ist . Sie haben sich geirrt: Loses Tippen ist schrecklich ! Es verstößt gegen das Lokalitätsprinzip. Wenn der Code einen Fehler enthält, sollte die Programmiersprache den Benutzer darüber informieren und eine Ausnahme verursachen, die so nahe wie möglich an der problematischen Stelle im Code liegt. Aber in all diesen Sprachen, die endlos Typen besetzen, bis etwas geklärt ist, endet die Kontrolle normalerweise, und wir erhalten das Ergebnis, gemessen daran, dass in unserem Programm irgendwo etwas nicht stimmt. Und wir müssen unser gesamtes Programm Zeile für Zeile debuggen, um diesen Fehler zu finden.
Lose Eingabe verschlechtert auch die Lesbarkeit des Codes, denn selbst wenn wir implizites Typ-Casting in einem Programm korrekt verwenden, geschieht dies für einen anderen Programmierer unerwartet.
In Python wie in C ++ geben solche Ausdrücke einen Fehler zurück.
>>> '4' - 3 TypeError >>> '4' + 3 TypeError
Weil Type Casting, wenn es wirklich notwendig ist, einfach explizit zu schreiben ist:
>>> int('4') + 3 7 >>> '4' + str(3) '43'
Dieser Code ist leicht zu lesen und zu pflegen. Er macht deutlich, was genau im Programm passiert, was zu diesem Ergebnis führt. Dies liegt daran, dass Python-Programmierer der Meinung sind, dass explizit besser als implizit ist und der Fehler nicht unbemerkt bleiben sollte.
Python ist eine stark typisierte Sprache, und die einzige implizite Typkonvertierung erfolgt bei arithmetischen Operationen mit ganzen Zahlen, deren Ergebnis als Bruchzahl ausgedrückt werden muss. Vielleicht sollte dies auch im Programm nicht erlaubt sein, aber in diesem Fall müssten zu viele Benutzer sofort den Unterschied zwischen Ganzzahlen und Gleitkommazahlen erklären, was ihre ersten Schritte in Python erschweren würde.
Fortsetzung: „ Python als ultimativer Fall von C ++. Teil 2/2 . "