minulla oli ohjelmointihaastattelu äskettäin puhelimen näyttö, jossa käytimme a yhteistyötekstieditori .
Minua pyydettiin toteuttamaan a tietty API ja päätti tehdä niin vuonna Python . Tiivistetään ongelman selvitys, sanotaan, että tarvitsin luokan, jonka esiintymät tallensivat joitain data
ja jotkut other_data
.
Hengitin syvään ja aloin kirjoittaa. Muutaman rivin jälkeen minulla oli jotain tällaista:
class Service(object): data = [] def __init__(self, other_data): self.other_data = other_data ...
Haastattelijani pysäytti minut:
data = []
. Mielestäni se ei ole kelvollinen Python? 'Tässä on ohjeet koodin muuttamiseen: jotta voin antaa käsityksen siitä, mihin olin menossa:
class Service(object): def __init__(self, other_data): self.data = [] self.other_data = other_data ...
Kuten käy ilmi, olimme molemmat väärässä. Todellinen vastaus oli ymmärtää ero Python-luokan attribuuttien ja Python-ilmentymien attribuuttien välillä.
Huomaa: jos sinulla on asiantuntijan käsitys luokan määritteistä, voit siirtyä eteenpäin Käytä koteloita .
Haastattelijani oli väärässä yllä olevassa koodissa On syntaktisesti pätevä.
Minäkin olin väärässä, koska se ei aseta oletusarvoa ilmentymämääritteelle. Sen sijaan se määrittelee data
kuten a luokassa määritteen arvo []
.
Kokemukseni mukaan Python-luokan määritteet ovat aihe, joka monet ihmiset tietävät jotain noin, mutta harvat ymmärtävät täysin.
Python-luokan attribuutti on luokan attribuutti (pyöreä, tiedän), eikä attribuutti ilmentymä luokan.
Käytetään Python-luokan esimerkkiä havainnollistamaan eroa. Täällä, class_var
on luokan attribuutti ja i_var
on instanssimäärite:
class MyClass(object): class_var = 1 def __init__(self, i_var): self.i_var = i_var
Huomaa, että kaikilla luokan esiintymillä on pääsy class_var
: een ja että sitä voidaan käyttää myös luokka itse :
foo = MyClass(2) bar = MyClass(3) foo.class_var, foo.i_var ## 1, 2 bar.class_var, bar.i_var ## 1, 3 MyClass.class_var ## <— This is key ## 1
Java- tai C ++ - ohjelmoijille luokan attribuutti on samanlainen - mutta ei identtinen - staattiselle jäsenelle. Näemme, miten ne eroavat myöhemmin.
Puhutaksemme lyhyesti siitä, mitä täällä tapahtuu Python-nimitilat .
TO nimitila on kartoitus nimistä esineisiin, ominaisuudella, että eri nimiavaruuksien nimien välillä ei ole yhtään suhdetta. Ne toteutetaan yleensä Python-sanakirjoina, vaikka tämä onkin tiivistetty.
Kontekstista riippuen sinun on ehkä käytettävä nimitilaa käyttämällä pistesyntaksi (esim. object.name_from_objects_namespace
) Tai paikallisena muuttujana (esim. object_from_namespace
). Konkreettisena esimerkkinä:
class MyClass(object): ## No need for dot syntax class_var = 1 def __init__(self, i_var): self.i_var = i_var ## Need dot syntax as we've left scope of class namespace MyClass.class_var ## 1
Python-luokat ja luokkojen esiintymillä kullakin on omat erilliset nimitilat, joita edustaa ennalta määritetyt määritteet MyClass.__dict__
ja instance_of_MyClass.__dict__
.
Kun yrität käyttää määritettä luokan ilmentymästä, se tarkastelee ensin sitä ilmentymä nimitila. Jos se löytää määritteen, se palauttaa siihen liittyvän arvon. Jos ei, niin sitten näyttää luokassa nimitila ja palauttaa määritteen (jos se on läsnä, heittää virhe muuten). Esimerkiksi:
foo = MyClass(2) ## Finds i_var in foo's instance namespace foo.i_var ## 2 ## Doesn't find class_var in instance namespace… ## So look's in class namespace (MyClass.__dict__) foo.class_var ## 1
Ilmentymän nimiavaruus on etusijalla luokan nimiavaruuteen nähden: jos molemmissa on määritteitä, joilla on sama nimi, ilmentymän nimiavaruus tarkistetaan ensin ja sen arvo palautetaan. Tässä on yksinkertaistettu versio koodista ( lähde ) attribuutin haulle:
def instlookup(inst, name): ## simplified algorithm... if inst.__dict__.has_key(name): return inst.__dict__[name] else: return inst.__class__.__dict__[name]
Ja visuaalisesti:
Tässä mielessä voimme ymmärtää, miten Python-luokan attribuutit käsittelevät tehtävää:
Jos luokan määritteeksi määritetään pääsy luokkaan, se ohittaa arvon kaikki tapauksia. Esimerkiksi:
foo = MyClass(2) foo.class_var ## 1 MyClass.class_var = 2 foo.class_var ## 2
Nimitilan tasolla… asetamme MyClass.__dict__['class_var'] = 2
. (Huomaa: tämä ei ole tarkka koodi (mikä olisi setattr(MyClass, 'class_var', 2)
) muodossa __dict__
palauttaa a dictproxy , muuttumaton kääre, joka estää suoran määrityksen, mutta se auttaa esittelyn vuoksi). Sitten, kun pääsemme foo.class_var
, class_var
on uusi arvo luokan nimiavaruudessa ja siten 2 palautetaan.
Jos Paython-luokan muuttuja asetetaan avaamalla ilmentymä, se ohittaa arvon vain kyseiseen tapaukseen . Tämä ohittaa olennaisesti luokan muuttujan ja muuttaa sen intuitiivisesti saatavana olevaksi ilmentymämuuttujaksi, vain kyseiseen tapaukseen . Esimerkiksi:
foo = MyClass(2) foo.class_var ## 1 foo.class_var = 2 foo.class_var ## 2 MyClass.class_var ## 1
Nimitilan tasolla… lisätään class_var
attribuutti foo.__dict__
, joten kun haemme foo.class_var
, palautamme 2. Samaan aikaan muut MyClass
tahtoa ei on class_var
heidän nimitiloissaan, joten he etsivät edelleen class_var
sisään MyClass.__dict__
ja palaa siten 1.
Tietokilpailukysymys: Entä jos luokan attribuutilla on muuttuva tyyppi ? Voit manipuloida (silpoa?) Luokan määritettä pääsemällä siihen tietyn esiintymän kautta ja puolestaan päätyä manipuloimalla viitattua objektia, jota kaikki esiintymät käyttävät (kuten huomautti Timothy Wiseman ).
Tämä voidaan parhaiten osoittaa esimerkillä. Palataan takaisin Service
-sivulle Määritin aiemmin ja näen, kuinka luokkamuuttujan käyttöni olisi voinut johtaa ongelmiin tiellä.
class Service(object): data = [] def __init__(self, other_data): self.other_data = other_data ...
Tavoitteenani oli saada tyhjä luettelo ([]
) oletusarvoksi data
: lle ja jokaiselle Service
olla omat tietonsa sitä muutettaisiin ajan myötä tapauskohtaisesti. Mutta tässä tapauksessa saamme seuraavan käyttäytymisen (muista, että Service
vie jonkin argumentin other_data
, joka on mielivaltainen tässä esimerkissä):
s1 = Service(['a', 'b']) s2 = Service(['c', 'd']) s1.data.append(1) s1.data ## [1] s2.data ## [1] s2.data.append(2) s1.data ## [1, 2] s2.data ## [1, 2]
Tämä ei ole hyvä - luokan muuttujan muuttaminen yhden esiintymän kautta muuttaa sitä kaikille muille!
Nimitilan tasolla… kaikki Service
-tapaukset käyttävät ja muokkaavat samaa luetteloa kansiossa Service.__dict__
tekemättä omia data
attribuutit niiden instanssien nimiavaruuksissa.
Voisimme kiertää tämän tehtävän avulla; toisin sanoen voisimme käyttää luettelon muutettavuuden hyödyntämisen sijasta Service
esineillä on omat luettelonsa seuraavasti:
s1 = Service(['a', 'b']) s2 = Service(['c', 'd']) s1.data = [1] s2.data = [2] s1.data ## [1] s2.data ## [2]
Tässä tapauksessa lisäämme s1.__dict__['data'] = [1]
, joten alkuperäinen Service.__dict__['data']
pysyy muuttumattomana.
Valitettavasti tämä edellyttää, että Service
käyttäjillä on läheistä tietoa sen muuttujista, ja heillä on altis virheille. Tavallaan olisimme puuttuneet oireisiin pikemminkin kuin syihin. Haluamme mieluummin jotain, joka oli rakenteeltaan oikein.
Oma ratkaisuni: jos määrität oletusarvon mahdolliselle Python-ilmentymämuuttujalle vain luokan muuttujan avulla, älä käytä muutettavia arvoja . Tässä tapauksessa jokainen Service
aikoi ohittaa Service.data
omalla instanssimääritteellään lopulta, joten tyhjän luettelon käyttäminen oletuksena johti pieneen virheeseen, joka oli helposti unohdettavissa. Yllä olevan sijasta voisimme joko:
Vältetään tyhjän luettelon (muutettavissa oleva arvo) käyttöä oletusarvona:
class Service(object): data = None def __init__(self, other_data): self.other_data = other_data ...
Tietenkin meidän on käsiteltävä None
tapauksessa, mutta se on pieni hinta.
Luokan määritteet ovat hankalia, mutta katsotaanpa muutamia tapauksia, joissa ne olisivat käteviä:
Vakioiden tallentaminen . Koska luokan määritteitä voidaan käyttää luokan itse määritteinä, on usein mukavaa käyttää niitä luokan laajuisten, luokkakohtaisten vakioiden tallentamiseen. Esimerkiksi:
class Circle(object): pi = 3.14159 def __init__(self, radius): self.radius = radius def area(self): return Circle.pi * self.radius * self.radius Circle.pi ## 3.14159 c = Circle(10) c.pi ## 3.14159 c.area() ## 314.159
Oletusarvojen määrittäminen . Pienenä esimerkkinä voimme luoda rajoitetun luettelon (ts. Luettelon, johon mahtuu vain tietty määrä elementtejä tai vähemmän) ja valita oletusarvoisesti 10 kohdetta:
class MyClass(object): limit = 10 def __init__(self): self.data = [] def item(self, i): return self.data[i] def add(self, e): if len(self.data) >= self.limit: raise Exception('Too many elements') self.data.append(e) MyClass.limit ## 10
Voimme sitten luoda myös instansseja, joilla on omat rajoituksensa, määrittelemällä instanssille limit
määritteen.
foo = MyClass() foo.limit = 50 ## foo can now hold 50 elements—other instances can hold 10
Tällä on järkeä vain, jos haluat tyypillisen MyClass
-esiintymän pitää vain 10 tai vähemmän elementtiä - jos annat kaikille instansseillesi erilaiset rajat, niin limit
pitäisi olla instanssimuuttuja. (Muista kuitenkin: ole varovainen, kun käytät muutettavia arvoja oletuksina.)
Seurataan kaikkia tietoja tietyn luokan kaikissa esiintymissä . Tämä on eräänlainen erityinen, mutta voisin nähdä skenaarion, jossa haluat ehkä käyttää tietyn luokan jokaiseen olemassa olevaan esiintymään liittyviä tietoja.
Jos haluat tehdä skenaariosta konkreettisemman, sanotaan, että meillä on Person
luokassa, ja jokaisella henkilöllä on name
. Haluamme seurata kaikkia käytettyjä nimiä. Yksi lähestymistapa saattaa olla toista roskakorin objektiluettelon yli , mutta luokan muuttujien käyttö on yksinkertaisempaa.
Huomaa, että tässä tapauksessa names
käytetään vain luokan muuttujana, joten muutettava oletus on hyväksyttävä.
class Person(object): all_names = [] def __init__(self, name): self.name = name Person.all_names.append(name) joe = Person('Joe') bob = Person('Bob') print Person.all_names ## ['Joe', 'Bob']
Voisimme jopa käyttää tätä suunnittelumallia kaikkien tietyn luokan kaikkien olemassa olevien esiintymien seuraamiseen vain joidenkin liittyvien tietojen sijaan.
class Person(object): all_people = [] def __init__(self, name): self.name = name Person.all_people.append(self) joe = Person('Joe') bob = Person('Bob') print Person.all_people ## [, ]
Esitys (tavallaan… katso alla).
Huomautus: Jos olet huolissasi suorituskyvystä tällä tasolla, et ehkä halua käyttää Pythonia ensinnäkin, koska erot ovat suuruusluokkaa kymmenes millisekuntia - mutta on silti hauskaa sietää hieman ja auttaa havainnollistamiseksi.
Muista, että luokan nimitila luodaan ja täytetään luokan määrittelyhetkellä. Tämä tarkoittaa, että teemme vain yhden tehtävän - koskaan - tietylle luokan muuttujalle, kun taas ilmentymämuuttujat on määritettävä joka kerta, kun uusi ilmentymä luodaan. Otetaan esimerkki.
def called_class(): print 'Class assignment' return 2 class Bar(object): y = called_class() def __init__(self, x): self.x = x ## 'Class assignment' def called_instance(): print 'Instance assignment' return 2 class Foo(object): def __init__(self, x): self.y = called_instance() self.x = x Bar(1) Bar(2) Foo(1) ## 'Instance assignment' Foo(2) ## 'Instance assignment'
Määrittelemme Bar.y
vain kerran, mutta instance_of_Foo.y
jokaisesta puhelusta __init__
.
Lisätodisteina käytetään Python-purku :
import dis class Bar(object): y = 2 def __init__(self, x): self.x = x class Foo(object): def __init__(self, x): self.y = 2 self.x = x dis.dis(Bar) ## Disassembly of __init__: ## 7 0 LOAD_FAST 1 (x) ## 3 LOAD_FAST 0 (self) ## 6 STORE_ATTR 0 (x) ## 9 LOAD_CONST 0 (None) ## 12 RETURN_VALUE dis.dis(Foo) ## Disassembly of __init__: ## 11 0 LOAD_CONST 1 (2) ## 3 LOAD_FAST 0 (self) ## 6 STORE_ATTR 0 (y) ## 12 9 LOAD_FAST 1 (x) ## 12 LOAD_FAST 0 (self) ## 15 STORE_ATTR 1 (x) ## 18 LOAD_CONST 0 (None) ## 21 RETURN_VALUE
Kun katsomme tavukoodia, on jälleen ilmeistä, että Foo.__init__
on tehtävä kaksi tehtävää, kun taas Bar.__init__
tekee vain yhden.
Miltä tämä voitto todella näyttää? Tunnustan ensimmäisenä, että ajoitustestit riippuvat suuresti usein hallitsemattomista tekijöistä ja niiden välisiä eroja on usein vaikea selittää tarkasti.
Luulen kuitenkin, että nämä pienet katkelmat (suoritetaan Pythonin kanssa aika module) auttaa kuvaamaan luokan ja esiintymämuuttujien välisiä eroja, joten olen sisällyttänyt ne joka tapauksessa.
Huomaa: Käytän MacBook Prota, jossa on OS X 10.8.5 ja Python 2.7.2.
10000000 calls to `Bar(2)`: 4.940s 10000000 calls to `Foo(2)`: 6.043s
Bar
: N alustus ovat nopeammin yli sekunnin, joten ero tässä näyttää olevan tilastollisesti merkitsevä.
Joten miksi näin on? Yksi spekulatiivinen selitys: teemme kaksi tehtävää Foo.__init__
: ssa, mutta vain yhden Bar.__init__
: ssa.
10000000 calls to `Bar(2).y = 15`: 6.232s 10000000 calls to `Foo(2).y = 15`: 6.855s 10000000 `Bar` assignments: 6.232s - 4.940s = 1.292s 10000000 `Foo` assignments: 6.855s - 6.043s = 0.812s
Huomaa: Asennuskoodia ei voi suorittaa uudelleen jokaisessa kokeessa aika , joten meidän on alustettava muuttujamme uudelleen kokeilussa. Aikojen toinen rivi edustaa edellä mainittuja aikoja vähennettynä aiemmin lasketuilla alustusajoilla.
Edellä esitetystä näyttää siltä, että Foo
kestää vain noin 60% niin kauan kuin Bar
käsittelemään tehtäviä.
Miksi näin on? Yksi spekulatiivinen selitys: kun määritämme Bar(2).y
, etsimme ensin ilmentymän nimiavaruutta (Bar(2).__dict__[y]
), emme löydä y
ja etsimme sitten luokan nimiavaruutta (Bar.__dict__[y]
) ja tee sitten oikea tehtävä. Kun osoitamme Foo(2).y
: lle, teemme puolet niin monta hakua, kuin osoitamme heti ilmentymän nimiavaruuteen (Foo(2).__dict__[y]
).
Yhteenvetona voidaan todeta, että vaikka näillä suorituskyvyn parannuksilla ei ole väliä todellisuudessa, nämä testit ovat mielenkiintoisia käsitteellisellä tasolla. Toivon, että nämä erot auttavat havainnollistamaan luokan ja ilmentymän muuttujien välisiä mekaanisia eroja.
Luokkamääritteet näyttävät olevan alikäytettyjä Pythonissa; monilla ohjelmoijilla on erilaiset vaikutelmat heidän työskentelystään ja miksi he voivat olla hyödyllisiä.
Otan: Python-luokan muuttujilla on paikkansa hyvän koodin koulussa. Varovasti käytettynä ne voivat yksinkertaistaa asioita ja parantaa luettavuutta. Mutta kun heidät heitetään huolimattomasti tiettyyn luokkaan, he varmasti kaatavat sinut.
Yksi asia, jonka halusin sisällyttää, mutta minulla ei ollut luonnollista sisäänkäyntipaikkaa ...
Pythonilla ei ole yksityinen muuttujat niin sanotusti, mutta toinen mielenkiintoinen suhde luokan ja ilmentymän nimeämisen välillä tulee nimen sekoittamisen kanssa.
Python-tyylioppaassa sanotaan, että pseudo-private -muuttujat on etuliitettävä kaksinkertaisella alaviivalla: __. Tämä ei ole vain merkki muille siitä, että muuttujasi on tarkoitus käsitellä yksityisesti, vaan myös tapa estää pääsy siihen. Tässä tarkoitan:
class Bar(object): def __init__(self): self.__zap = 1 a = Bar() a.__zap ## Traceback (most recent call last): ## File '', line 1, in ## AttributeError: 'Bar' object has no attribute '__baz' ## Hmm. So what's in the namespace? a.__dict__ {'_Bar__zap': 1} a._Bar__zap ## 1
Katsokaa sitä: esiintymäattribuutti __zap
lisätään automaattisesti luokan nimellä, jolloin saadaan _Bar__zap
Vaikka tämän nimen hallitseminen on edelleen määritettävissä ja haettavissa a._Bar__zap
: n avulla, se voi luoda yksityisen muuttujan, koska se estää sinua ja toiset pääsevät siihen vahingossa tai tietämättömyyden kautta.
Muokkaa: kuten Pedro Werneck huomautti ystävällisesti, tämän käytöksen tarkoituksena on suurelta osin auttaa alaluokkaa. vuonna PEP 8 -tyyliopas , heidän mielestään sillä on kaksi tarkoitusta: (1) estää alaluokkia pääsemästä tiettyihin määritteisiin ja (2) estää nimiavaruuden yhteenottoja näissä alaluokissa. Vaikka hyödyllistä, vaihtelevaa hallintaa ei pidä nähdä kutsuna kirjoittaa koodia oletetulla julkisen ja yksityisen erolla, kuten Java-sovelluksessa.
Liittyvät: Kehittyneempi: Vältä 10 yleisintä virhettä, joita Python-ohjelmoijat tekevätKuten nimestä voi päätellä, Python-nimitila on kartoitus nimistä esineisiin, sillä ominaisuudella, että eri nimitiloissa olevien nimien välillä ei ole nolla-suhdetta. Nimitilat toteutetaan yleensä Python-sanakirjoina, vaikka tämä onkin tiivistetty.
Pythonissa luokan menetelmä on menetelmä, jota kutsutaan luokan kontekstiksi. Tätä kutsutaan usein staattisiksi menetelmiksi muilla ohjelmointikielillä. Toisaalta esiintulomenetelmää kutsutaan kontekstina ilmentymäksi.
Tällöin ilmentymän nimiavaruus on etusijalla luokan nimiavaruuteen nähden. Jos molemmissa on määritteitä, joilla on sama nimi, ilmentymän nimiavaruus tarkistetaan ensin ja sen arvo palautetaan.