Python Language
Vanliga fallgropar
Sök…
Introduktion
Python är ett språk som är tänkt att vara tydligt och läsbart utan oklarheter och oväntat beteende. Tyvärr kan dessa mål inte uppnås i alla fall, och det är därför som Python har några få hörnfall där det kan göra något annorlunda än vad du förväntade dig.
Det här avsnittet visar några problem som du kan stöta på när du skriver Python-kod.
Ändra sekvensen du iterera över
En for
slinga iterates över en sekvens, så att ändra denna sekvens inuti slingan kan leda till oväntade resultat (särskilt när du lägger till eller tar bort element):
alist = [0, 1, 2]
for index, value in enumerate(alist):
alist.pop(index)
print(alist)
# Out: [1]
Obs: list.pop()
används för att ta bort element från listan.
Det andra elementet raderades inte eftersom iterationen går igenom indexen i ordning. Ovanstående slinga upprepas två gånger, med följande resultat:
# 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]
Detta problem uppstår på grund av att indexen förändras medan de upprepas i riktning mot ökande index. För att undvika detta problem kan du iterera igenom öglan bakåt :
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]
Genom att iterera genom slingan som börjar i slutet, eftersom objekt tas bort (eller läggs till) påverkar det inte indexen för objekt tidigare i listan. Så det här exemplet kommer att ta bort alla objekt som till och med från alist
.
Ett liknande problem uppstår när du sätter in eller lägger till element i en lista som du itererar över , vilket kan resultera i en oändlig slinga:
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]
Utan break
skulle slingan infoga 'a'
så länge datorn inte har slut på minnet och programmet får fortsätta. I en situation som denna föredras det vanligtvis att skapa en ny lista och lägga till objekt i den nya listan när du går igenom den ursprungliga listan.
När du använder en for
loop kan du inte ändra listelementen med platshållarens variabel :
alist = [1,2,3,4]
for item in alist:
if item % 2 == 0:
item = 'even'
print(alist)
# Out: [1,2,3,4]
I exemplet ovan, ändra item
faktiskt inte ändra något i den ursprungliga listan. Du måste använda listindex ( alist[2]
) och enumerate()
fungerar bra för detta:
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']
A while
slinga kan vara ett bättre val i vissa fall:
Om du kommer att ta bort alla objekt i listan:
zlist = [0, 1, 2]
while zlist:
print(zlist[0])
zlist.pop(0)
print('After: zlist =', zlist)
# Out: 0
# 1
# 2
# After: zlist = []
Även om du bara återställer zlist
kommer du att uppnå samma resultat;
zlist = []
Exemplet ovan kan också kombineras med len()
att stoppa efter en viss punkt, eller för att radera alla utom x
objekt i listan:
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]
Eller att gå igenom en lista medan du tar bort element som uppfyller ett visst villkor (i detta fall tar du bort alla jämna element):
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]
Lägg märke till att du inte ökar i
efter att du har tagit bort ett element. Genom att radera elementet på zlist[i]
har indexet för nästa objekt minskat med ett, så genom att kontrollera zlist[i]
med samma värde för i
i nästa iteration, kommer du att kontrollera nästa objekt i listan korrekt .
Ett motsatt sätt att tänka på att ta bort oönskade objekt från en lista är att lägga till önskade objekt i en ny lista . Följande exempel är ett alternativ till det senare while
loop-exempel:
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]
Här tränar vi önskade resultat till en ny lista. Vi kan sedan eventuellt tilldela den temporära listan till den ursprungliga variabeln.
Med denna tänktrend kan du åberopa en av Pythons mest eleganta och kraftfulla funktioner, listförståelser , som eliminerar tillfälliga listor och avviker från den tidigare diskuterade ideologin på platslista / indexmutationer.
zlist = [1,2,3,4,5]
[item for item in zlist if item % 2 != 0]
# Out: [1, 3, 5]
Möjligt standardargument
def foo(li=[]):
li.append(1)
print(li)
foo([2])
# Out: [2, 1]
foo([3])
# Out: [3, 1]
Den här koden fungerar som förväntat, men vad händer om vi inte klarar ett argument?
foo()
# Out: [1] As expected...
foo()
# Out: [1, 1] Not as expected...
Detta beror på att standardargument för funktioner och metoder utvärderas vid definitionstid snarare än körtid. Så vi har bara en enda instans av li
listan.
Sättet att ta sig runt det är att använda endast immutable-typer för standardargument:
def foo(li=None):
if not li:
li = []
li.append(1)
print(li)
foo()
# Out: [1]
foo()
# Out: [1]
Även om en förbättring och även if not li
korrekt utvärderar False
, gör många andra objekt också, till exempel sekvenser med noll längd. Följande exempelargument kan orsaka oavsiktliga resultat:
x = []
foo(li=x)
# Out: [1]
foo(li="")
# Out: [1]
foo(li=0)
# Out: [1]
Den idiomatiska metoden är att direkt kontrollera argumentet mot objektet None
:
def foo(li=None):
if li is None:
li = []
li.append(1)
print(li)
foo()
# Out: [1]
Lista multiplikation och vanliga referenser
Tänk på att skapa en kapslad liststruktur genom att multiplicera:
li = [[]] * 3
print(li)
# Out: [[], [], []]
Vid första anblicken skulle vi tro att vi har en lista med 3 olika kapslade listor. Låt oss försöka lägga till 1
till den första:
li[0].append(1)
print(li)
# Out: [[1], [1], [1]]
1
fick bifogas alla listor i li
.
Anledningen är att [[]] * 3
inte skapar en list
med 3 olika list
. Snarare skapar det en list
som håller 3 hänvisningar till samma list
objekt. Som sådan, när vi lägger till li[0]
är förändringen synlig i alla underelement av li
. Detta motsvarar:
li = []
element = [[]]
li = element + element + element
print(li)
# Out: [[], [], []]
element.append(1)
print(li)
# Out: [[1], [1], [1]]
Detta kan bekräftas ytterligare om vi skriver ut minnesadresserna i list
med hjälp av id
:
li = [[]] * 3
print([id(inner_list) for inner_list in li])
# Out: [6830760, 6830760, 6830760]
Lösningen är att skapa de inre listorna med en slinga:
li = [[] for _ in range(3)]
Istället för att skapa en enda list
och sedan göra tre referenser till den, skapar vi nu tre olika distinkta listor. Detta kan igen verifieras med id
funktionen:
print([id(inner_list) for inner_list in li])
# Out: [6331048, 6331528, 6331488]
Du kan också göra detta. Det gör att en ny tom lista skapas i varje append
samtal.
>>> li = []
>>> li.append([])
>>> li.append([])
>>> li.append([])
>>> for k in li: print(id(k))
...
4315469256
4315564552
4315564808
Använd inte index för att slinga över en sekvens.
Inte:
for i in range(len(tab)):
print(tab[i])
Gör :
for elem in tab:
print(elem)
for
kommer att automatisera de flesta iterationsoperationer för dig.
Använd räkna om du verkligen behöver både indexet och elementet .
for i, elem in enumerate(tab):
print((i, elem))
Var försiktig när du använder "==" för att kontrollera sann eller falskt
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
Kontrollera inte om du kan, bara gör det och hantera felet
Pythonistas säger vanligtvis "Det är lättare att be om förlåtelse än tillåtelse".
Inte:
if os.path.isfile(file_path):
file = open(file_path)
else:
# do something
Do:
try:
file = open(file_path)
except OSError as e:
# do something
Eller ännu bättre med Python 2.6+
:
with open(file_path) as file:
Det är mycket bättre eftersom det är mycket mer generiskt. Du kan applicera try/except
nästan vad som helst. Du behöver inte bry dig om vad du ska göra för att förhindra det, bara bry dig om det fel du riskerar.
Kontrollera inte mot typen
Python skrivs dynamiskt, och därför kontrollerar du om du gör att du tappar flexibiliteten. Använd istället anka-typ genom att kontrollera beteende. Om du förväntar dig en sträng i en funktion använder du str()
att konvertera ett objekt till en sträng. Om du förväntar dig en lista använder du list()
att konvertera alla iterable till en lista.
Inte:
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)
Do:
def foo(name) :
print(str(name).lower())
def bar(listing) :
l = list(listing)
l.extend((1, 2, 3))
return ", ".join(l)
På det sista sättet accepterar foo
alla objekt. bar
accepterar strängar, tuples, uppsättningar, listor och mycket mer. Billig TÖR.
Blanda inte mellanrum och flikar
Använd objekt som första förälder
Det här är svårt, men det kommer att bita dig när ditt program växer. Det finns gamla och nya klasser i Python 2.x
De gamla är, ja, gamla. De saknar några funktioner och kan ha besvärligt beteende med arv. För att vara användbar måste någon av din klass vara av "ny stil". För att göra det, få den att ärva från object
.
Inte:
class Father:
pass
class Child(Father):
pass
Do:
class Father(object):
pass
class Child(Father):
pass
I Python 3.x
alla klasser ny stil så du behöver inte göra det.
Initiera inte klassattribut utanför init- metoden
Människor som kommer från andra språk tycker det är frestande eftersom det är vad du gör i Java eller PHP. Du skriver klassnamnet, listar sedan dina attribut och ger dem ett standardvärde. Det verkar fungera i Python, men detta fungerar inte som du tror. Om du gör det kommer du att ställa in klassattribut (statiska attribut). När du försöker få objektattributet kommer det att ge dig dess värde om det inte är tomt. I så fall kommer det att returnera klassattributen. Det innebär två stora faror:
Om klassattributet ändras ändras det initiala värdet.
Om du ställer in ett muterbart objekt som ett standardvärde får du samma objekt som delas över instanser.
Inte (om du inte vill ha statisk):
class Car(object):
color = "red"
wheels = [Wheel(), Wheel(), Wheel(), Wheel()]
Gör:
class Car(object):
def __init__(self):
self.color = "red"
self.wheels = [Wheel(), Wheel(), Wheel(), Wheel()]
Heltal och strängidentitet
Python använder intern cache för en rad heltal för att minska onödiga omkostnader från deras upprepade skapande.
I själva verket kan detta leda till förvirrande beteende när man jämför heltalidentiteter:
>>> -8 is (-7 - 1)
False
>>> -3 is (-2 - 1)
True
och med hjälp av ett annat exempel:
>>> (255 + 1) is (255 + 1)
True
>>> (256 + 1) is (256 + 1)
False
Vänta, va?
Vi kan se att identiteten operation is
avkastningen True
för några heltal ( -3
, 256
) men ingen för andra ( -8
, 257
).
För att vara mer specifika cachas heltal i intervallet [-5, 256]
internt vid tolkstart och skapas endast en gång. Som sådan är de identiska och att jämföra sina identiteter med is
True
; heltal utanför detta intervall är (vanligtvis) skapade on-the-fly och deras identiteter jämförs med False
.
Detta är ett vanligt fallgrop eftersom detta är ett vanligt intervall för testning, men ofta nog, koden misslyckas i den senare iscenesättningsprocessen (eller ännu värre - produktion) utan någon uppenbar anledning efter att ha fungerat perfekt under utveckling.
Lösningen är att alltid jämföra värden med operatören jämlikhet ( ==
) och inte operatören för identitet ( is
).
Python håller också hänvisningar till vanligt använda strängar och kan resultera i på samma sätt förvirrande beteende när man jämnar identiteter (dvs. att använda is
) av strängar.
>>> 'python' is 'py' + 'thon'
True
Strängen 'python'
används vanligtvis, så Python har ett objekt som alla referenser till strängen 'python'
använder.
För ovanliga strängar misslyckas jämförelse av identitet även när strängarna är lika.
>>> '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
Så, precis som regeln för heltal, jämför alltid strängvärden med operatören jämlikhet ( ==
) och inte operatören för identitet ( is
).
Få tillgång till int-bokstävernas attribut
Du kanske har hört att allt i Python är ett objekt, till och med bokstavliga. Detta innebär att till exempel 7
är ett objekt, vilket innebär att det har attribut. Till exempel är ett av dessa attribut bit_length
. Den returnerar mängden bitar som behövs för att representera det värde det krävs.
x = 7
x.bit_length()
# Out: 3
Om du ser ovanstående kod fungerar du kanske intuitivt att 7.bit_length()
skulle fungera också, bara för att ta reda på att det höjer en SyntaxError
. Varför? eftersom tolkaren måste skilja mellan ett attributstillträde och ett flytande nummer (till exempel 7.2
eller 7.bit_length()
). Det kan inte, och det är därför som ett undantag tas upp.
Det finns några sätt att få tillgång till en int
literals attribut:
# parenthesis
(7).bit_length()
# a space
7 .bit_length()
Att använda två punkter (som den här 7..bit_length()
) fungerar inte i det här fallet, eftersom det skapar en float
letteral och floats har inte bit_length()
.
Detta problem existerar inte vid åtkomst float
literals' attribut eftersom interperter är 'smart' nog att veta att en float
bokstavlig inte kan innehålla två .
, till exempel:
7.2.as_integer_ratio()
# Out: (8106479329266893, 1125899906842624)
Kedja eller operatör
När du testar för någon av flera jämställdhetsjämförelser:
if a == 3 or b == 3 or c == 3:
det är frestande att förkorta detta till
if a or b or c == 3: # Wrong
Detta är fel; or
operatören har lägre prioritet än ==
, så uttrycket kommer att utvärderas som if (a) or (b) or (c == 3):
Rätt sätt är att uttryckligen kontrollera alla villkor:
if a == 3 or b == 3 or c == 3: # Right Way
Alternativt kan den inbyggda any()
funktionen any()
användas i stället för kedjade or
operatörer:
if any([a == 3, b == 3, c == 3]): # Right
Eller för att göra det mer effektivt:
if any(x == 3 for x in (a, b, c)): # Right
Eller för att göra det kortare:
if 3 in (a, b, c): # Right
Här använder vi in
operatören att testa om värdet finns i en tupel innehåller de värden vi vill jämföra mot.
På samma sätt är det felaktigt att skriva
if a == 1 or 2 or 3:
som bör skrivas som
if a in (1, 2, 3):
sys.argv [0] är namnet på filen som körs
Det första elementet i sys.argv[0]
är namnet på pythonfilen som körs. De återstående elementen är skriptargument.
# 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']
Ordböcker är oordnade
Du kan förvänta dig att en Python-ordlista ska sorteras efter nycklar som till exempel en C ++ std::map
, men detta är inte fallet:
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 har ingen inbyggd klass som automatiskt sorterar sina element efter nyckel.
Men om sortering inte är ett måste, och du bara vill att din ordbok ska komma ihåg ordningen för infogning av dess nyckel- / värdepar, kan du använda collections.OrderedDict
:
from collections import OrderedDict
oDict = OrderedDict([('first', 1), ('second', 2), ('third', 3)])
print([k for k in oDict])
# Out: ['first', 'second', 'third']
Kom ihåg att initiera en OrderedDict
med en standardordbok inte på något sätt kommer att OrderedDict
ordboken för dig. Allt som denna struktur gör är att bevara ordningen för infogning av nycklar.
Implementeringen av ordböcker ändrades i Python 3.6 för att förbättra deras minneskonsumtion. En biverkning av den nya implementeringen är att den också bevarar ordningen med sökordargument som skickas till en funktion:
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
Varning : se upp för att " den ordningsbevarande aspekten av denna nya implementering betraktas som en implementeringsdetalj och inte bör lita på " , eftersom det kan förändras i framtiden.
Global Interpreter Lock (GIL) och blockerande trådar
Det har skrivits gott om Pythons GIL . Det kan ibland orsaka förvirring när man hanterar flertrådade (inte förväxlas med multiprocess) -applikationer.
Här är ett exempel:
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")
Du kan förvänta dig att se Calculating...
skrivs ut omedelbart efter att tråden har startats, vi ville att beräkningen skulle ske i en ny tråd trots allt! Men i själva verket ser du att det skrivs ut när beräkningen är klar. Det beror på att den nya tråden förlitar sig på en C-funktion ( math.factorial
) som låser GIL medan den körs.
Det finns ett par sätt runt detta. Den första är att implementera din faktorfunktion i ursprungliga Python. Detta gör att huvudtråden kan greppa kontrollen när du är inne i din slinga. Nackdelen är att den här lösningen kommer att bli mycket långsammare eftersom vi inte använder C-funktionen längre.
def calc_fact(num):
""" A slow version of factorial in native Python """
res = 1
while num >= 1:
res = res * num
num -= 1
return res
Du kan också sleep
under en period innan du börjar körningen. Obs! Detta kommer faktiskt inte att låta ditt program avbryta beräkningen som sker i C-funktionen, men det kommer att låta din huvudtråd fortsätta efter spawn, vilket är vad du kan förvänta dig.
def calc_fact(num):
sleep(0.001)
math.factorial(num)
Variabel läcka i listförståelser och för slingor
Tänk på följande listaförståelse
i = 0
a = [i for i in range(3)]
print(i) # Outputs 2
Detta inträffar endast i Python 2 på grund av det faktum att listförståelsen "läcker" slingkontrollvariabeln i det omgivande omfånget ( källa ). Detta beteende kan leda till svåra att hitta buggar och det har rättats i Python 3 .
i = 0
a = [i for i in range(3)]
print(i) # Outputs 0
På samma sätt har slingor inget privat utrymme för sin iterationsvariabel
i = 0
for i in range(3):
pass
print(i) # Outputs 2
Denna typ av beteende förekommer både i Python 2 och Python 3.
För att undvika problem med läckande variabler, använd nya variabler i listförståelser och för slingor efter behov.
Flera avkastning
Funktion xyz returnerar två värden a och b:
def xyz():
return a, b
Kod som ringer xyz-butiker resulterar i en variabel förutsatt att xyz endast returnerar ett värde:
t = xyz()
Värdet på t
är faktiskt en tupel (a, b) så varje åtgärd på t
antar att det inte är en tupel kan misslyckas djupt i koden med ett oväntat fel om tuples.
TypeError: typ tuple definierar inte ... metod
Fixet skulle vara att göra:
a, b = xyz()
Nybörjare kommer att ha problem med att hitta orsaken till detta meddelande genom att bara läsa felmeddelandet om tuple!
Pythonic JSON-nycklar
my_var = 'bla';
api_key = 'key';
...lots of code here...
params = {"language": "en", my_var: api_key}
Om du är van vid JavaScript är variabel utvärdering i Python-ordböcker inte det du förväntar dig att det ska vara. Detta uttalande i JavaScript skulle resultera i params
enligt följande:
{
"language": "en",
"my_var": "key"
}
I Python skulle det emellertid resultera i följande ordbok:
{
"language": "en",
"bla": "key"
}
my_var
utvärderas och dess värde används som nyckel.