Апстракција података¶
Док се разматра широк скуп ствари у свету који нас окружује које би било корисно представити у програмима, открива се да већина њих има сложену структуру. На пример, географски положај се састоји из географске ширине и географске висине које образују пар, сложену вредност података којом програми могу руковати као појмовном јединицом, али која се такође састоји из два дела који се могу посматрати појединачно.
Употреба сложених података омогућава повећање модуларности програма. Уколико је могуће руковати географским положајима као целинама, тада је могуће заштитити делове програма који обављају израчунавања над положајима од појединости везаних за то како су ти положаји представљени. Општа техника изолације делова програма који се баве начином на који су подаци представљени од делова који се баве како се тим подацима манипулише је моћна методологија пројектовања која се назива апстракција података. Апстракција података у многоме олакшава пројектовање, одржавање и измену програма.
Апстракција података је по карактеру слична функционалној апстракцији. Приликом прављења функционалне апстракције, појединости о томе како се функција имплементира могу се потиснути, а сама та функција може бити замењена било којом другом функцијом са истим свеукупним понашањем. Другим речима, могуће је направити апстракцију која раздваја начин на који се функција употребљава од појединости о томе како је функција имплементирана. Аналогно томе, апстракција података изолује начин употребе сложених вредности података од појединости како су заправо ти подаци изграђени.
Основна идеја апстракције података је такво устројство програма да они оперишу над апстрактним подацима. Односно, програми треба да користе податке на такав начин да праве што је могуће мање претпоставки о тим подацима. Истовремено, конкретна представа податка је дефинисана као независни део програма.
Ова два дела програма, део који оперише над апстрактним подацима и део који дефинише њихову конкретну представу, су повезани малобројним скупом функција које имплементирају апстрактне податке у погледу конкретне представе. Ради илустрације ове технике, биће размотрено како пројектовати скуп функција за баратање рационалним бројевима.
Пример: рационални бројеви¶
Рационалан број је заправо однос (разломак) целих бројева. Рационални бројеви су важна поткласа реалних бројева. Разломци, као што су 1/3 или 17/29 се обично пишу као:
<бројилац>/<именилац>
где оба поља и <бројилац>
и <именилац>
представљају места за целобројне вредности. Наравно, оба су неопходна да би се тачно окарактерисала вредност рационалног броја. Заправо, дељењем целих бројева добија се float
апроксимативна вредност у покретном зарезу чиме се губи на прецизности представе.
>>> 1/3
0.3333333333333333
>>> 1/3 == 0.333333333333333300000 # Дељење целобројних вредности је апроксимативно
True
Међутим, могуће је направити тачан приказ рационалних бројева комбиновањем бројиоца и имениоца.
Познато је још из функционалних апстракција да се може почети са плодотворним програмирањем пре него што су имплементирани неки делови програма. Стога, претпоставимо за почетак да већ постоји начин да се направи рационалан број помоћу имениоца и делиоца. Такође, разумна је и претпоставка да за дати рационални број постоји начин да се изабере бројилац и именилац. Коначно, претпоставимо да су конструктор и селектори доступни као следеће три функције:
разломак(б, и)
враћа рационални број са бројиоцемб
и имениоцеми
.бројилац(x)
враћа бројилац рационалног бројаx
.именилац(x)
враћа именилац рационалног бројаx
.
Овде је искоришћена моћна стратегија за пројектовање програма: пуштање машти на вољу или узимање жељеног за стварно. Још увек није речено како је рационални број представљен или како функције разломак
, бројилац
и именилац
треба имплементирати. Чак и да ове три функције јесу дефинисане, могле би се употпунити функцијама за додавање, множење, исписивање, и проверу једнакости рационалних бројева:
>>> def збирРазломака(x, y):
... бx, иx = бројилац(x), именилац(x)
... бy, иy = бројилац(y), именилац(y)
... return разломак(бx * иy + бy * иx, иx * иy)
>>> def производРазломака(x, y):
... return разломак(бројилац(x) * бројилац(y), именилац(x) * именилац(y))
>>> def исписРазломка(x):
... print(бројилац(x), '/', именилац(x))
>>> def једнакостРазломака(x, y):
... return бројилац(x) * именилац(y) == бројилац(y) * именилац(x)
Сада имамо операције над рационалним бројевима дефинисане преко селекторских функција бројилац
и именилац
, и конструкторске функције разломак
, иако ове функције тек треба да буду дефинисане. Оно што је потребно јесте начин да се бројилац и именилац заједно слепе у једну сложену вредност.
Парови¶
Како би омогућио имплементацију конкретног нивоа апстракције података, Пајтон нуди сложену структуру под називом низ, која се може направити стављањем израза одвојених зарезима између угластих заграда. Такав израз се зове литерал низа.
>>> [10, 20]
[10, 20]
Члановима низа се може приступити на два начина. Први начин је путем познате методе вишеструке доделе која распакује низ у чланове и повезује сваки члан на различито име.
>>> пар = [10, 20]
>>> пар
[10, 20]
>>> x, y = пар
>>> x
10
>>> y
20
Друга метода за приступање члановима низа јесте преко оператора за одабир чланова, такође израженог преко угластих заграда. За разлику од литерала низа, израз у угластим заградама који непосредно следи након неког израза се не вреднује у низовну вредност, већ уместо тога бира члан из вредности претходног израза.
>>> пар[0]
10
>>> пар[1]
20
Низови у Пајтону (као и у многим другим програмским језицима) су 0-индексирани, што значи да индекс 0 означава први члан низа, индекс 1 означава други, и тако даље. Интуиција која подржава овај начин индексирања јесте да индекс заправо представља колико је одређени члан померен у односу на почетак низа.
Функција еквивалентна оператору за одабир чланова се назива getitem
, и она такође користи 0-индексиране позиције за одабир чланова низа.
>>> from operator import getitem
>>> getitem(пар, 0)
10
>>> getitem(пар, 1)
20
Двочлани низови нису једини начин представе парова у Пајтону. Било који начин спајања две вредности заједно у једну може се сматрати паром. Низови су уобичајен начин за то. Низови такође могу садржати и више од два члана, као што ће бити приказано касније у овом поглављу.
Представљање рационалних бројева¶
После свега, рационалан број се може представити као пар два цела броја: бројиоца и имениоца.
>>> def разломак(б, и):
... return [б, и]
>>> def бројилац(x):
... return x[0]
>>> def именилац(x):
... return x[1]
Заједно с раније дефинисаним аритметичким операцијама, могуће је манипулисати рационалним бројевима.
>>> половина = разломак(1, 2)
>>> исписРазломка(половина)
1 / 2
>>> трећина = разломак(1, 3)
>>> исписРазломка(производРазломака(половина, трећина))
1 / 6
>>> исписРазломка(збирРазломака(трећина, трећина))
6 / 9
Као што горњи пример показује, ова имплементација рационалних бројева не своди разломке на њихов најједноставнији облик, то јест облик у ком бројилац и именилац немају заједничких делитеља изузев јединице (односно, бројилац и именилац нису узајамно прости). Ова мана може се отклонити променом имплементације разломка. Уколико је при руци функција за израчунавање највећег заједничког делиоца (НЗД) два цела броја, тада се она може искористити за свођење имениоца и бројиоца на узајамно прост облик пре прављења пара. Као и многи други корисни алати, таква функција већ постоји у Пајтон стандардној библиотеци.
>>> from math import gcd
>>> def разломак(б, и):
... нзд = gcd(б, и)
... return (б//нзд, и//нзд)
Оператор целобројног дељења, //
, одсеца децимални део резултата дељења. Ипак, пошто се зна да су и б
и и
дељиви са нзд
, целобројно дељење је у овом случају егзактно јер не даје остатак. Ова прерађена имплементација разломка осигурава да се рационални бројеви изражавају у свом сведеном облику.
>>> исписРазломка(збирРазломака(трећина, трећина))
2 / 3
Приказано побољшање је постигнуто изменом конструктора без промене у било којој функцији која имплементира стварне аритметичке операције.
Апстракцијске баријере¶
Пре изношења додатних примера сложених података и апстракција података, размотримо нека питања постављена у примеру рационалних бројева. Операције су дефинисане преко конструктора разломак
и селектора бројилац
и именилац
. Уопштено говорећи, основна идеја апстракције података је идентификовање скупа основних операција преко којих ће бити изражено руковање вредностима неке врсте, а затим користити само те операције за баратање подацима. Ограничавањем броја и употребе операција на овај начин, далеко је лакше изменити представу апстрактних података у будућности (нпр. ради повећања ефикасности) без промене понашања програма.
За рационалне бројеве, различити делови програма манипулишу рационалним бројевима користећи различите операције, како је описано у следећој табели.
Делови програма који… |
Рукују разломцима као… |
Користећи само… |
---|---|---|
Користе рационалне бројеве за рачунање |
целинама |
|
Праве рационалне бројеве или имплементирају операције над њима |
имениоцима и бројиоцима |
|
Имплементирају селекторе и конструктор за рационалне бројеве |
двочланим низовима |
литерале низа и оператор за одабир чланова |
У сваком од горњих слојева, односно врста у табели, функције у последњој колони намећу апстракцијску баријеру. Ове функције се позивају од стране вишег нивоа, а имплементирају користећи нижи ниво апстракције.
До кршења апстракцијске баријере долази кад год део програма који може да користи функцију вишег нивоа уместо тога искористи функцију на нижем нивоу. Примера ради, функцију која израчунава квадрат рационалног броја најбоље је реализовати преко функције производРазломака
која не чини никакве претпоставке о начину имплементације рационалног броја.
>>> def квадратРазломка(x):
... return производРазломака(x, x)
Директно обраћање бројиоцима и имениоцима би кршило једну апстракцијску баријеру.
>>> def квадратРазломка_једнострукоКршење(x):
... return разломак(бројилац(x) * бројилац(x), именилац(x) * именилац(x))
Претпоставка да су разломци представљени користећи се двочланим низовима би прекршило две апстракцијске баријере.
>>> def квадратРазломка_двострукоКршење(x):
... return [x[0] * x[0], x[1] * x[1]]
Апстракцијске баријере олакшавају одржавање и промену програма. Што је мање функција које зависе од одређеног начина представе, то је потребно мање измена када се та представа жели променити. Све претходне имплементације функције квадратРазломка
имају истоветно и тачно понашање, али је само прва отпорна на будуће промене. Функцију квадратРазломка
не би било потребно ажурирати чак и ако би се изменила представа рационалних бројева. Насупрот томе, функцију квадратРазломка_једнострукоКршење
би требало мењати кад год се промене селектор или конструктор, док би функција квадратРазломка_двострукоКршење
захтевала ажурирање приликом сваке промене имплементације рационалних бројева.
Својства података¶
Апстракцијске баријере у многоме обликују начин размишљања о подацима. Исправна представа рационалног броја није ограничена на било коју одређену имплементацију (као што је двочлани низ) већ је то просто повратна вредност функције разломак
која може бити прослеђена функцијама бројилац
и именилац
. Поред тога, мора постојати одговарајући однос између конструктора и селектора. То јест, ако се из целих бројева б
и и
направи рационалан број x
, тада би требало и да је бројилац(x)/именилац(x)
једнако б/и
.
Уопштено гледано, апстрактне податке је могуће изразити користећи се збирком селектора и конструктора, заједно са још неким условима понашања. Све док су услови понашања задовољени (попут горњег својства дељења), селектори и конструктори ће представљати ваљану представу одређене врсте податка. Појединости у имлементацији испод апстракцијске баријере се могу променити, али ако понашање остане исто, апстракција податка такође остаје важећа, а самим тим и сваки програм написан користећи ову апстракцију ће остати исправан.
Ова тачка гледишта може бити широко примењена, укључујући ту и парове вредности који су коришћени за имплементацију рационалних бројева. Није много речено о томе шта је заправо пар, већ само да језик обезбеђује средства за прављење и манипулисање низовима са два члана. Неопходно понашање за имплементацију пара није ништа друго до тога да лепи две вредности заједно. Наведено преко стања понашања,
Уколико је пар
п
направљен из вредностиx
иy
, тадаизабери(п, 0)
враћаx
, аизабери(п, 1)
враћаy
.
Заправо низ као тип податка и није потребан за прављење парова. Уместо тога, могу се иплементирати две функције пар
и изабери
које испуњавају претходни опис подједнако као и двочлани низ.
>>> def пар(x, y):
... """Враћа функцију која представља пар."""
... def узми(индекс):
... if индекс == 0:
... return x
... elif индекс == 1:
... return y
... return узми
>>> def изабери(пар, индекс):
... """Враћа члан на индексу индекс пара пар."""
... return пар(индекс)
Овом имплементацијом, могуће је стварање и манипулисање паровима.
>>> п = пар(20, 14)
>>> изабери(п, 0)
20
>>> изабери(п, 1)
14
Претходно коришћење функција вишег реда уопште не одговара интуитивној представи како подаци треба да изгледају. Упркос томе, ове функције довољно добро представљају пар у досадашњим програмима. Функције су саме по себи довољне за представљање сложених података.
Смисао излагања функционалне представе пара није у томе да изнесе тврдњу по којој и сам Пајтон заправо то ради на овај начин (због ефикасности, низови су у стварности имплементирани директније), већ само да би могао радити и на овај начин. Функционална представа, иако нејасна и неразговетна, савршено је адекватна за представљање парова пошто задовољава све услове које парови морају да задовоље. Упражњавање апстракције података омогућава лак прелазак између различитих представа.