Olen tekemässä Pythonilla kirjastoa Elisa Viihteen ohjaukseen. Jokainen tallennettu ohjelma, asetettu ajastus tms. on olio. Viihteen APIsta johtuen on kuitenkin mahdollista, että samaa ohjelmaa vastaa kaksi eri oliota. Tällainen tilanne tulee esimerkiksi siinä tapauksessa, että hakee saman tallenteen sekä tallenteista että ohjelmaoppaasta. Nyt toisen olion uudelleennimeäminen ei vaikuta automaattisesti toiseen, sillä sivupyyntöjen vähentämiseksi nimeä ei haeta joka kerta uudestaan.
Mitä ratkaisua suosittelette, vai kannattaako tuo vain jättää kirjaston käyttäjän huoleksi?
Mitä jos muodostat ohjelmille itse jonkin yksilöllisen tunnisteen. Esim. aloitusajasta ja kanavasta muodostuvan.
Ohjelmilla kyllä on valmiiksi yksilöllinen id, joten se ei ole ongelma. Tarkoitatko, että en käyttäisi olioita ollenkaan? Mielestäni kyllä ohjelma.remove()
on siistimpi kuin eviihde_remove (yhteys, ohjelma)
.
Yksi vaihtoehto tietysti on muuttaa koko kirjastoa siten, että sen funktiot vastaavat suoraan viihteen APIa, jolloin ei tule ongelmia välimuistista.
No mitä jos määrität olion niin, että mikäli id:tä vastaava olio on jo olemassa, ei luodakaan uutta vaan palautetaan viittaus olemassaolevaan olioon.
Miten toteuttaisit tuon Pythonilla? Kirjasto toimii siten, että on yksi eViihde-olio, jonka metodit palauttavat muita olioita. Voin tietysti säilyttää kaikki oliot pääolion sanakirjassa, josta ne saa id:n perusteella. Luulen kuitenkin, että Python tarjoaa tähän elegantimman ratkaisun, ja juuri sitä olen hakemassa.
Tässä on yksi tapa toteuttaa luokan instanssien varastoiminen. Tein sen Python 2.7:llä, joten voit joutua muuttamaan joitakin kohtia tarpeen mukaan muita versioita varten. Käytetyn Pythonin täytyy olla sen verran uusi, että se tukee ns. uuden tyylin luokkia, jotka ovat olleet käytössä jostakin 2.X-versiosta lähtien.
Tätä ei ole mitenkään testattu oikeassa käytössä, joten se voi aiheuttaa yllättäviä virhetilanteita väärin käytettynä (tämä on sellainen "user error" sitten :-). Lisäksi olisi varmaan järkevää määritellä tärkeät kentät ominaisuuksiksi, eli propertyiksi. Ks. propertyt Pythonin dokumentaatiossa. Tällöin number-kentän muuttamisen jo luodussa oliossa voisi havaita ja siihen voisi jotenkin reagoida.
Idea on siis, että näissä esimerkkiolioissa jokainen samalla numerolla (param. number) luotu olio onkin sama instanssi. Loppukoodi demonstroi tätä luomalla kaksi Esimerkkiä, joiden ensimmäinen parametri on luku 3, sekä kolmannen, jossa kyseinen parametri on eri luku.
(Pahoittelen suomen ja englannin sekakielisyyttä koodinpätkässä. Lisäksi __str__-metodin tuloksesta jäi pari lainausmerkkiä pois.)
# -*- coding: utf-8 -*- # Esimerkki __new__-metodin omasta määritelystä. # Menettelyä ei ole testattu käytännössä, eikä siihen kannata siksi # luottaa kovin paljon. class Esimerkki(object): instance_cache = {} def __new__(cls, number, name): cache = Esimerkki.instance_cache if number in cache: return cache[number] else: new_instance = super(Esimerkki, cls).__new__(cls) cache[number] = new_instance return new_instance def __init__(self, number, name): self.number = number self.name = name def __str__(self): return "Esimerkki(%d, %s)" % (self.number, self.name) eka = Esimerkki(3, "makkara") print "Eka on", eka toka = Esimerkki(3, "peruna") koka = Esimerkki(7, "juusto") print "Nyt ne ovat", eka, toka, koka toka.name = "olut" print "Ja lopuksi ne ovat", eka, toka, koka
Tulostus, jonka sain on seuraava. Huomattavaa on se, että uuden olion luonti vaikuttaa jo olemassaolevan olion sisältöön, kun luontiin käytetty luku on sama. Lisäksi yhden olion kentän muuttaminen muuttaa sen toisessakin, koska kyseessä on itse asiassa kaksi viittausta samaan olioon.
Metodia __init__ kutsutaan aina, kun __new__ palauttaa instanssin.
Eka on Esimerkki(3, makkara) Nyt ne ovat Esimerkki(3, peruna) Esimerkki(3, peruna) Esimerkki(7, juusto) Ja lopuksi ne ovat Esimerkki(3, olut) Esimerkki(3, olut) Esimerkki(7, juusto)
Kiitos Pekka Karjalainen esimerkistä. __new__-metodi ja instance_cache oli se, mitä hain.
Miten antamasi koodi suoriutuu olioiden poistamisesta? Käsittääkseni instance_cacheen jää viittaus olioon, jolloin Python ei poista oliota ikinä. Oliotahan ei tarvitse pitää cachessa enää sen jälkeen, kun siihen ei ole viittauksia muualla.
Jälkimmäiseen ongelmaan auttaisi varmaan weakref.
Weakref näyttää tekevän sen mitä haluan, kiitos.
Hyvä, että ajattelitte tarkasti poistoakin. Ideani ja koodini on vielä sangen keskeneräinen. En myöskään miettinyt tarkasti miten tämä menettely toimisi perinnän kanssa, tai edes miten sen pitäisi järkevimmin toimia. Laiska ratkaisu olisi, että varoittaisi käyttäjiä olemaan perimättä luokasta, joka toimii tällä tavalla, niin ongelmaa ei tarvitsisi ratkaista ollenkaan.
Yksi ajatus kävi myös mielessä, nimittäin että tällaisen ominaisuuden voisi lisätä luokkaan luokkadekoraattorilla tai jopa metaclass-ominaisuudella, mutta omassa Python-harrastuksessa en ole näitä tekniikoita vielä soveltanut. Paha mennä siis äkkiä sanomaan ovatko ne parempia tapoja, vai aivan liikaa asiaan, joka hoituisi yksinkertaisestikin. Esim. oliokielissä, joissa muodostimien määrittely uudestaan ei ole näin vapaata, oikea tekniikka olisi varmaankin sellainen, mitä englanniksi kutsutaan joskos termillä "factory method".
Mietin vielä vähän tämän koodin toimintaa, ja jos olennaisia korjauksia (sellaisia joita ei vielä mainittu) on aihetta esittää, laitan vielä näytille toisen version koodista. Tai jos joku muu keksii, miten perinnän kanssa tulisi tässä toimia, kannattaa tietenkin kertoa. Olisi minullekin opiksi :)
Koodissa on vielä se ongelma, etten saa helposti liitettyä sitä useaan luokkaan muulla tavalla kuin copy-pastella, sillä konstruktorin parametrit eivät ole kaikissa luokissa samannimisiä. Muutenhan voisin laittaa __new__- ja __del__-metodit omaan luokkaansa ja periyttää muut siitä. Nyt esimerkiksi Folder-luokka on seuraavanlainen:
def __init__(self, viihde, folderid, parent = None, name = None, size = None, reccount = None):
Tärkeitä parametrejä __new__-metodin kannalta ovat viihde sekä folderid. Viihde-parametrissä on eViihde-olio, jonka sanakirjassa (cls, id)-avaimilla on siihen liittyvät oliot. Idhän on tässä tapauksessa folderid ja cls Folder. Melkein toimiva, ehkä hieman purkkamainen ratkaisu on käyttää __new__-metodissa *args-taulukon kahta ensimmäistä arvoa, mutta se rikkoo kword-yhteensopivuuden.
Voisin tietysti laittaa __new__-metodien yhteiset osat funktioon ja kirjoittaa jokaiselle luokalle oman metodinsa, joka poikkeaa vain parametrien nimien osalta ja kutsuu tuota funktiota.
Luin myös jostain, ettei __new__-metodia ole tarkoitettu tähän käyttöön. Tuskin tällaisilla seikoilla on näin pienessä kirjastossa merkitystä, mutta haluaisin silti tehtä asiat kunnolla.
Täällä suositellaan käyttämään Factorya __init__- ja __new__-metodien sijasta hieman samankaltaisessa tilanteessa. __new__:tä olisi kuulemma suositeltavaa käyttää vain kun perii oman luokan jostakin Pythonin perustyypistä, joka on muokkaamaton (immutable).
http://stackoverflow.com/questions/674304/
Ohjeet nyt ovat vain ohjeita. Niitä voi rikkoa harkiten.
Minusta kyllä sellainen ratkaisu, jonka voisi johtaa yo. vastauksessa tarjotusta esimerkistä voisi olla hyvä. Tekisit Factory-olion, jolle voisi rekisteröidä luokkien muodostimia. Muodostimet Pythonissa käyttäytyvät aika lailla kuten mikä tahansa funktio joten ne voi käsittääkseni vain välittää parametreinä ja pistää talteen.
Lisäksi muodostimen rekisteröinnin yhteydessä pitäisi antaa funktio (tai olio), joka valitsisi muodostimen argumenteista ne oleelliset ja jonka perusteella jokaisen uuden instanssin luomisen yhteydessä koodisi tarkistaisi, luodaanko todella uusi instanssi, vai palautetaanko edellinen. Tämän tekisit itse määrittelemillesi luokille.
Kirjastosi tarjoaisi sitten rajapinnan, jossa ei saisi kutsua Folder()-muodostinta suoraan, vaan jotakin seuraavan kaltaista: factory.create_Folder(...) (mutta paremmin nimetylle oliolle). Factoryn sisällä olisi tietenkin weakref-moduulista löytyvä sanakirja, ettei se pitäisi olioita turhaan hengissä.
Tämäkin tekisi kirjastosi luokista perimisen hankalaksi, koska uudet luokat pitäisi sitten kirjaston käyttäjän itse rekisteröidä oikealla tavalla. En siis vieläkään voi sanoa, että tämä olisi harkittu ratkaisu jota itsekin käyttäisin :)
Pitäisikö jopa kysyä Python-listalla? Minulla ei ole oikein aikaa miettiä tällaisia hienouksia juuri nyt, joten jätän keskustelun tässä vaiheessa paremmin osaaville & kerkeäville.
Aihe on jo aika vanha, joten et voi enää vastata siihen.