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:

Python 3.x 3.6
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

Python 2.x 2.7
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 .

Python 3.x 3.0
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.



Modified text is an extract of the original Stack Overflow Documentation
Licencjonowany na podstawie CC BY-SA 3.0
Nie związany z Stack Overflow