Python je interpretiran objektno usmerjen programski jezik na visoki ravni z dinamično semantiko. Zaradi vgrajenih podatkovnih struktur na visoki ravni v kombinaciji z dinamičnim tipkanjem in dinamičnim povezovanjem je zelo privlačna za Hiter razvoj aplikacij , kot tudi za uporabo kot skriptni ali lepilni jezik za povezovanje obstoječih komponent ali storitev. Python podpira module in pakete, s čimer spodbuja modularnost programa in ponovno uporabo kode.
Pythonova enostavna, enostavna sintaksa lahko zavede Razvijalci Pythona - še posebej tisti, ki so v jeziku novejši - ker so zamudili nekatere njegove tankočutnosti in podcenili moč jezika raznolik jezik Python .
S tem v mislih ta članek predstavlja seznam 10 najbolj subtilnih, težje ulovljivih napak, ki lahko ugriznejo še nekaj več napredni razvijalci Pythona zadaj.
(Opomba: Ta članek je namenjen naprednejšemu občinstvu kot Pogoste napake programerjev Python , ki je bolj namenjena tistim, ki so novejši od jezika.)
Python vam omogoča, da določite, da je argument funkcije neobvezno z zagotavljanjem a privzeta vrednost za to. Čeprav je to odlična lastnost jezika, lahko privzeta vrednost povzroči nekaj zmede spremenljiv . Na primer, razmislite o tej definiciji funkcije 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
Pogosta napaka je misel, da bo neobvezni argument nastavljen na določen privzeti izraz vsakič funkcija se pokliče brez navedbe vrednosti za neobvezni argument. Na primer v zgornji kodi lahko pričakujemo, da bo klic foo()
večkrat (tj. brez navedbe bar
argumenta) vedno vrne 'baz'
, saj bi bila predpostavka, da vsakič foo()
se pokliče (brez navedenega bar
argumenta) bar
je nastavljeno na []
(tj. nov prazen seznam).
Poglejmo pa, kaj se dejansko zgodi, ko to storite:
>>> foo() ['baz'] >>> foo() ['baz', 'baz'] >>> foo() ['baz', 'baz', 'baz']
Kaj? Zakaj je še naprej dodajal privzeto vrednost 'baz'
do an obstoječe seznam vsakič foo()
je bil poklican, namesto da bi ustvaril novo vsakič seznam?
Naprednejši programski odgovor na Python je tak privzeta vrednost za argument funkcije se oceni samo enkrat, in sicer v času, ko je funkcija definirana. Tako je bar
argument se inicializira na privzeti (tj. prazen seznam) samo, kadar foo()
je najprej definiran, nato pa pokliče na foo()
(tj. brez navedenega bar
argumenta) bo še naprej uporabljal isti seznam, na katerega bar
je bil prvotno inicializiran.
FYI, pogosta rešitev za to je naslednja:
>>> def foo(bar=None): ... if bar is None: # or if not bar: ... bar = [] ... bar.append('baz') ... return bar ... >>> foo() ['baz'] >>> foo() ['baz'] >>> foo() ['baz']
Upoštevajte naslednji primer:
>>> class A(object): ... x = 1 ... >>> class B(A): ... pass ... >>> class C(A): ... pass ... >>> print A.x, B.x, C.x 1 1 1
Smiselno.
>>> B.x = 2 >>> print A.x, B.x, C.x 1 2 1
Ja, spet po pričakovanjih.
kako ustvariti api v javascriptu
>>> A.x = 3 >>> print A.x, B.x, C.x 3 2 3
Kaj za $% #! & ?? Spremenili smo samo A.x
. Zakaj C.x
spremeniti tudi?
V Pythonu so spremenljivke razredov interno obdelane kot slovarji in sledijo temu, kar se pogosto imenuje Nalog za reševanje metode (MRO) . Torej v zgornji kodi, saj je atribut x
ni mogoče najti v razredu C
, iskali ga bodo v njegovih osnovnih razredih (samo A
v zgornjem primeru, čeprav Python podpira več dedovanja). Z drugimi besedami, C
nima svojega x
lastnina, neodvisna od A
. Torej, sklici na C.x
so dejansko sklici na A.x
. To povzroča težave s Pythonom, razen če se z njimi pravilno ravna. Več o tem atributi razreda v Pythonu .
Recimo, da imate naslednjo kodo:
>>> try: ... l = ['a', 'b'] ... int(l[2]) ... except ValueError, IndexError: # To catch both exceptions, right? ... pass ... Traceback (most recent call last): File '', line 3, in IndexError: list index out of range
Tukaj je težava v tem, da except
izjava ne ne vzemite seznam izjem, določenih na ta način. Namesto tega je v Pythonu 2.x sintaksa except Exception, e
se uporablja za vezavo izjeme na neobvezno drugi navedeni parameter (v tem primeru e
), da bo na voljo za nadaljnji pregled. Posledično je v zgornji kodi IndexError
izjema je ne ujeti except
izjava; Namesto tega je izjema namesto tega vezana na parameter z imenom IndexError
.
Pravilen način za ulov več izjem v except
stavek poda prvi parameter kot a tuple vsebuje vse izjeme, ki jih je treba ujeti. Za največjo prenosljivost uporabite tudi as
ključno besedo, saj to sintakso podpirata tako Python 2 kot Python 3:
>>> try: ... l = ['a', 'b'] ... int(l[2]) ... except (ValueError, IndexError) as e: ... pass ... >>>
Ločljivost obsega Python temelji na tem, kar je znano kot LEGB pravilo, ki je okrajšava za L okalno, JE zapiranje, G lobalni, B vgrajen. Zdi se dovolj preprosto, kajne? No, pravzaprav obstaja nekaj razlik v načinu delovanja v Pythonu, kar nas pripelje do pogostejše naprednejše težave s programiranjem Pythona spodaj. Upoštevajte naslednje:
>>> x = 10 >>> def foo(): ... x += 1 ... print x ... >>> foo() Traceback (most recent call last): File '', line 1, in File '', line 2, in foo UnboundLocalError: local variable 'x' referenced before assignment
V čem je problem?
Zgornja napaka se zgodi, ker ko naredite dodelitev na spremenljivko v obsegu, Python samodejno šteje, da je ta spremenljivka lokalna za ta obseg in zasenči katero koli podobno imenovano spremenljivko v katerem koli zunanjem obsegu.
Mnogi so s tem presenečeni, ko dobijo UnboundLocalError
v prej delujoči kodi, ko je spremenjena z dodajanjem stavka o dodelitvi nekje v telesu funkcije. (Več o tem lahko preberete tukaj .)
Posebej pogosto je, da pri uporabi spotakne razvijalce seznami . Upoštevajte naslednji primer:
>>> 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 '', line 1, in File '', line 2, in foo UnboundLocalError: local variable 'lst' referenced before assignment
Kaj? Zakaj foo2
bomba, medtem ko foo1
tekel v redu?
Odgovor je enak kot v prejšnjem primeru primera, vendar je res bolj subtilen. foo1
ne ustvarja dodelitev do lst
, medtem ko foo2
je. Spomin na to lst += [5]
je v resnici samo okrajšava za lst = lst + [5]
, vidimo, da poskušamo dodeliti vrednost do lst
(zato Python domneva, da je v lokalnem obsegu). Vendar vrednost, ki jo želimo dodeliti lst
temelji na lst
sama (spet zdaj domnevno lokalno), ki še ni opredeljena. Bum.
Težava z naslednjo kodo bi morala biti dokaj očitna:
>>> 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 '', line 2, in IndexError: list index out of range
Brisanje predmeta s seznama ali polja med iteracijo nad njim je težava s Pythonom, ki jo dobro pozna vsak izkušen razvijalec programske opreme. Čeprav je zgornji primer morda precej očiten, lahko to celo nenamerno ugrizne v kodi, ki je veliko bolj zapletena.
Na srečo Python vključuje številne elegantne programske paradigme, ki lahko ob pravilni uporabi znatno poenostavijo in poenostavijo kodo. Stranska prednost tega je, da je manj verjetno, da bo naključno brisanje elementa s seznama, medtem ko se bo iteriralo nad njim, ugriznilo preprostejšo kodo. Ena takih paradigem je seznami . Poleg tega so razumevanja seznamov še posebej koristna za izogibanje tej specifični težavi, kot kaže ta nadomestna izvedba zgornje kode, ki deluje popolnoma:
>>> 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]
Upoštevajoč naslednji primer:
>>> def create_multipliers(): ... return [lambda x : i * x for i in range(5)] >>> for multiplier in create_multipliers(): ... print multiplier(2) ...
Pričakujete lahko naslednje rezultate:
0 2 4 6 8
Toda dejansko dobite:
8 8 8 8 8
Presenečenje!
To se zgodi zaradi Pythona pozna vezava vedenje, ki pravi, da se vrednosti spremenljivk, uporabljenih pri zapiranju, poiščejo v času, ko je poklicana notranja funkcija. Torej, v zgornji kodi, kadar koli se pokliče katera od vrnjenih funkcij, vrednost i
je pogledal gor v okolici v času, ko se imenuje (in do takrat je zanka končana, zato je bila i
že dodeljena končna vrednost 4).
Rešitev te pogoste težave s Pythonom je malce kramp:
>>> 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
Voilà! Privzete argumente tukaj izkoriščamo za ustvarjanje anonimnih funkcij, da dosežemo želeno vedenje. Nekateri bi temu rekli elegantno. Nekateri bi temu rekli subtilno. Nekateri to sovražijo. Če pa ste razvijalec Pythona, je to v vsakem primeru pomembno razumeti.
Recimo, da imate dve datoteki, a.py
in b.py
, od katerih vsak uvozi drugega, in sicer:
V a.py
:
import b def f(): return b.x print f()
In v b.py
:
import a x = 1 def g(): print a.f()
Najprej poskusimo uvoziti a.py
:
>>> import a 1
Odlično delal. Morda vas to preseneti. Navsezadnje imamo tukaj krožni uvoz, kar bi verjetno moral predstavljati težavo, kajne?
kupna moč bo večja, ko
Odgovor je, da zgolj prisotnost krožnega uvoza sam po sebi ni težava v Pythonu. Če je bil modul že uvožen, je Python dovolj pameten, da ga ne poskuša znova uvoziti. Vendar pa lahko resnično naletite na težave, odvisno od točke, v kateri vsak modul poskuša dostopati do funkcij ali spremenljivk, opredeljenih v drugem.
Če se torej vrnemo k našemu primeru, ko smo uvozili a.py
, ni bilo težav pri uvozu b.py
, saj b.py
ne zahteva ničesar od a.py
opredeliti v času uvoza . Edina referenca v b.py
do a
je klic a.f()
. Toda ta klic je v g()
in nič v a.py
ali b.py
prikliče g()
. Življenje je torej dobro.
Kaj pa se zgodi, če poskusimo uvoziti b.py
(brez predhodnega uvoza a.py
, to je):
>>> import b Traceback (most recent call last): File '', line 1, in File 'b.py', line 1, in import a File 'a.py', line 6, in print f() File 'a.py', line 4, in f return b.x AttributeError: 'module' object has no attribute 'x'
Ojoj. To ni dobro! Tukaj je težava v tem, da med uvozom b.py
poskuša uvoziti a.py
, ki nato pokliče f()
, ki poskuša dostopati do b.x
. Toda b.x
še ni opredeljena. Zato AttributeError
izjema.
Vsaj ena rešitev tega je precej trivialna. Preprosto spremenite b.py
za uvoz a.py
znotraj g()
:
x = 1 def g(): import a # This will be evaluated only when g() is called print a.f()
Ne, ko ga uvozimo, je vse v redu:
>>> import b >>> b.g() 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'
Ena od lepot Pythona je bogastvo knjižničnih modulov, ki jih dobite 'iz škatle'. Če pa se temu zavestno ne izognete, v standardni knjižnici, ki je priložena Pythonu, ni tako težko naleteti na imenski spopad med imenom enega od vaših modulov in modulom z istim imenom. , morda imate v svoji kodi modul z imenom email.py
, ki bi bil v nasprotju s standardnim modulom z istim imenom).
To lahko privede do težav, na primer pri uvozu druge knjižnice, ki poskuša uvoziti različico modula Python Standard Library, toda ker imate modul z istim imenom, drugi paket pomotoma uvozi vašo različico namesto tiste znotraj Python Standard Library. Tu se dogajajo slabe napake Pythona.
Zato je treba paziti, da ne uporabljamo enakih imen kot v modulih standardne knjižnice Python. Preprosteje spremenite ime modula v paketu, kot pa datoteko Predlog za izboljšanje Pythona (PEP) zahtevati spremembo imena gorvodno in poskusiti to odobriti.
Upoštevajte naslednjo datoteko 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()
V Pythonu 2 to deluje dobro:
$ python foo.py 1 key error 1 $ python foo.py 2 value error 2
Zdaj pa se zavrtimo na Pythonu 3:
$ python3 foo.py 1 key error Traceback (most recent call last): File 'foo.py', line 19, in bad() File 'foo.py', line 17, in bad print(e) UnboundLocalError: local variable 'e' referenced before assignment
Kaj se je pravkar zgodilo tukaj? 'Težava' je v tem, da v Pythonu 3 objekt izjeme ni dostopen preko obsega except
blok. (Razlog za to je, da bi sicer ohranil referenčni cikel z ogrodjem sklada v pomnilniku, dokler zbiralnik smeti ne zažene in izbriše reference iz pomnilnika. Na voljo je več tehničnih podrobnosti o tem tukaj ).
Eden od načinov, kako se temu težavi izogniti, je ohranjanje sklicevanja na objekt izjeme zunaj obseg except
blok, tako da ostane dostopen. Tu je različica prejšnjega primera, ki uporablja to tehniko, s čimer dobimo kodo, ki je prijazna za Python 2 in 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()
Zagon tega na Py3k:
$ python3 foo.py 1 key error 1 $ python3 foo.py 2 value error 2
Joj!
(Mimogrede, naša Vodnik za najem Pythona razpravlja o številnih drugih pomembnih razlikah, ki se jih je treba zavedati pri selitvi kode iz Pythona 2 v Python 3.)
__del__
metodaRecimo, da ste to imeli v datoteki z imenom mod.py
:
import foo class Bar(object): ... def __del__(self): foo.cleanup(self.myhandle)
In potem ste poskusili to narediti iz another_mod.py
:
import mod mybar = mod.Bar()
Dobili bi grdo AttributeError
izjema.
Zakaj? Ker, kot poročajo tukaj , ko se tolmač izklopi, so globalne spremenljivke modula nastavljene na None
Kot rezultat, v zgornjem primeru, na točki, da __del__
se prikliče ime foo
je že nastavljeno na None
.
Rešitev tega nekoliko naprednejšega programskega problema Pythona bi bila uporaba atexit.register()
namesto tega. Na ta način, ko se vaš program konča z izvajanjem (ko se normalno izstopi, to je), se začnejo registrirani upravljavci prej tolmač je izključen.
S tem razumevanjem je popravek za zgornje mod.py
koda bi potem lahko izgledala nekako takole:
import foo import atexit def cleanup(handle): foo.cleanup(handle) class Bar(object): def __init__(self): ... atexit.register(cleanup, self.myhandle)
Ta izvedba zagotavlja čist in zanesljiv način klica vse potrebne funkcije čiščenja ob običajnem zaključku programa. Očitno je, da je do foo.cleanup
da se odločite, kaj storiti s predmetom, vezanim na ime self.myhandle
, vendar dobite idejo.
Python je zmogljiv in prilagodljiv jezik s številnimi mehanizmi in paradigmami, ki lahko močno izboljšajo produktivnost. Kot pri katerem koli programskem orodju ali jeziku je tudi v tem primeru omejeno razumevanje ali spoštovanje njegovih zmožnosti lahko včasih večja ovira kot korist, zato človek v pregovornem stanju »ve dovolj, da je lahko nevaren«.
neprekinjena dostava z uporabo pakirnice in teraforme
Seznanitev s ključnimi odtenki Pythona, kot so (vendar nikakor ne omejene na) zmerno napredne programske težave, predstavljene v tem članku, bo pomagalo optimizirati uporabo jezika in se izogniti nekaterim pogostejšim napakam.
Morda boste želeli preveriti tudi našo Insider's Guide to Python Interviewing za predloge glede vprašanj za razgovore, ki lahko pomagajo prepoznati strokovnjake za Python.
Upamo, da so vam kazalci v tem članku v pomoč in pozdravljamo vaše povratne informacije.