Python Language
Insidie comuni
Ricerca…
introduzione
Python è un linguaggio pensato per essere chiaro e leggibile senza ambiguità e comportamenti imprevisti. Sfortunatamente, questi obiettivi non sono raggiungibili in tutti i casi, ed è per questo che Python ha alcuni casi d'angolo in cui potrebbe fare qualcosa di diverso da quello che ti aspettavi.
Questa sezione ti mostrerà alcuni problemi che potresti incontrare durante la scrittura del codice Python.
Modifica della sequenza su cui stai iterando
Un ciclo for
itera su una sequenza, quindi alterare questa sequenza all'interno del ciclo potrebbe portare a risultati imprevisti (specialmente quando si aggiungono o rimuovono elementi):
alist = [0, 1, 2]
for index, value in enumerate(alist):
alist.pop(index)
print(alist)
# Out: [1]
Nota: list.pop()
viene utilizzato per rimuovere elementi dall'elenco.
Il secondo elemento non è stato cancellato perché l'iterazione attraversa gli indici in ordine. Il ciclo sopra riportato itera due volte, con i seguenti risultati:
# 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]
Questo problema sorge perché gli indici stanno cambiando mentre si sta ripetendo nella direzione di aumentare l'indice. Per evitare questo problema, puoi scorrere il ciclo all'indietro :
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]
Eseguendo il ciclo dall'inizio alla fine, quando gli elementi vengono rimossi (o aggiunti), non influisce sugli indici delle voci precedenti nell'elenco. Quindi questo esempio rimuoverà correttamente tutti gli elementi che sono anche da alist
.
Un problema simile si presenta quando si inseriscono o si aggiungono elementi a un elenco su cui si sta iterando , il che può generare un loop infinito:
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]
Senza la condizione di break
, il ciclo inserirà 'a'
fintanto che il computer non esaurirà la memoria e il programma potrà continuare. In una situazione come questa, in genere è preferibile creare un nuovo elenco e aggiungere elementi al nuovo elenco mentre si scorre l'elenco originale.
Quando si utilizza un ciclo for
, non è possibile modificare gli elementi dell'elenco con la variabile placeholder :
alist = [1,2,3,4]
for item in alist:
if item % 2 == 0:
item = 'even'
print(alist)
# Out: [1,2,3,4]
Nell'esempio sopra, la modifica item
non modifica nulla nella lista originale . È necessario utilizzare l'indice di lista ( alist[2]
) e enumerate()
funziona bene per questo:
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']
Un ciclo while
potrebbe essere una scelta migliore in alcuni casi:
Se stai per eliminare tutti gli elementi nell'elenco:
zlist = [0, 1, 2]
while zlist:
print(zlist[0])
zlist.pop(0)
print('After: zlist =', zlist)
# Out: 0
# 1
# 2
# After: zlist = []
Sebbene la semplice reimpostazione di zlist
lo stesso risultato;
zlist = []
L'esempio sopra può anche essere combinato con len()
per fermarsi dopo un certo punto, o per eliminare tutti gli elementi tranne x
nella lista:
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]
Oppure per scorrere un elenco durante l'eliminazione di elementi che soddisfano una determinata condizione (in questo caso l'eliminazione di tutti gli elementi pari):
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]
Si noti che non si incrementa i
dopo aver eliminato un elemento. zlist[i]
l'elemento su zlist[i]
, l'indice dell'elemento successivo è diminuito di uno, quindi controllando zlist[i]
con lo stesso valore per i
alla successiva iterazione, si verificherà correttamente l'elemento successivo nell'elenco .
Un modo contrario di pensare a rimuovere elementi indesiderati da una lista è aggiungere elementi desiderati a una nuova lista . L'esempio seguente è un'alternativa al secondo esempio di ciclo while
:
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]
Qui stiamo incanalando i risultati desiderati in una nuova lista. Possiamo quindi riassegnare facoltativamente l'elenco temporaneo alla variabile originale.
Con questa tendenza del pensiero, puoi invocare una delle più eleganti e potenti funzionalità di Python, la list comprehensions , che elimina le liste temporanee e le divergenze dall'ideologia sul listino sul posto / sull'indice degli indici.
zlist = [1,2,3,4,5]
[item for item in zlist if item % 2 != 0]
# Out: [1, 3, 5]
Argomento predefinito mutabile
def foo(li=[]):
li.append(1)
print(li)
foo([2])
# Out: [2, 1]
foo([3])
# Out: [3, 1]
Questo codice si comporta come previsto, ma cosa succede se non passiamo un argomento?
foo()
# Out: [1] As expected...
foo()
# Out: [1, 1] Not as expected...
Questo perché gli argomenti predefiniti di funzioni e metodi vengono valutati in fase di definizione piuttosto che in fase di esecuzione. Quindi abbiamo sempre una sola istanza della lista li
.
Il modo per aggirarlo è usare solo tipi immutabili per gli argomenti predefiniti:
def foo(li=None):
if not li:
li = []
li.append(1)
print(li)
foo()
# Out: [1]
foo()
# Out: [1]
Sebbene sia un miglioramento e sebbene if not li
correttamente per False
, anche molti altri oggetti funzionano come sequenze di lunghezza zero. I seguenti argomenti di esempio possono causare risultati indesiderati:
x = []
foo(li=x)
# Out: [1]
foo(li="")
# Out: [1]
foo(li=0)
# Out: [1]
L'approccio idiomatico consiste nel controllare direttamente l'argomento rispetto all'oggetto None
:
def foo(li=None):
if li is None:
li = []
li.append(1)
print(li)
foo()
# Out: [1]
Elenco di moltiplicazione e riferimenti comuni
Si consideri il caso di creare una struttura ad elenco nidificata moltiplicando:
li = [[]] * 3
print(li)
# Out: [[], [], []]
A prima vista, penseremmo di avere un elenco contenente 3 diverse liste annidate. Proviamo ad aggiungere 1
al primo:
li[0].append(1)
print(li)
# Out: [[1], [1], [1]]
1
stato aggiunto a tutte le liste in li
.
Il motivo è che [[]] * 3
non crea una list
di 3 list
diverse. Piuttosto, crea una list
contenente 3 riferimenti allo stesso oggetto list
. Come tale, quando aggiungiamo a li[0]
il cambiamento è visibile in tutti i sottoelementi di li
. Questo è l'equivalente di:
li = []
element = [[]]
li = element + element + element
print(li)
# Out: [[], [], []]
element.append(1)
print(li)
# Out: [[1], [1], [1]]
Questo può essere ulteriormente confermato se stampiamo gli indirizzi di memoria della list
contenuta usando id
:
li = [[]] * 3
print([id(inner_list) for inner_list in li])
# Out: [6830760, 6830760, 6830760]
La soluzione è creare le liste interne con un ciclo:
li = [[] for _ in range(3)]
Invece di creare una singola list
e quindi fare 3 riferimenti ad essa, ora creiamo 3 diversi elenchi distinti. Questo, ancora, può essere verificato usando la funzione id
:
print([id(inner_list) for inner_list in li])
# Out: [6331048, 6331528, 6331488]
Puoi anche farlo. Fa sì che venga creata una nuova lista vuota in ogni chiamata append
.
>>> li = []
>>> li.append([])
>>> li.append([])
>>> li.append([])
>>> for k in li: print(id(k))
...
4315469256
4315564552
4315564808
Non utilizzare l'indice per eseguire il loop su una sequenza.
Non:
for i in range(len(tab)):
print(tab[i])
Fai :
for elem in tab:
print(elem)
for
automatizzare la maggior parte delle operazioni di iterazione per te.
Usa enumerare se hai davvero bisogno sia dell'indice che dell'elemento .
for i, elem in enumerate(tab):
print((i, elem))
Fai attenzione quando usi "==" per controllare True o False
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
Non controllare se puoi, fallo e gestisci l'errore
Pythonistas di solito dice "È più facile chiedere perdono che il permesso".
Non:
if os.path.isfile(file_path):
file = open(file_path)
else:
# do something
Fare:
try:
file = open(file_path)
except OSError as e:
# do something
O ancora meglio con Python 2.6+
:
with open(file_path) as file:
È molto meglio perché è molto più generico. Puoi applicare try/except
a quasi tutto. Non è necessario preoccuparsi di cosa fare per prevenirlo, basta preoccuparsi dell'errore che si sta rischiando.
Non controllare contro il tipo
Python è digitato in modo dinamico, quindi il controllo per il tipo ti fa perdere la flessibilità. Invece, usa la digitazione anatra controllando il comportamento. Se ti aspetti una stringa in una funzione, usa str()
per convertire qualsiasi oggetto in una stringa. Se ti aspetti una lista, usa list()
per convertire qualsiasi iterabile in una lista.
Non:
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)
Fare:
def foo(name) :
print(str(name).lower())
def bar(listing) :
l = list(listing)
l.extend((1, 2, 3))
return ", ".join(l)
Usando l'ultima modalità, foo
accetterà qualsiasi oggetto. bar
accetterà corde, tuple, set, elenchi e molto altro. A buon mercato ASCIUTTO.
Non mescolare spazi e tabulazioni
Usa oggetto come primo genitore
È complicato, ma ti morderà man mano che il tuo programma cresce. Esistono classi vecchie e nuove in Python 2.x
I vecchi sono, bene, vecchi. Mancano di alcune funzionalità e possono avere un comportamento scomodo con l'ereditarietà. Per essere utilizzabile, qualsiasi classe deve essere del "nuovo stile". Per fare ciò, ereditarlo object
.
Non:
class Father:
pass
class Child(Father):
pass
Fare:
class Father(object):
pass
class Child(Father):
pass
In Python 3.x
tutte le classi sono di nuovo stile, quindi non è necessario farlo.
Non inizializzare gli attributi della classe al di fuori del metodo init
Persone provenienti da altre lingue trovano allettante perché è ciò che fai in Java o in PHP. Scrivi il nome della classe, quindi elenca i tuoi attributi e assegna loro un valore predefinito. Sembra funzionare in Python, tuttavia, questo non funziona come pensi. In questo modo verranno impostati gli attributi di classe (attributi statici), quindi quando tenterai di ottenere l'attributo dell'oggetto, ti darà il suo valore a meno che non sia vuoto. In tal caso restituirà gli attributi della classe. Implica due grandi rischi:
Se l'attributo della classe è cambiato, allora il valore iniziale è cambiato.
Se imposti un oggetto mutabile come valore predefinito, otterrai lo stesso oggetto condiviso tra le istanze.
Non (a meno che tu non voglia statico):
class Car(object):
color = "red"
wheels = [Wheel(), Wheel(), Wheel(), Wheel()]
Fare :
class Car(object):
def __init__(self):
self.color = "red"
self.wheels = [Wheel(), Wheel(), Wheel(), Wheel()]
Intero e identità di stringa
Python utilizza la memorizzazione nella cache interna per un intervallo di numeri interi per ridurre il sovraccarico non necessario dalla loro ripetuta creazione.
In effetti, questo può portare a comportamenti confusi quando si confrontano le identità dei numeri interi:
>>> -8 is (-7 - 1)
False
>>> -3 is (-2 - 1)
True
e, usando un altro esempio:
>>> (255 + 1) is (255 + 1)
True
>>> (256 + 1) is (256 + 1)
False
Aspetta cosa?
Possiamo vedere che l'operazione di identità is
resa True
per alcuni interi ( -3
, 256
) ma non per altri ( -8
, 257
).
Per essere più specifici, gli interi nell'intervallo [-5, 256]
sono memorizzati internamente nella cache durante l'avvio dell'interprete e vengono creati una sola volta. Come tali, essi sono identici e confrontando le loro identità con is
rendimenti True
; gli interi al di fuori di questo intervallo sono (di solito) creati al volo e le loro identità si confrontano con False
.
Questo è un errore comune poiché questo è un intervallo comune per i test, ma spesso abbastanza, il codice fallisce nel processo di gestione temporanea successivo (o peggio - produzione) senza una ragione apparente dopo aver lavorato perfettamente nello sviluppo.
La soluzione è confrontare sempre i valori usando l' operatore di uguaglianza ( ==
) e non l'operatore di identità ( is
).
Python mantiene anche i riferimenti alle stringhe di uso comune e può comportare un comportamento simile e confuso quando si confrontano le identità (cioè l'utilizzo di is
) delle stringhe.
>>> 'python' is 'py' + 'thon'
True
La stringa 'python'
è comunemente usata, quindi Python ha un oggetto che usa tutti i riferimenti alla stringa 'python'
.
Per stringhe non comuni, il confronto dell'identità non riesce anche se le stringhe sono uguali.
>>> '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
Quindi, proprio come la regola per Interi, confronta sempre i valori stringa usando l' operatore di uguaglianza ( ==
) e non l'operatore di identità ( is
).
Accesso agli attributi letterali di int
Potresti aver sentito che tutto in Python è un oggetto, anche letterale. Ciò significa che, ad esempio, 7
è anche un oggetto, il che significa che ha degli attributi. Ad esempio, uno di questi attributi è il bit_length
. Restituisce la quantità di bit necessari per rappresentare il valore su cui è chiamato.
x = 7
x.bit_length()
# Out: 3
Vedendo che il codice sopra funziona, potresti pensare intuitivamente che anche 7.bit_length()
funzionerebbe, solo per scoprire che genera un SyntaxError
. Perché? perché l'interprete deve distinguere tra un accesso di attributo e un numero mobile (ad esempio 7.2
o 7.bit_length()
). Non può, ed è per questo che viene sollevata un'eccezione.
Esistono alcuni modi per accedere agli attributi di un letterale int
:
# parenthesis
(7).bit_length()
# a space
7 .bit_length()
L'uso di due punti (come questo 7..bit_length()
) non funziona in questo caso, poiché ciò crea un valore letterale float
e i float non hanno il metodo bit_length()
.
Questo problema non esiste quando si accede agli attributi letterali float
poiché l'interperter è abbastanza "intelligente" da sapere che un letterale float
non può contenere due .
, per esempio:
7.2.as_integer_ratio()
# Out: (8106479329266893, 1125899906842624)
Concatenamento di o operatore
Durante il test per uno dei numerosi confronti di uguaglianza:
if a == 3 or b == 3 or c == 3:
si è tentati di abbreviare questo a
if a or b or c == 3: # Wrong
Questo è sbagliato; l'operatore or
ha una precedenza inferiore a ==
, quindi l'espressione sarà valutata come if (a) or (b) or (c == 3):
Il modo corretto è il controllo esplicito di tutte le condizioni:
if a == 3 or b == 3 or c == 3: # Right Way
In alternativa, è possibile utilizzare la funzione any()
integrata al posto di concatenati or
operatori:
if any([a == 3, b == 3, c == 3]): # Right
Oppure, per renderlo più efficiente:
if any(x == 3 for x in (a, b, c)): # Right
Oppure, per renderlo più breve:
if 3 in (a, b, c): # Right
Qui, usiamo l' in
all'operatore di verificare se il valore è presente in una tupla contenente i valori che vogliamo confrontare con.
Allo stesso modo, non è corretto scrivere
if a == 1 or 2 or 3:
che dovrebbe essere scritto come
if a in (1, 2, 3):
sys.argv [0] è il nome del file in esecuzione
Il primo elemento di sys.argv[0]
è il nome del file python in esecuzione. Gli elementi rimanenti sono gli argomenti dello script.
# 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']
I dizionari non sono ordinati
Ci si potrebbe aspettare che un dizionario Python sia ordinato per chiavi come, per esempio, una std::map
C ++, ma questo non è il caso:
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 non ha alcuna classe built-in che ordina automaticamente i suoi elementi per chiave.
Tuttavia, se l'ordinamento non è obbligatorio e vuoi che il tuo dizionario ricordi l'ordine di inserimento delle sue coppie chiave / valore, puoi utilizzare collections.OrderedDict
:
from collections import OrderedDict
oDict = OrderedDict([('first', 1), ('second', 2), ('third', 3)])
print([k for k in oDict])
# Out: ['first', 'second', 'third']
Tieni presente che l'inizializzazione di OrderedDict
con un dizionario standard non OrderedDict
in alcun modo il dizionario per te. Tutto ciò che questa struttura fa è preservare l'ordine di inserimento della chiave.
L'implementazione dei dizionari è stata modificata in Python 3.6 per migliorare il consumo di memoria. Un effetto collaterale di questa nuova implementazione è che mantiene anche l'ordine degli argomenti delle parole chiave passati a una funzione:
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
Avvertenza : fai attenzione che " l'aspetto di conservazione di questa nuova implementazione è considerato un dettaglio di implementazione e non dovrebbe essere invocato " , poiché potrebbe cambiare in futuro.
Global Interpreter Lock (GIL) e thread di blocco
Molto è stato scritto su GIL di Python . A volte può causare confusione quando si tratta di applicazioni multi-thread (da non confondere con multiprocesso).
Ecco un esempio:
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")
Ci si aspetterebbe di vedere Calculating...
stampato immediatamente dopo l'avvio del thread, volevamo che il calcolo si verificasse in un nuovo thread dopo tutto! Ma in realtà, vedi che viene stampato dopo il calcolo è completo. Questo perché il nuovo thread si basa su una funzione C ( math.factorial
) che bloccherà GIL durante l'esecuzione.
Ci sono un paio di modi per aggirare questo. Il primo è implementare la funzione fattoriale in Python nativo. Ciò consentirà al thread principale di prendere il controllo mentre sei all'interno del tuo loop. Il rovescio della medaglia è che questa soluzione sarà molto più lenta, dal momento che non usiamo più la funzione 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
Puoi anche sleep
per un periodo di tempo prima di iniziare l'esecuzione. Nota: questo non permetterà al tuo programma di interrompere il calcolo che avviene all'interno della funzione C, ma permetterà al tuo thread principale di continuare dopo lo spawn, che è ciò che potresti aspettarti.
def calc_fact(num):
sleep(0.001)
math.factorial(num)
Perdita variabile nella comprensione delle liste e nei cicli
Considera la seguente lista di comprensione
i = 0
a = [i for i in range(3)]
print(i) # Outputs 2
Ciò si verifica solo in Python 2 a causa del fatto che la comprensione della lista "perde" la variabile di controllo del ciclo nello scope circostante ( source ). Questo comportamento può portare a bug difficili da trovare ed è stato corretto in Python 3 .
i = 0
a = [i for i in range(3)]
print(i) # Outputs 0
Allo stesso modo, i cicli for non hanno scope privato per la loro variabile di iterazione
i = 0
for i in range(3):
pass
print(i) # Outputs 2
Questo tipo di comportamento si verifica sia in Python 2 che in Python 3.
Per evitare problemi con variabili che perdono, utilizzare nuove variabili nelle list comprehensions e nei loop appropriati.
Ritorno multiplo
La funzione xyz restituisce due valori aeb:
def xyz():
return a, b
Il codice che chiama xyz memorizza il risultato in una variabile assumendo che xyz restituisca un solo valore:
t = xyz()
Il valore di t
è in realtà una tupla (a, b) quindi qualsiasi azione su t
assumendo che non si tratti di una tupla potrebbe fallire nel profondo del codice con un errore imprevisto sulle tuple.
TypeError: type tuple does not define ... metodo
La soluzione sarebbe quella di fare:
a, b = xyz()
I principianti avranno difficoltà a trovare la ragione di questo messaggio leggendo solo il messaggio di errore tupla!
Chiavi Python JSON
my_var = 'bla';
api_key = 'key';
...lots of code here...
params = {"language": "en", my_var: api_key}
Se sei abituato a JavaScript, la valutazione variabile nei dizionari Python non sarà quella che ti aspetti che sia. Questa dichiarazione in JavaScript risulterebbe nell'oggetto params
come segue:
{
"language": "en",
"my_var": "key"
}
In Python, tuttavia, risulterebbe il seguente dizionario:
{
"language": "en",
"bla": "key"
}
my_var
viene valutato e il suo valore è usato come chiave.