Python on korkean tason olio- ja tulkittu ohjelmointikieli, jolla on dynaaminen semantiikka. Sen korkea integroitu tietorakenne yhdistettynä dynaamiseen kirjoittamiseen ja sitomiseen tekevät siitä erittäin houkuttelevan nopea sovelluskehitys , sekä käytettäväksi komentosarjakielenä tai liimana olemassa olevien komponenttien tai palveluiden yhdistämiseen. Python toimii moduulien ja pakettien kanssa, mikä edistää ohjelmien modulaarisuutta ja koodien uudelleenkäyttöä.
Yksinkertainen ja helppo oppia Python-syntakse, voit lähettää python-kehittäjät väärään suuntaan - etenkin ne, jotka oppivat kieltä - menettävät osan sen hienovaraisuuksista matkan varrella ja aliarvioivat kielen voiman Pythonin monipuolinen kieli .
Tässä mielessä tämä artikkeli esittelee 'top 10' -luettelon hienovaraisista, vaikeasti ymmärrettävistä virheistä, jotka voivat saada kiinni jopa joistakin edistyneimmistä Python-kehittäjistä.
( Huomautus: Tämä artikkeli on tarkoitettu edistyneemmälle yleisölle kuin Common Python Programmer Errors, joka on suunnattu enemmän niille, joilla kieli on uusi. )
Python antaa sinun määrittää, että funktion argumentti on valinnainen antamalla sille oletusarvo. Vaikka tämä on kielen hieno ominaisuus, se voi aiheuttaa sekaannusta oletusasetuksena vaihteleva . Harkitse esimerkiksi tätä Python-funktion määritelmää:
>>> 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
Yleinen väärinkäsitys on, että valinnainen argumentti asetetaan tietylle oletuslausekkeelle joka kerta, kun funktiota kutsutaan ilman tarvetta antaa arvoa valinnaiselle argumentille. Esimerkiksi yllä olevassa koodissa saatat odottaa soittavan foo()
useita kertoja (ts. määrittelemättä palkkiargumenttia) palauttaisi aina baz
, koska hypoteesi olisi, että joka kerta foo()
kutsutaan (ilman määritettyä pylväargumenttia) pylväsasetus on []
(eli uusi tyhjä luettelo).
Mutta katsotaanpa, mitä todella tapahtuu, kun tämä tehdään:
>>> foo() ['baz'] >>> foo() ['baz', 'baz'] >>> foo() ['baz', 'baz', 'baz']
Hei? Miksi oletusarvo oli baz
olemassa olevaan luetteloon joka kerta että foo()
kutsuttiin sen sijaan, että loisit uuden listan joka tilaisuudessa? Edistynein Python-ohjelmointivastaus on, funktion argumentin oletusarvo arvioidaan vain kerran, kun funktio määritetään. Siksi palkkiargumentti alustetaan oletusarvoonsa (eli tyhjään luetteloon) vain, kun foo()
on määritelty ensin, mutta sitten kutsuu foo()
(eli ilman määriteltyä bar
argumenttia) he käyttävät edelleen samaa luetteloa kuin bar
alun perin alustettiin.
Muuten, yleinen ratkaisu tähän on seuraava:
>>> def foo(bar=None): ... if bar is None: # or if not bar: ... bar = [] ... bar.append('baz') ... return bar ... >>> foo() ['baz'] >>> foo() ['baz'] >>> foo() ['baz']
Tarkastellaan seuraavaa esimerkkiä:
>>> class A(object): ... x = 1 ... >>> class B(A): ... pass ... >>> class C(A): ... pass ... >>> print A.x, B.x, C.x 1 1 1
Käydä järkeen.
>>> B.x = 2 >>> print A.x, B.x, C.x 1 2 1
Kyllä, jälleen odotetusti.
>>> A.x = 3 >>> print A.x, B.x, C.x 3 2 3
Mikä tämä on? Muutamme vain A.x
Miksi C.x
muuttunut myös?
Pythonissa luokan muuttujia käsitellään sisäisesti kuten sanakirjoja, ja ne seuraavat sitä, mihin usein viitataan Menetelmän tarkkuusjärjestys (MRO) . Joten yllä olevassa koodissa, koska attribuuttia x ei löydy luokasta C, sitä etsitään sen perusluokista (yllä olevassa esimerkissä vain A, vaikka Python tukee useita perintöjä). Toisin sanoen C: llä ei ole A: sta riippumatonta omaa ominaisuutta x. Siksi viitteet C.x: ään ovat itse asiassa viittauksia A.x: ään. Tämä aiheuttaa Python-ongelman, ellei sitä käsitellä oikein. Lisätietoja luokan määritteet Pythonissa .
Oletetaan, että sinulla on seuraava koodi:
>>> 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
Ongelmana on, että raportti except
se ei ota luetteloa tällä tavalla määritetyistä poikkeuksista. Sen sijaan Python 2.x -syntaksia except Exception, e
käytetään sitomaan poikkeus toiseen parametriin valinnainen määritelty (tässä tapauksessa e
), jotta se olisi käytettävissä lisätarkastuksia varten. Tämän seurauksena yllä olevassa koodissa poikkeus IndexError
ei ole raportin siepattu except
; pikemminkin poikkeus lopulta sidotaan parametriin nimeltä IndexError
.
Oikea tapa saada useita poikkeuksia raporttiin except
on määrittää ensimmäinen parametri a: ksi tupla A sisältää kaikki pyydettävät poikkeukset. Käytä myös maksimaalista siirrettävyyttä käyttämällä as
avainsanaa, koska Python 2 ja Python 3 tukevat syntaksia:
>>> try: ... l = ['a', 'b'] ... int(l[2]) ... except (ValueError, IndexError) as e: ... pass ... >>>
Pythonin laajuusresoluutio perustuu niin sanottuun sääntöön LEGB , joka on lyhenne sanoista L okaali, ON n sulkeminen, G lobal, B ylöspäin. Se näyttää melko suoraviivaiselta, eikö? No, oikeastaan on olemassa muutamia hienovaraisuuksia tämän toiminnassa Pythonissa, mikä johtaa meidät alla olevaan yleiseen, edistyneempään Python-ohjelmointiongelmaan.
Harkitse seuraavaa:
>>> 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
Mikä on ongelma?
Yllä oleva virhe johtuu siitä, että kun a tehtävä laajuuden muuttujalle, Python pitää muuttujaa automaattisesti paikallisena siinä laajuudessa ja seuraa mitä tahansa samankaltaista muuttujaa missä tahansa ulommassa laajuudessa.
Monet heistä ovat siis yllättyneitä saadessaan UnboundLocalError
edellisessä työkoodissa, kun sitä muokataan lisäämällä raporttilauseke jonnekin funktion runkoon. (Voit lukea lisää tästä tässä .)
On erityisen yleistä, että tämä hämmentää kehittäjiä käytön aikana luettelot . Harkitse seuraavaa esimerkkiä:
>>> 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
Hei? Miksi foo2
epäonnistui, kun taas foo1
toimi hyvin?
Vastaus on sama kuin edellisen esimerkin ongelma, mutta se on varmasti hienovaraisempi. foo1
Ei tee a tehtävä a lst
, kun taas foo2
jos se on. Muista, että lst += [5]
on oikeastaan lyhenne sanoista lst = lst + [5]
, näemme, että yritämme määrittää arvon lst
: lle (joten Python olettaa, että olet paikallisella alueella). Lst: lle yrittämämme arvo perustuu kuitenkin samaan lst
(jälleen oletetaan olevan paikallinen), joka on vielä määriteltävä. Puomi.
Seuraavan koodin ongelman pitäisi olla melko ilmeinen:
>>> 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
Kohteen poistaminen luettelosta tai taulukosta, samalla kun iteroidaan sen yli, on Python-ongelma, jonka kaikki kokeneet ohjelmistokehittäjät tuntevat. Vaikka yllä oleva esimerkki saattaa olla melko ilmeinen, edes edistyneet kehittäjät voivat vahingossa jäädä tämän paljon monimutkaisemman koodin ulkopuolelle.
Onneksi Python sisältää useita tyylikkäitä ohjelmointimalleja, jotka oikein käytettynä voivat johtaa merkittävästi yksinkertaistettuun ja virtaviivaiseen koodiin. Tämän toissijainen etu on, että yksinkertaisempi koodi jää vähemmän todennäköiseksi luettelon kohteen vahingossa tapahtuvan poistamisen aikana, kun iteroidaan-over-it-virhe. Yksi tällainen paradigma on [luettelon ymmärtäminen] ((https://docs.python.org/2/tutorial/datastructures.html#tut-listcomps). Toisaalta luettelon ymmärtäminen on erityisen hyödyllistä tämän ongelman välttämiseksi, kuten näkyy tässä yllä esitetyn koodin vaihtoehtoisessa toteutuksessa, joka toimii täydellisesti:
>>> 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]
Ottaen huomioon seuraavan esimerkin:
>>> def create_multipliers(): ... return [lambda x : i * x for i in range(5)] >>> for multiplier in create_multipliers(): ... print multiplier(2) ...
Sinun pitäisi odottaa seuraavaa tulosta:
0 2 4 6 8
Mutta saat todella:
8 8 8 8 8
Yllätys!
Tämä tapahtuu käyttäytymisen vuoksi myöhäinen linkki Python, joka sanoo, että sulkemisissa käytettyjen muuttujien arvot haetaan silloin, kun sisäinen toiminto kutsutaan. Joten yllä olevassa koodissa, kun jotain palautetuista funktioista kutsutaan, arvo i
halutaan sisään sen ympärillä oleva alue silloin, kun sitä kutsutaan (ja siinä vaiheessa ympyrä on valmis, joten i
: lle on jo annettu lopullinen arvo 4).
Ratkaisu tähän yleiseen Python-ongelmaan on vähän hakkerointi:
>>> 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à! Hyödynnämme oletusargumentteja anonyymien toimintojen luomiseksi halutun toiminnan saavuttamiseksi. Jotkut kutsuvat tätä tyylikkääksi. Jotkut kutsuisivat sitä hienovaraiseksi. Jotkut vihaavat sitä. Mutta jos olet Python-kehittäjä, tämä on tärkeää ymmärtää.
Oletetaan, että sinulla on kaksi tiedostoa, a.py ja b.py, joista kukin tuo toisen, seuraavasti:
sisään a.py
:
import b def f(): return b.x print f()
Ja b.py
import a x = 1 def g(): print a.f()
Yritetään ensin tuoda a.py
:
>>> import a 1
Se toimi hyvin. Ehkä olit yllättynyt. Loppujen lopuksi meillä on täällä kiertotalous, jonka oletettavasti pitäisi olla ongelma, eikö?
Vastaus on, että pelkkä läsnäolo pyöreä tuonti ei sinänsä ole ongelma Pythonissa. Jos moduuli on jo tuotu, Python on tarpeeksi älykäs, jotta se ei yritä tuoda sitä uudelleen. Riippuen siitä pisteestä, jossa kukin moduuli yrittää käyttää toisessa määriteltyjä toimintoja tai muuttujia, saatat kuitenkin törmätä ongelmiin.
Joten palataan esimerkkiin, kun tuot a.py
, minulla ei ollut ongelmia tuoda b.py
, koska b.py
ei vaadi mitään b.py
määriteltävä tuontihetkellä. Ainoa viite b.py
-sivulla a a
, on kutsu a.f()
Mutta puhelu on g()
eikä mitään kohdassa a.py
tai b.py
kutsuu g()
. Elämä on siis kaunista.
Mutta mitä tapahtuu, jos yrität tuoda b.py
(tietysti ilman aikaisempaa maahantuontia a.py
):
>>> 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'
Voi ei. Tuo ei ole hyvä! Ongelmana on, että tuontiprosessissa b.py
se yrittää tuoda a.py
, mikä sen seurauksena kutsuu f()
, joka puolestaan yrittää käyttää b.x
. Mutta b.x
sitä ei ole vielä määritelty. Tästä syystä poikkeus AttributeError
Ainakin yksi ratkaisu tähän on melko triviaali. Muokkaa vain b.py
tuoda a.py
sisällä g()
:
x = 1 def g(): import a # This will be evaluated only when g() is called print a.f()
Tuodessaan kaikki on hyvin:
>>> 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'
Yksi Pythonin eduista on suuri määrä kirjastomoduuleja, joita sillä on alusta alkaen. Mutta seurauksena on, että jos et tietoisesti välttää tätä, ei ole niin vaikeaa törmätä nimiristiriitaan yhden moduulisi nimen ja Pythonin mukana toimitettavan standardikirjaston saman nimisen moduulin välillä (esimerkiksi Esimerkiksi koodissasi voi olla email.py
-moduuli, joka olisi ristiriidassa samannimisen tavallisen kirjastomoduulin kanssa).
Tämä voi johtaa erittäin aggressiivisiin ongelmiin, kuten tuoda toinen kirjasto, joka puolestaan yrittää tuoda moduulin Python-standardikirjastot-version, mutta koska sinulla on jo saman niminen moduuli, toinen paketti tuo virheellisesti versiosi sisään Python-standardikirjastossa olevasta, ja tässä esiintyy vakavimmat virheet.
Siksi on vältettävä samojen nimien käyttöä kuin tavalliset Python-kirjastomoduulit. Sinulle on paljon helpompaa nimetä moduuli uudelleen paketissasi kuin esittää Python-parannusehdotus. (PEP) pyytää nimimuutosta alkupäässä ja hyväksymään se.
Harkitse seuraavaa tiedostoa 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()
Python 2: ssa tämä toimii hyvin:
$ python foo.py 1 key error 1 $ python foo.py 2 value error 2
Mutta nyt pyöritetään Python 3: a:
$ 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
Mitä täällä juuri tapahtui? 'Ongelma' on se, että Python 3: ssa poikkeusobjekti ei ole käytettävissä except
-lohkon ulkopuolella. (Syynä tähän on se, että se muuten säilyttäisi vertailusilmukan pinokehyksen kanssa muistissa, kunnes roskien kerääjä juoksee ja puhdistaa viitteet muistista. Lisää teknisiä yksityiskohtia on saatavilla tässä ).
Yksi tapa kiertää tämä ongelma on pitää viittaus poikkeusobjektiin lukuun ottamatta lukulohkoa niin, että se pysyy käytettävissä. Tässä on yllä olevan esimerkin versio, joka käyttää tätä tekniikkaa, joten annostelee koodi ja tekee siitä yhteensopivamman Python 2: n ja Python 3: n kanssa:
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()
Tämän toteutus tapahtuu Py3k: ssä:
$ python3 foo.py 1 key error 1 $ python3 foo.py 2 value error 2
¡ Yupi!
(Muuten, meidän Python Palkkausopas käsittelee useita tärkeitä eroja, jotka on otettava huomioon, kun siirrät koodisi Python 2: sta Python 3: een.)
__del__
: n väärinkäyttöOletetaan, että sinulla oli tämä tiedostossa nimeltä mod.py
:
import foo class Bar(object): ... def __del__(self): foo.cleanup(self.myhandle)
Ja sitten yritit tehdä tämän another_mod.py
:
import mod mybar = mod.Bar()
Saat ruman poikkeuksen AttributeError
.
Miksi? Koska, kuten on raportoitu tässä Kun tulkki kytketään pois päältä, moduulin globaaleiksi muuttujiksi asetetaan None
. Seurauksena on, että yllä olevassa esimerkissä siinä kohdassa __del__
kutsutaan, nimi foo on jo asetettu arvoon None
.
Ratkaisu tähän ongelmaan, joka on jonkin verran edistyneempi kuin Python-ohjelmointi, olisi käyttää atexit.register()
sen sijaan. Tällä tavalla, kun ohjelma on suoritettu (tarkoitan normaalisti poistumista), rekisteröidyt esimiehesi erotetaan. ennen että tulkki sammuu.
Tämän tiedon avulla ratkaisu edelliselle koodille mod.py
se voi olla jotain tällaista:
import foo import atexit def cleanup(handle): foo.cleanup(handle) class Bar(object): def __init__(self): ... atexit.register(cleanup, self.myhandle)
Tämä sovellus tarjoaa puhtaan ja luotettavan tavan kutsua kaikki tarvittavat puhdistustoiminnot ohjelman normaalin lopettamisen jälkeen. On selvää, että foo.cleanup on päättänyt, mitä tehdä nimelle self.myhandle liitettyyn esineeseen, mutta saat asian.
Python on tehokas ja joustava kieli, jolla on monia mekanismeja ja paradigmoja, jotka voivat parantaa huomattavasti tuottavuutta. Kuten minkä tahansa ohjelmiston tai kielityökalun kohdalla, rajoitettu ymmärrys tai kykyjen arvostaminen sen kyvyistä voi kuitenkin joskus olla enemmän este kuin omaisuus, jättäen meidät sananlaskuiseen tilaan 'tietää tarpeeksi vaaralliseksi'.
Tutustuminen Pythonin tärkeimpiin vivahteisiin, kuten (mutta ei suinkaan rajoitettu) tässä artikkelissa käsiteltyihin kohtalaisen edistyneisiin ohjelmointiongelmiin, auttaa optimoimaan kielen käyttöä välttäen joitain sen yleisimpiä virheitä.
Sinun tulisi tarkistaa meidän Sisäpiiriopas Python-haastatteluihin , ehdotuksia haastattelukysymyksistä, jotka voivat auttaa tunnistamaan Python-asiantuntijoita.
Toivomme, että tämän artikkelin vinkit ovat hyödyllisiä ja arvostamme palautettasi.