Python Language
Typowe pułapki
Szukaj…
Wprowadzenie
Python jest językiem, który ma być jasny i czytelny, bez żadnych dwuznaczności i nieoczekiwanych zachowań. Niestety te cele nie są osiągalne we wszystkich przypadkach i dlatego Python ma kilka narożnych przypadków, w których może zrobić coś innego niż się spodziewałeś.
W tej sekcji przedstawiono problemy, które mogą wystąpić podczas pisania kodu w języku Python.
Zmieniając sekwencję, w której iterujesz
Pętla for
iteruje sekwencję, więc zmiana tej sekwencji w pętli może doprowadzić do nieoczekiwanych wyników (szczególnie podczas dodawania lub usuwania elementów):
alist = [0, 1, 2]
for index, value in enumerate(alist):
alist.pop(index)
print(alist)
# Out: [1]
Uwaga: list.pop()
służy do usuwania elementów z listy.
Drugi element nie został usunięty, ponieważ iteracja przechodzi przez indeksy w kolejności. Powyższa pętla iteruje dwukrotnie, z następującymi wynikami:
# Iteration #1
index = 0
alist = [0, 1, 2]
alist.pop(0) # removes '0'
# Iteration #2
index = 1
alist = [1, 2]
alist.pop(1) # removes '2'
# loop terminates, but alist is not empty:
alist = [1]
Problem ten powstaje, ponieważ wskaźniki zmieniają się podczas iteracji w kierunku wzrostu wskaźnika. Aby uniknąć tego problemu, możesz iterować przez pętlę do tyłu :
alist = [1,2,3,4,5,6,7]
for index, item in reversed(list(enumerate(alist))):
# delete all even items
if item % 2 == 0:
alist.pop(index)
print(alist)
# Out: [1, 3, 5, 7]
Dzięki iteracji w pętli zaczynającej się na końcu, gdy elementy są usuwane (lub dodawane), nie wpływa to na indeksy elementów wcześniej na liście. Ten przykład prawidłowo usunie wszystkie przedmioty, które są nawet z alist
.
Podobny problem pojawia się podczas wstawiania lub dołączania elementów do listy, nad którą się iteruje , co może skutkować nieskończoną pętlą:
alist = [0, 1, 2]
for index, value in enumerate(alist):
# break to avoid infinite loop:
if index == 20:
break
alist.insert(index, 'a')
print(alist)
# Out (abbreviated): ['a', 'a', ..., 'a', 'a', 0, 1, 2]
Bez warunku break
pętla wstawiałaby 'a'
o ile w komputerze nie zabraknie pamięci i program będzie mógł kontynuować. W takiej sytuacji zwykle preferowane jest utworzenie nowej listy i dodawanie elementów do nowej listy podczas przeglądania listy oryginalnej.
Korzystając z pętli for
, nie można modyfikować elementów listy za pomocą zmiennej zastępczej :
alist = [1,2,3,4]
for item in alist:
if item % 2 == 0:
item = 'even'
print(alist)
# Out: [1,2,3,4]
W powyższym przykładzie zmiana item
nie zmienia niczego na oryginalnej liście . Musisz użyć indeksu listy ( alist[2]
), a enumerate()
działa dobrze w tym celu:
alist = [1,2,3,4]
for index, item in enumerate(alist):
if item % 2 == 0:
alist[index] = 'even'
print(alist)
# Out: [1, 'even', 3, 'even']
while
pętla może być lepszym wyborem w niektórych przypadkach:
Jeśli zamierzasz usunąć wszystkie elementy z listy:
zlist = [0, 1, 2]
while zlist:
print(zlist[0])
zlist.pop(0)
print('After: zlist =', zlist)
# Out: 0
# 1
# 2
# After: zlist = []
Chociaż samo zresetowanie zlist
da ten sam rezultat;
zlist = []
Powyższy przykład można również połączyć z len()
aby zatrzymać po pewnym punkcie lub usunąć wszystkie elementy oprócz x
na liście:
zlist = [0, 1, 2]
x = 1
while len(zlist) > x:
print(zlist[0])
zlist.pop(0)
print('After: zlist =', zlist)
# Out: 0
# 1
# After: zlist = [2]
Lub przeglądanie listy podczas usuwania elementów spełniających określony warunek (w tym przypadku usuwanie wszystkich elementów parzystych):
zlist = [1,2,3,4,5]
i = 0
while i < len(zlist):
if zlist[i] % 2 == 0:
zlist.pop(i)
else:
i += 1
print(zlist)
# Out: [1, 3, 5]
Zauważ, że nie zwiększasz i
po usunięciu elementu. Usuwając element na zlist[i]
, indeks następnego elementu zmniejszył się o jeden, więc sprawdzając zlist[i]
o tej samej wartości dla i
na następnej iteracji, będziesz poprawnie sprawdzał następny element na liście .
Przeciwnym sposobem myślenia o usuwaniu niechcianych elementów z listy jest dodawanie pożądanych elementów do nowej listy . Poniższy przykład jest alternatywą dla tego ostatniego while
przykład pętli:
zlist = [1,2,3,4,5]
z_temp = []
for item in zlist:
if item % 2 != 0:
z_temp.append(item)
zlist = z_temp
print(zlist)
# Out: [1, 3, 5]
Tutaj umieszczamy pożądane wyniki na nowej liście. Następnie możemy opcjonalnie ponownie przypisać listę tymczasową do oryginalnej zmiennej.
Z tym trendem myślenia możesz przywołać jedną z najbardziej eleganckich i potężnych funkcji Pythona, rozumienie list , które eliminuje listy tymczasowe i odbiega od wcześniej omówionej ideologii mutacji list / indeksów na miejscu.
zlist = [1,2,3,4,5]
[item for item in zlist if item % 2 != 0]
# Out: [1, 3, 5]
Zmienny domyślny argument
def foo(li=[]):
li.append(1)
print(li)
foo([2])
# Out: [2, 1]
foo([3])
# Out: [3, 1]
Ten kod zachowuje się zgodnie z oczekiwaniami, ale co jeśli nie przekażemy argumentu?
foo()
# Out: [1] As expected...
foo()
# Out: [1, 1] Not as expected...
Jest tak, ponieważ domyślne argumenty funkcji i metod są oceniane w czasie definicji, a nie w czasie wykonywania. Mamy więc tylko jedną instancję listy li
.
Sposób obejścia tego problemu polega na użyciu tylko niezmiennych typów dla domyślnych argumentów:
def foo(li=None):
if not li:
li = []
li.append(1)
print(li)
foo()
# Out: [1]
foo()
# Out: [1]
Chociaż poprawka i choć if not li
poprawnie ocenia na False
, to robi to również wiele innych obiektów, takich jak sekwencje o zerowej długości. Następujące przykładowe argumenty mogą powodować niezamierzone wyniki:
x = []
foo(li=x)
# Out: [1]
foo(li="")
# Out: [1]
foo(li=0)
# Out: [1]
Podejście idiomatyczne polega na bezpośrednim sprawdzeniu argumentu względem obiektu None
:
def foo(li=None):
if li is None:
li = []
li.append(1)
print(li)
foo()
# Out: [1]
Mnożenie listy i wspólne odniesienia
Rozważ przypadek utworzenia zagnieżdżonej struktury listy przez pomnożenie:
li = [[]] * 3
print(li)
# Out: [[], [], []]
Na pierwszy rzut oka wydaje się, że mamy listę zawierającą 3 różne listy zagnieżdżone. Spróbujmy dodać 1
do pierwszego:
li[0].append(1)
print(li)
# Out: [[1], [1], [1]]
1
został dołączony do wszystkich listach w li
.
Powodem jest to, że [[]] * 3
nie tworzy list
3 różnych list
. Zamiast tego tworzy list
zawierającą 3 odwołania do tego samego obiektu list
. Jako taki, kiedy dodajemy do li[0]
zmiana jest widoczna we wszystkich podelementach li
. Jest to równoważne z:
li = []
element = [[]]
li = element + element + element
print(li)
# Out: [[], [], []]
element.append(1)
print(li)
# Out: [[1], [1], [1]]
Można to dodatkowo potwierdzić, jeśli wydrukujemy adresy pamięci zawartej list
za pomocą id
:
li = [[]] * 3
print([id(inner_list) for inner_list in li])
# Out: [6830760, 6830760, 6830760]
Rozwiązaniem jest utworzenie wewnętrznych list za pomocą pętli:
li = [[] for _ in range(3)]
Zamiast tworzyć jedną list
a następnie tworzyć 3 odniesienia do niej, tworzymy teraz 3 różne odrębne listy. To ponownie można zweryfikować za pomocą funkcji id
:
print([id(inner_list) for inner_list in li])
# Out: [6331048, 6331528, 6331488]
Możesz także to zrobić. Powoduje to utworzenie nowej pustej listy w każdym append
połączenia.
>>> li = []
>>> li.append([])
>>> li.append([])
>>> li.append([])
>>> for k in li: print(id(k))
...
4315469256
4315564552
4315564808
Nie używaj indeksu do zapętlania sekwencji.
Nie:
for i in range(len(tab)):
print(tab[i])
Zrób :
for elem in tab:
print(elem)
for
zautomatyzuje większość operacji iteracyjnych.
Użyj wyliczenia, jeśli naprawdę potrzebujesz zarówno indeksu, jak i elementu .
for i, elem in enumerate(tab):
print((i, elem))
Zachowaj ostrożność, używając „==”, aby sprawdzić wartość Prawda lub Fałsz
if (var == True):
# this will execute if var is True or 1, 1.0, 1L
if (var != True):
# this will execute if var is neither True nor 1
if (var == False):
# this will execute if var is False or 0 (or 0.0, 0L, 0j)
if (var == None):
# only execute if var is None
if var:
# execute if var is a non-empty string/list/dictionary/tuple, non-0, etc
if not var:
# execute if var is "", {}, [], (), 0, None, etc.
if var is True:
# only execute if var is boolean True, not 1
if var is False:
# only execute if var is boolean False, not 0
if var is None:
# same as var == None
Nie sprawdzaj, czy możesz, po prostu zrób to i napraw błąd
Pythoniści zwykle mówią „łatwiej prosić o wybaczenie niż o pozwolenie”.
Nie:
if os.path.isfile(file_path):
file = open(file_path)
else:
# do something
Robić:
try:
file = open(file_path)
except OSError as e:
# do something
Lub jeszcze lepiej z Python 2.6+
:
with open(file_path) as file:
Jest o wiele lepszy, ponieważ jest o wiele bardziej ogólny. Możesz zastosować try/except
do prawie wszystkiego. Nie musisz się martwić, co zrobić, aby temu zapobiec, po prostu dbaj o ryzyko, które ryzykujesz.
Nie sprawdzaj z typem
Python jest typowany dynamicznie, dlatego sprawdzanie typu powoduje utratę elastyczności. Zamiast tego użyj pisania kaczego , sprawdzając zachowanie. Jeśli oczekujesz ciągu w funkcji, użyj str()
aby przekonwertować dowolny obiekt na ciąg. Jeśli oczekujesz listy, użyj list()
aby przekonwertować dowolną iterowalną listę.
Nie:
def foo(name):
if isinstance(name, str):
print(name.lower())
def bar(listing):
if isinstance(listing, list):
listing.extend((1, 2, 3))
return ", ".join(listing)
Robić:
def foo(name) :
print(str(name).lower())
def bar(listing) :
l = list(listing)
l.extend((1, 2, 3))
return ", ".join(l)
Używając ostatniego sposobu, foo
zaakceptuje dowolny obiekt. bar
akceptuje ciągi, krotki, zestawy, listy i wiele innych. Tani SUCHY.
Nie mieszaj spacji i tabulatorów
Użyj obiektu jako pierwszego rodzica
Jest to trudne, ale ugryzie Cię w miarę rozwoju programu. W Python 2.x
są stare i nowe klasy. Stare są, no cóż, stare. Brakuje im niektórych funkcji i mogą mieć dziwne zachowanie z dziedziczeniem. Aby była użyteczna, każda z twoich klas musi być w „nowym stylu”. Aby to zrobić, spraw, aby dziedziczył po object
.
Nie:
class Father:
pass
class Child(Father):
pass
Robić:
class Father(object):
pass
class Child(Father):
pass
W Python 3.x
wszystkie klasy mają nowy styl, więc nie musisz tego robić.
Nie inicjuj atrybutów klas poza metodą init
Ludzie pochodzący z innych języków uważają to za kuszące, ponieważ to właśnie robisz w Javie lub PHP. Pisz nazwę klasy, następnie wypisz swoje atrybuty i nadaj im wartość domyślną. Wygląda na to, że działa w Pythonie, ale to nie działa tak, jak myślisz. W ten sposób ustawisz atrybuty klasy (atrybuty statyczne), a następnie, gdy będziesz próbował uzyskać atrybut obiektu, da ci swoją wartość, chyba że będzie pusty. W takim przypadku zwróci atrybuty klasy. Oznacza to dwa duże zagrożenia:
Jeśli atrybut klasy zostanie zmieniony, wówczas zmieni się wartość początkowa.
Jeśli ustawisz zmienny obiekt jako wartość domyślną, otrzymasz ten sam obiekt współdzielony między instancjami.
Nie rób tego (chyba że chcesz statyczny):
class Car(object):
color = "red"
wheels = [Wheel(), Wheel(), Wheel(), Wheel()]
Robić :
class Car(object):
def __init__(self):
self.color = "red"
self.wheels = [Wheel(), Wheel(), Wheel(), Wheel()]
Tożsamość liczb całkowitych i ciągów
Python używa wewnętrznego buforowania dla zakresu liczb całkowitych, aby zredukować niepotrzebne koszty związane z ich wielokrotnym tworzeniem.
W efekcie może to prowadzić do nieporozumień podczas porównywania liczb całkowitych:
>>> -8 is (-7 - 1)
False
>>> -3 is (-2 - 1)
True
i na innym przykładzie:
>>> (255 + 1) is (255 + 1)
True
>>> (256 + 1) is (256 + 1)
False
Czekaj, co?
Widzimy, że operacja tożsamość is
plony True
dla pewnych liczb całkowitych ( -3
, 256
), ale nie dla innych ( -8
, 257
).
Mówiąc dokładniej, liczby całkowite z zakresu [-5, 256]
są buforowane wewnętrznie podczas uruchamiania interpretera i są tworzone tylko raz. Jako takie są one identyczne i porównywanie ich tożsamości ze is
plony True
; liczby całkowite poza tym zakresem są (zwykle) tworzone w locie, a ich tożsamości porównywane są z False
.
Jest to częsta pułapka, ponieważ jest to wspólny zakres do testowania, ale często kod zawiedzie w późniejszym procesie pomostowym (lub gorzej - w produkcji) bez wyraźnego powodu po perfekcyjnym opracowaniu.
Rozwiązaniem jest zawsze porównywać wartości za pomocą operatora równości ( ==
), a nie operatora tożsamości ( is
).
Python zachowuje również odwołania do często używanych ciągów i może powodować podobnie mylące zachowanie podczas porównywania tożsamości (tj. Używanie is
) ciągów.
>>> 'python' is 'py' + 'thon'
True
Ciąg 'python'
jest powszechnie używany, więc Python ma jeden obiekt, z którego korzystają wszystkie odwołania do ciągu 'python'
.
W przypadku rzadkich ciągów porównywanie tożsamości kończy się niepowodzeniem, nawet gdy ciągi znaków są równe.
>>> 'this is not a common string' is 'this is not' + ' a common string'
False
>>> 'this is not a common string' == 'this is not' + ' a common string'
True
Tak więc, podobnie jak reguła dla liczb całkowitych, zawsze porównuj wartości ciągów za pomocą operatora równości ( ==
), a nie operatora tożsamości ( is
).
Dostęp do atrybutów literałów int
Być może słyszałeś, że wszystko w Pythonie jest przedmiotem, nawet literałem. Oznacza to na przykład, że 7
jest również przedmiotem, co oznacza, że ma atrybuty. Na przykład jednym z tych atrybutów jest bit_length
. Zwraca liczbę bitów potrzebną do przedstawienia żądanej wartości.
x = 7
x.bit_length()
# Out: 3
Widząc, że powyższy kod działa, możesz intuicyjnie pomyśleć, że 7.bit_length()
by działał, tylko po to, aby dowiedzieć się, że wywołuje błąd SyntaxError
. Dlaczego? ponieważ interpreter musi odróżniać dostęp do atrybutu od liczby zmiennoprzecinkowej (na przykład 7.2
lub 7.bit_length()
). Nie może i dlatego powstaje wyjątek.
Istnieje kilka sposobów dostępu do atrybutów literałów int
:
# parenthesis
(7).bit_length()
# a space
7 .bit_length()
Użycie dwóch kropek (jak ta 7..bit_length()
) nie działa w tym przypadku, ponieważ tworzy literał float
a float
nie mają metody bit_length()
.
Ten problem nie występuje podczas uzyskiwania dostępu do atrybutów liczb float
, ponieważ interpreter jest wystarczająco „inteligentny”, aby wiedzieć, że literał float
nie może zawierać dwóch .
, na przykład:
7.2.as_integer_ratio()
# Out: (8106479329266893, 1125899906842624)
Łańcuch lub operator
Podczas testowania jednego z kilku porównań równości:
if a == 3 or b == 3 or c == 3:
pokusa, aby to skrócić
if a or b or c == 3: # Wrong
To jest źle; operator or
ma niższy priorytet niż ==
, więc wyrażenie będzie oceniane tak, if (a) or (b) or (c == 3):
Prawidłowym sposobem jest jawne sprawdzenie wszystkich warunków:
if a == 3 or b == 3 or c == 3: # Right Way
Zamiast tego można użyć wbudowanej any()
funkcji any()
zamiast łańcucha or
operatorów:
if any([a == 3, b == 3, c == 3]): # Right
Lub, aby zwiększyć efektywność:
if any(x == 3 for x in (a, b, c)): # Right
Lub, aby go skrócić:
if 3 in (a, b, c): # Right
W tym przypadku używamy operatora in
do testowania, czy wartość jest obecna w krotce zawierającej wartości, z którymi chcemy porównać.
Podobnie, pisanie jest nieprawidłowe
if a == 1 or 2 or 3:
które należy zapisać jako
if a in (1, 2, 3):
sys.argv [0] to nazwa wykonywanego pliku
Pierwszym elementem sys.argv[0]
jest nazwa wykonywanego pliku python. Pozostałe elementy to argumenty skryptu.
# script.py
import sys
print(sys.argv[0])
print(sys.argv)
$ python script.py
=> script.py
=> ['script.py']
$ python script.py fizz
=> script.py
=> ['script.py', 'fizz']
$ python script.py fizz buzz
=> script.py
=> ['script.py', 'fizz', 'buzz']
Słowniki są nieuporządkowane
Można oczekiwać, że słownik Pythona zostanie posortowany według kluczy, na przykład C ++ std::map
, ale tak nie jest:
myDict = {'first': 1, 'second': 2, 'third': 3}
print(myDict)
# Out: {'first': 1, 'second': 2, 'third': 3}
print([k for k in myDict])
# Out: ['second', 'third', 'first']
Python nie ma żadnej wbudowanej klasy, która automatycznie sortuje elementy według klucza.
Jeśli jednak sortowanie nie jest konieczne, a chcesz, aby słownik zapamiętywał kolejność wstawiania par klucz / wartość, możesz użyć collections.OrderedDict
.
from collections import OrderedDict
oDict = OrderedDict([('first', 1), ('second', 2), ('third', 3)])
print([k for k in oDict])
# Out: ['first', 'second', 'third']
Pamiętaj, że zainicjowanie OrderedDict
za pomocą standardowego słownika nie posortuje w żaden sposób słownika. Wszystko, co robi ta struktura, to zachowanie kolejności wprowadzania kluczy.
Implementacja słowników została zmieniona w Pythonie 3.6, aby poprawić zużycie pamięci. Efektem ubocznym tej nowej implementacji jest to, że zachowuje ona również kolejność argumentów słów kluczowych przekazywanych do funkcji:
def func(**kw): print(kw.keys())
func(a=1, b=2, c=3, d=4, e=5)
dict_keys(['a', 'b', 'c', 'd', 'e']) # expected order
Uwaga : uważaj, że „ aspekt zachowania porządku w tej nowej implementacji jest uważany za szczegół implementacji i nie należy na nim polegać ” , ponieważ może się to zmienić w przyszłości.
Global Interpreter Lock (GIL) i blokujące wątki
Wiele napisano o GIL Pythona . Czasami może powodować zamieszanie w przypadku aplikacji wielowątkowych (nie mylić z aplikacjami wieloprocesowymi).
Oto przykład:
import math
from threading import Thread
def calc_fact(num):
math.factorial(num)
num = 600000
t = Thread(target=calc_fact, daemon=True, args=[num])
print("About to calculate: {}!".format(num))
t.start()
print("Calculating...")
t.join()
print("Calculated")
Można się spodziewać, że funkcja Calculating...
wydrukowana natychmiast po uruchomieniu wątku. Chcieliśmy jednak, aby obliczenia zostały wykonane w nowym wątku! Ale w rzeczywistości widać, że jest drukowany po zakończeniu obliczeń. Wynika to z faktu, że nowy wątek opiera się na funkcji C ( math.factorial
), która blokuje GIL podczas jego wykonywania.
Jest na to kilka sposobów. Pierwszym z nich jest zaimplementowanie funkcji silni w natywnym języku Python. To pozwoli głównemu wątkowi przejąć kontrolę, gdy jesteś w pętli. Minusem jest to, że to rozwiązanie będzie znacznie wolniejsze, ponieważ nie używamy już funkcji C.
def calc_fact(num):
""" A slow version of factorial in native Python """
res = 1
while num >= 1:
res = res * num
num -= 1
return res
Możesz także sleep
przez pewien czas przed rozpoczęciem egzekucji. Uwaga: w rzeczywistości nie pozwoli to programowi przerwać obliczeń zachodzących w funkcji C, ale pozwoli na kontynuację głównego wątku po spawnie, czego można się spodziewać.
def calc_fact(num):
sleep(0.001)
math.factorial(num)
Zmienny wyciek w opisach list i pętlach
Rozważ poniższe rozumienie listy
i = 0
a = [i for i in range(3)]
print(i) # Outputs 2
Dzieje się tak tylko w Pythonie 2 ze względu na fakt, że rozumienie listy „wycieka” zmienną sterującą pętli do otaczającego zakresu ( źródła ). To zachowanie może prowadzić do trudnych do znalezienia błędów i zostało naprawione w Pythonie 3 .
i = 0
a = [i for i in range(3)]
print(i) # Outputs 0
Podobnie dla pętli nie ma prywatnego zakresu dla ich zmiennej iteracyjnej
i = 0
for i in range(3):
pass
print(i) # Outputs 2
Ten typ zachowania występuje zarówno w Python 2, jak i Python 3.
Aby uniknąć problemów z wyciekającymi zmiennymi, używaj nowych zmiennych w opisach list i w pętlach, stosownie do przypadku.
Wielokrotny zwrot
Funkcja xyz zwraca dwie wartości aib:
def xyz():
return a, b
Wywołanie kodu przechowuje wyniki xyz w jednej zmiennej, zakładając, że xyz zwraca tylko jedną wartość:
t = xyz()
Wartość t
jest w rzeczywistości krotką (a, b), więc każde działanie na t
zakładając, że nie jest krotką, może zawieść głęboko w kodzie z nieoczekiwanym błędem dotyczącym krotek.
Błąd typu: krotka typu nie definiuje ... metody
Poprawka polegałaby na:
a, b = xyz()
Początkujący będą mieli problemy ze znalezieniem przyczyny tego komunikatu, czytając tylko komunikat o błędzie krotki!
Pythoniczne klucze JSON
my_var = 'bla';
api_key = 'key';
...lots of code here...
params = {"language": "en", my_var: api_key}
Jeśli jesteś przyzwyczajony do JavaScript, ocena zmiennych w słownikach Pythona nie będzie taka, jakiej oczekujesz. Ta instrukcja w JavaScript spowoduje, że obiekt params
wyglądał następująco:
{
"language": "en",
"my_var": "key"
}
W Pythonie miałoby to jednak następujący słownik:
{
"language": "en",
"bla": "key"
}
my_var
jest obliczany, a jego wartość jest używana jako klucz.