Entdecke und verstehe Python durch überraschende Code-Schnipsel.
Übersetzungen: English | Chinesisch 中文 | Vietnamesisch Tiếng Việt | Spanisch Español | Koreanisch 한국어 | Russisch Русский | Übersetzung hinzufügen
Andere Modi: Interaktive Website | Interaktives Notebook | CLI
Python, bekannt als gut designte High-Level und Interpreter-basierte Programmiersprache, stellt viele Features zur Verfügung, um dem Programmierer das Leben zu erleichtern. Allerdings kann es vorkommen, dass ein Python-Schnipsel ein unerwartetes Verhalten zeigt.
Hier ist ein schönes Projekt, das versucht die Dinge aufzuzeigen, die bei einigen Code-Schnipseln unter der Haube passieren und darüber hinaus einige weniger bekannte Features von Python zu erklären.
Während manche Beispiele nicht unbedingt beeindruckend erscheinen, zeigen sie dennoch interessante Details von Python, die dir womöglich noch nicht aufgefallen sind. Ich finde, dass es eine schöne Möglichkeit ist, die Interna einer Programmiersprache zu lernen und ich glaube das findest du auch !
Wenn du ein erfahrener Python-Programmierer bist, kannst du dies als Herausforderung ansehen, um möglichst viel beim ersten Anlauf richtig zu machen. Du hast vielleicht manches schon erlebt, sodass ich möglicherweise alte Erinnerungen wecken kann! 😅
PS: Wenn du bereits mehrfach hier warst, kannst du dich hier über neue Modifikationen informieren (die Beispiele, die mit einem Stern markiert sind, sind Teil des letzten Releases).
Also, los gehts...
is not ...
ist nicht is (not ...)
del
Operation
''
) mit Slices der Länge 0 in der ursprünglichen Zeichenkette übereinstimmenAlle Beispiele sind nach folgendem Muster aufgebaut:
# Vorbereitung des Codes. # Vorbereitung für etwas Magisches...Ausgabe (Python version(en)):
>>> triggering_statement Irgendeine unerwartete Ausgabe(Optional): Eine Zeile, die die unerwartete Ausgabe beschreibt.
- Kurze Erklärung was und warum es passiert.
# Aufsetzen des Codes # Mehr Beispiele für ein besseres Verständnis (wenn erforderlich)Ausgabe (Python version(en)):
>>> trigger # Ein Beispiel, das es einfach macht, die Magie zu verstehen # Eine begründete Ausgabe
Note: Alle Beispiele sind mit Pythons 3.5.2 interaktiven Interpreter getestet, und sie sollten für alle Python Versionen funktionieren. Ausnahmen werden vor dem Ausgabe kenntlich gemacht.
Ein guter Weg, um die Beispiele bestmöglich zu nutzen, ist es, sie von anfang an durchzugehen und bei jedem Beispiel folgendes zu tun:
PS: Du kannst dir auch WTFPython im Terminal ansehen, indem du das pypi package nutzt:
$ pip install wtfpython -U $ wtfpythonKapitel: Fordere dein Gehirn heraus! ▶ Das Wichtigste zuerst! *
Aus irgendwelchen Gründen ist der "Walrus" Operator (:=
) in Python 3.8 ziemlich beliebt. Lass uns starten,
1.
# Python version 3.8+ >>> a = "wtf_walrus" >>> a 'wtf_walrus' >>> a := "wtf_walrus" File "<stdin>", line 1 a := "wtf_walrus" ^ SyntaxError: invalid syntax >>> (a := "wtf_walrus") # Das funktioniert merkwürdigerweise 'wtf_walrus' >>> a 'wtf_walrus'
2 .
# Python version 3.8+ >>> a = 6, 9 >>> a (6, 9) >>> (a := 6, 9) (6, 9) >>> a 6 >>> a, b = 6, 9 # Typisches Auspacken >>> a, b (6, 9) >>> (a, b = 16, 19) # Oops File "<stdin>", line 1 (a, b = 16, 19) ^ SyntaxError: invalid syntax >>> (a, b := 16, 19) # Dies gibt ein eigenartiges 3-Tupel aus (6, 16, 19) >>> a # Ist a immernoch unverändert ? 6 >>> b 16
Schneller Rückblick zum Walrus Operator
Der Walrus Operator (:=
) wurde in Python 3.8 eingeführt. Er kann in Situationen sinvoll sein, in denen du ein Wert einer Variablen in einem Ausdruck zuweisen möchtest.
def irgendeine_funktion(): # Irgendeine Berechnung, die teuer ist (=> sehr viel Zeit und Ressourcen in Aspruch nimmt) # time.sleep(1000) return 5 # Anstatt: if irgendeine_funktion(): print(irgendeine_funktion()) # Schlechter Stil, da die Funktion zweimal aufgerufen wird # Oder: a = irgendeine_funktion() if a: print(a) # Nun kannst du folgendes schreiben: if a := irgendeine_funktion(): print(a)
Ausgabe (> 3.8):
Das hat uns eine Zeile Code erspart. Zudem spart es einen zusätzlichen Aufruf der Funktion irgendeine_funktion
.
Nichtgeklammerte "Zuweisung Ausdruck" (Verwendung des Walrus-Operator), ist auf der obersten Ebene beschränkt, daher der Syntaxfehler
in der Anweisung a := "wtf_walrus"
des erstes Schnipsels. Einklammeren hat funktioniert und wies a
zu.
Wie immer, Einklammerung eines Ausdrucks, welcher =
- Operator enthält, ist nicht erlaubt. Daher der Syntaxfehler in (a, b = 6, 9)
.
Die Syntax des Walrus Operators lautet wie folgt: NAME:= ausdruck
, wobei NAME
ist ein gültiger Identifier, und ausdruck
ist ein gültiger Ausdruck. Zudem werden iterierbares Verpacken und Entpacken nicht unterstützt, d.h.:
(a := 6, 9)
ist äquivalent zu ((a := 6), 9)
und zu (a, 9)
(where a
's value is 6')
>>> (a := 6, 9) == ((a := 6), 9) True >>> x = (a := 696, 9) >>> x (696, 9) >>> x[0] is a # Both reference same memory location True
Ähnlich: (a, b := 16, 19)
ist äquivalent zu (a, (b := 16), 19)
, was einfach ein 3-Tupel ist.
1.
>>> a = "irgendein_string" >>> id(a) 140420665652016 >>> id("irgendein" + "_" + "string") # Beachte, dass beide ids dieselben sind. 140420665652016
2.
>>> a = "wtf" >>> b = "wtf" >>> a is b True >>> a = "wtf!" >>> b = "wtf!" >>> a is b False
3.
>>> a, b = "wtf!", "wtf!" >>> a is b # Alle Versionen außer 3.7.x True >>> a = "wtf!"; b = "wtf!" >>> a is b # Das wird True oder False ausgeben, je nach dem wo du es aufrufst (Python Shell / iPython / in einem Skript) False
# Dieses mal in einer Datei: some_file.py a = "wtf!" b = "wtf!" print(a is b) # Gibt True aus, wenn das Modul aufgerufen wird!
4.
Ausgabe (< Python3.7 )
>>> 'a' * 20 is 'aaaaaaaaaaaaaaaaaaaa' True >>> 'a' * 21 is 'aaaaaaaaaaaaaaaaaaaaa' False
Ergibt Sinn, Oder?
Das Verhalten im ersten und zweiten Schnipsel erklärt sich durch eine CPython Optimierung (auch string interning genannt), die versucht, existierende immutable Objekte zu nutzen anstatt jedes mal ein neues Objekt zu erstellen.
Nachdem "interned" (festgehalten) wurde, kann es sein, dass viele Variablen dasselbe String-Objekt im Speicher referenzieren (man spart also Speicher).
In den Schnipseln oben werden Strings implizit festgehalten. Die Entscheidung, wann ein String implizit festgehalten wird, ist von der Implementierung abhängig. Es gibt einige Regeln, die benutzt werden können, um zu erahnen, ob ein String festgehalten wird oder nicht:
'wtf'
wird festgehalten, aber ''.join(['w', 't', 'f'])
nicht)'wtf!'
nicht festgehalten wurde (wegen !
). Die CPython-Implementierung dieser Regel kann hier gefunden werden Wenn a
und b
in derselben Zeile auf "wtf!"
gesetzt werden, erzeugt der Python Interpreter ein neues Objekt, welches von der zweiten Variable zur selben Zeit referenziert wird. Wenn du es in zwei verschiedenen Zeilen deklarierst, dann "weiß" der Interpreter nicht, dass "wtf!"
als Objekt schon existiert (weil "wtf!"
nicht implizit festgehalten wird, siehe obige Auflistung). Es ist eine Compilezeit-Optimierung. Diese Optimierung gilt nicht für 3.7.x Versionen von CPython (siehe dieses Issue).
Eine Compile-Unit ist eine interaktive Umgebung, wie z.B. IPython besteht aus einen einzigen Statement, während es aus einem ganzen Modul im Falle von Modulen besteht. a, b = "wtf!", "wtf!"
ist ein einziges Statement, während a = "wtf!"; b = "wtf!"
zwei Statements in einer Zeile sind. Das erklärt, warum die Identitäten a = "wtf!"; b = "wtf!"
verschieden sind. Es erklärt auch, warum sie dieselben sind, wenn sie in some_file.py
aufgerufen werden.
Die abrupte Veränderung in der Ausgabe des 4.Schnipsel ist der peephole Optimierung Technik geschuldet, auch als Constant Folding bekannt. Das bedeutet, der Ausdruck 'a'*20
wird durch 'aaaaaaaaaaaaaaaaaaaa'
während der Kompilierung ersetzt, um ein paar Taktzyklen während der Laufzeit zu sparen. Constant Folding wird nur für String mit einer kleineren Länge als 21 angewendet. (Wieso ? Stelle dir die Größe einer .pyc
Datei vor, die durch den Ausdruck 'a'*10**10
generiert wurde). Hier ist die Quelle der Implementierung dafür.
Notiz: In Python 3.7, konstantes Folding wurde vom peephole-Optimierer zum neuen AST-Optimierer verschoben (mit ein paar Veränderungen in der Logik), d.h. das 4.Schnipsel funktioniert in Python 3.7 nicht. Du kannst hier mehr darüber erfahren.
>>> (False == False) in [False] # ergibt Sinn False >>> False == (False in [False]) # ergibt Sinn False >>> False == False in [False] # Was nun? True >>> True is False == False False >>> False is False is False True >>> 1 > 0 < 1 True >>> (1 > 0) < 1 False >>> 1 > (0 < 1) False
Zitat von https://docs.python.org/3/reference/expressions.html#comparisons
Formally, if a, b, c, ..., y, z are expressions and op1, op2, ..., opN are comparison operators, then a op1 b op2 c ... y opN z is equivalent to a op1 b and b op2 c and ... y opN z, except that each expression is evaluated at most once.
Übersetzt:
Formal ausgedrückt: wenn a, b, c, ..., y, z Ausdrücke und op1, op2, ..., opN Vergleichsoperatoren sind, dann sind a op1 b op2 c ... y opN z äquivalent zu a op1 b and b op2 c and ... y opN z, mit der Ausnahme, dass jeder Ausdruck höchstens einmal ausgewertet wird.
Während dieses Verhalten in den Beispielen vielleicht unsinnig erscheint, kann es super verwendet werden, z.B. a == b == c
und 0 <= x <= 100
.
False is False is False
ist äquivalent zu (False is False) and (False is False)
True is False == False
ist äquivalent zu (True is False) and (False == False)
und während der erste Teil des Statements (True is False
) zu False
ausgewertet wird, wird der gesamt Ausdruck zu False
ausgewertet.1 > 0 < 1
ist äquivalent zu (1 > 0) and (0 < 1)
which evaluates to True
.(1 > 0) < 1
ist äquivalent zu True < 1
und
>>> int(True) 1 >>> True + 1 # Nicht relevant für dieses Beispiel, aber trotzdem nur zum Spaß 2So wird
1 < 1
zu False
ausgewertetis
Operator nicht nutzt
Das folgende Beispiel ist im Internet überall bekannt.
1.
>>> a = 256 >>> b = 256 >>> a is b True >>> a = 257 >>> b = 257 >>> a is b False
2.
>>> a = [] >>> b = [] >>> a is b False >>> a = tuple() >>> b = tuple() >>> a is b True
3. Ausgabe
>>> a, b = 257, 257 >>> a is b True
Ausgabe (Python 3.7.x spezifisch)
>>> a, b = 257, 257 >>> a is b False
Der Unterschied zwischen is
und ==
is
Operator checkt, ob sich beide Operanden auf dasselbe Objekt beziehen (i.e., it checks if the identity of the operands matches or not).==
Operator vergleicht die Werte der beiden Operanden und überprüft, ob diese gleich sind.is
wird für Beziehungsgleichheit und ==
für Wertgleichheit benutzt. Ein Beispiel, um das Gesagte zu vertiefen:
>>> class A: pass >>> A() is A() # Das sind zwei leere Objekte an zwei verschiedenen Orten im Speicher. False
256
ist ein existierendes Objekt, aber 257
nicht
Wenn du Python startest, werden die Nummern von -5
bis 256
bereitgestellt. Diese Nummern werden sehr oft benutzt, also ergibt es Sinn, sie schnell bereit zu haben.
Zitat von https://docs.python.org/3/c-api/long.html
The current implementation keeps an array of integer objects for all integers between -5 and 256, when you create an int in that range you just get back a reference to the existing object. So it should be possible to change the value of 1. I suspect the behavior of Python, in this case, is undefined. :-)
Übersetzung:
Die momentane Implementation stellt ein Array aus Integer-Objekten für alle Integer zwischen -5 und 256 bereit. Wenn du einen int in diesem Bereich erstellst, bekommst du nur eine Referenz auf das existierende Objekt zurück. Also sollte es möglich sein, den Wert von 1 zu ändern. Ich vermute das Verhalten von Python ist in diesem Fall undefiniert. :-)
>>> id(256) 10922528 >>> a = 256 >>> b = 256 >>> id(a) 10922528 >>> id(b) 10922528 >>> id(257) 140084850247312 >>> x = 257 >>> y = 257 >>> id(x) 140084850247440 >>> id(y) 140084850247344
Hier ist der Interpreter nicht schlau genug während des Ausführens von y = 257
zu erkennen, dass wir bereits ein Integer mit dem Wert 257
erstellt haben und daher wird ein neues Objekt im Speicher angelegt.
Ähnliche Optimierungen treffen auf andere immutable Objekte zu, z.B. leere Tuples. Da Listen mutable sind, wird [] is []
zu False
ausgewertet und () is ()
wird zu True
ausgewertet. Das erklärt unser zweiter Schnipsel. Lass uns mit dem dritten Beispiell weiter machen:
Sowohl a
und b
beziehen sich auf dasselbe Objekt wenn sie in derselben Zeile mit demselben Wert initialisiert werden.
Ausgabe
>>> a, b = 257, 257 >>> id(a) 140640774013296 >>> id(b) 140640774013296 >>> a = 257 >>> b = 257 >>> id(a) 140640774013392 >>> id(b) 140640774013488
Wenn a und b in derselben Zeile auf 257
gesetzt werden, erstellt der Python Interpreter ein neues Objekt, und referenziert die zweite Variable zur selben Zeit. Wenn man es in verschiedenen Zeilen macht, dann weiß Python nicht, dass eine 257
als Objekt schon existiert.
Es ist eine Compiler Optimierung, die speziell für die interaktive Umgebung gilt. Wenn man zwei Zeilen in einem Live Interpreter eingibt, dann werden sie getrennt kompiliert und auch getrennt optimiert. Wenn du dieses Beispiel in einer .py
Datei ausprobierst, würdest du nicht dasselbe Verhalten beobachten, denn die Datei wird auf einmal kompiliert. Diese Optimierung ist nicht auf Integer beschränkt, sie funktioniert auch für andere immutable Datentypen, wie z.B. Strings (siehe auch das Beispiel "Strings können manchmal schwierig sein") oder floats:
>>> a, b = 257.0, 257.0 >>> a is b True
Warum funktioniert das nicht in Python 3.7 ? Die abstrakte Antwort ist: Die Compiler Optimierungen sind von der Implementierung abhängig (es verändert sich mit der Version, dem Betriebssystem, etc.). Ich versuche noch herauszufinden, welche Implementierungsänderung dieses Problem verursacht. Für Updates, schaue dieses Issue an.
1.
some_dict = {} some_dict[5.5] = "JavaScript" some_dict[5.0] = "Ruby" some_dict[5] = "Python"
Ausgabe:
>>> some_dict[5.5] "JavaScript" >>> some_dict[5.0] # "Python" hat die Existenz von "Ruby" ausgelöscht ? "Python" >>> some_dict[5] "Python" >>> complex_five = 5 + 0j >>> type(complex_five) complex >>> some_dict[complex_five] "Python"
Warum also ist Python überall zu finden ?
Einzigartigkeit der Schlüssel in einem Python Dictionary wird durch Äquivalenz, nicht durch Identität festgestellt. Obwohl also 5
, 5.0
, und 5 + 0j
verschiedene Objekte unterschiedlichen Typs sind, können sie, da sie gleichwertig sind, nicht im gleichen dict
(oder set
) sein. Sobald du einen von ihnen einfügst, wird der Versuch, nach einem anderen, aber gleichwertigen Schlüssel zu suchen, mit dem ursprünglichen zugeordneten Wert erfolgreich sein (und nicht mit einem KeyError
fehlschlagen):
>>> 5 == 5.0 == 5 + 0j True >>> 5 is not 5.0 is not 5 + 0j True >>> some_dict = {} >>> some_dict[5.0] = "Ruby" >>> 5.0 in some_dict True >>> (5 in some_dict) and (5 + 0j in some_dict) True
Das gilt auch, wenn ein Item festgelegt wird. Wenn du also some_dict[5] = "Python"
ausführst, findet Python das existierende Item mit demselben Key 5.0 -> "Ruby"
, überschreibt den Wert an dieser Stelle, und lässt den originalen Wert unangetastet.
>>> some_dict {5.0: 'Ruby'} >>> some_dict[5] = "Python" >>> some_dict {5.0: 'Python'}
Wie können wir also den Key zu 5
anstelle von 5.0
updaten? Wir können das tatsächlich nicht an dieser Stelle tun, aber wir können den Schlüssel zuerst löschen (del some_dict[5.0]
), und ihn danach neu festzulegen (some_dict[5]
), um den Schlüssel 5
zu bekommen, anstelle des floats 5.0
. Das wird jedoch nur in seltenen Fällen benötigt.
Wie hat Python 5
in einem Dictionary gefunden, welches 5.0
enthält? Python tut dies in konstanter Zeit, ohne jedes Item zu scannen, indem es Hash-Funktionen benutzt. Wenn Python den Key foo
in einem Dictionary nachschlägt, dann verarbeitet es zuerst hash(foo)
(was in konstanter Zeit läuft). Da es in Python notwendig ist, dass Objekte, die als gleich gelten auch den gleichen Hash-Wert haben ( siehe hier docs), haben 5
, 5.0
, und 5 + 0j
denselben Hash-Wert.
>>> 5 == 5.0 == 5 + 0j True >>> hash(5) == hash(5.0) == hash(5 + 0j) True
Note: Das Inverse ist nicht unbedingt wahr: Objekte mit gleichem Hashwert können evtl. ungleich sein. (Das versursacht was auch als hash collision bekannt ist), und und verschlechtert die zeitlich konstante Leistung, die Hashing normalerweise bietet.
Ausgabe:
>>> WTF() == WTF() # zwei verschiedene Instanzen können nicht gleich sein False >>> WTF() is WTF() # Idetitäten sind ebenfalls unterschiedlich False >>> hash(WTF()) == hash(WTF()) # Hash-Werte _sollten_ ebenfalls verschieden sein True >>> id(WTF()) == id(WTF()) True
Wenn id
genannt wurde, hat Python hat ein WTF
class-Objekt gebaut und es der id
-Funktion übergeben. Die id
-Funktion nimmt die id
(den Speicherort), und wirft das Objekt weg. Das Objekt ist zerstört.
When we do this twice in succession, Python allocates the same memory location to this second object as well. Da (in CPython) id
denselben Speicherort wie die Objekt-Id benutzt, ist die id der beiden Objekte dieselbe.
Also ist die Id des Objektes nur für die Lebensdauer des Objektes einzigartig. Nachdem das Objekt zerstört wurde, oder bevor es gebaut wird, kann etwas anderes diese Id haben.
Abe warum wurde der is
zu False
ausgewertet? Lass uns das anhand dieses Schnipsels betrachten.
class WTF(object): def __init__(self): print("I") def __del__(self): print("D")
Ausgabe:
>>> WTF() is WTF() I I D D False >>> id(WTF()) == id(WTF()) I D I D True
Wie du villeicht beobachtest hast, ist die Reihenfolge, in der die Objekte zerstört werden, das, was hier den Unterschied macht.
from collections import OrderedDict dictionary = dict() dictionary[1] = 'a'; dictionary[2] = 'b'; ordered_dict = OrderedDict() ordered_dict[1] = 'a'; ordered_dict[2] = 'b'; another_ordered_dict = OrderedDict() another_ordered_dict[2] = 'b'; another_ordered_dict[1] = 'a'; class DictWithHash(dict): """ Ein Dictionary auch __hash__ magic implementiert. """ __hash__ = lambda self: 0 class OrderedDictWithHash(OrderedDict): """ Ein OrderedDict was auch __hash__ magic implementiert. """ __hash__ = lambda self: 0
Ausgabe
>>> dictionary == ordered_dict # Wenn a == b True >>> dictionary == another_ordered_dict # und b == c True >>> ordered_dict == another_ordered_dict # warum ist dann c != a ?? False # Wir wissen alle, dass ein Set nur aus einzigartigen Elementen besteht, # Lass uns ein Set aus Dictionaries bauen und sehen, was passiert... >>> len({dictionary, ordered_dict, another_ordered_dict}) Traceback (most recent call last): File "<stdin>", line 1, in <module> TypeError: unhashable type: 'dict' # Ergibt Sinn, denn ein Dictionary implementiert nicht __hash__, lass uns unsere # Wrapper-Klasse benutzen. >>> dictionary = DictWithHash() >>> dictionary[1] = 'a'; dictionary[2] = 'b'; >>> ordered_dict = OrderedDictWithHash() >>> ordered_dict[1] = 'a'; ordered_dict[2] = 'b'; >>> another_ordered_dict = OrderedDictWithHash() >>> another_ordered_dict[2] = 'b'; another_ordered_dict[1] = 'a'; >>> len({dictionary, ordered_dict, another_ordered_dict}) 1 >>> len({ordered_dict, another_ordered_dict, dictionary}) # verändere die Reihenfolge 2
Was geht hier vor ?
Der Grund, warum die intransitive Gleichheit zwischen dictionary
, ordered_dict
und another_ordered_dict
nicht gilt, liegt in der __eq__
Methode und wie diese in der OrderedDict
-Klasse implementiert ist. Aus der Dokumentation
Gleichheitstests zwischen OrderedDict-Objekten sind ordnungsabhängig und werden als
list(od1.items())==list(od2.items())
implementiert. Gleichheitstests zwischenOrderedDict
-Objekten und anderen Mapping-Objekten sind nicht ordnungsabhängig wie bei regulären Dictionaries.
Der Grund für diese Gleichheit im Verhalten ist, dass sie es ermöglicht, OrderedDict
-Objekte direkt überall dort zu ersetzen, wo ein reguläres Wörterbuch verwendet wird.
Okay, warum also hat die Änderung der Reihenfolge Auswirkungen auf die Länge des erzeugten set
-Objekts? Die Antwort ist das Fehlen der intransitiven Gleichheit. Da Mengen "ungeordnete" Sammlungen von eindeutigen Elementen sind, sollte die Reihenfolge, in der die Elemente eingefügt werden, keine Rolle spielen. Aber in diesem Fall spielt sie doch eine Rolle. Lass uns das ein wenig aufschlüsseln
>>> some_set = set() >>> some_set.add(dictionary) # das sind die Mapping-Objekte von unseren Schnipseln oben >>> ordered_dict in some_set True >>> some_set.add(ordered_dict) >>> len(some_set) 1 >>> another_ordered_dict in some_set True >>> some_set.add(another_ordered_dict) >>> len(some_set) 1 >>> another_set = set() >>> another_set.add(ordered_dict) >>> another_ordered_dict in another_set False >>> another_set.add(another_ordered_dict) >>> len(another_set) 2 >>> dictionary in another_set True >>> another_set.add(another_ordered_dict) >>> len(another_set) 2
Die Inkonsistenz liegt bei another_ordered_dict in another_set
, was False
ist, weil ordered_dict
bereits in another_set
enthalten ist und wie schon vorher beobachtet, ordered_dict == another_ordered_dict
ist False
.
def some_func(): try: return 'from_try' finally: return 'from_finally' def another_func(): for _ in range(3): try: continue finally: print("Finally!") def one_more_func(): # Ein gotcha! try: for i in range(3): try: 1 / i except ZeroDivisionError: # Lass es uns hier hin packen und es außerhalb des Loops behandeln raise ZeroDivisionError("A trivial divide by zero error") finally: print("Iteration", i) break except ZeroDivisionError as e: print("Zero division error occurred", e)
Ausgabe:
>>> some_func() 'from_finally' >>> another_func() Finally! Finally! Finally! >>> 1 / 0 Traceback (most recent call last): File "<stdin>", line 1, in <module> ZeroDivisionError: division by zero >>> one_more_func() Iteration 0
return
-, break
- oder continue
-Anweisung in einem try
("try…finally") Anweisung ausgeführt wird, dann wird der finally
-Abschnitt am Ende ebenfalls ausgeführt.return
-Anweisung bestimmt. Da der finally
-Abschnitt immer ausgeführt wird, wird eine return
-Anweisung im finally
-Abschnitt immer die letzte sein, die ausgeführt wird.finally
-Abschnitt eine return
- oder break
-Anweisung ausführt, dann wird die kurzzeitige Exception verworfen.some_string = "wtf" some_dict = {} for i, some_dict[i] in enumerate(some_string): i = 10
Ausgabe:
>>> some_dict # Ein indiziertes dictionary erscheint. {0: 'w', 1: 't', 2: 'f'}
Eine for
Anweisung ist in Python Syntax wie folgt definiert:
for_stmt: 'for' exprlist 'in' testlist ':' suite ['else' ':' suite]
exprlist
ist dabei das Zuweisungsziel. Das heißt, dass das Äquivalente von {exprlist} = {next_value}
im Iterable executed for each item ist. Ein interessantes Beispiel, was dies verdeutlicht:
for i in range(4): print(i) i = 10
Ausgabe:
Hast du erwartet, dass die Schleife nur einmal läuft ?
💡 Erklärung:
i = 10
hat niemals einen Effekt auf die Schleife, aufgrund der Funktionsweise von for-Schleifen in Python. Vor dem Beginn jeder Iteration, wird das nächste Objekt, was vom Iterator (in diesem Fall range(4)
) bereitgestellt wird, wird ausgepackt und der Zielliste zugewiesen (in diesem Fall i
).Die enumerate(some_string)
Funktion liefert ein neuen Wert i
(ein Zähler, der aufwärts läuft) und ein Character vom String some_string
in jeder Iteration. Dann wird der gerade erzeugte Wert i
des Dictionaries some_dict
als Key zu diesem Character gesetzt. Das Verhalten der Schleife kann wie folgt vereinfacht werden:
>>> i, some_dict[i] = (0, 'w') >>> i, some_dict[i] = (1, 't') >>> i, some_dict[i] = (2, 'f') >>> some_dict
1.
array = [1, 8, 15] # Ein typischer Generator-Ausdruck gen = (x for x in array if array.count(x) > 0) array = [2, 8, 22]
Ausgabe:
>>> print(list(gen)) # Wo sind die anderen Variablen hin? [8]
2.
array_1 = [1,2,3,4] gen_1 = (x for x in array_1) array_1 = [1,2,3,4,5] array_2 = [1,2,3,4] gen_2 = (x for x in array_2) array_2[:] = [1,2,3,4,5]
Ausgabe:
>>> print(list(gen_1)) [1, 2, 3, 4] >>> print(list(gen_2)) [1, 2, 3, 4, 5]
3.
array_3 = [1, 2, 3] array_4 = [10, 20, 30] gen = (i + j for i in array_3 for j in array_4) array_3 = [4, 5, 6] array_4 = [400, 500, 600]
Ausgabe:
>>> print(list(gen)) [401, 501, 601, 402, 502, 602, 403, 503, 603]
In einem Generator-Ausdruck wird die in
-Klausel zur Deklarationszeit ausgewertet, während die Bedingungsklausel zur Laufzeit ausgewertet wird.
Vor der Laufzeit wird array
wieder der Liste [2, 8, 22]
zugewiesen, und da von 1
, 8
und 15
, nur die Anzahl von 8
größer als 0
ist, liefert der Generator nur 8
.
Die Unterschiede in der Ausgabe von g1
und g2
im zweiten Teil sind auf die Art und Weise zurückzuführen, wie den Variablen array_1
und array_2
neue Werte zugewiesen werden.
Im ersten Fall wird array_1
zum neuen Objekt [1,2,3,4,5]
gebunden und da die in
-Klausel zur Deklarationszeit ausgewertet wird, bezieht es sich immer noch auf das alte Objekt [1,2,3,4]
(was nicht zerstört wird).
Im zweiten Fall updated die Slice-Anweisung an array_2
das gleiche alte Objekt [1,2,3,4]
zu [1,2,3,4,5]
. Daher verweisen sowohl g2
als auch array_2
immer noch auf dasselbe Objekt (welches nun zu [1,2,3,4,5]
geupdated wird).
Okay, wenn wir die Logik anwenden, die wir bis jetzt kennengelernt haben, sollte dann der Wert von list(gen)
im dritten Schnipsel nicht [11, 21, 31, 12, 22, 32, 13, 23, 33]
sein? (weil array_3
und array_4
sich genauso wie array_1
verhalten werden). Die Grund, warum (nur) die Werte von array_4
geändert werden, wird im PEP-289 erklärt.
Nur der äußerste for-Ausdruck wird direkt ausgewertet, die anderen Ausdrücke werden zurückgestellt, bis der Generator ausgeführt wird.
is not ...
ist nicht is (not ...)
>>> 'something' is not None True >>> 'something' is (not None) False
is not
ist ein einzelner binärer Operator, der anderes Verhalten zeigt, als wenn man is
und not
einzeln benutzt.is not
wird zu False
ausgewertet wenn die Variablen auf beiden Seiten des Operators auf dasselbe Objekt verweisen, andernfalls zu True
.(not None)
zu True
ausgewertet, denn der Wert None
ist False
im booleschen Kontext, also wird der Ausdruck zu 'something' is True
ausgewertet.# Lass uns eine Zeile initialisieren row = [""] * 3 #row i['', '', ''] # Lass uns ein board bauen board = [row] * 3
Ausgabe:
>>> board [['', '', ''], ['', '', ''], ['', '', '']] >>> board[0] ['', '', ''] >>> board[0][0] '' >>> board[0][0] = "X" >>> board [['X', '', ''], ['X', '', ''], ['X', '', '']]
Wir haben nicht dreimal "X"
zugewiesen, oder?
Wenn wir die Variable row
initialisieren, dann erklärt diese Visualisierung, was im Speicher passiert
Und wenn das board
durch Multiplizieren der row
initialisiert wird, dann passiert das hier innerhalb des Speichers (jedes der Elemente board[0]
, board[1]
und board[2]
ist eine Referenz aud dieselbe Liste, aud die row
verweist)
Wir können dieses Szenario umfahren, indem wir nicht die row
Variable zum generieren von board
benutzen. (gefragt hier).
>>> board = [['']*3 for _ in range(3)] >>> board[0][0] = "X" >>> board [['X', '', ''], ['', '', ''], ['', '', '']]▶ Schrödingers Variable *
funcs = [] results = [] for x in range(7): def some_func(): return x funcs.append(some_func) results.append(some_func()) # Beachte hier den Funktionsaufruf funcs_results = [func() for func in funcs]
Ausgabe (Python version):
>>> results [0, 1, 2, 3, 4, 5, 6] >>> funcs_results [6, 6, 6, 6, 6, 6, 6]
The values of x
were different in every iteration prior to appending some_func
to funcs
, but all the functions return 6 when they're evaluated after the loop completes.
>>> powers_of_x = [lambda x: x**i for i in range(10)] >>> [f(2) for f in powers_of_x] [512, 512, 512, 512, 512, 512, 512, 512, 512, 512]
x
in dem umgebenden Kontext nach, anstatt den Wert von x
zum Zeitpunkt der Erstellung der Funktion zu benutzen. Also verwenden alle Funktionen den letzten Wert, der der Variable zugewiesen wurde, für ihre Berechnungen. Wir können beobachten, dass x
vom umgebenen Kontext verwendet wird (d.h. keine lokale Variable) mit:>>> import inspect >>> inspect.getclosurevars(funcs[0]) ClosureVars(nonlocals={}, globals={'x': 6}, builtins={}, unbound=set())
Da x
ein globaler Wert ist, können wir den Wert, den funcs
nachschlägt und zurüchgibt, verändern, indem wir x
updaten:
>>> x = 42 >>> [func() for func in funcs] [42, 42, 42, 42, 42, 42, 42]
x
zu diesem Zeitpunkt speichert.funcs = [] for x in range(7): def some_func(x=x): return x funcs.append(some_func)
Ausgabe:
>>> funcs_results = [func() for func in funcs] >>> funcs_results [0, 1, 2, 3, 4, 5, 6]
x
wird nicht länger im globalen Scope verwendet:
>>> inspect.getclosurevars(funcs[0]) ClosureVars(nonlocals={}, globals={}, builtins={}, unbound=set())
1.
>>> isinstance(3, int) True >>> isinstance(type, object) True >>> isinstance(object, type) True
Was ist also die ultimative Basisklasse? Die Verwirrung wird noch größer:
2.
>>> class A: pass >>> isinstance(A, A) False >>> isinstance(type, type) True >>> isinstance(object, object) True
3.
>>> issubclass(int, object) True >>> issubclass(type, object) True >>> issubclass(object, type) False
type
ist eine Metaklasse in Python.object
in Python, was Klassen und ihre Objekte (Instanzen) einschließt.type
ist eine Metaklasse der Klasse object
, und jede Klasse (einschließlich type
) hat direkt oder indirekt von object
geerbt.object
und type
. Die Verwirrung im obigen Schnipsel ergibt sich weil wir über diese Beziehungen (issubclass
und isinstance
) in Form von Python-Klassen denken. Die Beziehung zwischen object
und type
kann nicht in reinem Python reproduziert werden. Um präziser zu sein, die folgenden Beziehungen können nicht in reinem Python reproduziert werden:
object
und type
(beide sind Instanzen voneinander und von sich selbst) existieren in Python, weil auf dem Level der Implementierung "geschummelt" wird.Ausgabe:
>>> from collections import Hashable >>> from collections.abc import Hashable >>> issubclass(list, object) True >>> issubclass(object, Hashable) True >>> issubclass(list, Hashable) False
Die Unterklassenbeziehungen sollten transitiv sein, nicht wahr? (d.h., wenn A
eine Unterklasse von B
ist, und B
eine Unterklasse von C
, dann sollte A
eine Unterklasse von C
sein)
__subclasscheck__
in einer Metaklasse zu definieren.issubclass(cls, Hashable)
aufgerufen wird, sucht es einfach nach nicht-Falsey "__hash__
" Methoden in cls
oderallem, von dem es erbt.object
hashable ist, aber list
nicht-hashable, bricht es die transitive Relation.class SomeClass: def method(self): pass @classmethod def classm(cls): pass @staticmethod def staticm(): pass
Ausgabe:
>>> print(SomeClass.method is SomeClass.method) True >>> print(SomeClass.classm is SomeClass.classm) False >>> print(SomeClass.classm == SomeClass.classm) True >>> print(SomeClass.staticm is SomeClass.staticm) True
Wenn wir zweimal auf classm
zugreifen, bekommen wir ein gleiches Objekt, aber nicht dasselbe oder? Lass uns sehen, was mit Instanzen von SomeClass
passiert:
o1 = SomeClass() o2 = SomeClass()
Ausgabe:
>>> print(o1.method == o2.method) False >>> print(o1.method == o1.method) True >>> print(o1.method is o1.method) False >>> print(o1.classm is o1.classm) False >>> print(o1.classm == o1.classm == o2.classm == SomeClass.classm) True >>> print(o1.staticm is o1.staticm is o2.staticm is SomeClass.staticm) True
Der zweifache Zugriff auf classm
oder method
, erzeugt gleiche, aber nicht gleiche Objekte für dieselbe Instanz von SomeClass
.
self
als erstes Argument, obwohl es nicht explizit übergeben wird).>>> o1.method <bound method SomeClass.method of <__main__.SomeClass object at ...>>
o1.method is o1.method
niemals wahr. Der Zugriff auf Klassenattribute (im Gegensatz zu Instanzen) erzeugt jedoch keine Methoden; also ist SomeClass.method is SomeClass.method
wahr.>>> SomeClass.method <function SomeClass.method at ...>
classmethod
(Klassenmethoden) transformiert Funktionen in Klassenmethoden. Klassenmethoden sind Deskriptoren, die, wenn auf sie zugegriffen wird, ein Methodenobjekt erzeugen, welches die Klasse (Typ) des Objektes bindet, anstelle des Objektes selbst.>>> o1.classm <bound method SomeClass.classm of <class '__main__.SomeClass'>>
SomeClass.classm is SomeClass.classm
unwahr.>>> SomeClass.classm <bound method SomeClass.classm of <class '__main__.SomeClass'>>
o1.method == o1.method
wahr, auch wenn sie nicht das gleiche Objekt im Speicher sind.staticmethod
transformiert Funktionen in ein "no-op" Deskriptor, welches die Funktion so zurückgibt, wie sie ist. Es werden nie Methodenobjekte erzeugt, also ist der Vergleich mit is
wahr.>>> o1.staticm <function SomeClass.staticm at ...> >>> SomeClass.staticm <function SomeClass.staticm at ...>
self
einfügen zu können, was die Leistung negativ beeinflusst. CPython 3.7 löste dies, indem neue Opcodes eingeführt wurden, die den Aufruf von Methoden behandeln, ohne die temporären Methodenobjekte zu erzeugen. Das wird nur genutzt, wenn die Funktion, auf die zugegriffen wird, tatsächlich aufgerufen wird, also sind die Schnipsel hier nicht betroffen. Sie erzeugen also immer noch Methoden :)>>> all([True, True, True]) True >>> all([True, True, False]) False >>> all([]) True >>> all([[]]) False >>> all([[[]]]) True
Warum ist diese Änderung True/False ?
Die Implementierung der all
Funktion ist äquivalent zu
def all(iterable): for element in iterable: if not element: return False return True
all([])
gibt True
zurück, da das Iterable leer ist.
all([[]])
gibt False
zurück, weil das übergebene Array ein Element hat, []
, und in Python, eine leere Liste False
ist.
all([[[]]])
und höhere, rekursive Varianten sind immer True
, weil das einzelne Element des übergebenen Arrays ([[...]]
) nicht länger leer ist, und Listen mit Werten True
sind.
Ausgabe (< 3.6):
>>> def f(x, y,): ... print(x, y) ... >>> def g(x=4, y=5,): ... print(x, y) ... >>> def h(x, **kwargs,): File "<stdin>", line 1 def h(x, **kwargs,): ^ SyntaxError: invalid syntax >>> def h(*args,): File "<stdin>", line 1 def h(*args,): ^ SyntaxError: invalid syntax
Ausgabe:
>>> print("\"") " >>> print(r"\"") \" >>> print(r"\") File "<stdin>", line 1 print(r"\") ^ SyntaxError: EOL while scanning string literal >>> r'\'' == "\\'" True
r
gekennzeichnet), werden die Backslashes so wie sie sind weitergegeben, zusammen mit den Verhalten, dass die folgenden Zeichen escaped werden.
>>> r'wt\"f' == 'wt\\"f' True >>> print(repr(r'wt\"f') 'wt\\"f' >>> print("\n") >>> print(r"\\n") '\\n'
print(r"\")
) escaped der Backslash das abschließende Anführungszeichen, wodurch der Parser kein terminierendes Anführungszeichen bekommt (daher der SyntaxError
). Deshalb funktionieren Backslashes am Ende eines raw-Strings nicht.Ausgabe:
>>> not x == y True >>> x == not y File "<input>", line 1 x == not y ^ SyntaxError: invalid syntax
==
Operator hat eine höhere Priorität als der not
-Operator in Python.not x == y
äquivalent zu not (x == y)
, was äquivalent zu not (True == False)
ist und schließlich zu True
ausgewertet wird.x == not y
wirft einen SyntaxError
, weil man es mit (x == not) y
gleich setzen könnte und nicht x == (not y)
, was man zuerst erwarten würde.not
-Token als Teil de not in
-Operators (weil sowohl der ==
- als auch der not in
-Operator die gleiche Priorität haben), aber nachdem er kein in
-Token, welches nach einem not
-Token folgt, gefunden hat, wirft er einen SyntaxError
.Ausgabe:
>>> print('wtfpython''') wtfpython >>> print("wtfpython""") wtfpython >>> # Die folgende Anweisung wirft einen `SyntaxError` >>> # print('''wtfpython') >>> # print("""wtfpython") File "<input>", line 3 print("""wtfpython") ^ SyntaxError: EOF while scanning triple-quoted string literal
>>> print("wtf" "python")
wtfpython
>>> print("wtf" "") # or "wtf"""
wtf
'''
und """
sind auch String-Trennzeichen in Python, was einen SyntaxError hervorruft, weil der Python-Interpreter ein abschließendes dreifaches Anführungszeichen als Trennzeichen erwartet, während er das momentane String-Literal mit dreifachen Anführungszeichen scannt.1.
# Ein einfaches Beispiel, um die Anzahl der Booleans und # der Integer in einem Iterable mit gemischten Datentypen zu zählen. mixed_list = [False, 1.0, "some_string", 3, True, [], False] integers_found_so_far = 0 booleans_found_so_far = 0 for item in mixed_list: if isinstance(item, int): integers_found_so_far += 1 elif isinstance(item, bool): booleans_found_so_far += 1
Ausgabe:
>>> integers_found_so_far 4 >>> booleans_found_so_far 0
2.
>>> some_bool = True >>> "wtf" * some_bool 'wtf' >>> some_bool = False >>> "wtf" * some_bool ''
3.
def tell_truth(): True = False if True == False: print("I have lost faith in truth!")
Ausgabe (< 3.x):
>>> tell_truth() I have lost faith in truth!
bool
ist eine Unterklasse von int
in Python
>>> issubclass(bool, int) True >>> issubclass(int, bool) False
Zudem sind True
und False
Instanzen von int
>>> isinstance(True, int) True >>> isinstance(False, int) True
Der Integer Wert von True
ist 1
und der von False
ist 0
.
>>> int(True) 1 >>> int(False) 0
Siehe auch diese StackOverflow Antwort für die Begründung dahinter.
Anfänglich hatte Python keinen bool
Typ (Leute benutzten 0 für False und nicht-null Werte wie 1 für True). True
, False
, und ein bool
Typ wurden in den 2.x Versionen hinzugefügt, aber, für Rückwärtskompatibilität, konnten True
und False
nicht zu Konstanten gemacht werden. Sie waren nur built-in Variablen, was es möglich machte, sie neu zuzuweisen.
Python 3 war rückwärtskompatibel, das Problem wurde schließlich gelöst, und daher wird der letzte Schnipsel nicht mit Python 3.x funktionieren!
1.
class A: x = 1 class B(A): pass class C(A): pass
Ausgabe:
>>> A.x, B.x, C.x (1, 1, 1) >>> B.x = 2 >>> A.x, B.x, C.x (1, 2, 1) >>> A.x = 3 >>> A.x, B.x, C.x # C.x wurde verändert, aber B.x nicht (3, 2, 3) >>> a = A() >>> a.x, A.x (3, 3) >>> a.x += 1 >>> a.x, A.x (4, 3)
2.
class SomeClass: some_var = 15 some_list = [5] another_list = [5] def __init__(self, x): self.some_var = x + 1 self.some_list = self.some_list + [x] self.another_list += [x]
Ausgabe:
>>> some_obj = SomeClass(420) >>> some_obj.some_list [5, 420] >>> some_obj.another_list [5, 420] >>> another_obj = SomeClass(111) >>> another_obj.some_list [5, 111] >>> another_obj.another_list [5, 420, 111] >>> another_obj.another_list is SomeClass.another_list True >>> another_obj.another_list is some_obj.another_list True
+=
Operator modifiziert das veränderbare Objekt in-place ohne ein neues Objekt zu erzeugen. Also beeinflusst das Ändern eines Attributtes von einer Instanz die anderen Instanzen und die Klassenattribute.some_iterable = ('a', 'b') def some_func(val): return "something"
Ausgabe (<= 3.7.x):
>>> [x for x in some_iterable] ['a', 'b'] >>> [(yield x) for x in some_iterable] <generator object <listcomp> at 0x7f70b0a4ad58> >>> list([(yield x) for x in some_iterable]) ['a', 'b'] >>> list((yield x) for x in some_iterable) ['a', None, 'b', None] >>> list(some_func((yield x)) for x in some_iterable) ['a', 'something', 'b', 'something']
yield
in Generatoren und Comprehensions.yield
in List-Comprehensions und wird einen SyntaxError
werfen.1.
def some_func(x): if x == 3: return ["wtf"] else: yield from range(x)
Ausgabe (> 3.3):
>>> list(some_func(3)) []
Wo ist das "wtf"
hin? Liegt es an einem besonderen Effekt von yield from
? Lass uns das bestätigen:
2.
def some_func(x): if x == 3: return ["wtf"] else: for i in range(x): yield i
Ausgabe:
>>> list(some_func(3)) []
Das gleiche Ergebnis; hat also auch nicht funktioniert.
return
-Anweisung mit Werten innerhalb eines Generators zu benutzen (Siehe PEP380). Die offiziellen docs sagen:"...
return expr
in einem Generator führt zu einemStopIteration(expr)
, was beim Verlassen des Generators geworfen wird."
Im Fall von some_func(3)
wird StopIteration
am Beginn wegen der return
-Anweisung geworfen. Die StopIteration
Exception wird automatisch innerhalb des list(...)
-Wrapppers und der for
-Schleife abgefangen. Daher enden die beiden Schnipsel mit einer leeren Liste.
Um ["wtf"]
vom Generator some_func
zu bekommen, müssen wir die StopIteration
Exception auffangen:
try: next(some_func(3)) except StopIteration as e: some_string = e.value
1.
a = float('inf') b = float('nan') c = float('-iNf') # Diese Strings sind case-insensitiv d = float('nan')
Ausgabe:
>>> a inf >>> b nan >>> c -inf >>> float('some_other_string') ValueError: could not convert string to float: some_other_string >>> a == -c # inf==inf True >>> None == None # None == None True >>> b == d # but nan!=nan False >>> 50 / a 0.0 >>> a / a nan >>> 23 + b nan
2.
>>> x = float('nan') >>> y = x / x >>> y is y # Identität funktioniert True >>> y == y # Gleichheit von y schlägt fehl False >>> [y] == [y] # aber die Gleichheit für die Liste, die y enthält, gelingt True
'inf'
und 'nan'
sind spezielle Strings (case-insensitiv), die, wenn sie explizit zu floats
getypcasted werden, benutzt werden, um die mathematische "Unendlichkeit" und "not a number" zu repräsentieren.
Da nach dem IEEE Standard NaN != NaN
, bircht die Befolgung dieser Regel die Reflexivitätsannahme eines Collection-Elements in Python, d.h. wenn x
Teil einer Collection wie einer list
ist, dann basieren die Implementierungen, wie zum Beispiel Vergleiche, auf der Annahme, dass x == x
. Aufgrund dieser Annahme, wird die Identität zuerst verglichen (da dies schneller ist), während die beiden Elemente verglichen werden, und die Werte werden nur verglichen, wenn die Identitäten ungleich sind. Der folgende Schnipsel wird die Dinge klarer erscheinen lassen:
>>> x = float('nan') >>> x == x, [x] == [x] (False, True) >>> y = float('nan') >>> y == y, [y] == [y] (False, True) >>> x == y, [x] == [y] (False, False)
Da die Identitäten von x
und y
unterschiedlich sind, werden die Werte betrachtet, die ebenfalls unterschiedlich sind; deshalb gibt der Vergleich dieses Mal False
zurück.
Interessant zu lesen: Reflexivity, and other pillars of civilization
Das sieht vielleicht trivial aus, wenn du weißt wie Referenzen in Python funktionieren.
some_tuple = ("A", "tuple", "with", "values") another_tuple = ([1, 2], [3, 4], [5, 6])
Ausgabe:
>>> some_tuple[2] = "change this" TypeError: 'tuple' object does not support item assignment >>> another_tuple[2].append(1000) #Das wirft keinen Fehler >>> another_tuple ([1, 2], [3, 4], [5, 6, 1000]) >>> another_tuple[2] += [99, 999] TypeError: 'tuple' object does not support item assignment >>> another_tuple ([1, 2], [3, 4], [5, 6, 1000, 99, 999])
Aber ich dachte Tupel sind unveränderlich...
Zitat von https://docs.python.org/3/reference/datamodel.html
Unveränderliche Sequenzen Ein Objekt eines unveränderlichen Sequenztypen kann nicht nach der Erzeugung verändert werden. (Wenn das Objekt Referenzen zu anderen Objekten enthält, können die anderen Objekte veränderlich und modifizierbar sein; jedoch kann die Collection von Objekten, die von einem unveränderlichen Objekt referenziert werden, nicht geändert werden.)
Der +=
Operator verändert die Liste in-place. Die Element-Zuweisung funktioniert nicht, aber wenn die Exception auftritt, wurde das Element bereits an Ort und Stelle verändert.
Es gibt auch eine Erklärung im offiziellen Python FAQ.
e = 7 try: raise Exception() except Exception as e: pass
Ausgabe (Python 2.x):
>>> print(e) # gibt nichts aus
Ausgabe (Python 3.x):
>>> print(e) NameError: name 'e' is not defined
Quelle: https://docs.python.org/3/reference/compound_stmts.html#except
Wenn eine Exception mit dem as
-Target zugewiesen wurde, wird sie am Ende der except
-Klausel gelöscht. Das ist so, als ob
übersetzt wurde in
except E as N: try: foo finally: del N
Das bedeutet, dass einer Exception ein anderer Name zugewiesen werden muss, wenn man es nach einer except-Klausel benutzen möchte. Exceptions werden gelöscht, weil sie mit dem angehängten Traceback einen Referenzzyklus mit dem SatckFrame bilden, der alle Locals in diesem Frame am Leben erhält, bis die nächste Garbage Collection stattfindet.
Die Klauseln besitzen keinen Scope in Python. Alles im Beispiel liegt im selben Scope, und die Variable e
wurde entfernt, da die except-Klausel ausgeführt wurde. Das gleiche gilt nicht für Funktionen, die ihren eigenen inneren Scope haben. Das Beispiel unten erläutert dies:
def f(x): del(x) print(x) x = 5 y = [5, 4, 3]
Ausgabe:
>>> f(x) UnboundLocalError: local variable 'x' referenced before assignment >>> f(y) UnboundLocalError: local variable 'x' referenced before assignment >>> x 5 >>> y [5, 4, 3]
In Python 2.x wird der Variable e
eine Exception()
-Instanz zugewiesen, wenn du also versuchst, dies auszugeben, wird gar nichts ausgegeben.
Ausgabe (Python 2.x):
>>> e Exception() >>> print e # Nichts wird ausgegeben!
class SomeClass(str): pass some_dict = {'s': 42}
Ausgabe:
>>> type(list(some_dict.keys())[0]) str >>> s = SomeClass('s') >>> some_dict[s] = 40 >>> some_dict # erwartet: zwei verschiedene key-value-Paare {'s': 40} >>> type(list(some_dict.keys())[0]) str
Das Objekt s
und der String "s"
hashen auf denselben Wert, weil SomeClass
erbt die __hash__
-Methode von der str
-Klasse.
SomeClass("s") == "s"
wird zu True
ausgewertet, weil SomeClass
auch die __eq__
-Methode von der str
-Klasse erbt.
Da beide Objekte auf denselben Wert hashen und gleich sind, werden sie durch denselben Key im Dictionary repräsentiert.
Für das gewünschte Verhalten, können wir die __eq__
-Methode in SomeClass
neu definieren:
class SomeClass(str): def __eq__(self, other): return ( type(self) is SomeClass and type(other) is SomeClass and super().__eq__(other) ) # Wenn wir ein benutzerdefiniertes __eq__ definieren, stoppt Python automatisch die Vererbung der # __hash__ Methode, also müssen wir diese auch noch definieren __hash__ = str.__hash__ some_dict = {'s':42}
Ausgabe:
>>> s = SomeClass('s') >>> some_dict[s] = 40 >>> some_dict {'s': 40, 's': 42} >>> keys = list(some_dict.keys()) >>> type(keys[0]), type(keys[1]) (__main__.SomeClass, str)
Ausgabe:
(target_list "=")+ (expression_list | yield_expression)
undEine Zuweisungsanweisung wertet eine Liste von Ausdrücken aus (denk daran, dass dies ein einzelner Ausdruck oder eine durch Komma getrennte Liste sein kann, wobei letzteres ein Tupel ergibt) und weist dem einzelnen resultierenden Objekt jeder der Ziellisten zu, von links nach rechts.
Das +
in (target_list "=")+
meint, dass es eine oder mehrere Ziellisten geben kann. In diesem Fall sind die Ziellisten a, b
und a[b]
(beachte, dass die Liste von Ausdrücken exakt eine ist, was in unserem Fall {}, 5
ist).
Nachdem die Liste der Ausdrücke ausgewertet wurde, wird ihr Wert von links nach rechts in die Ziellisten entpackt. Also wird, in unserem Fall, zuerst das Tupel {}, 5
zu a, b
entpackt und wir haben nun a = {}
und b = 5
.
a
wird nun {}
zugewiesen, was ein veränderliches Objekt ist.
Die zweite Zielliste ist a[b]
(vielleicht hättest du erwartet, dass dies einen Fehler wirft, da sowohl a
als auch b
nicht in der Anweisung vorher definiert wurden. Aber denk daran, dass wir gerade a
dem {}
und b
der 5
zugewiesen haben).
Jetzt setzen wir den Schlüssel 5
im Dictionary auf das Tupel ({}, 5)
, was einen Zirkelschluss erzeugt (das {...}
in der Ausgabe bezieht sich auf dasselbe Objekt, was a
bereits referenziert). Ein weiteres, einfacheres Beispiel einer zirkulären Referenz:
>>> some_list = some_list[0] = [0] >>> some_list [[...]] >>> some_list[0] [[...]] >>> some_list is some_list[0] True >>> some_list[0][0][0][0][0][0] == some_list True
Ähnlich verhält es sich in unserem Beispiel (a[b][0]
ist dasselbe Objekt wie a
)
Um zusammenzufassen, kannst du das Beispiel wie folgt aufgliedern
Und der zirkuläre Bezug lässt sich durch die Tatsache rechtfertigen, dass a[b][0]
dasselbe Objekt ist wie a
>>> # Python 3.10.6 >>> int("2" * 5432) >>> # Python 3.10.8 >>> int("2" * 5432)
Ausgabe:
>>> # Python 3.10.6 222222222222222222222222222222222222222222222222222222222222222... >>> # Python 3.10.8 Traceback (most recent call last): ... ValueError: Exceeds the limit (4300) for integer string conversion: value has 5432 digits; use sys.set_int_max_str_digits() to increase the limit.
Die Aufforderung int()
funktioniert gut in Python 3.10.6 und gibt einen ValueError in Python 3.10.8 aus. Beachte, dass Python auch mit großen ganzen Zahlen arbeiten kann. Der Fehler tritt nur auf, wenn du zwischen Integern und Strings konvertiert wird.
Glücklicherweise kannst du den Grenzwert für die zulässige Anzahl von Ziffern erhöhen, wenn du erwartest, dass ein Vorgang diesen Grenzwert überschreitet. Um das zu tun, kannst du folgendes benutzen:
Check die Dokumentation für mehr Details über das Verändern des Default-Limits, wenn du erwartest, dass dein Code diesen Wert übersteigt.
▶ Modifizieren eines Dictionarys während einer Iterationx = {0: None} for i in x: del x[i] x[i+1] = None print(i)
Ausgabe (Python 2.7- Python 3.5):
Ja, es läuft exakt acht mal und stoppt dann.
RuntimeError: dictionary keys changed during iteration
Exception sehen, wenn du so etwas versuchst.del
Operation
class SomeClass: def __del__(self): print("Deleted!")
Ausgabe: 1.
>>> x = SomeClass() >>> y = x >>> del x # das sollte "Deleted!" ausgeben >>> del y Deleted!
Endlich wird deleted ausgegeben. Vielleicht erahnst du schon schon, warum __del__
nicht schon bei unserem ersten Versuch, x
zu löschen, aufgerufen wurde. Ergänzen wir das Beispiel um weitere Aspekte
2.
>>> x = SomeClass() >>> y = x >>> del x >>> y # check, ob y existiert <__main__.SomeClass instance at 0x7f98a1a67fc8> >>> del y # Wie vorher sollte das "Deleted!" ausgeben >>> globals() # oh, das hat es nicht. Lass uns alle globalen Variablen checken und das bestätigen Deleted! {'__builtins__': <module '__builtin__' (built-in)>, 'SomeClass': <class __main__.SomeClass at 0x7f98a1a5f668>, '__package__': None, '__name__': '__main__', '__doc__': None}
Okay, jetzt ist es gelöscht 😕
del x
ruft nicht direkt x.__del__()
auf.del x
gestoßen wird, dann löscht Python den Namen x
vom momentanen Scope und dekrementiert den Referenz-Counter des Objektes, welches x
referenziert, um 1. __del__()
wird nur aufgerufen, wenn der Referenz-Counter des Objektes 0 erreicht.__del__()
nicht aufgerufen, weil die vorherige Anweisung (>>> y
) im interaktiven Interpreter eine neue Referenz zum selben Objekt erzeugt hat (spezifisch die magische Variable _
, welche das Ergebnis des letzten, nicht-None
Ausdrucks der REPL referenziert), und daher den Referenz-Counter davon abhält, die 0 zu erreichen, wenn del y
gelesen wurde.globals
(oder irgendetwas, dass kein Ergebnis hat, dass None
ist) wurde _
angewiesen, dass neue Ergebnis zu referenzieren, wodurch die bestehende Referenz fallen gelassen wurde. Nun hat der Referenz-Counter 0 erreicht und wir können sehen, dass "Deleted!" ausgegeben wurde (Endlich!)1.
a = 1 def some_func(): return a def another_func(): a += 1 return a
2.
def some_closure_func(): a = 1 def some_inner_func(): return a return some_inner_func() def another_closure_func(): a = 1 def another_inner_func(): a += 1 return a return another_inner_func()
Ausgabe:
>>> some_func() 1 >>> another_func() UnboundLocalError: local variable 'a' referenced before assignment >>> some_closure_func() 1 >>> another_closure_func() UnboundLocalError: local variable 'a' referenced before assignment
Wenn du eine einer Variablen in einem Scope etwas zuweist, dann wird das für diesen Scope lokal. Also wird a
lokal für den Scope von another_func
, aber es wurde vorher nicht im selben Scope initialisiert, was einen Fehler wirft.
Um die Outer-Scope-Variable a
in another_func
zu modifizieren, müssen wir das global
Schlüsselwort verwenden.
def another_func() global a a += 1 return a
Ausgabe:
In another_closure_func
wird a
für den Scope von another_inner_func
lokal, aber es wurde vorher nicht im selben Scope initialisiert, was der Grund für den Fehler ist.
Um die Outer-Scope-Variable a
in another_inner_func
zu modifizieren, brauchen wir das nonlocal
Schlüsselwort. Die nonlocal-Anweisung bezieht sich auf Variablen, die im nächstgelegenen äußeren Scope (globaler exkludiert) definiert wurden.
def another_func(): a = 1 def another_inner_func(): nonlocal a a += 1 return a return another_inner_func()
Ausgabe:
Die Schlüsselwörter global
und nonlocal
teilen dem Python Interpreter mit, keine neuen Variablen zu deklarieren und diese im entsprechenden äußeren Scope nachzuschlagen.
Lies diesen kurzen, aber tollen Artikel durch, der ein super Leitfaden ist, um mehr über Namespaces und Scope-Auflösung in Python zu lernen.
list_1 = [1, 2, 3, 4] list_2 = [1, 2, 3, 4] list_3 = [1, 2, 3, 4] list_4 = [1, 2, 3, 4] for idx, item in enumerate(list_1): del item for idx, item in enumerate(list_2): list_2.remove(item) for idx, item in enumerate(list_3[:]): list_3.remove(item) for idx, item in enumerate(list_4): list_4.pop(idx)
Ausgabe:
>>> list_1 [1, 2, 3, 4] >>> list_2 [2, 4] >>> list_3 [] >>> list_4 [2, 4]
Kannst du erklären, warum die Ausgabe [2, 4]
ist?
Es ist nie eine gute Idee ein Objekt zu verändern, worüber du gerade iterierst. Korrekt wäre es stattdessen über eine Kopie des Objektes zu iterieren, und list_3[:]
tut genau das.
>>> some_list = [1, 2, 3, 4] >>> id(some_list) 139798789457608 >>> id(some_list[:]) # Beachte, dass Python ein neues Objekt für geslicte Listen baut. 139798779601192
Unterschied zwischen del
, remove
, und pop
:
del var_name
entfernt nur die Bindung von var_name
vom lokalen oder globalen Namespace (Deshalb ist list_1
davon nicht betroffen).remove
entfernt den ersten Wert, der übereinstimmt, keinen spezifischen Index, und wirft einen ValueError
, wenn der Wert nicht gefunden werden konnte.pop
entfernt ein Element am angegebenen Index und gibt dieses zurück und wirft einen IndexError
, wenn ein falscher Index angegeben wurde.Warum ist die Ausgabe [2, 4]
?
1
von list_2
oder list_4
entfernen, ist der Inhalt der Listen nun [2, 3, 4]
. Die verbliebenen Elemente werden heruntergeschoben, d.h.., 2
ist am Index 0, und 3
ist am Index 1. Da die nächste Iteration an Index 1 nachsehen wird (wo 3
steht), wird die 2
komplett übersprungen. Ähnliches wird anderen Element in einer Listensequenz passieren.>>> numbers = list(range(7)) >>> numbers [0, 1, 2, 3, 4, 5, 6] >>> first_three, remaining = numbers[:3], numbers[3:] >>> first_three, remaining ([0, 1, 2], [3, 4, 5, 6]) >>> numbers_iter = iter(numbers) >>> list(zip(numbers_iter, first_three)) [(0, 0), (1, 1), (2, 2)] # so weit, so gut, lass uns den Rest zippen >>> list(zip(numbers_iter, remaining)) [(4, 3), (5, 4), (6, 5)]
Wo ist das Element 3
von der Liste numbers
hin?
def zip(*iterables): sentinel = object() iterators = [iter(it) for it in iterables] while iterators: result = [] for it in iterators: elem = next(it, sentinel) if elem is sentinel: return result.append(elem) yield tuple(result)
result
hinzu, indem es die Funktion next
auf ihnen aufruft, und stoppt immer dann, wenn einer der Iterables aufgebraucht ist.result
Liste verworfen werden. Das ist mit der 3
in numbers_iter
passiert.zip
für das obige sähe wie folgt aus:
>>> numbers = list(range(7)) >>> numbers_iter = iter(numbers) >>> list(zip(first_three, numbers_iter)) [(0, 0), (1, 1), (2, 2)] >>> list(zip(remaining, numbers_iter)) [(3, 3), (4, 4), (5, 5), (6, 6)]Das erste Argument von zip sollte das mit der geringsten Anzahl an Elementen sein.
1.
for x in range(7): if x == 6: print(x, ': for x inside loop') print(x, ': x in global')
Ausgabe:
6 : for x inside loop 6 : x in global
Aber x
wurde nie außerhalb des Scopes der for-Schleife definiert...
2.
# Las uns dieses Mal x zuerst initialisieren x = -1 for x in range(7): if x == 6: print(x, ': for x inside loop') print(x, ': x in global')
Ausgabe:
6 : for x inside loop 6 : x in global
3.
Ausgabe (Python 2.x):
>>> x = 1 >>> print([x for x in range(5)]) [0, 1, 2, 3, 4] >>> print(x) 4
Ausgabe (Python 3.x):
>>> x = 1 >>> print([x for x in range(5)]) [0, 1, 2, 3, 4] >>> print(x) 1
In Python benutzen for-Schleifen den Scope, in dem sie existieren und lassen ihre definierte Schleifenvariable zurück. Das passiert auch, wenn wir diese Variable vorher im globalen Namespace definieren. In diesem Fall wird die bestehende Variable neu gebunden.
Die Unterschiede in der Ausgabe des Python 2.x und Python 3.x Interpreters für List-Comprehensions können durch die folgende Änderung erklärt werden, welche im What’s New In Python 3.0 Changelog dokumentiert wurde:
"List-Comprehensions unterstützen nicht länger die syntaktische Form
[... for var in item1, item2, ...]
. Benutze[... for var in (item1, item2, ...)]
stattdessen. Beachte zudem, dass List-Comprehensions eine andere Semantik haben: sie sind eher syntaktischer Zucker für einen Generator-Ausdruck innerhalb eineslist()
-Konstruktors und insbesondere werden die Steuervariablen der Schleife nicht mehr in den umgebenden Scope geleakt."
def some_func(default_arg=[]): default_arg.append("some_string") return default_arg
Ausgabe:
>>> some_func() ['some_string'] >>> some_func() ['some_string', 'some_string'] >>> some_func([]) ['some_string'] >>> some_func() ['some_string', 'some_string', 'some_string']
Die vorgegebenen veränderbaren Argumente von Funktionen in Python werden nicht wirklich jedes Mal, wenn du die Funktion aufrufst, initialisiert. Stattdessen wird der zuletzt zugewiesene Wert als default genommen. Als wir explizit []
an some_func
als Argument übergeben haben, dann wurde der default-Wert der default_arg
-Variable nicht benutzt, also hat die Funktion das zurückgegeben, was wir erwartet hatten.
def some_func(default_arg=[]): default_arg.append("some_string") return default_arg
Ausgabe:
>>> some_func.__defaults__ #Das zeigt die default-Werte der Argumente für die Funktion ([],) >>> some_func() >>> some_func.__defaults__ (['some_string'],) >>> some_func() >>> some_func.__defaults__ (['some_string', 'some_string'],) >>> some_func([]) >>> some_func.__defaults__ (['some_string', 'some_string'],)
Eine typische Vorgehensweise um Fehler, ausgelöst durch veränderbare Argumente, zu verhindern ist es, None
dem default-Wert zuzuweisen und später zu überprüfen, ob irgendein Wert an die Funktion übergeben wird, der diesem Argument entspricht. Beispiel:
def some_func(default_arg=None): if default_arg is None: default_arg = [] default_arg.append("some_string") return default_arg
some_list = [1, 2, 3] try: # Das sollte einen ``IndexError`` werfen print(some_list[4]) except IndexError, ValueError: print("Caught!") try: # Das sollte einen ``ValueError`` werfen some_list.remove(4) except IndexError, ValueError: print("Caught again!")
Ausgabe (Python 2.x):
Caught! ValueError: list.remove(x): x not in list
Ausgabe (Python 3.x):
File "<input>", line 3 except IndexError, ValueError: ^ SyntaxError: invalid syntax
Um mehrere Exceptions der except-Klausel hinzuzufügen, musst du diese als Tupel mit Klammern und als erstes Argument übergeben. Das zweite Argument ist ein optionaler Name, der, wenn angegeben, die Exception-Instanz bindet, die geworfen wurde. Beispiel:
some_list = [1, 2, 3] try: # Das sollte einen ``ValueError`` werfen some_list.remove(4) except (IndexError, ValueError), e: print("Caught again!") print(e)
Ausgabe (Python 2.x):
Caught again!
list.remove(x): x not in list
Ausgabe (Python 3.x):
File "<input>", line 4 except (IndexError, ValueError), e: ^ IndentationError: unindent does not match any outer indentation level
Die Exceptions und die Variable mit einem Komma zu trennen ist veraltet und funktioniert in Python 3 nicht mehr; der korrekte Weg wäre as
zu benutzen. Beispiel:
some_list = [1, 2, 3] try: some_list.remove(4) except (IndexError, ValueError) as e: print("Caught again!") print(e)
Ausgabe:
Caught again!
list.remove(x): x not in list
1.
a = [1, 2, 3, 4] b = a a = a + [5, 6, 7, 8]
Ausgabe:
>>> a [1, 2, 3, 4, 5, 6, 7, 8] >>> b [1, 2, 3, 4]
2.
a = [1, 2, 3, 4] b = a a += [5, 6, 7, 8]
Ausgabe:
>>> a [1, 2, 3, 4, 5, 6, 7, 8] >>> b [1, 2, 3, 4, 5, 6, 7, 8]
a += b
verhält sich nicht immer wie a = a + b
. Klassen können die op=
Operatoren unterschiedlich implementieren und Listen tun das.
Der Ausdruck a = a + [5,6,7,8]
generiert eine neue Liste und setzt a
s Referenz auf diese neue Liste, wodurch b
unverändert bleibt.
Der Ausdruck a += [5,6,7,8]
wird tatsächlich zu einer "extend"-Funktion gemapped wird, die auf der Liste arbeitet, sodass a
und b
immer noch auf dieselbe Liste zeigen, die in-place modifiziert wurde.
1.
x = 5 class SomeClass: x = 17 y = (x for i in range(10))
Ausgabe:
>>> list(SomeClass.y)[0] 5
2.
x = 5 class SomeClass: x = 17 y = [x for i in range(10)]
Ausgabe (Python 2.x):
Ausgabe (Python 3.x):
Lass uns eine naive Funktion implementieren, um das mittlere Element einer Liste zu bekommen:
def get_middle(some_list): mid_index = round(len(some_list) / 2) return some_list[mid_index - 1]
Python 3.x:
>>> get_middle([1]) # sieht gut aus 1 >>> get_middle([1,2,3]) # sieht gut aus 2 >>> get_middle([1,2,3,4,5]) # huh? 2 >>> len([1,2,3,4,5]) / 2 # gut 2.5 >>> round(len([1,2,3,4,5]) / 2) # Warum? 2
Sieht so aus, als ob Python 2.5 zu 2 rundet.
round()
banker's rounding, wodurch 0,5er-Brüche auf die nächste gerade Zahl gerundet werden:>>> round(0.5) 0 >>> round(1.5) 2 >>> round(2.5) 2 >>> import numpy # numpy tut dasselbe >>> numpy.round(0.5) 0.0 >>> numpy.round(1.5) 2.0 >>> numpy.round(2.5) 2.0
get_middle([1])
nur 1 zurückgegeben hat, weil der Index round(0.5) - 1 = 0 - 1 = -1
war, wodurch das letzte Element in der Liste zurückgibt.Ich habe bis heute keinen einzigen erfahrenen Pythonisten getroffen, der nicht auf eines oder mehrere der folgenden Szenarien gestoßen ist:
1.
x, y = (0, 1) if True else None, None
Ausgabe:
>>> x, y # erwartet: (0, 1) ((0, 1), None)
2.
t = ('one', 'two') for i in t: print(i) t = ('one') for i in t: print(i) t = () print(t)
Ausgabe:
3.
ten_words_list = [
"some",
"very",
"big",
"list",
"that"
"consists",
"of",
"exactly",
"ten",
"words"
]
Ausgabe
>>> len(ten_words_list) 9
4. Not asserting strongly enough
a = "python" b = "javascript"
Ausgabe:
# Eine assert-Anweisung mit einer assertion-Fehlermeldung. >>> assert(a == b, "Both languages are different") # Kein AssertionError wurde geworfen
5.
some_list = [1, 2, 3] some_dict = { "key_1": 1, "key_2": 2, "key_3": 3 } some_list = some_list.append(4) some_dict = some_dict.update({"key_4": 4})
Ausgabe:
>>> print(some_list) None >>> print(some_dict) None
6.
def some_recursive_func(a): if a[0] == 0: return a[0] -= 1 some_recursive_func(a) return a def similar_recursive_func(a): if a == 0: return a a -= 1 similar_recursive_func(a) return a
Ausgabe:
>>> some_recursive_func([5, 0]) [0, 0] >>> similar_recursive_func(5) 4
Für 1 wäre die korrekte Anweisung für das erwartete Verhalten x, y = (0, 1) if True else (None, None)
.
Für 2 wäre die korrekte Anweisung für das erwartete Verhalten t = ('one',)
oder t = 'one',
(fehlendes Komma) andernfalls wird der Interpreter t
als ein str
betrachten und iteriert Character für Character über es.
()
ist ein spezielles Token und bezeichnet ein leeres tuple
.
In 3, wie du vielleicht schon mitbekommen hast, fehlt ein Komma nach dem fünften Element ("that"
) in der Liste. Also bei implizierter String-Literal-Konkatenation:
>>> ten_words_list ['some', 'very', 'big', 'list', 'thatconsists', 'of', 'exactly', 'ten', 'words']
Es wurde kein AssertionError
im 4.ten Schnipsel geworfen, weil wir statt den individuellen Ausdruck a == b
zu asserten, das ganze Tupel asserten. Der folgende Schnipsel wird die Dinge klarer werden lassen:
>>> a = "python" >>> b = "javascript" >>> assert a == b Traceback (most recent call last): File "<stdin>", line 1, in <module> AssertionError >>> assert (a == b, "Values are not equal") <stdin>:1: SyntaxWarning: assertion is always true, perhaps remove parentheses? >>> assert a == b, "Values are not equal" Traceback (most recent call last): File "<stdin>", line 1, in <module> AssertionError: Values are not equal
Was den fünften Schnipsel betrifft, so modifizieren die meisten Methoden, die die Elemente von Sequenzen/Mapping-Objekten modifizieren, wie list.append
, dict.update
, list.sort
, die Objekte in-place und geben None
zurück. Der Grund dafür ist eine Leistungsverbesserung, indem die Erstellung einer Kopie vermieden wird, wenn die Operation in-place ausgeführt werden kann (Verweis nach hier).
Das letzte sollte ziemlich offensichtlich sein. Ein veränderliches Objekt (wie list
) kann in der Funktion geändert werden und die Neuzuweisung eines unveränderlichen Wertes (a -= 1
) ist keine Änderung des WErtes
Wenn du dir dieser Kleinigkeiten bewusst bist, dann kannst du dir auf lange Sicht stundenlanges Debugging sparen.
>>> 'a'.split() ['a'] # ist dasselbe wie >>> 'a'.split(' ') ['a'] # aber >>> len(''.split()) 0 # ist nicht dasselbe wie >>> len(''.split(' ')) 1
' '
, aber der Dokumentation zufolge
Wenn ein Separator nicht angegeben oder
None
ist, dann wird ein anderer Splitting-Algorithmus angewendet: aufeinanderfolgende Leerzeichen werden als ein einziges Trennzeichen betrachtet, und das Ergebnis enthält keine leeren Strings am Anfang oder Ende, wenn der String führende oder nachfolgende Leerzeichen enthält. Folglich gibt das Splitten eines leeren Strings oder eines Strings, der nur ein Leerzeichen enthält, mit einen None-Separator[]
zurück. Wenn ein Separator gegeben ist,Aufeinanderfolgende Begrenzungszeichen werden nicht zusammengefasst und gelten als Begrenzung leerer Strings. (zum Beispiel,'1,,2'.split(',')
gibt['1', '', '2']
zurück). Einen leeren String mit einem angegebenen Separator zu splitten gibt['']
zurück.
>>> ' a '.split(' ') ['', 'a', ''] >>> ' a '.split() ['a'] >>> ''.split(' ') ['']
# Datei: module.py def some_weird_name_func_(): print("works!") def _another_weird_name_func(): print("works!")
Ausgabe
>>> from module import * >>> some_weird_name_func_() "works!" >>> _another_weird_name_func() Traceback (most recent call last): File "<stdin>", line 1, in <module> NameError: name '_another_weird_name_func' is not defined
Es ist oft ratsam, keine Wildcard-Importe zu verwenden. Der erste offensichtliche Grund dafür ist, dass bei Wildcard-Importen die Namen mit einem führenden Unterstrich nicht importiert werden. Das kann zu Fehlern während der Laufzeit führen.
Hätten wir diese from ... import a, b, c
Syntax benutzt, wäre der obige NameError
nicht aufgetreten.
>>> from module import some_weird_name_func_, _another_weird_name_func >>> _another_weird_name_func() works!
Wenn du wirklich Wildcard-Importe verwenden willst, musst du in deinem Modul die Liste __all__
definieren, die eine Liste der öffentlichen Objekte enthält, die bei Wildcard-Importen zur Verfügung stehen werden.
__all__ = ['_another_weird_name_func'] def some_weird_name_func_(): print("works!") def _another_weird_name_func(): print("works!")
Ausgabe
>>> _another_weird_name_func() "works!" >>> some_weird_name_func_() Traceback (most recent call last): File "<stdin>", line 1, in <module> NameError: name 'some_weird_name_func_' is not defined
>>> x = 7, 8, 9 >>> sorted(x) == x False >>> sorted(x) == sorted(x) True >>> y = reversed(x) >>> sorted(y) == sorted(y) False
Die sorted
Methode gibt immer eine Liste zurück, und das Vergleichen von Listen und Tupeln gibt in Python immer False
zurück.
>>> [] == tuple() False >>> x = 7, 8, 9 >>> type(x), type(sorted(x)) (tuple, list)
Im Gegensatz zu sorted
, gibt die reversed
Methode einen Iterator zurück. Warum? Weil Sortieren vorraussetzt, dass der Iterator entweder in-place modifiziert wird oder einen extra Container (eine Liste) benutzt, während die Umkehrung einfach durch Iteration vom letzten Index zum ersten funktionieren kann.
Also während des Vergleichs sorted(y) == sorted(y)
, wird der erste Aufruf von sorted()
den Iterator y
konsumieren, und der nächste Aufruf wird nur eine leere Liste zurückgeben.
>>> x = 7, 8, 9 >>> y = reversed(x) >>> sorted(y), sorted(y) ([7, 8, 9], [])
from datetime import datetime midnight = datetime(2018, 1, 1, 0, 0) midnight_time = midnight.time() noon = datetime(2018, 1, 1, 12, 0) noon_time = noon.time() if midnight_time: print("Time at midnight is", midnight_time) if noon_time: print("Time at noon is", noon_time)
Ausgabe (< 3.5):
('Time at noon is', datetime.time(12, 0))
The midnight time is not printed.
Vor Python 3.5, wurde der boolesche Wert für das datetime.time
-Objekt als False
betrachtet, wenn wenn es Mitternacht in UTC dargestellt hätte. Es ist fehleranfällig, wenn die if obj:
-Syntax verwendet wird, um zu prüfen, ob obj
null oder ein Äquivalent von "leer" ist.
Dieser Abschnitt enthält ein paar weniger bekannte und interessante Dinge über Python, die den meisten Anfängern wie mir nicht bekannt sind (nun aber schon).
▶ Okay Python, kannst du mich fliegen lassen?Nun hier wären wir:
Ausgabe: Sshh... It's a super-secret.
antigravity
Modul ist eines der wenigen easter eggs, die von Python Entwicklern veröffentlicht werden.import antigravity
öffnet deinen Webbrowser und zeigt zum klassischen XKCD Comic.from goto import goto, label for i in range(9): for j in range(9): for k in range(9): print("I am trapped, please rescue!") if k == 2: goto .breakout # Ausbrechen aus einer tief verschachtelten Schleife label .breakout print("Freedom!")
Ausgabe (Python 2.3):
I am trapped, please rescue! I am trapped, please rescue! Freedom!
goto
in Python wurde als Aprilscherz am 1. April 2004 angekündigt.goto
in Python nicht verwendet wird.Wenn du zu den Leuten gehörst, die keine Leerzeichen in Python verwenden wollen, um Bereiche zu kennzeichnen, kannst du den C-Stil {} verwenden, indem du folgendes importierst:
from __future__ import braces
Ausgabe:
File "some_file.py", line 1 from __future__ import braces SyntaxError: not a chance
Klammern? Niemals! Wenn du enttäuscht bist, nutze Java. Okay, eine weitere überraschende Sache, kannst du herausfinden, wo SyntaxError
im __future__
Mmodul geworfen wird code?
__future__
Modul wird normalerweise benutzt, um Features von zukünftigen Python Versionen bereitzustellen. Das "future" is in diesem spezifischen Kontext ironisch gemeint.future.c
verfügbar.future.c
ausgeführt, bevor es als normale Importanweisung behandelt wird.Ausgabe (Python 3.x)
>>> from __future__ import barry_as_FLUFL >>> "Ruby" != "Python" # Hier bestehen keine Zweifel File "some_file.py", line 1 "Ruby" != "Python" ^ SyntaxError: invalid syntax >>> "Ruby" <> "Python" True
Das wars schon.
-Das ist relevant für PEP-401, was am 1. April 2009 veröffentlicht wurde (nun weißt du, was das heißt).
Zitat vom PEP-401
Die FLUFL hat erkannt, dass der Ungleichheitsoperator != in Python 3.0 ein schrecklicher, fingerschmerzverursachender Fehler war, und führt den <> Diamantoperator als einzige Schreibweise wieder ein.
Es gab mehrere Dinge, die Onkel Barry im PEP teilte; du kannst das hier nachlesen.
Es funktioniert gut in einem Interactive Environment, aber es wirft einen SyntaxError
, wenn du es in einer Python-Datei ausführst (siehe auch dieses issue). Du kannst jedoch die Anweisung innerhalb einer eval
oder compile
verpacken, um es zum Laufen zu bekommen:
from __future__ import barry_as_FLUFL print(eval('"Ruby" <> "Python"'))
Warte, was ist this? this
ist love ❤️
Ausgabe:
Der Zen von Python, von Tim Peters
Beautiful is better than ugly.
Explicit is better than implicit.
Simple is better than complex.
Complex is better than complicated.
Flat is better than nested.
Sparse is better than dense.
Readability counts.
Special cases aren't special enough to break the rules.
Although practicality beats purity.
Errors should never pass silently.
Unless explicitly silenced.
In the face of ambiguity, refuse the temptation to guess.
There should be one-- and preferably only one --obvious way to do it.
Although that way may not be obvious at first unless you're Dutch.
Now is better than never.
Although never is often better than *right* now.
If the implementation is hard to explain, it's a bad idea.
If the implementation is easy to explain, it may be a good idea.
Namespaces are one honking great idea -- let's do more of those!
Das ist der Zen von Python!
>>> love = this >>> this is love True >>> love is True False >>> love is False False >>> love is not True or False True >>> love is not True or False; love is love # Liebe ist kompliziert True
this
Modul in Python ist ein Easter Egg für den Zen von Python (PEP 20).love is not True or False; love is love
ist ironisch, aber selbsterklärend (wenn nicht, sieh dir die Beispiele zu den is
und is not
Operatoren an).The else
clause for loops. Ein typisches Beispiel wäre:
def does_exists_num(l, to_find): for num in l: if num == to_find: print("Exists!") break else: print("Does not exist")
Ausgabe:
>>> some_list = [1, 2, 3, 4, 5] >>> does_exists_num(some_list, 4) Exists! >>> does_exists_num(some_list, -1) Does not exist
Die else
-Klausel in der Behandlung von Exceptions. Ein Beispiel:
try: pass except: print("Exception occurred!!!") else: print("Try block executed successfully...")
Ausgabe:
Try block executed successfully...
else
-Klausel nach einer Schleife wird nur dann ausgeführt, wenn es kein explizites break
nach allen Iterationen gibt. Du kannst es dir als eine "nobreak"-Klausel vorstellen.else
-Klausel nach einem try-Block wird auch "completion clause" genannt, da wenn die else
-Klausel in einer try
-Anweisung erreicht wird, dies bedeutet, dass der try-Block tatsächlich komplett erfolgreich durchlief.def some_func(): Ellipsis
Ausgabe
>>> some_func() # Keine Ausgabe, Kein Fehler >>> SomeRandomString Traceback (most recent call last): File "<stdin>", line 1, in <module> NameError: name 'SomeRandomString' is not defined >>> Ellipsis Ellipsis
Ellipsis
ein global verfügbares built-in Objekt, was äquivalent zu ...
ist.pass
-Anweisung)>>> import numpy as np >>> three_dimensional_array = np.arange(8).reshape(2, 2, 2) array([ [ [0, 1], [2, 3] ], [ [4, 5], [6, 7] ] ])Also ist unser
three_dimensional_array
ein Array von Arrays von Arrays. Angenommen, wir wollen das zweite Element (index 1
) aller innersten Arrays ausgeben, dann können wir Ellipsis verwenden, um alle vorhergehenden Dimensionen zu umgehen
>>> three_dimensional_array[:,:,1] array([[1, 3], [5, 7]]) >>> three_dimensional_array[..., 1] # Benutzen der Ellipsis. array([[1, 3], [5, 7]])Beachte: dies funktioniert für eine beliebige Anzahl von Dimensionen. Auf diese Weise kannst du sogar die erste und letzte Dimension auswählen und die mittleren ignorieren. (
n_dimensional_array[firs_dim_slice, ..., last_dim_slice]
)
(Callable[..., int]
oder Tuple[str, ...]
))Die Schreibweise ist beabsichtigt. Bitte schicke keinen Patch hierfür ab.
Ausgabe (Python 3.x):
>>> infinity = float('infinity') >>> hash(infinity) 314159 >>> hash(float('-inf')) -314159
float('-inf')
"-10⁵ x π" in Python 3, während er in Python 2 "-10⁵ x e" ist.1.
class Yo(object): def __init__(self): self.__honey = True self.bro = True
Ausgabe:
>>> Yo().bro True >>> Yo().__honey AttributeError: 'Yo' object has no attribute '__honey' >>> Yo()._Yo__honey True
2.
class Yo(object): def __init__(self): # Versuchen wir es dieses Mal mit etwas Symmetrischem self.__honey__ = True self.bro = True
Ausgabe:
>>> Yo().bro True >>> Yo()._Yo__honey__ Traceback (most recent call last): File "<stdin>", line 1, in <module> AttributeError: 'Yo' object has no attribute '_Yo__honey__'
Warum hat Yo()._Yo__honey
funktioniert?
3.
_A__variable = "Some value" class A(object): def some_func(self): return __variable # noch nirgends initialisiert
Ausgabe:
>>> A().__variable Traceback (most recent call last): File "<stdin>", line 1, in <module> AttributeError: 'A' object has no attribute '__variable' >>> A().some_func() 'Some value'
__
(doppelter Unterstrich a.k.a "dunder") anfangen und nicht mit mehr als einem Unterstrich am Ende enden, indem er _NameOfTheClass
voranstellt.__honey
-Attribut im ersten Schnipsel zugreifen zu können, müssten wir _Yo
vorne anhängen, was Konflikte mit demselben Attributsnamen, das in einer anderen Klasse definiert ist, verhindern würde.__variable
in der Anweisung return __variable
wurde zu _A__variable
, was zufällig auch der Name der Variablen ist, die wir im äußeren Scope deklariert haben.Ausgabe:
>>> value = 11 >>> valuе = 32 >>> value 11
Was?
Beachte: Um dies zu reproduzieren, kopiere einfach die Anweisungen aus dem obigen Ausschnitt und füge sie in deine Datei/Shell ein.
Einige nicht-westliche Schriftzeichen sehen genauso aus wie die Buchstaben des englischen Alphabets, werden aber von Interpreter als unterschiedlich angesehen.
>>> ord('е') # kyrillisches 'e' (Ye) 1077 >>> ord('e') # lateinisches 'e', wie es in Englisch benutzt wird und auch auf einer Standard-Tastatur vorkommt 101 >>> 'е' == 'e' False >>> value = 42 # lateinisches e >>> valuе = 23 # kyrillisches 'e', der Python 2.x Interpreter würde hier einen `SyntaxError` werfen >>> value 42
Die built-in ord()
-Funktion gibt den Unicode eines Characters zurück code point, und unterschiedliche Codepositionen des kyrillischen "e" und des lateinischen "e" begründen das Verhalten des obigen Beispiels.
# `pip install numpy` vorher. import numpy as np def energy_send(x): # Initialisiere ein numpy array np.array([float(x)]) def energy_receive(): # Gib ein leeres numpy array zurück return np.empty((), dtype=np.float).tolist()
Ausgabe:
>>> energy_send(123.456) >>> energy_receive() 123.456
Wo ist der Nobelpreis?
energy_send
erzeugt wurde, nicht zurückgegeben wird, so dass Speicherplatz für die Neuzuweisung frei ist.numpy.empty()
gibt den nächsten freien Speicherplatz zurück, ohne ihn neu zu initialisieren. Dieser Speicherplatz ist zufällig der gleiche, der gerade freigegeben wurde (normalerweise, aber nicht immer).def square(x): """ Eine einfahce Funktion, um das Quadrat einer Zahl durch Addition zu bestimmen """ sum_so_far = 0 for counter in range(x): sum_so_far = sum_so_far + x return sum_so_far
Ausgabe (Python 2.x):
Sollte das nicht 100 sein?
Note: Wenn du das Ergebnis nicht reproduzieren kannst, versuch die Datei mixed_tabs_and_spaces.py via shell auszuführen.
Vermische keine Leerzeichen und Tabs! Das Zeichen unmittelbar vor dem Return ist ein "Tab", und der Code ist an anderer Stelle im Beispiel um ein Vielfaches von "4 Leerzeichen" eingerückt.
So geht Python mit Tabs um:
Als erstes werden Tabs (von links nach rechts) durch ein bis acht Leerzeichen ersetzt, so dass die Gesamtzahl der Zeichen bis einschließlich der Ersetzung ein Vielfaches von acht ist <...>
So wird "tab" in der letzten Zeile der Funktion square
durch acht Leerzeichen ersetzt und gelangt in die Schleife.
Python 3 ist so freundlich, in solchen Fällen automatisch einen Fehler zu melden.
Ausgabe (Python 3.x):
TabError: inconsistent use of tabs and spaces in indentation
# Benutzen von "+" mit drei Strings: >>> timeit.timeit("s1 = s1 + s2 + s3", setup="s1 = ' ' * 100000; s2 = ' ' * 100000; s3 = ' ' * 100000", number=100) 0.25748300552368164 # Benutzen von "+=" mit drei Strings: >>> timeit.timeit("s1 += s2 + s3", setup="s1 = ' ' * 100000; s2 = ' ' * 100000; s3 = ' ' * 100000", number=100) 0.012188911437988281
+=
ist schneller als +
, um mehr als zwei String zu konkatenieren, weil der erste String (Beispiel: s1
für s1 += s2 + s3
), während der Berechnung des gesamten Strings, nicht zerstört wird.def add_string_with_plus(iters): s = "" for i in range(iters): s += "xyz" assert len(s) == 3*iters def add_bytes_with_plus(iters): s = b"" for i in range(iters): s += b"xyz" assert len(s) == 3*iters def add_string_with_format(iters): fs = "{}"*iters s = fs.format(*(["xyz"]*iters)) assert len(s) == 3*iters def add_string_with_join(iters): l = [] for i in range(iters): l.append("xyz") s = "".join(l) assert len(s) == 3*iters def convert_list_to_string(l, iters): s = "".join(l) assert len(s) == 3*iters
Ausgabe:
# Ausgeführt in der ipython-Shell unter Verwendung von %timeit für eine bessere Lesbarkeit der Ergebnisse. # Du kannst das timeit-Modul auch in der normalen Python-Shell/scriptm= verwenden, Beispielverwendung unten # timeit.timeit('add_string_with_plus(10000)', number=1000, globals=globals()) >>> NUM_ITERS = 1000 >>> %timeit -n1000 add_string_with_plus(NUM_ITERS) 124 µs ± 4.73 µs per loop (mean ± std. dev. of 7 runs, 100 loops each) >>> %timeit -n1000 add_bytes_with_plus(NUM_ITERS) 211 µs ± 10.5 µs per loop (mean ± std. dev. of 7 runs, 1000 loops each) >>> %timeit -n1000 add_string_with_format(NUM_ITERS) 61 µs ± 2.18 µs per loop (mean ± std. dev. of 7 runs, 1000 loops each) >>> %timeit -n1000 add_string_with_join(NUM_ITERS) 117 µs ± 3.21 µs per loop (mean ± std. dev. of 7 runs, 1000 loops each) >>> l = ["xyz"]*NUM_ITERS >>> %timeit -n1000 convert_list_to_string(l, NUM_ITERS) 10.1 µs ± 1.06 µs per loop (mean ± std. dev. of 7 runs, 1000 loops each)
Erhöhen wir die Anzahl der Iterationen um den Faktor 10.
>>> NUM_ITERS = 10000 >>> %timeit -n1000 add_string_with_plus(NUM_ITERS) # Linearer Anstieg der Ausführungszeit 1.26 ms ± 76.8 µs per loop (mean ± std. dev. of 7 runs, 1000 loops each) >>> %timeit -n1000 add_bytes_with_plus(NUM_ITERS) # Quadratische Anstieg 6.82 ms ± 134 µs per loop (mean ± std. dev. of 7 runs, 1000 loops each) >>> %timeit -n1000 add_string_with_format(NUM_ITERS) # Linearer Anstieg 645 µs ± 24.5 µs per loop (mean ± std. dev. of 7 runs, 1000 loops each) >>> %timeit -n1000 add_string_with_join(NUM_ITERS) # Linearer Anstieg 1.17 ms ± 7.25 µs per loop (mean ± std. dev. of 7 runs, 1000 loops each) >>> l = ["xyz"]*NUM_ITERS >>> %timeit -n1000 convert_list_to_string(l, NUM_ITERS) # Linearer Anstieg 86.3 µs ± 2 µs per loop (mean ± std. dev. of 7 runs, 1000 loops each)
Du kannst mehr über timeit hier timeit oder hier %timeit lesen. Sie werden verwendet, um die Ausführungszeit von Codestücken zu messen.
Benutze +
nicht, um lange Strings zu generieren — In Python ist str
unveränderlich, daher müssen der linke und der rechte String für jedes Paar von Verkettungen in den neuen String kopiert werden. Wenn du 4 String mit Länge 10 konkatenierst, dann kopierst du (10+10) + ((10+10)+10) + (((10+10)+10)+10) = 90 Characters anstatt nur 40 Characters. Die Situation verschlechtert sich quadratisch mit zunehmender Anzahl und Größe der Zeichenketten (mit den Ausführungszeiten der Funktion add_bytes_with_plus
begründet)
Daher ist es ratsam, die Syntax .format.
oder %
zu verwenden. (sie sind jedoch bei sehr kurzen Zeichenfolgen etwas langsamer als x
).
Oder besser, wenn du bereits Inhalte in Form eines iterierbaren Objekts zur Verfügung hast, dann verwende ''.join(iterable_object)
, was viel schneller ist.
Im Gegensatz zu add_bytes_with_plus
zeigte add_string_with_plus
aufgrund der im vorherigen Beispiel besprochenen +=
-Optimierungen keinen quadratischen Anstieg der Ausführungszeit. Wäre die Anweisung s = s + "x" + "y" + "z"
statt s += "xyz"
gewesen, wäre der Anstieg quadratisch gewesen.
def add_string_with_plus(iters): s = "" for i in range(iters): s = s + "x" + "y" + "z" assert len(s) == 3*iters >>> %timeit -n100 add_string_with_plus(1000) 388 µs ± 22.4 µs per loop (mean ± std. dev. of 7 runs, 1000 loops each) >>> %timeit -n100 add_string_with_plus(10000) # Quadratischer Anstieg der Ausführungszeit 9 ms ± 298 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)
So viele Möglichkeiten einen gigantischen String zu erzeugen und zu formatieren stehen irgendwie in Kontrast zum Zen von Python, nachdem gilt:
Es sollte einen - und vorzugsweise nur einen - offensichtlichen Weg geben, dies zu tun.
dict
Lookups *
some_dict = {str(i): 1 for i in range(1_000_000)} another_dict = {str(i): 1 for i in range(1_000_000)}
Ausgabe:
>>> %timeit some_dict['5'] 28.6 ns ± 0.115 ns per loop (mean ± std. dev. of 7 runs, 10000000 loops each) >>> some_dict[1] = 1 >>> %timeit some_dict['5'] 37.2 ns ± 0.265 ns per loop (mean ± std. dev. of 7 runs, 10000000 loops each) >>> %timeit another_dict['5'] 28.5 ns ± 0.142 ns per loop (mean ± std. dev. of 7 runs, 10000000 loops each) >>> another_dict[1] # Versuch, auf einen Schlüssel zuzugreifen, der nicht existiert Traceback (most recent call last): File "<stdin>", line 1, in <module> KeyError: 1 >>> %timeit another_dict['5'] 38.5 ns ± 0.0913 ns per loop (mean ± std. dev. of 7 runs, 10000000 loops each)
Wieso werden dieselben lookups immer langsamer?
str
, int
, beliebige Objekte ...) behandelt, und eine spezialisierte Funktion für den häufigen Fall von Dictionaries, die nur aus str
-Schlüsseln bestehen.lookdict_unicode
in CPythons code genannt) weiß, dass alle vorhandenen Schlüssel (einschließlich des nachgeschlagenen Schlüssels) Strings sind, und verwendet den schnelleren und einfacheren Stringvergleich, um Schlüssel zu vergleichen, anstatt die Methode __eq__
aufzurufen.dict
-Instanz mit einem Nicht-str
-Schlüssel zugegriffen wird, wird sie so geändert, dass zukünftige Suchvorgänge die generische Funktion verwenden.dict
-Instanz nicht umkehrbar, und der Schlüssel muss nicht einmal im Dictionary vorhanden sein. Deshalb hat der Versuch eines fehlgeschlagenen Nachschlagevorgangs denselben Effekt.dict
s *
import sys class SomeClass: def __init__(self): self.some_attr1 = 1 self.some_attr2 = 2 self.some_attr3 = 3 self.some_attr4 = 4 def dict_size(o): return sys.getsizeof(o.__dict__)
Ausgabe: (Python 3.8, oder Python 3 Versionen können ein bisschen variieren)
>>> o1 = SomeClass() >>> o2 = SomeClass() >>> dict_size(o1) 104 >>> dict_size(o2) 104 >>> del o1.some_attr1 >>> o3 = SomeClass() >>> dict_size(o3) 232 >>> dict_size(o1) 232
Versuchen wir es noch einmal... in einem neuen Interpreter:
>>> o1 = SomeClass() >>> o2 = SomeClass() >>> dict_size(o1) 104 # wie erwartet >>> o1.some_attr5 = 5 >>> o1.some_attr6 = 6 >>> dict_size(o1) 360 >>> dict_size(o2) 272 >>> o3 = SomeClass() >>> dict_size(o3) 232
Was führt dazu, dass diese Dictionaries aufgebläht werden? Und warum werden neu erstellte Objekte ebenfalls aufgebläht?
__init__
der allerersten erstellten Instanz, ohne ein "unshare" zu verursachen). Wenn mehrere Instanzen existieren, wenn eine Größenänderung stattfindet, wird das Key-Sharing für alle zukünftigen Instanzen der gleichen Klasse deaktiviert: CPython kann nicht mehr feststellen, ob die Instanzen den gleichen Satz von Attributen verwenden und entscheidet sich, den Versuch, ihre Schlüssel zu teilen, abzubrechen.__init__
initialisiert werden!join()
ist eine String-Operation anstatt einer List-Operation. (auf den ersten Blick nicht einfach zu verstehen)
💡 Erklärung: Wenn join()
eine Methode auf einem String ist, dann kann es auf irgendeinem Iterable operieren (Liste, Tuple, Iteratoren). Wäre es eine Methode auf einer Liste, müsste sie von jedem Typ separat implementiert werden. Außerdem macht es nicht viel Sinn, eine String-spezifische Methode auf eine generische API eines listen
-Objektes anzuwenden
Ein paar komisch aussehende aber semantisch korrekte Statements:
[] = ()
ist ein semantisch korrektes Statement (entpacken eines leeren Tuples
in eine leere Liste
)'a'[0][0][0][0][0]
ist auch semantisch korrect, weil Python keinen Char-Datentyp wie viele andere Sprachen, die auf C basieren, hat. Also ergibt das Auswählen eines einzelnen Characters aus einem String einen Single-character String.3 --0-- 5 == 8
und --5 == 5
sind beides semantisch korrekte Statements und wird zu True
ausgewertet.Sei a
eine Zahl, ++a
und --a
sind beides valide Python Statements aber sie verhaten sich nicht so wie vergleichbare Statements in Sprachen wie C, C++, oder Java.
>>> a = 5 >>> a 5 >>> ++a 5 >>> --a 5
💡 Erklärung:
++
Operator in der Python Grammatik. Es handelt sich eigentlich um zwei +
Operatoren.++a
parst eigentlich +(+a)
was übersetzt wird zu a
. In ähnlicher Weise kann die Ausgabe des Statement --a
begründet werden.Der Walrus Operator in Python sollte dir bekannt sein. Aber hast du schon mal von dem space-invader Operator gehört?
>>> a = 42 >>> a -=- 1 >>> a 43
Er wird als alternativer Inkrementierungsoperator verwendet, zusammen mit einem anderen Operator
💡 Erklärung: Dieser Streich kommt von Raymond Hettingers Tweet. Der Space Invader Operator ist tatsächlich nur ein schlecht formatiertes a -= (-1)
, was äquivalent zu a = a - (- 1)
ist. Ähnliches gilt für a += (+ 1)
.
Python hat einen undokumentierten umgekehrten Implikations Operator.
>>> False ** False == True True >>> False ** True == False True >>> True ** False == True True >>> True ** True == True True
💡 Erklärung: Wenn du False
und True
durch 0 und 1 ersetzt und dann rechnest, dann ist die Wahrheitstabelle äquivalent zu einem Operator der umgekehrten Implikation. (Quelle)
Weil wir über Operatoren sprechen, erwähnen wir auch den @
Ooperator, der für Matrixmultiplikation benutzt wird (Keine Sorge, diese mal ist es ernst).
>>> import numpy as np >>> np.array([2, 2, 2]) @ np.array([7, 8, 8]) 46
💡 Erklärung: Der @
Operator wurde, mit der wissenschaftlichen Community im Hinterkopf, in Python 3.5 eingeführt. Jedes Objekt kann die magische __matmul__
Method überladen, um ein bestimmtes Verhalten für diesen Operator zu definieren.
Ab Python 3.8 kannst du eine typische f-String-Syntax wie f'{some_var=}
für schnelles Debugging verwenden. Zum Beispiel:
>>> some_string = "wtfpython" >>> f'{some_string=}' "some_string='wtfpython'"
Python nutzt 2 Bytes die Speicherung lokaler Variablen in Funktionen. Theoretisch heißt das, dass nur 65536 Variablen in einer Funktion gespeichert werden können. Allerdings hat Python eine praktische, eingebaute Lösung, die benutzt werden kann, um mehr als 2^16 Variablennamen zu speichern. Der folgende Code demonstriert, was im Stack passiert, wenn mehr als 65536 lokale Variablen definiert werden (Warnung: Dieser Code gibt ungefähr 2^18 Zeilen Text aus, also sei darauf vorbereitet!):
import dis exec(""" def f(): """ + """ """.join(["X" + str(x) + "=" + str(x) for x in range(65539)])) f() print(dis.dis(f))
Mehrere Python Threads werden deinen Python code nicht gleichzeitig laufen lassen (Ja, du hast richtig gehört!). Es mag intuitiv erscheinen, mehrere Threads zu erzeugen, die dann deinen Python code gleichzeitig ausführen, aber wegen dem Global Interpreter Lock in Python, sorgst du nur dafür, dass die Threads abwechselnd auf demselben Kern ausgeführt werden. Python Threads sind gut für IO-gebundene Aufgaben, aber um tatsächliche Parallelisierung für CPU-gebundene Aufgaben in Python zu erreichen, solltest du lieber das Python multiprocessing Modul benutzen.
Manchmal gibt die print
Methode die Werte nicht sofort aus. Zum Beispiel:
# Datei some_file.py import time print("wtfpython", end="_") time.sleep(3)
Das wird wtfpython
nach 3 Sekunden, aufgrund des end
Argumentes ausgeben, wei der Ausgabe-Puffer, entweder nach \n
oder wenn das Programm die Ausführung beendet, geflusht wird. Mit dem Argument flush=True
können wir den Puffer zum Flushen zwingen.
List slicing mit Indices, die nicht innerhalb der Grenzen liegen, wirft keinen Fehler
>>> some_list = [1, 2, 3, 4, 5] >>> some_list[111:] []
Slicing eines Iterables erzeugt nicht immer ein neues Objekt. Zum Beispiel:
>>> some_str = "wtfpython" >>> some_list = ['w', 't', 'f', 'p', 'y', 't', 'h', 'o', 'n'] >>> some_list is some_list[:] # False erwartet, weil ein neues Objekt erzeugt wird. False >>> some_str is some_str[:] # True, weil Strings immutable sind, daher ist die Erstellung eines neuen Objektes sinnlos. True
int('١٢٣٤٥٦٧٨٩')
gibt 123456789
in Python 3 zurück. In Python umfassen Dezimalzeichen auch Ziffernzeichen, und alle Zeichen, die zum Erstellen von Dezimal-Radix-Nummern benutzt werden können, z.B. U+0660, ARABIC-INDIC DIGIT ZERO. Hier ist eine interessante Geschichte im Zusammenhang mit diesem Verhalten in Python.
Du kannst numerische Literale mit Unterstrichen trennen, um die Lesbarkeit zu erhöhen (Python 3 spezifisch).
>>> sechs_millionen = 6_000_000 >>> sechs_millionen 6000000 >>> sechs_millionen = 0xF00D_CAFE >>> sechs_millionen 4027435774
'abc'.count('') == 4
. Hier ist eine ungefähre Implementierung der Methode count
, die die Dinge klarer machen würde
def count(s, sub): result = 0 for i in range(len(s) + 1 - len(sub)): result += (s[i:i + len(sub)] == sub) return result
Das Verhalten ist darauf zurückzuführen, dass leere Teilstrings (''
) mit Slices der Länge 0 in der ursprünglichen Zeichenkette übereinstimmen
Ein paar Wege, wie du zu wtfpython beitragen kannst:
Für mehr Details, wirf bitte einen Blick auf CONTRIBUTING.md. Erstelle ruhig ein neues Issue, um zu diskutieren.
PS: Bitte keine Anfragen mit Links erstellen. Es werden keine Links hinzugefügt, es sein denn sie sind für das Projekt relevant.
Die Idee und das Design für diese Sammlung wurden durch Denys Dovhans tolles Projekt inspiriert wtfjs. Der überragende Support von Pythonisten hat es zu dem gemacht, was es heute ist.
Ein paar nützliche Links!Wenn du wtfpython cool findest, kannst du diese Quick-Links nutzen, um es mit deinen Freunden zu teilen:
Brauchst du eine pdf version?Ich habe ein paar Anfragen für eine pdf (und epub) Version von wtfpython erhalten. Du kannst hier deine Daten angeben, um sie schnellstmöglich zu bekommen.
Das ist alles, Freunde! Um neue Updates zu erhalten, kannst du deine email hier hinzufügen.
RetroSearch is an open source project built by @garambo | Open a GitHub Issue
Search and Browse the WWW like it's 1997 | Search results from DuckDuckGo
HTML:
3.2
| Encoding:
UTF-8
| Version:
0.7.4