Cod Python Buggy: Cele mai frecvente 10 greșeli pe care le fac dezvoltatorii Python
Publicat: 2022-03-11Despre Python
Python este un limbaj de programare de nivel înalt, interpretat, orientat pe obiecte, cu semantică dinamică. Structurile de date construite la nivel înalt, combinate cu tastarea dinamică și legarea dinamică, îl fac foarte atractiv pentru dezvoltarea rapidă a aplicațiilor, precum și pentru utilizare ca limbaj de scripting sau lipire pentru a conecta componente sau servicii existente. Python acceptă module și pachete, încurajând astfel modularitatea programului și reutilizarea codului.
Despre acest articol
Sintaxa simplă și ușor de învățat a lui Python îi poate induce în eroare pe dezvoltatorii Python – în special pe cei care sunt mai nou în limbaj – să rateze unele dintre subtilitățile sale și să subestimeze puterea diversității limbajului Python.
Având în vedere acest lucru, acest articol prezintă o listă de „top 10” cu greșeli oarecum subtile, mai greu de surprins, care îi pot afecta chiar și pe unii dezvoltatori Python mai avansați în spate.
(Notă: acest articol este destinat unui public mai avansat decât Common Mistakes of Python Programers, care este orientat mai mult către cei care sunt mai nou în limbaj.)
Greșeala comună #1: Folosirea greșită a expresiilor ca valori implicite pentru argumentele funcției
Python vă permite să specificați că un argument de funcție este opțional , oferind o valoare implicită pentru acesta. Deși aceasta este o caracteristică excelentă a limbii, poate duce la o anumită confuzie atunci când valoarea implicită este modificabilă . De exemplu, luați în considerare această definiție a funcției 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
O greșeală comună este de a crede că argumentul opțional va fi setat la expresia implicită specificată de fiecare dată când funcția este apelată fără a furniza o valoare pentru argumentul opțional. În codul de mai sus, de exemplu, ne-am putea aștepta ca apelarea foo()
în mod repetat (adică, fără a specifica un argument bar
) ar returna întotdeauna 'baz'
, deoarece presupunerea ar fi că de fiecare dată când este apelat foo()
(fără o bar
). argument specificat) bar
este setată la []
(adică, o nouă listă goală).
Dar să ne uităm la ce se întâmplă de fapt când faci asta:
>>> foo() ["baz"] >>> foo() ["baz", "baz"] >>> foo() ["baz", "baz", "baz"]
huh? De ce a continuat să atașeze valoarea implicită a "baz"
la o listă existentă de fiecare dată când a fost apelat foo()
în loc să creeze o listă nouă de fiecare dată?
Răspunsul de programare Python mai avansat este că valoarea implicită pentru un argument al funcției este evaluată o singură dată, în momentul în care funcția este definită. Astfel, argumentul bar
este inițializat la implicit (adică, o listă goală) numai atunci când foo()
este definit mai întâi, dar apoi apelurile la foo()
(adică, fără un argument bar
specificat) vor continua să folosească aceeași listă pentru care bar
a fost inițial inițial.
FYI, o soluție comună pentru aceasta este următoarea:
>>> def foo(bar=None): ... if bar is None: # or if not bar: ... bar = [] ... bar.append("baz") ... return bar ... >>> foo() ["baz"] >>> foo() ["baz"] >>> foo() ["baz"]
Greșeala comună #2: Folosirea incorect a variabilelor de clasă
Luați în considerare următorul exemplu:
>>> class A(object): ... x = 1 ... >>> class B(A): ... pass ... >>> class C(A): ... pass ... >>> print Ax, Bx, Cx 1 1 1
Are sens.
>>> Bx = 2 >>> print Ax, Bx, Cx 1 2 1
Da, din nou așa cum era de așteptat.
>>> Ax = 3 >>> print Ax, Bx, Cx 3 2 3
Ce $%#!& ?? Am schimbat doar Ax
. De ce s-a schimbat și Cx
?
În Python, variabilele de clasă sunt gestionate intern ca dicționare și urmează ceea ce este adesea denumit ordine de rezoluție a metodei (MRO). Deci, în codul de mai sus, deoarece atributul x
nu se găsește în clasa C
, va fi căutat în clasele sale de bază (doar A
în exemplul de mai sus, deși Python acceptă moșteniri multiple). Cu alte cuvinte, C
nu are propria sa proprietate x
, independent de A
. Astfel, referirile la Cx
sunt de fapt referiri la Ax
. Acest lucru cauzează o problemă cu Python, dacă nu este tratată corect. Aflați mai multe despre atributele clasei în Python.
Greșeala comună #3: specificarea incorectă a parametrilor pentru un bloc de excepție
Să presupunem că aveți următorul cod:
>>> 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
Problema aici este că instrucțiunea except
nu preia o listă de excepții specificate în acest mod. Mai degrabă, în Python 2.x, sintaxa except Exception, e
este folosită pentru a lega excepția la al doilea parametru opțional specificat (în acest caz e
), pentru a-l face disponibil pentru inspecție ulterioară. Ca rezultat, în codul de mai sus, excepția IndexError
nu este capturată de instrucțiunea except
; mai degrabă, excepția ajunge să fie legată de un parametru numit IndexError
.
Modul corect de a captura mai multe excepții într-o instrucțiune except
este de a specifica primul parametru ca un tuplu care conține toate excepțiile care trebuie capturate. De asemenea, pentru portabilitate maximă, utilizați cuvântul cheie as
, deoarece acea sintaxă este acceptată atât de Python 2, cât și de Python 3:
>>> try: ... l = ["a", "b"] ... int(l[2]) ... except (ValueError, IndexError) as e: ... pass ... >>>
Greșeala comună #4: Înțelegerea greșită a regulilor de aplicare Python
Rezoluția domeniului Python se bazează pe ceea ce este cunoscut sub denumirea de regula LEGB , care este prescurtarea pentru Local, E nclosing, G lobal , Built-in. Pare destul de simplu, nu? Ei bine, de fapt, există câteva subtilități în modul în care funcționează acest lucru în Python, ceea ce ne duce la problema comună mai avansată de programare Python de mai jos. Luați în considerare următoarele:
>>> 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
Care este problema?
Eroarea de mai sus apare deoarece, atunci când faceți o atribuire unei variabile dintr-un domeniu, acea variabilă este considerată automat de către Python ca fiind locală în acel domeniu și umbră orice variabilă cu nume similar în orice domeniu exterior.
Mulți sunt, prin urmare, surprinși să primească un UnboundLocalError
în codul care funcționează anterior atunci când este modificat prin adăugarea unei instrucțiuni de atribuire undeva în corpul unei funcții. (Puteți citi mai multe despre asta aici.)
Este deosebit de obișnuit ca acest lucru să împiedice dezvoltatorii atunci când folosesc liste. Luați în considerare următorul exemplu:
>>> 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
huh? De ce a bombardat foo2
în timp ce foo1
a funcționat bine?
Răspunsul este același ca în problema exemplului anterior, dar este, desigur, mai subtil. foo1
nu face o atribuire la lst
, în timp ce foo2
este. Reținând că lst += [5]
este de fapt doar prescurtarea pentru lst = lst + [5]
, vedem că încercăm să atribuim o valoare lui lst
(prin urmare, presupusă de Python ca fiind în domeniul local). Cu toate acestea, valoarea pe care căutăm să o atribuim lui lst
se bazează pe lst
însuși (din nou, acum se presupune că se află în domeniul local), care nu a fost încă definit. Bum.
Greșeala comună #5: Modificarea unei liste în timp ce o iterați
Problema cu următorul cod ar trebui să fie destul de evidentă:
>>> 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
Ștergerea unui element dintr-o listă sau dintr-o matrice în timp ce se repetă peste el este o problemă Python care este binecunoscută oricărui dezvoltator de software cu experiență. Dar, în timp ce exemplul de mai sus poate fi destul de evident, chiar și dezvoltatorii avansați pot fi mușcați neintenționat de acest lucru într-un cod care este mult mai complex.
Din fericire, Python încorporează o serie de paradigme de programare elegante care, atunci când sunt utilizate în mod corespunzător, pot duce la un cod semnificativ simplificat și simplificat. Un beneficiu secundar al acestui lucru este că codul mai simplu este mai puțin probabil să fie mușcat de eroarea de ștergere accidentală a unui element din listă în timp ce se repetă. O astfel de paradigmă este cea a înțelegerii listelor. Mai mult, listele de înțelegere sunt deosebit de utile pentru a evita această problemă specifică, așa cum arată această implementare alternativă a codului de mai sus, care funcționează perfect:

>>> 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]
Greșeala obișnuită #6: Confuză modul în care Python leagă variabilele în închideri
Luând în considerare următorul exemplu:
>>> def create_multipliers(): ... return [lambda x : i * x for i in range(5)] >>> for multiplier in create_multipliers(): ... print multiplier(2) ...
S-ar putea să vă așteptați la următoarea ieșire:
0 2 4 6 8
Dar de fapt primești:
8 8 8 8 8
Surprinde!
Acest lucru se întâmplă din cauza comportamentului de legare tardiv al lui Python, care spune că valorile variabilelor utilizate în închideri sunt căutate în momentul în care funcția internă este apelată. Deci, în codul de mai sus, ori de câte ori oricare dintre funcțiile returnate este apelată, valoarea lui i
este căutată în domeniul înconjurător în momentul în care este apelată (și până atunci, bucla s-a finalizat, așa că i
-a fost deja atribuită finalul său valoarea 4).
Soluția la această problemă comună Python este un pic de hack:
>>> 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
Voila! Profităm de argumentele implicite aici pentru a genera funcții anonime pentru a obține comportamentul dorit. Unii ar numi asta elegant. Unii l-ar numi subtil. Unii îl urăsc. Dar dacă ești un dezvoltator Python, este important să înțelegi în orice caz.
Greșeala comună #7: Crearea dependențelor modulelor circulare
Să presupunem că aveți două fișiere, a.py
și b.py
, fiecare dintre ele importându-l pe celălalt, după cum urmează:
În a.py
:
import b def f(): return bx print f()
Și în b.py
:
import a x = 1 def g(): print af()
Mai întâi, să încercăm să importăm a.py
:
>>> import a 1
A funcționat bine. Poate asta te surprinde. La urma urmei, avem aici un import circular care ar trebui să fie o problemă, nu-i așa?
Răspunsul este că simpla prezență a unui import circular nu este în sine o problemă în Python. Dacă un modul a fost deja importat, Python este suficient de inteligent pentru a nu încerca să-l reimporteze. Cu toate acestea, în funcție de punctul în care fiecare modul încearcă să acceseze funcțiile sau variabilele definite în celălalt, este posibil să întâmpinați într-adevăr probleme.
Deci, revenind la exemplul nostru, când am importat a.py
, nu a avut nicio problemă la importul b.py
, deoarece b.py
nu necesită ca nimic de la a.py
să fie definit în momentul în care este importat . Singura referință în b.py
la a
este apelul la af()
. Dar apelul este în g()
și nimic din a.py
sau b.py
invocă g()
. Deci viața este bună.
Dar ce se întâmplă dacă încercăm să importăm b.py
(fără a fi importat anterior a.py
, adică):
>>> 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. Asta nu e bine! Problema aici este că, în procesul de import a b.py
, încearcă să importe a.py
, care la rândul său apelează f()
, care încearcă să acceseze bx
. Dar bx
nu a fost încă definit. De aici excepția AttributeError
.
Cel puțin o soluție la acest lucru este destul de banală. Pur și simplu modificați b.py
pentru a importa a.py
în g()
:
x = 1 def g(): import a # This will be evaluated only when g() is called print af()
Nu, când îl importăm, totul este bine:
>>> 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'
Greșeala comună #8: Numele se ciocnește cu modulele Python Standard Library
Una dintre frumusețile lui Python este bogăția de module de bibliotecă cu care vine „din cutie”. Dar, ca rezultat, dacă nu o eviți în mod conștient, nu este atât de dificil să te confrunți cu o ciocnire de nume între numele unuia dintre modulele tale și un modul cu același nume din biblioteca standard care este livrat cu Python (de exemplu , este posibil să aveți un modul numit email.py
în codul dvs., care ar fi în conflict cu modulul bibliotecii standard cu același nume).
Acest lucru poate duce la probleme noduroase, cum ar fi importarea unei alte biblioteci care, la rândul său, încearcă să importe versiunea Python Standard Library a unui modul, dar, deoarece aveți un modul cu același nume, celălalt pachet importă din greșeală versiunea dvs. în loc de cea din interior. biblioteca standard Python. Aici apar erorile Python proaste.
Prin urmare, trebuie avut grijă să evitați utilizarea acelorași nume ca cele din modulele Python Standard Library. Este mult mai ușor pentru dvs. să schimbați numele unui modul din pachetul dvs. decât să depuneți o propunere de îmbunătățire a Python (PEP) pentru a solicita o schimbare a numelui în amonte și pentru a încerca să obțineți aprobarea.
Greșeala comună #9: Eșecul în abordarea diferențelor dintre Python 2 și Python 3
Luați în considerare următorul fișier 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()
Pe Python 2, aceasta funcționează bine:
$ python foo.py 1 key error 1 $ python foo.py 2 value error 2
Dar acum haideți să facem un învârtire pe 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
Ce tocmai sa întâmplat aici? „Problema” este că, în Python 3, obiectul excepție nu este accesibil dincolo de domeniul de aplicare al blocului except
. (Motivul pentru aceasta este că, în caz contrar, ar păstra un ciclu de referință cu cadrul stivei în memorie până când colectorul de gunoi rulează și șterge referințele din memorie. Mai multe detalii tehnice despre acest lucru sunt disponibile aici).
O modalitate de a evita această problemă este menținerea unei referințe la obiectul excepție în afara domeniului de aplicare al blocului except
, astfel încât acesta să rămână accesibil. Iată o versiune a exemplului anterior care utilizează această tehnică, obținând astfel cod care este atât pentru Python 2, cât și pentru 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()
Rulând acest lucru pe Py3k:
$ python3 foo.py 1 key error 1 $ python3 foo.py 2 value error 2
Vai!
(De altfel, Ghidul nostru de angajare Python discută o serie de alte diferențe importante de care trebuie să fii conștient la migrarea codului de la Python 2 la Python 3.)
Greșeala comună #10: Folosirea greșită a metodei __del__
Să presupunem că ai avut asta într-un fișier numit mod.py
:
import foo class Bar(object): ... def __del__(self): foo.cleanup(self.myhandle)
Și apoi ai încercat să faci asta de pe another_mod.py
:
import mod mybar = mod.Bar()
Veți obține o excepție urâtă AttributeError
.
De ce? Deoarece, așa cum este raportat aici, atunci când interpretul se închide, variabilele globale ale modulului sunt toate setate la None
. Ca rezultat, în exemplul de mai sus, în momentul în care __del__
este invocat, numele foo
a fost deja setat la None
.
O soluție la această problemă de programare Python oarecum mai avansată ar fi folosirea atexit.register()
în schimb. În acest fel, când programul dvs. se termină de execuție (adică când ieșiți în mod normal), handlerele dvs. înregistrate sunt dezactivate înainte ca interpretul să fie oprit.
Cu această înțelegere, o remediere pentru codul mod.py
de mai sus ar putea arăta cam așa:
import foo import atexit def cleanup(handle): foo.cleanup(handle) class Bar(object): def __init__(self): ... atexit.register(cleanup, self.myhandle)
Această implementare oferă o modalitate curată și fiabilă de a apela orice funcționalitate de curățare necesară la terminarea normală a programului. Evident, depinde de foo.cleanup
să decidă ce să facă cu obiectul legat de numele self.myhandle
, dar înțelegi ideea.
Învelire
Python este un limbaj puternic și flexibil, cu multe mecanisme și paradigme care pot îmbunătăți considerabil productivitatea. Ca și în cazul oricărui instrument sau limbaj software, totuși, a avea o înțelegere sau o apreciere limitată a capacităților sale poate fi uneori mai mult un impediment decât un beneficiu, lăsându-l în starea proverbială de „a ști suficient pentru a fi periculos”.
Familiarizarea cu nuanțele cheie ale Python, cum ar fi (dar nu limitat la) problemele de programare moderat avansate ridicate în acest articol, va ajuta la optimizarea utilizării limbajului evitând în același timp unele dintre erorile sale mai frecvente.
De asemenea, vă recomandăm să consultați Ghidul nostru pentru interviuri Python pentru sugestii cu privire la întrebările de interviu care pot ajuta la identificarea experților Python.
Sperăm că ați găsit indicațiile din acest articol utile și bineveniți feedback-ul dvs.