Codice buggy Python: i 10 errori più comuni commessi dagli sviluppatori Python

Pubblicato: 2022-03-11

A proposito di Python

Python è un linguaggio di programmazione interpretato, orientato agli oggetti e di alto livello con semantica dinamica. Le sue strutture di dati integrate di alto livello, combinate con la tipizzazione dinamica e l'associazione dinamica, lo rendono molto interessante per lo sviluppo rapido di applicazioni, nonché per l'uso come linguaggio di scripting o colla per connettere componenti o servizi esistenti. Python supporta moduli e pacchetti, incoraggiando così la modularità del programma e il riutilizzo del codice.

A proposito di questo articolo

La sintassi semplice e facile da imparare di Python può indurre in errore gli sviluppatori Python, specialmente quelli che sono nuovi al linguaggio, facendogli perdere alcune delle sue sottigliezze e sottovalutando il potere del diverso linguaggio Python.

Con questo in mente, questo articolo presenta un elenco della "top 10" di errori alquanto sottili e più difficili da cogliere che possono mordere anche alcuni sviluppatori Python più avanzati nella parte posteriore.

(Nota: questo articolo è destinato a un pubblico più avanzato rispetto agli errori comuni dei programmatori Python, che è più rivolto a coloro che sono nuovi al linguaggio.)

Errore comune n. 1: uso improprio delle espressioni come valori predefiniti per gli argomenti delle funzioni

Python consente di specificare che un argomento di funzione è facoltativo fornendo un valore predefinito per esso. Sebbene questa sia un'ottima caratteristica del linguaggio, può creare confusione quando il valore predefinito è mutevole . Ad esempio, considera questa definizione di funzione Python:

 >>> def foo(bar=[]): # bar is optional and defaults to [] if not specified ... bar.append("baz") # but this line could be problematic, as we'll see... ... return bar

Un errore comune è pensare che l'argomento facoltativo verrà impostato sull'espressione predefinita specificata ogni volta che la funzione viene chiamata senza fornire un valore per l'argomento facoltativo. Nel codice sopra, ad esempio, ci si potrebbe aspettare che la chiamata ripetuta di foo() (cioè senza specificare un argomento bar ) restituisca sempre 'baz' , poiché l'assunto sarebbe che ogni volta che foo() viene chiamato (senza una bar argomento specificato) la bar è impostata su [] (ovvero, un nuovo elenco vuoto).

Ma diamo un'occhiata a cosa succede effettivamente quando lo fai:

 >>> foo() ["baz"] >>> foo() ["baz", "baz"] >>> foo() ["baz", "baz", "baz"]

Eh? Perché continuava ad aggiungere il valore predefinito di "baz" a un elenco esistente ogni volta che veniva chiamato foo() , invece di creare un nuovo elenco ogni volta?

La risposta di programmazione Python più avanzata è che il valore predefinito per un argomento di funzione viene valutato solo una volta, nel momento in cui la funzione viene definita. Pertanto, l'argomento bar viene inizializzato al suo valore predefinito (cioè, un elenco vuoto) solo quando foo() viene prima definito, ma poi le chiamate a foo() (cioè, senza un argomento bar specificato) continueranno a utilizzare lo stesso elenco per quale bar è stata inizialmente inizializzata.

Cordiali saluti, una soluzione alternativa comune per questo è la seguente:

 >>> def foo(bar=None): ... if bar is None: # or if not bar: ... bar = [] ... bar.append("baz") ... return bar ... >>> foo() ["baz"] >>> foo() ["baz"] >>> foo() ["baz"]

Errore comune n. 2: utilizzo errato delle variabili di classe

Considera il seguente esempio:

 >>> class A(object): ... x = 1 ... >>> class B(A): ... pass ... >>> class C(A): ... pass ... >>> print Ax, Bx, Cx 1 1 1

Ha senso.

 >>> Bx = 2 >>> print Ax, Bx, Cx 1 2 1

Sì, ancora una volta come previsto.

 >>> Ax = 3 >>> print Ax, Bx, Cx 3 2 3

Che cosa $%#!& ?? Abbiamo solo cambiato Ax . Perché anche Cx è cambiato?

In Python, le variabili di classe sono gestite internamente come dizionari e seguono ciò che viene spesso definito Method Resolution Order (MRO). Quindi nel codice sopra, poiché l'attributo x non si trova nella classe C , verrà cercato nelle sue classi di base (solo A nell'esempio sopra, sebbene Python supporti eredità multiple). In altre parole, C non ha la sua proprietà x , indipendente da A . Pertanto, i riferimenti a Cx sono in effetti riferimenti ad Ax . Ciò causa un problema con Python a meno che non venga gestito correttamente. Ulteriori informazioni sugli attributi di classe in Python.

Errore comune n. 3: specificare parametri in modo errato per un blocco di eccezioni

Supponiamo di avere il seguente codice:

 >>> try: ... l = ["a", "b"] ... int(l[2]) ... except ValueError, IndexError: # To catch both exceptions, right? ... pass ... Traceback (most recent call last): File "<stdin>", line 3, in <module> IndexError: list index out of range

Il problema qui è che l'istruzione exclude non accetta un elenco di except specificate in questo modo. Piuttosto, in Python 2.x, la sintassi except Exception, e viene utilizzata per associare l'eccezione al secondo parametro opzionale specificato (in questo caso e ), in modo da renderlo disponibile per ulteriori ispezioni. Di conseguenza, nel codice precedente, l' except IndexError non viene intercettata dall'istruzione exclude; piuttosto, l'eccezione finisce invece per essere associata a un parametro denominato IndexError .

Il modo corretto per intercettare più except in un'istruzione exclude è specificare il primo parametro come una tupla contenente tutte le eccezioni da intercettare. Inoltre, per la massima portabilità, usa la parola chiave as , poiché tale sintassi è supportata sia da Python 2 che da Python 3:

 >>> try: ... l = ["a", "b"] ... int(l[2]) ... except (ValueError, IndexError) as e: ... pass ... >>>

Errore comune n. 4: incomprensione delle regole dell'ambito di Python

La risoluzione dell'ambito Python si basa su quella che è nota come regola LEGB, che è l'abbreviazione di Local, Enclosing , G lobal , B uilt-in. Sembra abbastanza semplice, giusto? Bene, in realtà, ci sono alcune sottigliezze nel modo in cui funziona in Python, il che ci porta al problema di programmazione Python più avanzato comune di seguito. Considera quanto segue:

 >>> x = 10 >>> def foo(): ... x += 1 ... print x ... >>> foo() Traceback (most recent call last): File "<stdin>", line 1, in <module> File "<stdin>", line 2, in foo UnboundLocalError: local variable 'x' referenced before assignment

Qual è il problema?

L'errore precedente si verifica perché, quando si esegue un'assegnazione a una variabile in un ambito, quella variabile viene automaticamente considerata da Python come locale a tale ambito e oscura qualsiasi variabile con nome simile in qualsiasi ambito esterno.

Molti sono quindi sorpresi di ottenere un UnboundLocalError nel codice precedentemente funzionante quando viene modificato aggiungendo un'istruzione di assegnazione da qualche parte nel corpo di una funzione. (Puoi leggere di più su questo qui.)

È particolarmente comune che ciò faccia inciampare gli sviluppatori quando si utilizzano gli elenchi. Considera il seguente esempio:

 >>> lst = [1, 2, 3] >>> def foo1(): ... lst.append(5) # This works ok... ... >>> foo1() >>> lst [1, 2, 3, 5] >>> lst = [1, 2, 3] >>> def foo2(): ... lst += [5] # ... but this bombs! ... >>> foo2() Traceback (most recent call last): File "<stdin>", line 1, in <module> File "<stdin>", line 2, in foo UnboundLocalError: local variable 'lst' referenced before assignment

Eh? Perché foo2 bomba mentre foo1 funzionava bene?

La risposta è la stessa del problema dell'esempio precedente, ma è certamente più sottile. foo1 non sta effettuando un'assegnazione a lst , mentre foo2 lo è. Ricordando che lst += [5] è in realtà solo una scorciatoia per lst = lst + [5] , vediamo che stiamo tentando di assegnare un valore a lst (quindi presunto da Python nell'ambito locale). Tuttavia, il valore che stiamo cercando di assegnare a lst è basato su lst stesso (di nuovo, ora si presume che sia nell'ambito locale), che non è stato ancora definito. Boom.

Errore comune n. 5: modificare un elenco durante l'iterazione su di esso

Il problema con il codice seguente dovrebbe essere abbastanza ovvio:

 >>> odd = lambda x : bool(x % 2) >>> numbers = [n for n in range(10)] >>> for i in range(len(numbers)): ... if odd(numbers[i]): ... del numbers[i] # BAD: Deleting item from a list while iterating over it ... Traceback (most recent call last): File "<stdin>", line 2, in <module> IndexError: list index out of range

L'eliminazione di un elemento da un elenco o da un array durante l'iterazione su di esso è un problema Python ben noto a qualsiasi sviluppatore di software esperto. Ma mentre l'esempio sopra può essere abbastanza ovvio, anche gli sviluppatori avanzati possono essere colpiti involontariamente da questo nel codice che è molto più complesso.

Fortunatamente, Python incorpora una serie di eleganti paradigmi di programmazione che, se usati correttamente, possono risultare in un codice notevolmente semplificato e snello. Un vantaggio collaterale di questo è che è meno probabile che il codice più semplice venga morso dall'eliminazione accidentale di un elemento dell'elenco durante l'iterazione su di esso. Uno di questi paradigmi è quello della comprensione delle liste. Inoltre, le comprensioni degli elenchi sono particolarmente utili per evitare questo problema specifico, come mostrato da questa implementazione alternativa del codice precedente che funziona perfettamente:

 >>> odd = lambda x : bool(x % 2) >>> numbers = [n for n in range(10)] >>> numbers[:] = [n for n in numbers if not odd(n)] # ahh, the beauty of it all >>> numbers [0, 2, 4, 6, 8]

Errore comune n. 6: confusione su come Python leghi le variabili nelle chiusure

Considerando il seguente esempio:

 >>> def create_multipliers(): ... return [lambda x : i * x for i in range(5)] >>> for multiplier in create_multipliers(): ... print multiplier(2) ...

Potresti aspettarti il ​​seguente output:

 0 2 4 6 8

Ma in realtà ottieni:

 8 8 8 8 8

Sorpresa!

Ciò accade a causa del comportamento di associazione tardiva di Python che afferma che i valori delle variabili utilizzate nelle chiusure vengono cercati nel momento in cui viene chiamata la funzione interna. Quindi nel codice sopra, ogni volta che viene chiamata una qualsiasi delle funzioni restituite, il valore di i viene cercato nell'ambito circostante nel momento in cui viene chiamato (e a quel punto il ciclo è completato, quindi i è già stato assegnato il suo finale valore di 4).

La soluzione a questo problema comune di Python è un po' un trucco:

 >>> def create_multipliers(): ... return [lambda x, i=i : i * x for i in range(5)] ... >>> for multiplier in create_multipliers(): ... print multiplier(2) ... 0 2 4 6 8

Ecco! Stiamo sfruttando gli argomenti predefiniti qui per generare funzioni anonime al fine di ottenere il comportamento desiderato. Alcuni lo chiamerebbero elegante. Alcuni lo chiamerebbero sottile. Alcuni lo odiano. Ma se sei uno sviluppatore Python, è importante capirlo in ogni caso.

Errore comune n. 7: creazione di dipendenze di moduli circolari

Diciamo che hai due file, a.py e b.py , ognuno dei quali importa l'altro, come segue:

In a.py :

 import b def f(): return bx print f()

E in b.py :

 import a x = 1 def g(): print af()

Per prima cosa, proviamo a importare a.py :

 >>> import a 1

Ha funzionato bene. Forse questo ti sorprende. Dopotutto, qui abbiamo un'importazione circolare che presumibilmente dovrebbe essere un problema, no?

La risposta è che la semplice presenza di un'importazione circolare non è di per sé un problema in Python. Se un modulo è già stato importato, Python è abbastanza intelligente da non tentare di reimportarlo. Tuttavia, a seconda del punto in cui ogni modulo sta tentando di accedere a funzioni o variabili definite nell'altro, potresti effettivamente incorrere in problemi.

Quindi, tornando al nostro esempio, quando abbiamo importato a.py , non ha avuto problemi a importare b.py , poiché b.py non richiede la definizione di nulla da a.py al momento dell'importazione . L'unico riferimento in b.py ad a è la chiamata a af() . Ma quella chiamata è in g() e niente in a.py o b.py invoca g() . Quindi la vita è bella.

Ma cosa succede se proviamo a importare b.py (senza aver precedentemente importato a.py , cioè):

 >>> import b Traceback (most recent call last): File "<stdin>", line 1, in <module> File "b.py", line 1, in <module> import a File "a.py", line 6, in <module> print f() File "a.py", line 4, in f return bx AttributeError: 'module' object has no attribute 'x'

Uh Oh. Questo non è buono! Il problema qui è che, nel processo di importazione b.py , tenta di importare a.py , che a sua volta chiama f() , che tenta di accedere a bx . Ma bx non è stato ancora definito. Da qui l'eccezione AttributeError .

Almeno una soluzione a questo è abbastanza banale. Modifica semplicemente b.py per importare a.py all'interno di g() :

 x = 1 def g(): import a # This will be evaluated only when g() is called print af()

No quando lo importiamo, va tutto bene:

 >>> import b >>> bg() 1 # Printed a first time since module 'a' calls 'print f()' at the end 1 # Printed a second time, this one is our call to 'g'

Errore comune n. 8: conflitto di nomi con i moduli della libreria standard Python

Una delle bellezze di Python è la ricchezza di moduli di libreria che viene fornito con "out of the box". Ma di conseguenza, se non lo stai evitando consapevolmente, non è così difficile imbattersi in uno scontro di nomi tra il nome di uno dei tuoi moduli e un modulo con lo stesso nome nella libreria standard fornita con Python (ad esempio , potresti avere un modulo chiamato email.py nel tuo codice, che sarebbe in conflitto con il modulo della libreria standard con lo stesso nome).

Questo può portare a grossi problemi, come importare un'altra libreria che a sua volta tenta di importare la versione della libreria standard Python di un modulo ma, poiché hai un modulo con lo stesso nome, l'altro pacchetto importa erroneamente la tua versione invece di quella all'interno la libreria standard Python. È qui che si verificano errori Python negativi.

Pertanto, è necessario prestare attenzione per evitare di utilizzare gli stessi nomi di quelli nei moduli Python Standard Library. È molto più facile per te cambiare il nome di un modulo all'interno del tuo pacchetto che presentare una proposta di miglioramento Python (PEP) per richiedere una modifica del nome a monte e provare a farlo approvare.

Errore comune n. 9: non riuscire a risolvere le differenze tra Python 2 e Python 3

Considera il seguente file foo.py :

 import sys def bar(i): if i == 1: raise KeyError(1) if i == 2: raise ValueError(2) def bad(): e = None try: bar(int(sys.argv[1])) except KeyError as e: print('key error') except ValueError as e: print('value error') print(e) bad()

Su Python 2, questo funziona bene:

 $ python foo.py 1 key error 1 $ python foo.py 2 value error 2

Ma ora diamoci un vortice su Python 3:

 $ python3 foo.py 1 key error Traceback (most recent call last): File "foo.py", line 19, in <module> bad() File "foo.py", line 17, in bad print(e) UnboundLocalError: local variable 'e' referenced before assignment

Cos'è appena successo qui? Il "problema" è che, in Python 3, l'oggetto except non è accessibile oltre l'ambito del blocco exclude. (La ragione di ciò è che, altrimenti, manterrebbe un ciclo di riferimento con lo stack frame in memoria fino a quando il Garbage Collector non esegue ed elimina i riferimenti dalla memoria. Maggiori dettagli tecnici su questo sono disponibili qui).

Un modo per evitare questo problema consiste nel mantenere un riferimento all'oggetto except al di fuori dell'ambito del blocco exclude in modo che rimanga accessibile. Ecco una versione dell'esempio precedente che utilizza questa tecnica, producendo così un codice compatibile sia con Python 2 che con Python 3:

 import sys def bar(i): if i == 1: raise KeyError(1) if i == 2: raise ValueError(2) def good(): exception = None try: bar(int(sys.argv[1])) except KeyError as e: exception = e print('key error') except ValueError as e: exception = e print('value error') print(exception) good()

Eseguendo questo su Py3k:

 $ python3 foo.py 1 key error 1 $ python3 foo.py 2 value error 2

Yippee!

(Per inciso, la nostra Guida all'assunzione di Python discute una serie di altre importanti differenze di cui tenere conto durante la migrazione del codice da Python 2 a Python 3.)

Errore comune n. 10: uso improprio del metodo __del__

Diciamo che lo avevi in ​​un file chiamato mod.py :

 import foo class Bar(object): ... def __del__(self): foo.cleanup(self.myhandle)

E poi hai provato a farlo da un another_mod.py :

 import mod mybar = mod.Bar()

Otterresti una brutta eccezione AttributeError .

Come mai? Perché, come riportato qui, quando l'interprete si spegne, le variabili globali del modulo sono tutte impostate su None . Di conseguenza, nell'esempio precedente, nel momento in cui viene invocato __del__ , il nome foo è già stato impostato su None .

Una soluzione a questo problema di programmazione Python un po' più avanzato sarebbe usare invece atexit.register() . In questo modo, quando il tuo programma ha terminato l'esecuzione (quando esce normalmente, cioè), i tuoi gestori registrati vengono avviati prima che l'interprete venga spento.

Con questa comprensione, una correzione per il codice mod.py sopra potrebbe quindi assomigliare a questo:

 import foo import atexit def cleanup(handle): foo.cleanup(handle) class Bar(object): def __init__(self): ... atexit.register(cleanup, self.myhandle)

Questa implementazione fornisce un modo pulito e affidabile per chiamare qualsiasi funzionalità di pulizia necessaria alla normale terminazione del programma. Ovviamente spetta a foo.cleanup decidere cosa fare con l'oggetto legato al nome self.myhandle , ma tu hai l'idea.

Incartare

Python è un linguaggio potente e flessibile con molti meccanismi e paradigmi che possono migliorare notevolmente la produttività. Come con qualsiasi strumento o linguaggio software, tuttavia, avere una comprensione o un apprezzamento limitati delle sue capacità a volte può essere più un impedimento che un vantaggio, lasciando uno nel proverbiale stato di "sapere abbastanza per essere pericoloso".

Familiarizzare con le sfumature chiave di Python, come (ma non solo) i problemi di programmazione moderatamente avanzati sollevati in questo articolo, aiuterà a ottimizzare l'uso del linguaggio evitando alcuni dei suoi errori più comuni.

Potresti anche dare un'occhiata alla nostra Insider's Guide to Python Interviewing per suggerimenti su domande di intervista che possono aiutare a identificare gli esperti Python.

Ci auguriamo che tu abbia trovato utili i suggerimenti in questo articolo e accogliamo con favore il tuo feedback.