Odkrywanie i rozumienie Pythona z pomocą zaskakujących fragmentów kodu.
Tłumaczenia: Angielski English | Chiński 中文 | Dodaj tłumaczenie
Inne tryby: Interaktywny | CLI
Python, będąc wspaniale zaprojektowanym, wysoko-poziomowym, interpretowanym językiem programowania, zapewnia nam wiele funkcji zwiększających wygodę programowania. Czasami jednak, wyniki pewnych fragmentów kodu mogą okazać się niejasne na pierwszy rzut oka.
wtfpython to zabawny projekt starający się wytłumaczyć co dokładnie dzieje się pod maską Pythona podczas używania pewnych nieintuicyjnych i mniej znanych funkcji.
Podczas gdy niektóre z Przykładów, które znajdziesz poniżej, mogą nie być typowo WTF, to jednak pokazują pewne interesujące zachowania Pythona, których możesz nie znać. Uważam, że jest to dobry sposób na poznanie wnętrza języka programowania i wierzę, że Ciebie też to zainteresuje!
Jeśli jesteś zaawansowanym programistą Pythona, możesz wziąć za wyzwanie samodzielne rozwiązanie tych problemów. Może doświadczyłeś już pewnych z nich i przypomnisz sobie tamte piękne chwile! 😅
PS: Jeśli jesteś tu po raz kolejny, o nowościach i modyfikacjach dowiesz się stąd.
No to lecimy...
- Structure of the Examples
- Usage
- 👀 Examples
- Section: Strain your brain!
- ▶ First things first! *
- ▶ Strings can be tricky sometimes
- ▶ Hash brownies
- ▶ Deep down, we're all the same.
- ▶ Disorder within order *
- ▶ Keep trying... *
- ▶ For what?
- ▶ Evaluation time discrepancy
- ▶ How not to use
is
operator - ▶
is not ...
is notis (not ...)
- ▶ A tic-tac-toe where X wins in the first attempt!
- ▶ The sticky output function
- ▶ The chicken-egg problem *
- ▶ Subclass relationships
- ▶ All-true-ation *
- ▶ The surprising comma
- ▶ Strings and the backslashes
- ▶ not knot!
- ▶ Half triple-quoted strings
- ▶ What's wrong with booleans?
- ▶ Class attributes and instance attributes
- ▶ Non-reflexive class method *
- ▶ yielding None
- ▶ Yielding from... return! *
- ▶ Nan-reflexivity *
- ▶ Mutating the immutable!
- ▶ The disappearing variable from outer scope
- ▶ The mysterious key type conversion
- ▶ Let's see if you can guess this?
- Section: Slippery Slopes
- ▶ Modifying a dictionary while iterating over it
- ▶ Stubborn
del
operation - ▶ The out of scope variable
- ▶ Deleting a list item while iterating
- ▶ Lossy zip of iterators *
- ▶ Loop variables leaking out!
- ▶ Beware of default mutable arguments!
- ▶ Catching the Exceptions
- ▶ Same operands, different story!
- ▶ Be careful with chained operations
- ▶ Name resolution ignoring class scope
- ▶ Needles in a Haystack *
- ▶ Splitsies *
- ▶ Wild imports *
- ▶ All sorted? *
- ▶ Midnight time doesn't exist?
- Section: The Hidden treasures!
- Section: Appearances are deceptive!
- Section: Miscellaneous
- Section: Strain your brain!
- Contributing
- Acknowledgements
- 🎓 License
Wszystkie przykłady posiadają strukturę jak poniżej:
# Wprowadzenie kodu. # Przygotowanie pod magię...Wynik (wersja lub wersje Pythona):
>>> wyrażenie_uruchamiające Jakiś nieoczekiwany wynik(Opcjonalnie): Jedna linia wyjaśniająca nieoczekiwany wynik.
- Krótkie omówienie co i dlaczego się wydarzyło.
# Wprowadzenie kodu. # Wprowadzenie przykładów wyjaśniających (jeśli niezbędne)Wynik (wersja lub wersje Pythona):
>>> uruchomienie # jakiegoś przykładu, który może w prosty sposób wytłumaczyć magie # jakiś rezultat
Uwaga: Wszystkie przykłady zostały przetestowane w środowisku Python 3.5.2 interactive interpreter, i powinny działać we wszystkich wersjach Python 3, chyba że przed opisaniem wyniku stwierdzono inaczej.
Dobrym sposobem na wyciągnięcie jak najwięcej z poniższych przykładów, w mojej opinii, jest czytanie ich chronologicznie i dla każdego przykładu:
- Ostrożne przeczytanie kodu inicjującego przykład. Jeśli jesteś doświadczonym programistą Pythona, przez większość czasu z powodzeniem będziesz przewidywał, co się wydarzy.
- Przeczytanie wyniku i:
- Porównanie czy wynik jest taki jak się tego spodziewałeś.
- Upewnienie się, że rozumiesz powód, dla którego wynik jest właśnie taki.
- Jeśli nie rozumiesz (co jest całkowicie w porządku), weź głęboki oddech i przeczytaj wyjaśnienie (a jeśli nadal nie rozumiesz, daj znać! stwórz issue tutaj).
- Jeśli rozumiesz, poklep się po ramieniu i kontynuuj z następnym przykładem.
PS: Możesz również czytać WTFPython z użyciem wiersza poleceń / terminala (tylko oryginalna wersja angielska) używając paczkę pypi,
$ pip install wtfpython -U
$ wtfpython
Z jakiegoś powodu udostępniony w Python 3.8 "Walrus" operator (:=
) stał się całkiem popularny. Sprawdźmy go!
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") # A tutaj działa
>>> a
'wtf_walrus'
2 .
# Python version 3.8+
>>> a = 6, 9
>>> a
(6, 9)
>>> (a := 6, 9)
>>> a
6
>>> a, b = 6, 9 # Typowy unpacking
>>> a, b
(6, 9)
>>> (a, b = 16, 19) # Oops
File "<stdin>", line 1
(a, b = 6, 9)
^
SyntaxError: invalid syntax
>>> (a, b := 16, 19) # Tutaj printuje dziwny 3-wartościowy tuple
(6, 16, 19)
>>> a # a nadal bez zmian?
6
>>> b
16
Szybkie przypomnienie o walrus operator
Walrus operator (:=
) został wprowadzony w Python 3.8 i może być przydatny w sytuacjach gdy chcesz nadać wartość zmiennej wewnątrz wyrażenia.
def some_func():
# Załóżmy tutaj jakieś ciężkie obliczenia
# time.sleep(1000)
return 5
# Więc zamiast
if some_func():
print(some_func()) # Co nie jest dobrą praktyką bo obliczenia dzieją się dwa razy
# lub zamiast
a = some_func()
if a:
print(a)
# Możesz śmiało użyć
if a := some_func():
print(a)
Wynik (> 3.8):
5
5
5
To pozwoliło zaoszczędzić linię kodu i zapobiegło niejawnemu użyciu some_func
drugi raz.
-
Niezawarcie w nawiasach "wyrażenia przypisania" (użycia walrus operator) jest niedozwolone, stąd
SyntaxError
przya := "wtf_walrus"
w pierwszym fragmencie kodu. Wzięcie go w nawias zadziałało jak tego oczekiwaliśmy, przypisując wartość do zmienneja
. -
Typowo, wzięcie w nawias wyrażenia zawierającego
=
jest niedozwolone. Stąd syntax error przy(a, b = 6, 9)
. -
Składnia Walrus operator ma formułę
NAZWA: wyrażenie
, gdzieNAZWA
to poprawny identyfikator, awyrażenie
jest poprawnym wyrażeniem. Dlatego pakowanie i rozpakowywanie iterałów nie jest wspierane, co znaczy, że-
(a := 6, 9)
jest tożsame z((a := 6), 9)
jak również z(a, 9)
(gdzie wartośća
to 6')>>> (a := 6, 9) == ((a := 6), 9) True >>> x = (a := 696, 9) >>> x (696, 9) >>> x[0] is a # Oba wskazują to samo miejsce w pamięci True
-
Podobnie
(a, b := 16, 19)
jest tożsame z(a, (b := 16), 19)
, które jest niczym innym jak 3-wartościowym tuplem.
-
1.
>>> a = "some_string"
>>> id(a)
140420665652016
>>> id("some" + "_" + "string") # Zauważ, że obydwa id są takie same.
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 # Wszystkie wersje Python oprócz 3.7.x
True
>>> a = "wtf!"; b = "wtf!"
>>> a is b # To zwróci True lub False zależnie od tego jak skrypt zostanie wywołany (python shell / ipython / jako skrypt)
False
# Tym razen w jakimś pliku some_file.py
a = "wtf!"
b = "wtf!"
print(a is b)
# printuje True gdy ten moduł jest wywołany w innym module!
4.
Wynik (< Python3.7 )
>>> 'a' * 20 is 'aaaaaaaaaaaaaaaaaaaa'
True
>>> 'a' * 21 is 'aaaaaaaaaaaaaaaaaaaaa'
False
Zrozumiałe, no nie?
- Zachowanie w pierwszym i drugim fragmencie jest związane z optymalizacją CPython (nazywaną string interning [w polskiej wersji będę używał określenia 'odcinanie', które dobrze oddaje sens tego działania]), która stara się użyć w pewnych przypadkach istniejące niemutowalne obiekty zamiast tworzyć nowy obiekt za każdym razem.
- Po byciu 'odciętym', wiele zmiennych odwołuje się do tego obiektu string w pamięci (oszczędzając użycie pamięci tym sposobem).
- We fragmentach powyżej stringi są niejawnie odcinane. Decyzja o tym, kiedy niejawnie odciąć stringa jest zależna od implementacji. Są pewne zasady, które mogą pomóc w odgadnięciu czy string będzie odcięty czy nie:
- Wszystkie stringi o ilości znaków 0 lub jeden są odcinane.
- Stringi są odcinane w momencie kompilacji (
'wtf'
będzie odcięte ale już''.join(['w', 't', 'f']
nie będzie) - Stringi, które nie są zbudowane ze znaków ASCII, cyfr lub znaków podkreślenia, nie są odcinane. To tłumaczy dlaczego
'wtf!'
nie zostało odcięte posiadając!
. Implementacja tych zasad w CPython jest do sprawdzenia tutaj
- Gdy do
a
ib
jest przypisane"wtf!"
w tym samym wierszu kodu, interpreter Pythona tworzy nowy obiekt, na który w tym samym czasie wskazuje pierwszą i drugą zmienną. Jeśli przypisania są w oddzielnych liniach, interpreter nie wie, że jest jużwtf!
jako obiekt (ponieważ"wtf!"
nie jest niejawnie odcięty w zgodzie z faktami powyżej). Jest to optymalizacja w czasie kompilacji. Ta optymalizacja nie występuje w wersjach 3.7.x CPython (w tym issue znajdziesz dyskusję na ten temat). - Jednostka kompilująca w środowisku interaktywnym, takim jak IPython składa się z pojedynczego wyrażenia, natomiast w przypadku modułów składa się z całego modułu.
a, b = "wtf!", "wtf!"
to pojedyncze wyrażenie, podczas gdya = "wtf!"; b = "wtf!"
to dwa wyrażenia w jednym wierszu. To wyjaśnia, dlaczego tożsamości są różne wa = "wtf!"; b = "wtf!"
, a także wyjaśnia, dlaczego są one takie same, gdy są wywoływane wsome_file.py
- Nagła zmiana wyniku czwartego fragmentu jest spowodowana techniką peephole optimization znaną jako składanie stałych (Constant folding). Oznacza to, że wyrażenie
'a'*20
jest zastępowane przez'aaaaaaaaaaaaaaaaaaa'
podczas kompilacji, aby zaoszczędzić kilka cykli zegara w czasie wykonywania. Zwijanie stałych występuje tylko w przypadku stringów o długości mniejszej niż 20. (Dlaczego? Wyobraź sobie jakiś plik.pyc
wygenerowany jako rezultat wyrażenia'a'*10**10
). Tutaj znajdziesz opis implementacji tej optymalizacji. - Uwaga: W Python 3.7 składanie stałych zostało przeniesione z optymizatora peephole do nowego optymizatora AST z pewnymi zmianami w logice, stąd trzeci fragment nie działa dla Python 3.7. Więcej na ten temat możesz przeczytać tutaj.
1.
some_dict = {}
some_dict[5.5] = "JavaScript"
some_dict[5.0] = "Ruby"
some_dict[5] = "Python"
Wynik:
>>> some_dict[5.5]
"JavaScript"
>>> some_dict[5.0] # czy "Python" zamordował "Ruby"?
"Python"
>>> some_dict[5]
"Python"
>>> complex_five = 5 + 0j
>>> type(complex_five)
complex
>>> some_dict[complex_five]
"Python"
Więc dlaczego "Python" jest w każdym z miejsc?
-
Słowniki (dicty) w pythonie podczas wywoływania i przypisywania sprawdzają równość wartości i porównują wartość hasha aby stwierdzić czy klucze są tożsame.
-
W pythonie niemutowalne obiekty mające tą samą wartość mają zawsze taki sam hash.
>>> 5 == 5.0 == 5 + 0j True >>> hash(5) == hash(5.0) == hash(5 + 0j) True
Uwaga: Obiekty z różnymi wartościami również mogą mieć taki sam hash (jest to znane jako hash collision).
-
Gdy wyrażenie
some_dict[5] = "Python"
jest wykonywane, istniejąca wartość "Ruby" jest nadpisywana przez "Python" ponieważ python rozpoznaje5
i5.0
jako ten sam klucz w słownikusome_dict
. -
Ta odpowiedź na StackOverflow przedstawia racjonalne wyjaśnienie stojące za tym problemem.
class WTF:
pass
Output:
>>> WTF() == WTF() # dwie oddzielne instancje nie mogą być równe
False
>>> WTF() is WTF() # tożsamości obiektów również są różne
False
>>> hash(WTF()) == hash(WTF()) # hashe _powinny_ być również różne
True
>>> id(WTF()) == id(WTF())
True
-
Gdy
id
zostało wywołane, python stworzył obiekt klasyWTF
i podał go do funkcjiid
. Funkcjaid
użyła id obiektu (adres obiektu w pamięci), i odrzuciła sam obiekt, który został usunięty. -
Gdy zrobimy to raz za razem, python alokuje ten sam adres pamięci również do drugiego obiektu. Odkąd (w CPython)
id
używa adresu pamięci jako id obiektu, id obydwu obiektów będzie tym samym id. -
A więc id obiektu będzie unikalne tylko na czas istnienia tego obiektu. Po tym jak obiekt zostanie usunięty lub przed tym jak zostanie stworzony, inny obiekt może posiadać to id.
-
ale dlaczego operator
is
zwróciłFalse
? Spójrzmy na fragment kodu.class WTF(object): def __init__(self): print("I") def __del__(self): print("D")
Wynik:
>>> WTF() is WTF() I I D D False >>> id(WTF()) == id(WTF()) I D I D True
Jak widać, kolejność, w której obiekty są niszczone tłumaczy różnicę.
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):
"""
Dict z implementacją magicznej metody __hash__.
"""
__hash__ = lambda self: 0
class OrderedDictWithHash(OrderedDict):
"""
OrderedDict z implementacją magicznej metody __hash__.
"""
__hash__ = lambda self: 0
Wynik
>>> dictionary == ordered_dict # Jeśli a == b ...
True
>>> dictionary == another_ordered_dict # ...i b == c
True
>>> ordered_dict == another_ordered_dict # ...to dlaczego nie c == a ??
False
# Wszyscy wiemy, że typ set zawiera jedynie unikalne elementy,
# spróbujmy więc stworzyć set z tych słowników i sprawdźmy co się wydarzy...
>>> len({dictionary, ordered_dict, another_ordered_dict})
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: unhashable type: 'dict'
# Ma to sens jako, że dict nie posiada implementacji metody __hash__.
# Użyjmy naszych klas-wrapperów.
>>> 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}) # zmieniając kolejność
2
Co się tutaj odwaliło?
-
Równość pomiędzy
dictionary
,ordered_dict
ianother_ordered_dict
nie występuje z powodu metody__eq__
zaimplementowanej w klasieOrderedDict
. temat do sprawdzenia tutajSprawdzenie równości obiektów OrderedDict jest wrażliwe na kolejność, ponadto będąc zaimplementowane jako
list(od1.items())==list(od2.items())
.Sprawdzenie równości pomiędzy obiektamiOrderedDict
i innymi obiektami mapującymy (Mapping objects) jest niewrażliwe na kolejność jak przy zwykłych słownikach (dict). -
Powód, dla którego taka implementacja sprawdzania równości została wprowadzona, to umożliwienie obiektom
OrderedDict
bycie bezpośrednim substytutem podstawowych obiektówdict
tam są użyte. -
OK, ale dlaczego zmiana kolejności wpływa na wygenerowanie obiektu
set
? Odpowiedzią jest po prostu brak przenoszonej równości. Jako, że sety są "nieuporządkowanymi" kolekcjami unikalnych elementów, kolejność dodawanych elementów nie powinna mieć znaczenia. Jednak w tej sytuacji ta własność nie ma znaczenia. Sprawdźmy to.>>> some_set = set() >>> some_set.add(dictionary) # dodajemy obiekty mapujące z fragmentów kodu wyżej >>> 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
A więc niespójność występuje przez
another_ordered_dict in another_set
równeFalse
ponieważordered_dict
był już obecny wanother_set
i jak zaobserwowano wcześniej,ordered_dict == another_ordered_dict
jest równeFalse
.
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(): # Przyłapana!
try:
for i in range(3):
try:
1 / i
except ZeroDivisionError:
# Wyrzućmy błąd tutaj i zajmijmy się nim na zewnątrz pętli for
raise ZeroDivisionError("A trivial divide by zero error")
finally:
print("Iteration", i)
break
except ZeroDivisionError as e:
print("Zero division error ocurred", e)
Wynik:
>>> 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
- Jeśli
return
,break
lubcontinue
są wywołane w sekcjitry
wyrażenia "try…finally", sekcjafinally
jest również wywoływana na koniec. - Wartość zwracana jest determinowana przez ostatni wywołany
return
. Jako, że sekcjafinally
jest zawsze wywoływana,return
wywoływany wfinally
będzie zawsze tym ostatnim. - Ciekawostką jest, że jeśli sekcja
finally
wywołujereturn
lubbreak
to tymczasowo zapamiętany wyjątek (błąd) zostaje zapomniany.
some_string = "wtf"
some_dict = {}
for i, some_dict[i] in enumerate(some_string):
i = 10
Output:
>>> some_dict # Powstaje słownik z indeksami
{0: 'w', 1: 't', 2: 'f'}
-
Wyrażenie
for
jest zdefiniowane w Python grammar jako:for_stmt: 'for' exprlist 'in' testlist ':' suite ['else' ':' suite]
Gdzie
exprlist
to cel przypisania wartości. Oznacza to, że tożsame dla{exprlist} = {next_value}
jest wykonanie dla każdej wartości w iteratorze. Ciekawy przykład, który to ilustruje:for i in range(4): print(i) i = 10
Wynik:
0 1 2 3
Spodziewałeś się wykonania pętli tylko raz?
💡 Wyjaśnienie:
- Wyrażenie przypisania
i = 10
nigdy nie wpływa na wykonanie pętli, z uwagi na to jak działa wykonywanie pętli w Python. Przed rozpoczęciem każdej iteracji, kolejna wartość wydawana przez iterator (range(4)
w tym przypadku) jest wypakowana i przypisana do listy zmiennych docelowych (i
w tym przypadku).
- Wyrażenie przypisania
-
Funkcja
enumerate(some_string)
wydaje nową wartośći
(licznik narastający) i literę zsome_string
w każdej iteracji. Następnie ustawia (dopiero co przypisany) kluczi
słownikasome_dict
do tej litery. Rozwinięcie pętli można pokazać prościej jako:>>> 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]
# Typowo stworzony generator
gen = (x for x in array if array.count(x) > 0)
array = [2, 8, 22]
Wynik:
>>> print(list(gen)) # Gdzie podziały się pozostałe wartości?
[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]
Wynik:
>>> 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]
Wynik:
>>> print(list(gen))
[401, 501, 601, 402, 502, 602, 403, 503, 603]
-
W generatorach człon
in
jest sprawdzany w czasie deklaracji ale człon warunkujący już podczas wykonywania. -
Przed samym wykonaniem
array
jest ponownie przypisany do listy[2, 8, 22]
, i skoro brakuje w niej1
i15
, a tylko ilość8
jest większa niż0
, generator wydaje tylko8
. -
Różnica pomiędzy wynikami
gen_1
igen_2
w drugim fragmencie wynika z tego jak zmiennearray_1
iarray_2
mają ponownie przypisywane wartości. -
W pierwszym przypadku
array_1
jest związany z nowym obiektem[1,2,3,4,5]
i skoroin
jest sprawdzany w czasie deklaracji to nadal odnosi się do starego obiektu[1,2,3,4]
(który został zniszczony). -
W drugim przypadku, przypisanie części (slice) listy
array_2
aktualizuje ten stary obiekt[1,2,3,4]
do[1,2,3,4,5]
. Stąd obagen_2
iarray_2
nadal wskazują na ten sam obiekt (który został teraz zaktualizowany do[1,2,3,4,5]
). -
OK, idąc za omawianą logiką, czy wartość
list(gen)
w trzecim fragmencie nie powinna być równa[11, 21, 31, 12, 22, 32, 13, 23, 33]
? (skoroarray_3
iarray_4
będą się zachowywały jakarray_1
). Powód, dla którego tylko wartości zarray_4
zostały zaktualizowane znajduje się w PEP-289Tylko wyrażenie najbardziej na wierzchu (pierwsze) pętli for jest sprawdzane natychmiast, pozostałe wyrażenia są odraczane do momentu uruchomienia generatora.
Poniżej jest dobrze znany i spotykany w internecie przykład.
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. Wynik
>>> a, b = 257, 257
>>> a is b
True
Wynik (jedynie dla Python 3.7.x)
>>> a, b = 257, 257
>> a is b
False
Różnica pomiędzy is
i ==
is
sprawdza czy oba argumenty wskazują na ten sam obiekt(np. sprawdza czy id argumentów jest takie samo).==
sprawdza wartości argumentów i to czy są one takie same.- Więc
is
jest do sprawdzania wskazywania tego samego obiektu a==
do sprawdzania równowartości. Przykład by to rozjaśnić,>>> class A: pass >>> A() is A() # Są to dwa puste obiekty trzymane w dwóch różnych miejscach w pamięci. False
256
to istniejący już obiekt, a 257
nie
Kiedy włączasz pythona, numery od -5
do 256
będą już alokowane. Są one bardzo często używane, stąd rozsądne jest mieć je już w pogotowiu.
Cytując z https://docs.python.org/3/c-api/long.html
Aktualna implementacja zachowuje tablicę obiektów typu integer dla wszystkich liczb pomiędzy -5 i 256, gdy tworzysz int z tego zasięgu, po prostu odwołujesz się do istniejącego już obiektu. A więc powinna być możliwa zmiana wartości 1. Podejrzewam, że zachowanie Pythona, w tym wypadku, nie zostało zdefiniowane. :-)
>>> 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
Interpreter nie jest tutaj wystarczająco mądry aby podczas wykonywania kodu rozpoznać przy y = 257
, że stworzyliśmy już wcześniej int o wartości 257,
i tworzy kolejny obiekt w pamięci.
Podobna optymalizacja aplikuje się do innych niemutowalnych obiektów, w tym pustych tupli. Skoro listy są mutowalne, to [] is []
zwróci False
a () is ()
zwróci True
. To wyjaśnia nasz drugi fragment kodu. A teraz przejdźmy do trzeciego,
Oba a
i b
wskazują na ten sam obiekt gdy są inicjalizowane w takiej samej wartości w tym samym wierszu.
Wynik
>>> a, b = 257, 257
>>> id(a)
140640774013296
>>> id(b)
140640774013296
>>> a = 257
>>> b = 257
>>> id(a)
140640774013392
>>> id(b)
140640774013488
-
Gdy do a i b zostaje przypisane
257
w tym samym wierszu, interpreter pythona tworzy nowy obiekt i przypisuje go do drugiej zmiennej w tym samym czasie. Jeśli zrobisz to w oddzielnych wersach, interpreter "nie wie", że jest już taki obiekt jak257
. -
Jest to optymalizacja kompilatora i bezpośrednio odnosi się do interaktywnego środowiska. Jeśli wprowadzisz dwa wersy kodu do interpretera 'w locie', będą kompilowane oddzielnie, stąd też optymalizowane oddzielnie. Jeśli sp©óbujesz tego przykładu w pliku
.py
, nie zobaczysz tego samego zachowania ponieważ plik jest kompilowany w całości. Ta optymalizacja nie jest ograniczona do integerów i działa również dla innych typów niemutowalnych takich jak stringi albo floaty,>>> a, b = 257.0, 257.0 >>> a is b True
-
Dlaczego nie działa to w Python 3.7? Ponieważ pewne optymalizacje kompilatora są zaimplementowane zależnie od pewnych zmiennych (np. wersji Pythona, systemu operacyjnego, itp.). Nadal dochodzę do tego jaka dokładnie implementacja zmieniła to zachowanie, co możesz śledzić w tym issue.
>>> 'something' is not None
True
>>> 'something' is (not None)
False
is not
to jednostkowy operator binarny, a jego zachowanie różni się od użyciais
inot
oddzielnie.is not
zwracaFalse
jeśli zmienne po obu stronach operatora wskazują ten sam obiekt, aTrue
w sytuacji odwrotnej.
# Stwórzmy jeden wiersz
row = [""] * 3 #wiersz i['', '', '']
# A teraz całą planszę
board = [row] * 3
Wynik:
>>> board
[['', '', ''], ['', '', ''], ['', '', '']]
>>> board[0]
['', '', '']
>>> board[0][0]
''
>>> board[0][0] = "X"
>>> board
[['X', '', ''], ['X', '', ''], ['X', '', '']]
Ale przecież nie przypisaliśmy trzech "X"
, co nie?
Ta wizualizacja przedstawia co się dzieje gdy inicjalizujemy zmienną row
A gdy board
zostaje zainicjalizowany poprzez pomnożenie zmiennej row
, to dzieje się wewnątrz pamięci (każdy z elementów board[0]
, board[1]
i board[2]
wskazuje na tę samą listę przypisaną do zmiennej row
)
Możemy uniknąć tego problemu nie używając row
aby wygenerować board
. (Zapytano w tym issue).
>>> board = [['']*3 for _ in range(3)]
>>> board[0][0] = "X"
>>> board
[['X', '', ''], ['', '', ''], ['', '', '']]
1.
funcs = []
results = []
for x in range(7):
def some_func():
return x
funcs.append(some_func)
results.append(some_func()) # zauważ, że wywołujemy tu funkcję
funcs_results = [func() for func in funcs]
Wynik:
>>> results
[0, 1, 2, 3, 4, 5, 6]
>>> funcs_results
[6, 6, 6, 6, 6, 6, 6]
Pomimo, że wartości zmiennej x
były różne w każdej iteracji przed dodaniem some_func
do funcs
, wszystkie funkcje zwróciły 6.
2.
>>> 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]
-
Kiedy definiujemy wewnątrz pętli funkcję, która używa zmiennych pętli, obliczenie tej funkcji (stworzonej wewnątrz pętli) jest powiązane ze zmienną, a nie z jej wartością. Stąd wszystkie funkcje używają do obliczeń ostatniej wartości przypisanej do tej zmiennej.
-
Aby zachowanie było zgodne z oczekiwanym, wystarczy użyć zmiennej z pętli jako parametru dla funkcji. Dlaczego to zadziała? Ponieważ zdefiniuje to zmnienną ponownie w zakresie funkcji.
funcs = [] for x in range(7): def some_func(x=x): return x funcs.append(some_func)
Wynik:
>>> funcs_results = [func() for func in funcs] >>> funcs_results [0, 1, 2, 3, 4, 5, 6]
1.
>>> isinstance(3, int)
True
>>> isinstance(type, object)
True
>>> isinstance(object, type)
True
Więc, która z klas jest tą "ostateczną" klasą bazową? Tu mamy więcej niezrozumiałych rzeczy,
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
to metaklasa w Pythonie.- Wszystko jest
object
(obiektem) w Pythonie, włącznie z klasami i ich obiektami (instancjami). - klasa
type
jest metaklasą klasyobject
, a każda klasa (włączająctype
) dziedziczy bezpośrednio lub pośrednio poobject
. - Nie można wskazać jasno klasy bazowej pomiędzy
object
itype
. Niejasność z powyższych fragmentów kodu bierze się z tego, że postrzegamy te relacje (issubclass
iisinstance
) w kontekście klas Pythona. Relacja pomiędzyobject
itype
nie może zostać zreprodukowana w czystym pythonie. Dla doprecyzowania, następujące relacje nie mogą zostać odtworzone w czystym pythonie,- klasa A jest instancją klasy B, a klasa B jest instancją klasy A.
- klasa A jest instancją samej siebie.
- Relacje pomiędzy
object
itype
(gdzie obie są instancjami tej drugiej, a przy tym też samej siebie) występuje w Pythonnie przez "oszukiwanie" na poziomie implementacji języka.
Output:
>>> from collections import Hashable
>>> issubclass(list, object)
True
>>> issubclass(object, Hashable)
True
>>> issubclass(list, Hashable)
False
Spodziewali byśmy się, że powiązania będą przechodziły, prawda? (np., jeśli A
jest subklasą B
, i B
jest subklasą C
to A
powinno być subklasą C
)
- Relacje subklas nie koniecznie przechodzą w Pythonie. Każdy może zdefiniować swoje własne sprawdzenie
__subclasscheck__
w metaklasie. - Gdy
issubclass(cls, Hashable)
jest wywołana, po prostu szuka nie-Falsującą metodę "__hash__
" wcls
lub w czymkolwiek z czego ona dziedziczy. - Jako, że
object
jest hashowalny, alist
nie jest hashowalna, psuje to przechodzenie w relacji. - Bardziej dokładne wytłumaczenie znajduje się tutaj.
>>> all([True, True, True])
True
>>> all([True, True, False])
False
>>> all([])
True
>>> all([[]])
False
>>> all([[[]]])
True
Skąd się bierze ta różnica True-False?
-
Implementacja funkcji
all
jest tożsama z -
def all(iterable): for element in iterable: if not element: return False return True
-
all([])
zwracaTrue
bo iterator jest pusty. -
all([[]])
zwracaFalse
ponieważnot []
toTrue
a więc jest tożsame znot False
jako, że lista wewnątrz iteratora jest pusta. -
all([[[]]])
i wyższe warianty będą zawszeTrue
jako, żenot [[]]
,not [[[]]]
itd. są tożsame znot True
.
Wynik (< 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
- Przecinek na końcu nie zawsze jest dozwolony w formalnej liście parametrów funkcji Pythona.
- W Pythonie lista argumentów jest częściowo zdefiniowana z przecinkami wiodącymi, a częściowo z przecinkami końcowymi. Ten konflikt powoduje sytuacje, w których przecinek jest uwięziony w środku i żadna reguła go nie akceptuje.
- Uwaga: Problem przecinka końcowego naprawiono w Python 3.6. Uwagi w tym miejscu w skrócie omawiają różne zastosowania przecinków końcowych w Pythonie.
Wynik:
>>> print("\"")
"
>>> print(r"\"")
\"
>>> print(r"\")
File "<stdin>", line 1
print(r"\")
^
SyntaxError: EOL while scanning string literal
>>> r'\'' == "\\'"
True
- W typowym stringu pythona, backslash używany jest jako dzika karta dla znaków o specjalnym użyciu w pythonie (jak pojedynczy cudzysłów, cudzysłów, i sam backslash).
>>> 'wt\"f' 'wt"f'
- W 'surowym' (raw) stringu (na co wskazuje przedrostek
r
), backslashe przechodzą same, a przy tym są też dziką kartą dla kolejnego znaku.>>> r'wt\"f' == 'wt\\"f' True >>> print(repr(r'wt\"f') 'wt\\"f' >>> print("\n") >>> print(r"\\n") '\\\\n'
— Oznacza to, że gdy parser napotka backslash w surowym stringu, oczekuje kolejnego znaku następującego po nim. W naszym przypadku (print(r"\")
), backslash był dziką kartą dla końcowego cudzysłowu, pozostawiając parser bez zamykającego cudzysłowu (stąd SyntaxError
). Dlatego odwrotne ukośniki nie działają na końcu surowego stringa.
x = True
y = False
Wynik:
>>> not x == y
True
>>> x == not y
File "<input>", line 1
x == not y
^
SyntaxError: invalid syntax
- Pierwszeństwo operatorów wpływa na to jak wyrażenie jest wykonywane, a
==
ma wyższe pierwszeństwo niż operatornot
w Pythonie. - Stąd
not x == y
jest tożsame znot (x == y)
, które jest tożsame znot (True == False)
ostatecznie zwracająceTrue
. - Jednak
x == not y
podnosiSyntaxError
ponieważ wyrażenie jest tożsame z(x == not) y
a niex == (not y)
, czego można nie przewidzieć na pierwszy rzut oka. - Parser spodziewa się słowa
not
jako części operatoranot in
(ponieważ oba operatory==
inot in
mają ten sam poziom pierwszeństwa), jednak nie mogąc znaleźć słowain
za słowemnot
podnosi błądSyntaxError
.
Wynik:
>>> print('wtfpython''')
wtfpython
>>> print("wtfpython""")
wtfpython
>>> # The following statements raise `SyntaxError`
>>> # print('''wtfpython')
>>> # print("""wtfpython")
File "<input>", line 3
print("""wtfpython")
^
SyntaxError: EOF while scanning triple-quoted string literal
- Python wspiera wewnętrzną konkatenacje stringów, Przykład,
>>> print("wtf" "python") wtfpython >>> print("wtf" "") # or "wtf""" wtf
'''
i"""
to również ograniczniki stringów w Pythonie (patrz docstring), co powoduje wystąpienie błędu składni ponieważ interpreter Pythona skanując kolejne znaki (uznając je za wnętrze stringa) oczekuje kolejnego potrójnego cudzysłowu jako ogranicznika zamykającego, którego nie znajduje.
1.
# Prosty przykład zliczania ilości booli i
# integerów w iteratorze z różnymi typami danych.
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
Wynik:
>>> 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!")
Wynik (< 3.x):
>>> tell_truth()
I have lost faith in truth!
-
bool
jest podklasąint
w Pythonie>>> issubclass(bool, int) True >>> issubclass(int, bool) False
-
stąd,
True
iFalse
są instancjamiint
>>> isinstance(True, int) True >>> isinstance(False, int) True
-
wartość integera
True
to1
aFalse
to0
.>>> int(True) 1 >>> int(False) 0
-
Zerknij na pytanie na StackOverflow, które to wyjaśnia.
-
Początkowo Python nie posiadał typu
bool
(używano 0 zamiast false i wartości niezerowej, przykładowo 1, dla true).True
,False
, i typbool
zostały dodane w wersji 2.x, ale dla kompatybilności wstecznej,True
iFalse
nie mogły zostać stworzone jako stałe. Były więc wbudowanymi zmiennymi, co pozwoliło na zmiany przypisania do zmiennych. -
Python 3 jest niekompatybilny wstecz. Problem został ostatecznie naprawiony. Stąd też ostatni fragment kodu nie zadziała w Python 3.x!
1.
class A:
x = 1
class B(A):
pass
class C(A):
pass
Wynik:
>>> 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 zmieniono ale już B.x nie
(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]
Wynik:
>>> 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
- Zmienne klasowe i zmienne instancji klas są trzymane wewnętrznie w obiektach klas w słownikach (dict). Jeśli nazwa zmiennej nie zostanie znaleziona w słowniku aktualnej klasy, superklasy są przeszukiwane w następnej kolejności.
- Operator
+=
modyfikuje mutowalny obiekt w miejscu bez tworzenia nowego obiektu. Stąd modyfikacja atrybutu instancji wpływa na inne instancje i atrybuty samej klasy.
class SomeClass:
def instance_method(self):
pass
@classmethod
def class_method(cls):
pass
Wynik:
>>> SomeClass.instance_method is SomeClass.instance_method
True
>>> SomeClass.class_method is SomeClass.class_method
False
>>> id(SomeClass.class_method) == id(SomeClass.class_method)
True
-
Powodem, dla którego
SomeClass.class_method is SomeClass.class_method
jest równeFalse
jest dekorator@classmethod
.>>> SomeClass.instance_method <function __main__.SomeClass.instance_method(self)> >>> SomeClass.class_method <bound method SomeClass.class_method of <class '__main__.SomeClass'>
Przy każdym dostępie do
SomeClass.class_method
tworzy się nowa metoda wiążąca (bound method). -
id(SomeClass.class_method) == id(SomeClass.class_method)
zwracaTrue
ponieważ drugie przypisanie pamięci dlaclass_method
dzieje się w tym samym miejscu gdzie dealokacja pierwszej.
some_iterable = ('a', 'b')
def some_func(val):
return "something"
Wynik (<= 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']
- Jest to bug w CPython's związany z obsługą
yield
w generatorach i składaniach (comprehensions). - Kod i wyjaśnienie do znalezienia tu: https://stackoverflow.com/questions/32139885/yield-in-list-comprehensions-and-generator-expressions
- Powiązany raport: http://bugs.python.org/issue10544
- Python 3.8+ nie zezwala już na użycie
yield
wewnatrz list składanych i podniesieSyntaxError
.
1.
def some_func(x):
if x == 3:
return ["wtf"]
else:
yield from range(x)
Wynik (> 3.3):
>>> list(some_func(3))
[]
Gdzie podziało się "wtf"
? Czy wynika to z jakiegoś specjalnego zachowania yield from
? Sprawdźmy to,
2.
def some_func(x):
if x == 3:
return ["wtf"]
else:
for i in range(x):
yield i
Wynik:
>>> list(some_func(3))
[]
Wynik ten sam, tu też nie zadziałało.
- Od Python 3.3 w przód, możliwe stało się użycie
return
z wartością wewnątrz generatorów (Patrz PEP380). I oficjalną dokumentację która mówi o tym,
"...
return expr
w generatorach powoduje podniesienieStopIteration(expr)
przy wyjściu z generatora."
-
W przypadku gdy
some_func(3)
, podnoszony jestStopIteration
na samym początku z uwagi nareturn
. WyjątekStopIteration
jest automatycznie przechwytywany wewnątrz wraperalist(...)
i w pętlifor
. Stąd powyższe fragmenty zwracają puste listy. -
Aby otrzymać
["wtf"]
z generatora,some_func
musi przechwycić wyjątekStopIteration
,try: next(some_func(3)) except StopIteration as e: some_string = e.value
>>> some_string ["wtf"]
1.
a = float('inf')
b = float('nan')
c = float('-iNf') # Wielkość znaków nie ma tu znaczenia
d = float('nan')
Wynik:
>>> 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 # identyczność jest zachowana
True
>>> y == y # równoważność nie jest zachowana
False
>>> [y] == [y] # ale już równoważność list zawierających y jest zachowana
True
-
'inf'
i'nan'
to specjalne stringi (nie wpływa na nie wielkość liter), które, jeśli zostaną ręcznie konwertowane na typfloat
, są używane jako matematyczna reprezentacja kolejno "nieskończoności" i "not-a-number". -
Zgodnie ze standardami IEEE
NaN != NaN
, a przestrzeganie tej zasady psuje założenie odbicia kolekcji elementów w Python np. jeślix
jest częścią kolekcji typulist
, implementacje takie jak porównania bazują na założeniu, żex == x
. Przez to założenie identyczność jest sprawdzana w pierwszej kolejności (bo jest to szybsze) podczas porównywania dwóch obiektów, a wartości są porównywane tylko wtedy gdy nie występuje identyczność. Poniższy fragment rozjaśni tę sprawę,>>> x = float('nan') >>> x == x, [x] == [x] (False, True) >>> y = float('nan') >>> y == y, [y] == [y] (False, True) >>> x == y, [x] == [y] (False, False)
Skoro identyczność
x
iy
nie wystąpiła, wartości są brane pod uwagę, a one również się rówżnią; stąd sprawdzenie zwracaFalse
. -
Dla zainteresowanych: Reflexivity, and other pillars of civilization
To może wydawać się trywialne jeśli wiesz jak działają odniesienia w Pythonie.
some_tuple = ("A", "tuple", "with", "values")
another_tuple = ([1, 2], [3, 4], [5, 6])
Wynik:
>>> some_tuple[2] = "change this"
TypeError: 'tuple' object does not support item assignment
>>> another_tuple[2].append(1000) #To nie podniesie błędu
>>> 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])
Myślałem, że tuple są niemutowalne...
-
Cytując https://docs.python.org/2/reference/datamodel.html
Sekwencje niemutowalne Obiekt typu sekwencji niemutowalnej nie może zostać zmieniany po stworzeniu. (Jeśli obiekt zawiera referencje do innych obiektów, te obiekty mogą być mutowalne i mogą być modyfikowane; jednak kolekcja obiektów, na którye obiekt niemutowalny bezpośrednio wskazuje nie może zostać zmodyfikowana.)
-
+=
zmienia listę w miejscu. Przypisanie nie działa, jednak gdy podniesiony zostaje wyjątek, obiekt został już zmieniony w miejscu.
e = 7
try:
raise Exception()
except Exception as e:
pass
Wynik (Python 2.x):
>>> print(e)
# prints nothing
Wynik (Python 3.x):
>>> print(e)
NameError: name 'e' is not defined
-
Źródło: https://docs.python.org/3/reference/compound_stmts.html#except
Gdy wyjątek jest przypisywany z użyciem
as
do zmiennej, jest usuwany na końcu sekcjiexcept
. To tak jakbyexcept E as N: foo
zostało użyte w sposób poniżej
except E as N: try: foo finally: del N
To znaczy, że wyjątek musi być przypisany do innej zmiennej aby móc być wywołany po sekcji
except
. Wyjątki są usuwane ponieważ jeśli są śledzone, tworzą pętle referencji na stosie ramowym, przechowyjąc wszystkie zmienne lokalne w tym stosie póki nie zadziała garbage collector. -
Sekcje nie są objęte zakresem w Pythonie. Wszystko w przykładzie znajduje się w tym samym zakresie, a zmienna
e
została usunięta z powodu wykonania sekcjiexcept
. To samo dotyczy funkcji, które mają swoje oddzielne wewnętrzne zakresy. Poniższy przykład to przedstawia:def f(x): del(x) print(x) x = 5 y = [5, 4, 3]
Wynik:
>>>f(x) UnboundLocalError: local variable 'x' referenced before assignment >>>f(y) UnboundLocalError: local variable 'x' referenced before assignment >>> x 5 >>> y [5, 4, 3]
-
W Python 2.x, zmienna
e
zostaje przypisana do instancjiException()
, więc przy próbie printowania nie printuje się.Wynik (Python 2.x):
>>> e Exception() >>> print e # Nothing is printed!
class SomeClass(str):
pass
some_dict = {'s': 42}
Wynik:
>>> type(list(some_dict.keys())[0])
str
>>> s = SomeClass('s')
>>> some_dict[s] = 40
>>> some_dict # spodziewane: dwie różne pary klucz-wartość
{'s': 40}
>>> type(list(some_dict.keys())[0])
str
-
Oba obiekty
s
i string"s"
są hashowane do tej samej wartości ponieważSomeClass
dziedziczy metodę__hash__
po klasiestr
. -
SomeClass("s") == "s"
zwracaTrue
ponieważSomeClass
dziedziczy również metodę__eq__
z klasystr
. -
Jako, że oba obiekty mają ten sam hash i są równe, są reprezentowane przez ten sam klucz w słowniku.
-
Dla wymaganego działania możemy sami zdefiniować metodę
__eq__
wSomeClass
class SomeClass(str): def __eq__(self, other): return ( type(self) is SomeClass and type(other) is SomeClass and super().__eq__(other) ) # Kiedy definiujemy własną metodę __eq__, Python automatycznie przestaje dziedziczyć # metodę __hash__ , więc musimy ją również zdefiniować __hash__ = str.__hash__ some_dict = {'s':42}
Wynik:
>>> 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)
a, b = a[b] = {}, 5
Wynik:
>>> a
{5: ({...}, 5)}
- W nawiązaniu do Python language reference, wyrażenia przypisujące mają formułę
i
(target_list "=")+ (expression_list | yield_expression)
Wyrażenie przypisania wykonuje listę wyrażeń (pamiętaj, że może to być pojedyncze wyrażenie lub lista oddzielona przecinkami, ta ostatnia daje tuple) i przypisuje pojedynczy wynikowy obiekt do każdej z list docelowych, od lewej do prawej.
-
+
w(target_list "=")+
oznacza, że może być jedna lub więcej niż jedna lista docelowa. W tym przypadku, listami docelowymi sąa, b
ia[b]
(zauważ, że lista wyrażeń jest tylko jedna, w naszym przypadku jest to{}, 5
). -
Po tym jak lista wyrażeń została wykonana, jej wartości zostają wypakowane do list docelowych od lewej do prawej. Więc w naszym wypadku najpierw tuple
{}, 5
jest wypakowany doa, b
z czego otrzymujemya = {}
ib = 5
. -
a
jest teraz przypisane do{}
, które jest obiektem mutowalnym. -
Druga lista docelowa to
a[b]
(możesz spodziewać się, że wyrzuci błąd ponieważ obaa
ib
nie zostały zdefiniowane we wcześniejszym wyrażeniu. Jednak pamiętaj, że właśnie przypisaliśmya
do{}
ib
do5
). -
Teraz przypisujemy klucz
5
w słowniku do tuple({}, 5)
tworząc zapętloną referencję ({...}
w wyniku odnosi się do tego sameho obiektu, do którego odnosi się juża
). Inny prostszy przykład zapętlonej referencji to>>> 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
Podobna sytuacja jest w naszym przykładzie (
a[b][0]
to ten sam obiekt coa
) -
Podsumowując, możesz rozbić ten przykład do
a, b = {}, 5 a[b] = a, b
A zapentlona referencja może zostać uzasadniona faktem, że
a[b][0]
to ten sam obiekt coa
>>> a[b][0] is a True
x = {0: None}
for i in x:
del x[i]
x[i+1] = None
print(i)
Wynik (Python 2.7- Python 3.5):
0
1
2
3
4
5
6
7
Tak, działa przez osiem pętli i się zatrzymuje.
- Iteracje na słowniku, który edytujesz podczas iteracji, nie są wspierane
- Iteruje 8 razy ponieważ jest to wielkość przy której słownik zwiększa użytą ilość pamięci aby móc mieć więcej kluczy (mamy 8 usinięć, więc zmiana wielkości jest potrzebna). Jest to właściwie detal implementacyjny.
- To jak obsługiwane są usunięte klucze i kiedy wystąpi zmiana wielkości pamięci mo że być różne dla różnych implementacji Pythona.
- Dla wersji Pythona innych niż 2.7 do 3.5, zliczanie może być inne niż do 8 (jednak jakiekolwiek by nie było, będzie takie same za każdym uruchomieniem). Możesz znaleźć więcej informacji na ten temat tutaj lub w tym wątku StackOverflow.
- W Python 3.8 i kolejnych, napotkasz wyjątek
RuntimeError: dictionary keys changed during iteration
przy próbie wykonania tego kodu.
class SomeClass:
def __del__(self):
print("Deleted!")
Wynik: 1.
>>> x = SomeClass()
>>> y = x
>>> del x # powinno wyprintować "Deleted!"
>>> del y
Deleted!
W końcu usunięte. Możliwe, że wywnioskowałeś co zablokowało wywołanie __del__
prtzypierwszej próbie usunięcia x
. Dodajmy jeszcze plot twist.
2.
>>> x = SomeClass()
>>> y = x
>>> del x
>>> y # sprawdźmy czy y istnieje
<__main__.SomeClass instance at 0x7f98a1a67fc8>
>>> del y # Jak poprzednio powinno wyprintować "Deleted!"
>>> globals() # ale tego nie zrobiło. Sprawdźmy nasze zmienne globalne dla potwierdzenia
Deleted!
{'__builtins__': <module '__builtin__' (built-in)>, 'SomeClass': <class __main__.SomeClass at 0x7f98a1a5f668>, '__package__': None, '__name__': '__main__', '__doc__': None}
OK, teraz jest usunięte 😕
del x
nie wywołuje bezpośredniox.__del__()
.- Za każdym razem gdy
del x
jest napotykane, Python zmniejsza ilość istniejącychx
o jeden, ix.__del__()
gdy referencyjne podliczenie x’ów dotarło do zera. - W drugim wyniku,
y.__del__()
nie został wywołany przez pojawienie się poprzedniego wyrażenia (>>> y
) w interpreterze interaktywnym, który stworzył kolejną referencję tego obiektu, stąd podliczenie referencji nie dotarło do zera gdydel y
zostało wywołane. - Wywołanie
globals
sprawiło, że istniejące referencje zostały zniszczone, i dzięki temu widzimy "Deleted!" wyprintowane (w końcu!).
a = 1
def some_func():
return a
def another_func():
a += 1
return a
Wynik:
>>> some_func()
1
>>> another_func()
UnboundLocalError: local variable 'a' referenced before assignment
-
Gdy tworzysz przypisanie do zmiennej w zakresie funkcji, zostaje ona zmienną lokalną. Więc
a
staje się lokalne w zakresieanother_func
, jednak nie została wcześniej zainicjalizowana w tym zakresie, przez co otrzymujemy błąd. -
Przeczytaj ten świetny, krótki poradnik aby dowiedzieć się więcej o tym jak działają zakresy i przestrzenie zmiennych w pythonie.
-
Aby dokonać zmiany zmiennej
a
z zewnętrznego zakresu wewnątrzanother_func
, użyj komendyglobal
.def another_func() global a a += 1 return a
Wynik:
>>> another_func() 2
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)
Wynik:
>>> list_1
[1, 2, 3, 4]
>>> list_2
[2, 4]
>>> list_3
[]
>>> list_4
[2, 4]
Cgadniesz skąd wzięło się [2, 4]
?
-
Nie powinno się zmieniać obiektów, po których się iteruje w trakcie iteracji. Poprawne będzie iterowanie po kopii, a
list_3[:]
również wystarczy.>>> some_list = [1, 2, 3, 4] >>> id(some_list) 139798789457608 >>> id(some_list[:]) # Zwróć uwagę, że python tworzy nowy obiekt w takim wypadku. 139798779601192
Różnica pomiędzy del
, remove
, i pop
:
del var_name
po prostu usuwa przypisanie zmiennejvar_name
z lokalnej lub globalnej przestrzeni zmiennych (Dlategolist_1
nie zmieniła się).remove
usuwa pierwszą pasującą wartość, a nie specyficzny indeks, i podnosiValueError
jeśli wartość nie jest odnaleziona.pop
usuwa element z podanym indeksem i zwraca go, podnosi teżIndexError
jeśli podano niepoprawny indeks.
Dlaczego wynik jest równy [2, 4]
?
- Iteracja po liście jest wykonywana indeks po indeksie, a gdy usuniemy
1
zlist_2
lublist_4
, zawartość listy staje się równa[2, 3, 4]
. Pozostałe elementy zostają przesunięte w dół, np2
do indeksu 0, a3
do indeksu 1. Jako, że kolejna iteracja będzie szukała indeksu 1 (czyli wartości3
),2
zostaje całkowicie pominięta. Podobna rzecz stanie sie z każdym zmienionym elementem w liście.
- Na StackOverflow wątek wyjaśnia przykład
- Zerknij również na StackOverflow wątek przedstawiający podobny przykład dla słowników.
>>> 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)]
# a teraz zipujemy pozostałe
>>> list(zip(numbers_iter, remaining))
[(4, 3), (5, 4), (6, 5)]
Gdzie podziała się 3
z listy numbers
?
- Z dokumentacji pythona, tak prezentuje się przybliżona implementacja funkcji zip,
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)
- Tak więc funkcja pobiera dowolną liczbę iterowalnych obiektów, dodaje każdy z ich elementów do listy
result
wywołując na nich funkcjęnext
i zatrzymuje się, gdy którykolwiek z iterowalnych elementów zostanie wyczerpany. - Zastrzeżenie polega na tym, że gdy jakikolwiek element iteracyjny zostanie wyczerpany, istniejące elementy na liście
result
są odrzucane. Tak stało się z3
wnumbers_iter
. - Prawidłowym sposobem wykonania powyższego przy użyciu
zip
byłoby,Pierwszym argumentem zip powinien być ten z najmniejszą liczbą elementów.>>> 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)]
1.
for x in range(7):
if x == 6:
print(x, ': for x inside loop')
print(x, ': x in global')
Wynik:
6 : for x inside loop
6 : x in global
Ale x
nigdy nie zostało zdefiniowane poza zakresem pętli for...
2.
# Tym razem najpierw zdefiniujmy x
x = -1
for x in range(7):
if x == 6:
print(x, ': for x inside loop')
print(x, ': x in global')
Wynik:
6 : for x inside loop
6 : x in global
3.
Wynik (Python 2.x):
>>> x = 1
>>> print([x for x in range(5)])
[0, 1, 2, 3, 4]
>>> print(x)
4
Wynik (Python 3.x):
>>> x = 1
>>> print([x for x in range(5)])
[0, 1, 2, 3, 4]
>>> print(x)
1
-
W Pythonie pętle for używają zakresu, w którym istnieją, i pozostawiają za sobą zdefiniowaną zmienną pętli. Dotyczy to również sytuacji, gdy wcześniej jawnie zdefiniowaliśmy zmienną for-loop w globalnej przestrzeni nazw. W takim przypadku ponownie powiąże istniejącą zmienną.
-
Różnice w wynikach interpreterów Python 2.x i Python 3.x dla przykładu ze zrozumieniem list można wyjaśnić, postępując zgodnie ze zmianą udokumentowaną w Co nowego w Pythonie 3.0 dziennik zmian:
"Listy składane nie obsługują już formy składniowej
[... for var in item1, item2, ...]
. Zamiast tego użyj[... for var in (item1, item2,...)]
. Zauważ też, że listy składane mają inną semantykę: są bliższe specyfice składni dla wyrażenia generatora wewnątrz konstruktoralist()
, a w szczególności zmienne sterujące pętli nie wyciekają już do zewnętrznego zasięgu."
def some_func(default_arg=[]):
default_arg.append("some_string")
return default_arg
Wynik:
>>> some_func()
['some_string']
>>> some_func()
['some_string', 'some_string']
>>> some_func([])
['some_string']
>>> some_func()
['some_string', 'some_string', 'some_string']
-
Domyślne zmienne argumenty funkcji w Pythonie tak naprawdę nie są inicjowane za każdym razem, gdy wywołujesz funkcję. Zamiast tego ostatnio przypisana do nich wartość jest używana jako wartość domyślna. Kiedy jawnie przekazaliśmy
[]
dosome_func
jako argumentu, domyślna wartość zmiennejdefault_arg
nie została użyta, więc funkcja zwróciła to co zgodne z oczekiwaniami.def some_func(default_arg=[]): default_arg.append("some_string") return default_arg
Wynik:
>>> some_func.__defaults__ #Spowoduje to wyświetlenie domyślnych wartości argumentów funkcji ([],) >>> 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'],)
-
Powszechną praktyką unikania błędów spowodowanych mutowalnymi argumentami jest przypisanie
None
jako wartości domyślnej, a następnie sprawdzenie, czy jakakolwiek wartość jest przekazywana do funkcji odpowiadającej temu argumentowi. Przykład:def some_func(default_arg=None): if not default_arg: default_arg = [] default_arg.append("some_string") return default_arg
some_list = [1, 2, 3]
try:
# To powinno podnieść ``IndexError``
print(some_list[4])
except IndexError, ValueError:
print("Caught!")
try:
# To powinno podnieść ``ValueError``
some_list.remove(4)
except IndexError, ValueError:
print("Caught again!")
Wynik (Python 2.x):
Caught!
ValueError: list.remove(x): x not in list
Wynik (Python 3.x):
File "<input>", line 3
except IndexError, ValueError:
^
SyntaxError: invalid syntax
-
Aby dodać wiele wyjątków do wyrażenia except, musisz przekazać je w tuplu jako pierwszy argument. Drugi argument to opcjonalna nazwa, która po dostarczeniu powiąże podniesioną instancję Exception. Przykład,
some_list = [1, 2, 3] try: # To powinno podnieść ``ValueError`` some_list.remove(4) except (IndexError, ValueError), e: print("Caught again!") print(e)
Wynik (Python 2.x):
Caught again! list.remove(x): x not in list
Wynik (Python 3.x):
File "<input>", line 4 except (IndexError, ValueError), e: ^ IndentationError: unindent does not match any outer indentation level
-
Oddzielenie wyjątku od zmiennej przecinkiem jest przestarzałe i nie działa w Pythonie 3; prawidłowym sposobem jest użycie
as
. Przykład,some_list = [1, 2, 3] try: some_list.remove(4) except (IndexError, ValueError) as e: print("Caught again!") print(e)
Wynik:
Caught again! list.remove(x): x not in list
1.
a = [1, 2, 3, 4]
b = a
a = a + [5, 6, 7, 8]
Output:
>>> 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]
Output:
>>> a
[1, 2, 3, 4, 5, 6, 7, 8]
>>> b
[1, 2, 3, 4, 5, 6, 7, 8]
-
a += b
doesn't always behave the same way asa = a + b
. Classes may implement theop=
operators differently, and lists do this. -
The expression
a = a + [5,6,7,8]
generates a new list and setsa
's reference to that new list, leavingb
unchanged. -
The expression
a += [5,6,7,8]
is actually mapped to an "extend" function that operates on the list such thata
andb
still point to the same list that has been modified in-place.
>>> (False == False) in [False] # makes sense
False
>>> False == (False in [False]) # makes sense
False
>>> False == False in [False] # now what?
True
>>> True is False == False
False
>>> False is False is False
True
>>> 1 > 0 < 1
True
>>> (1 > 0) < 1
False
>>> 1 > (0 < 1)
False
As per https://docs.python.org/2/reference/expressions.html#not-in
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.
While such behavior might seem silly to you in the above examples, it's fantastic with stuff like a == b == c
and 0 <= x <= 100
.
False is False is False
is equivalent to(False is False) and (False is False)
True is False == False
is equivalent toTrue is False and False == False
and since the first part of the statement (True is False
) evaluates toFalse
, the overall expression evaluates toFalse
.1 > 0 < 1
is equivalent to1 > 0 and 0 < 1
which evaluates toTrue
.- The expression
(1 > 0) < 1
is equivalent toTrue < 1
andSo,>>> int(True) 1 >>> True + 1 #not relevant for this example, but just for fun 2
1 < 1
evaluates toFalse
1.
x = 5
class SomeClass:
x = 17
y = (x for i in range(10))
Output:
>>> list(SomeClass.y)[0]
5
2.
x = 5
class SomeClass:
x = 17
y = [x for i in range(10)]
Output (Python 2.x):
>>> SomeClass.y[0]
17
Output (Python 3.x):
>>> SomeClass.y[0]
5
- Scopes nested inside class definition ignore names bound at the class level.
- A generator expression has its own scope.
- Starting from Python 3.X, list comprehensions also have their own scope.
I haven't met even a single experience Pythonist till date who has not come across one or more of the following scenarios,
1.
x, y = (0, 1) if True else None, None
Output:
>>> x, y # expected (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)
Output:
one
two
o
n
e
tuple()
3.
ten_words_list = [
"some",
"very",
"big",
"list",
"that"
"consists",
"of",
"exactly",
"ten",
"words"
]
Output
>>> len(ten_words_list)
9
4. Not asserting strongly enough
a = "python"
b = "javascript"
Output:
# An assert statement with an assertion failure message.
>>> assert(a == b, "Both languages are different")
# No AssertionError is raised
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})
Output:
>>> 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
Output:
>>> some_recursive_func([5, 0])
[0, 0]
>>> similar_recursive_func(5)
4
-
For 1, the correct statement for expected behavior is
x, y = (0, 1) if True else (None, None)
. -
For 2, the correct statement for expected behavior is
t = ('one',)
ort = 'one',
(missing comma) otherwise the interpreter considerst
to be astr
and iterates over it character by character. -
()
is a special token and denotes emptytuple
. -
In 3, as you might have already figured out, there's a missing comma after 5th element (
"that"
) in the list. So by implicit string literal concatenation,>>> ten_words_list ['some', 'very', 'big', 'list', 'thatconsists', 'of', 'exactly', 'ten', 'words']
-
No
AssertionError
was raised in 4th snippet because instead of asserting the individual expressiona == b
, we're asserting entire tuple. The following snippet will clear things up,>>> 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 aren not equal
-
As for the fifth snippet, most methods that modify the items of sequence/mapping objects like
list.append
,dict.update
,list.sort
, etc. modify the objects in-place and returnNone
. The rationale behind this is to improve performance by avoiding making a copy of the object if the operation can be done in-place (Referred from here). -
Last one should be fairly obvious, passing mutable object (like
list
) results in a call by reference, whereas an immutable object (likeint
) results in a call by value. -
Being aware of these nitpicks can save you hours of debugging effort in the long run.
>>> 'a'.split()
['a']
# is same as
>>> 'a'.split(' ')
['a']
# but
>>> len(''.split())
0
# isn't the same as
>>> len(''.split(' '))
1
- It might appear at first that the default separator for split is a single space
' '
, but as per the docsIf sep is not specified or is
None
, a different splitting algorithm is applied: runs of consecutive whitespace are regarded as a single separator, and the result will contain no empty strings at the start or end if the string has leading or trailing whitespace. Consequently, splitting an empty string or a string consisting of just whitespace with a None separator returns[]
. If sep is given, consecutive delimiters are not grouped together and are deemed to delimit empty strings (for example,'1,,2'.split(',')
returns['1', '', '2']
). Splitting an empty string with a specified separator returns['']
. - Noticing how the leading and trailing whitespaces are handled in the following snippet will make things clear,
>>> ' a '.split(' ') ['', 'a', ''] >>> ' a '.split() ['a'] >>> ''.split(' ') ['']
# File: module.py
def some_weird_name_func_():
print("works!")
def _another_weird_name_func():
print("works!")
Output
>>> 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
-
It is often advisable to not use wildcard imports. The first obvious reason for this is, in wildcard imports, the names with a leading underscore get imported. This may lead to errors during runtime.
-
Had we used
from ... import a, b, c
syntax, the aboveNameError
wouldn't have occurred.>>> from module import some_weird_name_func_, _another_weird_name_func >>> _another_weird_name_func() works!
-
If you really want to use wildcard imports, then you'd have to define the list
__all__
in your module that will contain a list of public objects that'll be available when we do wildcard imports.__all__ = ['_another_weird_name_func'] def some_weird_name_func_(): print("works!") def _another_weird_name_func(): print("works!")
Output
>>> _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
-
The
sorted
method always returns a list, and comparing lists and tuples always returnsFalse
in Python. -
>>> [] == tuple() False >>> x = 7, 8, 9 >>> type(x), type(sorted(x)) (tuple, list)
-
Unlike
sorted
, thereversed
method returns an iterator. Why? Because sorting requires the iterator to be either modified in-place or use an extra container (a list), whereas reversing can simply work by iterating from the last index to the first. -
So during comparison
sorted(y) == sorted(y)
, the first call tosorted()
will consume the iteratory
, and the next call will just return an empty list.>>> 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)
Output (< 3.5):
('Time at noon is', datetime.time(12, 0))
The midnight time is not printed.
Before Python 3.5, the boolean value for datetime.time
object was considered to be False
if it represented midnight in UTC. It is error-prone when using the if obj:
syntax to check if the obj
is null or some equivalent of "empty."
Section: The Hidden treasures!
This section contains a few lesser-known and interesting things about Python that most beginners like me are unaware of (well, not anymore).
Well, here you go
import antigravity
Output: Sshh... It's a super-secret.
antigravity
module is one of the few easter eggs released by Python developers.import antigravity
opens up a web browser pointing to the classic XKCD comic about Python.- Well, there's more to it. There's another easter egg inside the easter egg. If you look at the code, there's a function defined that purports to implement the XKCD's geohashing algorithm.
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 # breaking out from a deeply nested loop
label .breakout
print("Freedom!")
Output (Python 2.3):
I am trapped, please rescue!
I am trapped, please rescue!
Freedom!
- A working version of
goto
in Python was announced as an April Fool's joke on 1st April 2004. - Current versions of Python do not have this module.
- Although it works, but please don't use it. Here's the reason to why
goto
is not present in Python.
If you are one of the people who doesn't like using whitespace in Python to denote scopes, you can use the C-style {} by importing,
from __future__ import braces
Output:
File "some_file.py", line 1
from __future__ import braces
SyntaxError: not a chance
Braces? No way! If you think that's disappointing, use Java. Okay, another surprising thing, can you find where's the SyntaxError
raised in __future__
module code?
- The
__future__
module is normally used to provide features from future versions of Python. The "future" in this specific context is however, ironic. - This is an easter egg concerned with the community's feelings on this issue.
- The code is actually present here in
future.c
file. - When the CPython compiler encounters a future statement, it first runs the appropriate code in
future.c
before treating it as a normal import statement.
Output (Python 3.x)
>>> from __future__ import barry_as_FLUFL
>>> "Ruby" != "Python" # there's no doubt about it
File "some_file.py", line 1
"Ruby" != "Python"
^
SyntaxError: invalid syntax
>>> "Ruby" <> "Python"
True
There we go.
-
This is relevant to PEP-401 released on April 1, 2009 (now you know, what it means).
-
Quoting from the PEP-401
Recognized that the != inequality operator in Python 3.0 was a horrible, finger-pain inducing mistake, the FLUFL reinstates the <> diamond operator as the sole spelling.
-
There were more things that Uncle Barry had to share in the PEP; you can read them here.
-
It works well in an interactive environment, but it will raise a
SyntaxError
when you run via python file (see this issue). However, you can wrap the statement inside aneval
orcompile
to get it working,from __future__ import barry_as_FLUFL print(eval('"Ruby" <> "Python"'))
import this
Wait, what's this? this
is love ❤️
Output:
The Zen of Python, by 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!
It's the Zen of 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 # Love is complicated
True
this
module in Python is an easter egg for The Zen Of Python (PEP 20).- And if you think that's already interesting enough, check out the implementation of this.py. Interestingly, the code for the Zen violates itself (and that's probably the only place where this happens).
- Regarding the statement
love is not True or False; love is love
, ironic but it's self-explanatory (if not, please see the examples related tois
andis not
operators).
The else
clause for loops. One typical example might be:
def does_exists_num(l, to_find):
for num in l:
if num == to_find:
print("Exists!")
break
else:
print("Does not exist")
Output:
>>> some_list = [1, 2, 3, 4, 5]
>>> does_exists_num(some_list, 4)
Exists!
>>> does_exists_num(some_list, -1)
Does not exist
The else
clause in exception handling. An example,
try:
pass
except:
print("Exception occurred!!!")
else:
print("Try block executed successfully...")
Output:
Try block executed successfully...
- The
else
clause after a loop is executed only when there's no explicitbreak
after all the iterations. You can think of it as a "nobreak" clause. else
clause after a try block is also called "completion clause" as reaching theelse
clause in atry
statement means that the try block actually completed successfully.
def some_func():
Ellipsis
Output
>>> some_func()
# No output, No Error
>>> SomeRandomString
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
NameError: name 'SomeRandomString' is not defined
>>> Ellipsis
Ellipsis
- In Python,
Ellipsis
is a globally available built-in object which is equivalent to...
.>>> ... Ellipsis
- Eliipsis can be used for several purposes,
- As a placeholder for code that hasn't been written yet (just like
pass
statement) - In slicing syntax to represent the full slices in remaining direction
So our>>> import numpy as np >>> three_dimensional_array = np.arange(8).reshape(2, 2, 2) array([ [ [0, 1], [2, 3] ], [ [4, 5], [6, 7] ] ])
three_dimensional_array
is an array of array of arrays. Let's say we want to print the second element (index1
) of all the innermost arrays, we can use Ellipsis to bypass all the preceding dimensionsNote: this will work for any number of dimensions. You can even select slice in first and last dimension and ignore the middle ones this way (>>> three_dimensional_array[:,:,1] array([[1, 3], [5, 7]]) >>> three_dimensional_array[..., 1] # using Ellipsis. array([[1, 3], [5, 7]])
n_dimensional_array[firs_dim_slice, ..., last_dim_slice]
)- In type hinting to indicate only a part of the type (like
(Callable[..., int]
orTuple[str, ...]
)) - You may also use Ellipsis as a default function argument (in the cases when you want to differentiate between the "no argument passed" and "None value passed" scenarios).
- As a placeholder for code that hasn't been written yet (just like
The spelling is intended. Please, don't submit a patch for this.
Output (Python 3.x):
>>> infinity = float('infinity')
>>> hash(infinity)
314159
>>> hash(float('-inf'))
-314159
- Hash of infinity is 10⁵ x π.
- Interestingly, the hash of
float('-inf')
is "-10⁵ x π" in Python 3, whereas "-10⁵ x e" in Python 2.
1.
class Yo(object):
def __init__(self):
self.__honey = True
self.bro = True
Output:
>>> Yo().bro
True
>>> Yo().__honey
AttributeError: 'Yo' object has no attribute '__honey'
>>> Yo()._Yo__honey
True
2.
class Yo(object):
def __init__(self):
# Let's try something symmetrical this time
self.__honey__ = True
self.bro = True
Output:
>>> 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__'
Why did Yo()._Yo__honey
work?
3.
_A__variable = "Some value"
class A(object):
def some_func(self):
return __variable # not initiatlized anywhere yet
Output:
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
AttributeError: 'A' object has no attribute '__variable'
>>> >>> A().some_func()
'Some value'
- Name Mangling is used to avoid naming collisions between different namespaces.
- In Python, the interpreter modifies (mangles) the class member names starting with
__
(double underscore a.k.a "dunder") and not ending with more than one trailing underscore by adding_NameOfTheClass
in front. - So, to access
__honey
attribute in the first snippet, we had to append_Yo
to the front, which would prevent conflicts with the same name attribute defined in any other class. - But then why didn't it work in the second snippet? Because name mangling excludes the names ending with double underscores.
- The third snippet was also a consequence of name mangling. The name
__variable
in the statementreturn __variable
was mangled to_A__variable
, which also happens to be the name of the variable we declared in the outer scope. - Also, if the mangled name is longer than 255 characters, truncation will happen.
Output:
>>> value = 11
>>> valuе = 32
>>> value
11
Wut?
Note: The easiest way to reproduce this is to simply copy the statements from the above snippet and paste them into your file/shell.
Some non-Western characters look identical to letters in the English alphabet but are considered distinct by the interpreter.
>>> ord('е') # cyrillic 'e' (Ye)
1077
>>> ord('e') # latin 'e', as used in English and typed using standard keyboard
101
>>> 'е' == 'e'
False
>>> value = 42 # latin e
>>> valuе = 23 # cyrillic 'e', Python 2.x interpreter would raise a `SyntaxError` here
>>> value
42
The built-in ord()
function returns a character's Unicode code point, and different code positions of Cyrillic 'e' and Latin 'e' justify the behavior of the above example.
# `pip install nump` first.
import numpy as np
def energy_send(x):
# Initializing a numpy array
np.array([float(x)])
def energy_receive():
# Return an empty numpy array
return np.empty((), dtype=np.float).tolist()
Output:
>>> energy_send(123.456)
>>> energy_receive()
123.456
Where's the Nobel Prize?
- Notice that the numpy array created in the
energy_send
function is not returned, so that memory space is free to reallocate. numpy.empty()
returns the next free memory slot without reinitializing it. This memory spot just happens to be the same one that was just freed (usually, but not always).
def square(x):
"""
A simple function to calculate the square of a number by addition.
"""
sum_so_far = 0
for counter in range(x):
sum_so_far = sum_so_far + x
return sum_so_far
Output (Python 2.x):
>>> square(10)
10
Shouldn't that be 100?
Note: If you're not able to reproduce this, try running the file mixed_tabs_and_spaces.py via the shell.
-
Don't mix tabs and spaces! The character just preceding return is a "tab", and the code is indented by multiple of "4 spaces" elsewhere in the example.
-
This is how Python handles tabs:
First, tabs are replaced (from left to right) by one to eight spaces such that the total number of characters up to and including the replacement is a multiple of eight <...>
-
So the "tab" at the last line of
square
function is replaced with eight spaces, and it gets into the loop. -
Python 3 is kind enough to throw an error for such cases automatically.
Output (Python 3.x):
TabError: inconsistent use of tabs and spaces in indentation
# using "+", three strings:
>>> timeit.timeit("s1 = s1 + s2 + s3", setup="s1 = ' ' * 100000; s2 = ' ' * 100000; s3 = ' ' * 100000", number=100)
0.25748300552368164
# using "+=", three strings:
>>> timeit.timeit("s1 += s2 + s3", setup="s1 = ' ' * 100000; s2 = ' ' * 100000; s3 = ' ' * 100000", number=100)
0.012188911437988281
+=
is faster than+
for concatenating more than two strings because the first string (example,s1
fors1 += s2 + s3
) is not destroyed while calculating the complete string.
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
Output:
# Executed in ipython shell using %timeit for better readablity of results.
# You can also use the timeit module in normal python shell/scriptm=, example usage below
# 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)
Let's increase the number of iterations by a factor of 10.
>>> NUM_ITERS = 10000
>>> %timeit -n1000 add_string_with_plus(NUM_ITERS) # Linear increase in execution time
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) # Quadratic increase
6.82 ms ± 134 µs per loop (mean ± std. dev. of 7 runs, 1000 loops each)
>>> %timeit -n1000 add_string_with_format(NUM_ITERS) # Linear increase
645 µs ± 24.5 µs per loop (mean ± std. dev. of 7 runs, 1000 loops each)
>>> %timeit -n1000 add_string_with_join(NUM_ITERS) # Linear increase
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) # Linear increase
86.3 µs ± 2 µs per loop (mean ± std. dev. of 7 runs, 1000 loops each)
-
You can read more about timeit or %timeit on these links. They are used to measure the execution time of code pieces.
-
Don't use
+
for generating long strings — In Python,str
is immutable, so the left and right strings have to be copied into the new string for every pair of concatenations. If you concatenate four strings of length 10, you'll be copying (10+10) + ((10+10)+10) + (((10+10)+10)+10) = 90 characters instead of just 40 characters. Things get quadratically worse as the number and size of the string increases (justified with the execution times ofadd_bytes_with_plus
function) -
Therefore, it's advised to use
.format.
or%
syntax (however, they are slightly slower than+
for very short strings). -
Or better, if already you've contents available in the form of an iterable object, then use
''.join(iterable_object)
which is much faster. -
Unlike
add_bytes_with_plus
because of the+=
optimizations discussed in the previous example,add_string_with_plus
didn't show a quadratic increase in execution time. Had the statement beens = s + "x" + "y" + "z"
instead ofs += "xyz"
, the increase would have been quadratic.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) # Quadratic increase in execution time 9 ms ± 298 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)
-
So many ways to format and create a giant string are somewhat in contrast to the Zen of Python, according to which,
There should be one-- and preferably only one --obvious way to do it.
-
join()
is a string operation instead of list operation. (sort of counter-intuitive at first usage)💡 Explanation: If
join()
is a method on a string, then it can operate on any iterable (list, tuple, iterators). If it were a method on a list, it'd have to be implemented separately by every type. Also, it doesn't make much sense to put a string-specific method on a genericlist
object API. -
Few weird looking but semantically correct statements:
[] = ()
is a semantically correct statement (unpacking an emptytuple
into an emptylist
)'a'[0][0][0][0][0]
is also a semantically correct statement as strings are sequences(iterables supporting element access using integer indices) in Python.3 --0-- 5 == 8
and--5 == 5
are both semantically correct statements and evaluate toTrue
.
-
Given that
a
is a number,++a
and--a
are both valid Python statements but don't behave the same way as compared with similar statements in languages like C, C++, or Java.>>> a = 5 >>> a 5 >>> ++a 5 >>> --a 5
💡 Explanation:
- There is no
++
operator in Python grammar. It is actually two+
operators. ++a
parses as+(+a)
which translates toa
. Similarly, the output of the statement--a
can be justified.- This StackOverflow thread discusses the rationale behind the absence of increment and decrement operators in Python.
- There is no
-
You must be aware of the Walrus operator in Python. But have you ever heard about the space-invader operator?
>>> a = 42 >>> a -=- 1 >>> a 43
It is used as an alternative incrementation operator, together with another one
>>> a +=+ 1 >>> a >>> 44
💡 Explanation: This prank comes from Raymond Hettinger's tweet. The space invader operator is actually just a malformatted
a -= (-1)
. Which is equivalent toa = a - (- 1)
. Similar for thea += (+ 1)
case. -
Python has an undocumented converse implication operator.
>>> False ** False == True True >>> False ** True == False True >>> True ** False == True True >>> True ** True == True True
💡 Explanation: If you replace
False
andTrue
by 0 and 1 and do the maths, the truth table is equivalent to a converse implication operator. (Source) -
Since we are talking operators, there's also
@
operator for matrix multiplication (don't worry, this time it's for real).>>> import numpy as np >>> np.array([2, 2, 2]) @ np.array([7, 8, 8]) 46
💡 Explanation: The
@
operator was added in Python 3.5 keeping sthe cientific community in mind. Any object can overload__matmul__
magic method to define behavior for this operator. -
From Python 3.8 onwards you can use a typical f-string syntax like
f'{some_var=}
for quick debugging. Example,>>> some_string = "wtfpython" >>> f'{some_string=}' "string='wtfpython'"
-
Python uses 2 bytes for local variable storage in functions. In theory, this means that only 65536 variables can be defined in a function. However, python has a handy solution built in that can be used to store more than 2^16 variable names. The following code demonstrates what happens in the stack when more than 65536 local variables are defined (Warning: This code prints around 2^18 lines of text, so be prepared!):
import dis exec(""" def f(): """ + """ """.join(["X" + str(x) + "=" + str(x) for x in range(65539)])) f() print(dis.dis(f))
-
Multiple Python threads won't run your Python code concurrently (yes, you heard it right!). It may seem intuitive to spawn several threads and let them execute your Python code concurrently, but, because of the Global Interpreter Lock in Python, all you're doing is making your threads execute on the same core turn by turn. Python threads are good for IO-bound tasks, but to achieve actual parallelization in Python for CPU-bound tasks, you might want to use the Python multiprocessing module.
-
Sometimes, the
print
method might not print values immediately. For example,# File some_file.py import time print("wtfpython", end="_") time.sleep(3)
This will print the
wtfpython
after 10 seconds due to theend
argument because the output buffer is flushed either after encountering\n
or when the program finishes execution. We can force the buffer to flush by passingflush=True
argument. -
List slicing with out of the bounds indices throws no errors
>>> some_list = [1, 2, 3, 4, 5] >>> some_list[111:] []
-
Slicing an iterable not always creates a new object. For example,
>>> some_str = "wtfpython" >>> some_list = ['w', 't', 'f', 'p', 'y', 't', 'h', 'o', 'n'] >>> some_list is some_list[:] # False expected because a new object is created. False >>> some_str is some_str[:] # True because strings are immutable, so making a new object is of not much use. True
-
int('١٢٣٤٥٦٧٨٩')
returns123456789
in Python 3. In Python, Decimal characters include digit characters, and all characters that can be used to form decimal-radix numbers, e.g. U+0660, ARABIC-INDIC DIGIT ZERO. Here's an interesting story related to this behavior of Python. -
You can seperate numeric literals with underscores (for better readablity) from Python 3 onwards.
>>> six_million = 6_000_000 >>> six_million 6000000 >>> hex_address = 0xF00D_CAFE >>> hex_address 4027435774
-
'abc'.count('') == 4
. Here's an approximate implementation ofcount
method, which would make the things more cleardef count(s, sub): result = 0 for i in range(len(s) + 1 - len(sub)): result += (s[i:i + len(sub)] == sub) return result
The behavior is due to the matching of empty substring(
''
) with slices of length 0 in the original string.
That's all folks!
A few ways in which you can contribute to wtfpython,
- Suggesting new examples
- Helping with translation (See issues labeled translation)
- Minor corrections like pointing out outdated snippets, typos, formatting errors, etc.
- Identifying gaps (things like inadequate explanation, redundant examples, etc.)
- Any creative suggestions to make this project more fun and useful
Please see CONTRIBUTING.md for more details. Feel free to create a new issue to discuss things.
PS: Please don't reach out with backlinking requests, no links will be added unless they're highly relevant to the project.
The idea and design for this collection were initially inspired by Denys Dovhan's awesome project wtfjs. The overwhelming support by Pythonistas gave it the shape it is in right now.
- https://www.youtube.com/watch?v=sH4XF6pKKmk
- https://www.reddit.com/r/Python/comments/3cu6ej/what_are_some_wtf_things_about_python
- https://sopython.com/wiki/Common_Gotchas_In_Python
- https://stackoverflow.com/questions/530530/python-2-x-gotchas-and-landmines
- https://stackoverflow.com/questions/1011431/common-pitfalls-in-python
- https://www.python.org/doc/humor/
- https://github.com/cosmologicon/pywat#the-undocumented-converse-implication-operator
- https://www.codementor.io/satwikkansal/python-practices-for-efficient-code-performance-memory-and-usability-aze6oiq65
- https://github.com/wemake-services/wemake-python-styleguide/search?q=wtfpython&type=Issues
If you like wtfpython, you can use these quick links to share it with your friends,
If you're interested in more content like this, you can share your email here. PS: On a sidenote, consider buying me a meal or planting a tree.