2017 m. gegužės 15 d.

Pycon 2017 LT: Prototipinio OOP įgyvendinimas su Python

Šeštadienį Kaune vyko Pycon LT 2017 konferencija (http://pycon.lt/). Čia mano pranešimo ("Prototipinio OOP įgyvendinimas su Python"), kurį aš skaičiau, tekstas.

Skaidrės:
http://petraszd.com/pycon-2017/

Kodo pavyzdžiai:
https://bitbucket.org/petraszd/pyconlt-2017-demo
Pačios kalbos įrašas:
https://www.youtube.com/watch?v=Jcgp5-8_XkU



--------------------

Sveiki,

Aš esu Petras Zdanavičius. Šiandieną aš jums pristatysiu pranešimą apie prototipinį OOP ir parodysiu demonstracinį jo įgyvendinimą Python pagalba.

Pranešimas nebus naudingas praktiškai. Tai daugiau kompiuterių mokslo ar net kompiuterių filosofijos tema. Filosofijos, nes aš kaip ir kiekvienas programuotojas, mėgstu savintis visuomenėje gerbiamus irba mėgstamus titulus. Kaip menininkas, kūrėjas, filosofas [pauzė], ninzė. Ar roko žvaigždė (rockstar).

Nes jo -- kiekvienas programuotojas tamsioje kamūrkėje po 80-uom valandų per savaitę kalantis kodą, turi ekvivalentų gyvenimo būdą kaip roko žvaigždė.

Apie ką aš čia? A. Tai va, aš jums papasakosiu apie prototipais paremtą OOP.

OOP Apibrėžimas I

Jeigu, jūs programuojate (ypač jeigu jūs programuojate Python), jūs esate susidūręs su OOP.

OOP - programavimo paradigma, kompiuterinių programų architektūroje naudojanti objektus ir jų sąveikas [Wikipedia]

Čia Lietuvos Wikipedia taip sako. Aš nieko nesuprantu, kas čia bandoma pasakyti.

OOP Apibrėžimas II

Mano apibrėžimas. OOP:

- Yra objektai
- Objekto viduje saugoma jo būsena. Atributų pavidalu
- Objektas turi sąsają, bendravimui su išoria. Metodų pavidalu
- Objektai sąveikauja vieni su kitais ne tiesiogiai keisdami vienas kito būseną, bet siuntinėdami vienas kitam žinutes. Metodų kvietimo pavidalu

[Petras (c)]

"Klasikinis" OOP

Kai mes sakome OOP, tai galvoje paprastai turime klasėmis paremtą variantą. Nu tą klasikinį. Nu tą, kaip Java. Kur yra klasės. Jose apsirašai, kaip veiks objektai. Ta klasė, tai tarsi kažkoks Platono formų teorijos atitikmuo. Kur tobulos formos egzistuoja tik abstrakčiame idėjų pasaulyje. O empiriniame pasaulyje (Dar žinomame kaip realybė) egzistuoja tik netobulos jų kopijos. Panašiai kaip tobulos klasės ir netobuli objektai.

OOP Istorija

1970-iais Alan Kay vadovaujama Xerox PARC komanda sukuria Smalltalk programavimo kalbą. Kalba netinkamas žodis. Programavimo sistemą. Programavimo mašiną? Tai turėjo būti nauja paradigma. Kaip jie patys teigė "naujojo pasaulio" "Žmogaus ir mašinos simbiozė" paradigma. Ar nesakiau, kad programuotojai mėgsta savintis skambius žodžius.

Ir jų sistema labai skyrėsi nuo to, ką turime dabar. Ta programavimo kalba nebuvo atskiriama nuo IDE (Integruota kūrimo aplinkos). Ta prasme, viskas buvo vienoje aplikacijoje. Ir teksto redaktorius ir versijavimas, ir dokumentacija, ir atsarginės kopijos, ir programos būsena irgi buvo Smalltalk mašinoje.

Iš dabartinių aplinkų turbūt būtų galima palyginti su Racket. Arba žaidimų varikliukų aplinkomis. Tokių kaip Unreal ar Unity. Kurie lenkia visas šlubas sistemas irba konfigūracijas, kurias mes naudojame programuodami Web su Python. Ir lenkia keliais dešimtmečiais.

Bet grįžtant prie istorijos. Tada atėjo Bjarne Stroustrup. Jam patiko OOP idėjos. Ir jis jas paėmė ir dalį jų daugmaž pritempė prie C ir parašė transliatorių į C, kurį pavadino C++.

Tada atėjo Syn Microsystems, pasakė, kad C++ yra perdaug kosmosas. Paėmė C++ pseudo OOP ir apgludino aštrius kampus. Dabar mes todėl turime Java. Java bei C++ šiaip laikais yra laikomos kaip etaloninės, pavyzdinės OOP kalbos. O Alan Kay verkia kamputyje.

Prototipinio OOP Istorija

Toje pačioje Xerox PARC toks David Ungar ir toks Randall Smith dirbdami su Smalltalk'u nusprendžia, kad tas dualizmas tarp klasių ir objektų yra ne fengšui. Kad kodo bazės egzistavimo cikle vis tiek prisireikia keisti klasių struktūrą. Kartais vien dėl to, kad nu va reikia tokio vieno objekto. Nu vieno vienintelio mažučiuko. Nu bet labai reikia. Nu bet būtent tau reikia vieno objekto. Nu reeeiiikiaaa. Ir dėl to vieno objekto reikia griauti velniop visą klasių hierarchiją ir kažkaip įterpti tą naują klasę tam naujam objektui.

Nors realiai, tai paimi bet kurią esamą klasę, prirašai ten if'ų ir normaliai. Sukasi kaip bitė.

Tai jiems kilo klausimas: "o kas jeigu galėtum praplėsti ne klases, o objektus". Iš to automatiškai seka kitas klausimas: "o kas jeigu nėra klasių, o yra tik objektai?".

Self

Ir būtent taip gimė programavimo kalba "Self".

Kadangi tais laikais žmonės vis dar nebuvo praradę ūpo eksperimentuot su kodo redaktoriais (kitaip nei šiais, kai naujausi ir populiariausi kodo redaktoriai yra naršyklės... Kaip "Visual Studio Code" ar "Atom"), tai jie paėmė ir tuo pačiu sukūrė integruotą kūrimo aplinką, kuri atrodė taip.

[Self nuotrauka]

(Petro pastaba po visko: patingėjiau pasidaryti Self VM nuotrauką. Tai ir nebuvo nei nuotraukos, nei šito teksto)

JavaScript

Kalba buvo eksperimentitnė. Ji vis dar gyva. Bet šiaip visas prototipinis konceptas būtų miręs, jeigu ne 10 dienų Brendan Eich gyvenime. Brendan'as 1995 turėjo tokią užduotį sukurti programavimo kalbą. Jis tuo metu buvo susižavėjęs dviem kitoms kalbomis. "Schema" (kas yra Lisp-1) ir jau minėtoji "Self". Tai jis ir kūrė kalbą, kuri buvo keista tų dviejų sąjunga. Neekstremaliai humaniškai funkcinė (bet ne visai) prototipais paremta OOP kalba.

Aij, ir tuo metu buvo ant bangos Java, tai Brendan'ui buvo pasakyta, kad jo kalba turi atrodyti kaip Java.

Štai kaip mes dabar turime turbūt pačią populiariausią kaip nepagrindinę programavimo kalbą JavaScript. Nepagrindinę, nes paprastai žmonės programuoja kažkuo ir tada ant viršaus dar JavaScript.

Ir didokas procentas JavaScript programuotojų nė velnio nežino, kad ten yra kažkokie prototipai ir iš vis, kas jie ir kaip jie veikia.

Dėl to aš jums pabandysiu parodyti Python'o kodo pagalba, kas yra prototipais paremta objektinė sistema.

Kodas (Kas yra kanoninis objektas)

Pagal Java'inį OOP apibrėžimą yra trys OOP banginiai: enkapsulecija, paveldėjimas ir polimorfizmas. Pats Python'as realiai iš jų įgyvendina tik paveldėjimą. Tai čia mes irgi daugiausia dėmesio kreipsime į paveldėjimą.

Klasikiniame modelyje paveldėjimas vyksta klasių lygmenyje. O prototipiniame paveldėjimas vyksta objektų lygmenyje.

Pradėkime nuo to, kad tai yra prototipinis modelis. Turi būti bent vienas kanoninis objektas, kūrį galime praplėsti savo reikmėmis.

Mes kurdami naudojame Python programavimo kalbą. Viena pagrindinių ir galingiausių Python'o kalbos įrankių yra žodynas (dict). Tai ir mes savo implementacijoje kaip vidinę objekto struktūrą naudosime Objektą

Object = {
    '__proto__': {}
}


Kaip matote, tai yra tiesiog žodynas, savyje turintis vieną mums magišką raktą `__proto__`. Kas esate susidūrę su JavaScript, jau galite įtarti kaip viskas veiks.

Ir tuo pačiu susitarsime, kad niekad pačio žodyno tiesiogiai neliesime. Tam naudosime `proto` modulio funkcijas.

Taigi, pradžioja mums reikia būdų tą objektą praplėsti ir tuo pačiu būdo kaip sukurti naują objektą, jeigu mums nusispjauti, kas jo prototipas.

obj1 = p.create({'a': 'a-1', 'b': 'b-1'})
obj2 = p.extend(obj1)

assert obj1['a'] == 'a-1'
assert obj2['__proto__'] == obj1

print("It is fine!!!")


Tam bus dvi funkcijos. `create`, kuri pagal nutylėjimą praplės standartinį `Object` ir tuo pačiu priskirs jam kažkokius atributus.

Kita bus `extend`. Jos esmė bus praplėsti jau egzistuojantį objektą.

Pažiūrim, ar jos veikia. Veikia. Dabar pažiūrim, kaip jos parašytos.

def extend(other, keys=None):
    obj = {'__proto__': other}
    if keys is not None:
        obj.update(keys)
    return obj


def create(keys=None):
    if keys and '__proto__' in keys:
        prototype = keys.pop('__proto__')
    else:
        prototype = Object
    obj = extend(prototype, keys)
    return obj


Ganėtinai paprastai. Pirma sukuria naują žodyną. `__proto__` raktą nurodo į praplečiamą objektą ir jeigu reikia sukuria naujus reikšmių raktus.

`create` tuo tarpu tiesiog shortcut'as į `extend`.

Einam prie įdomesnio funkcionalumo. Galimybės keisti objektų būseną. Funkcinio programavimo fanai ir karvės to nesupras.

obj1 = p.create({'a': 'a-1'})

obj2 = p.extend(obj1)
p.set(obj2, 'b', 'b-2')

assert p.get(obj2, 'a') == 'a-1'
assert p.get(obj2, 'b') == 'b-2'

p.set(obj2, 'a', 'a-2')
assert p.get(obj2, 'a') == 'a-2'
assert p.get(obj2, 'b') == 'b-2'

assert p.get(obj1, 'a') == 'a-1'

assert p.get(obj2, 'c') is None
print("It is fine!!!")


Mes norime gauti ir keisti objektų atributus. Kadangi susitarėme, kad tiesiogiai pačių Python'o žodynų neliesime, tai tam turime dvi pagalbines funkcijas, išradingais pavadinimais `get` ir `set`.

Pažiūrim, ar veikia. Veikia. Pažiūrim, kaip ir kodėl veikia.

def set(obj, attribute_name, value):
    obj[attribute_name] = value
    return value


def get(obj, attribute_name):
    current_obj = obj
    while True:
        if attribute_name in current_obj:
            return current_obj[attribute_name]
        current_obj = current_obj.get('__proto__', None)
        if current_obj is None:
            break

    return None


`set` yra žymiai paprastesnė. Ji tiesiog pakeičia arba sukuria vidinio Python žodyno raktą. Viskas.

`get` yra įdomesnis. Jame ir yra visa esmė. Pradedame nuo esamo objekto ir ieškome norimo atributo jame. Jeigu neradome keliaujame giliau į jo prototipą.  Vėl ieškome atributo. Ir taip kol ką nors surandame arba baigiasi prototipai.

Pastaba: Norint sudirbti algoritmą tereikia paimti sukurti 2 objektus ir jų prototipus nurodyti vienas į kitą.

Ir čia yra visa esmė.

Bet OOP nebūtų OOP, jeigu nebūtų metodų ir jų kvietimo.

def get_two():
    return 2


obj3 = p.create({'get_two': get_two})
assert p.call(obj3, 'get_two') == 2

obj4 = p.extend(obj3)
assert p.call(obj4, 'get_two') == 2

print("It is fine!!!")


Čia turime `call` funkciją. Aš jums jos implementaticos dar nerodysiu, nes pereisime prie magijos.

def get_foo_plus_1():
    return p.get(this, 'foo') + 1


proto_obj = p.create({
    'get_foo_plus_1': get_foo_plus_1
})


obj5 = p.extend(proto_obj, {'foo': 1})
obj6 = p.extend(proto_obj, {'foo': 2})

assert p.call(obj5, 'get_foo_plus_1') == 2
assert p.call(obj6, 'get_foo_plus_1') == 3
print("It is fine!!!"
)

Kažkaip norisi įdomesnių metodų. Kurie pasiektų patį objektą. Tam mums reikia `this`.

Visas šitas marazmas, beje, veikia.

Implementacija. Gink die, nedarykite niekad taip.

def call(obj, attribute_name, *args):
    function = get(obj, attribute_name)

    this_backup = this
    _builtins.this = obj
    result = function(*args)
    _builtins.this = this_backup
    return result


try:
    import builtins as _builtins
except ImportError:
    import __builtin__ as _builtins
_this = create()
_builtins.this = _this


Jeigu paimsite JavaScript. Tai toje kalboje visada yra `this` kintamasis. Visada. Net ir globalioje vardų erdvėje (namespace). Tai ir mes tokį va sukuriame. Geras? Ne? Python 2 ir 3 skiriasi, bet abu leidžia nesudėtingai šaudyti sau į kojas.

Tada `call`. Paprasta. Pernaudojame `get`, kad gauti funkciją. Nes ji gali būti ir giliau. Išsisaugojame dabartinę `this` reikšmę. Nustatome ją į einamą objektą. Iškviečiam funkciją. Gražiname prieš tai buvusį this.

Pabaiga

Kaip ir viskas. Ačiū, kad klausėte. Jeigu turite klausimų (nelabai įsivaizduoju kokių), tai mielai į juos atsakysiu.