Modulaarsus ja koodistruktuur¶
Modulaarne programmeerimine¶
Suuremate programmide lähtekoodi pole otstarbekas salvestada ühte faili. Tarkvara paremaks haldamiseks ja koodide taaskasutuse võimaldamiseks on programmi lähtekood tihti jaotatud erinevatesse failidesse.
- Paljude tänapäeva programmide taga on meeletu hulk koodi. Probleemi ja sellele vastav lahendus jagatakse osadeks.
- Kui olemasolevad alaüksused on ikka liiga keerulised, siis jagatakse need omakorda veel osadeks jne, kuni saadakse paraja suurusega ülesanded, mida programmeerija suudab oma peas piisava täpsusega hallata.
- Loodud alaüksused ja neile vastavad lahendused (st. programmiosad) peavad üksteisest võimalikult vähe sõltuma, vastasel juhul peab programmeerija mõtlema mitmele ülesandele korraga.
- Funktsioonide/protseduuride (või muude objektide) lokaalne skoop võimaldab vajadusel funktsiooni/protseduuri sisu ümber kirjutada ilma, et see põhjustaks ootamatusi ülejäänud süsteemis.
Projekti koodi loetavus¶
Mõned inimesed arvavad, et kui kood töötab, siis töö sellega lõppeb. Tõenäoliselt need inimesed ei saa kunagi headeks progejateks. Kuna programmeerimine nõuab tihti rühmatööd, peab kood olema lisaks autorile ka teistele arusaadav. Isegi projekti üksi arendades võib olla kogu protsess drastiliselt raskendatud, kui sa ei jälgi koodi modulaarsuse ja struktureerituse reegleid.
Moodul¶
Pythoni moodul on .py laiendiga fail, mis sisaldab Pythoni programmeerimiskeeles kirjutatud lähekoodi (funktsioonide, protseduuride, muutujate/atribuutide ja klasside definitsioone, jne).
- Pythoni mooduli nimi on faili nimi ilma selle laiendita, nt.
foo.py
korral on mooduli nimifoo
- .pyc, .pyo $-$ Pythoni ettekompileeritud ja optimiseeritud koodid .py failist
- .so (Linux), .dll (Windows) $-$ Pythoni laiendusmoodulid (kompileeritud teegid)
Tänases loengus: Alustame enda loodud moodulitega ja siis proovime importida Pythoni standardteeke ning lõpuks ka kolmandate osapoolte teeke ja mooduleid.
Enda loodud moodulite import¶
Paiguta programmi mooduli failid samasse kausta $-$ projekti töökausta.
Moodul moodulFunkid
(moodulFunkid.py) sisaldab kursuse jooksul loodud funktsioonide definitsioone. Joonis 1 kujutab kahte moodulit mis asuvad samas arvuti failisüsteemi kaustas ehk kataloogis. Kursuse kodulehelt on võimalik alla laadida Joonisel 1 kujutatud projekti failid mis on salvestatud faili 1.zip.
Joonis 1. Moodulid mis asuvad samas kataloogis ehk kaustas. |
NB! Seega, kui soovid teatud moodulis defineeritud objekte taaskasutada mingis muus moodulis paiguta mõlemad moodulid samasse töökausta.
Lausend import
¶
Teise mooduli kasutamiseks, hetkel avatud moodulis, tuleb see importida kasutades lausendit import
. Importida saame terve mooduli (import
või konstruktsioon import/as
või konstruktsioon from/import/*
) või konktreetseid objekte kasutades konstruktsioone from/import
, from/import/as
. Kõik erinevad inportimise viisid on loetletud allpool:
import <moodul>
import <moodul> as <nimi> # Tavaliselt lühendame nime.
from <moodul> import <atribuut1>, <atribuut2>, <funktsioon1>, <klass1>
from <moodul> import <atribuut1> as <nimi>
from <moodul> import * # Impordib kõik, koodis mooduli nimele viitamine pole vajalik.
Kui imporditavat moodulit ei leita, siis tõstatakse erisus ImportError
.
Näide: Avame ja uurime moodulit Loeng10_1
millega on kaasas moodul moodulFunkid
(1.zip fail).
import moodulFunkid
print(dir(moodulFunkid)) # Mooduli sisu.
['__builtins__', '__cached__', '__doc__', '__file__', '__loader__', '__name__', '__package__', '__spec__', 'astenda', 'cacheit', 'factorial', 'fibonacci', 'keskmine', 'kuup', 'loendi_keskmine', 'my_map', 'namefunc', 'pi', 'polunoom', 'recursive_factorial', 'recursive_fibonacci', 'ruut', 'ruutvorrand', 'ruutvorrand1', 'ruutvorrand2', 'solkija']
Kindlale objektile viitamiseks kasutame punkti <moodul>.<objekt>
:
print(moodulFunkid) # Mis andmetüübiga on tegu?
print(moodulFunkid.ruutvorrand) # Viitan kindlale objektile.
<module 'moodulFunkid' from '/Users/dmitrikartofelev/Documents/WORK Teaching/YFX0500/Loeng 10/moodulFunkid.py'> <function ruutvorrand at 0x1043f3060>
Moodulis defineeritud atribuudile ehk muutujanimele viitamine:
moodulFunkid.pi # Atribuut, moodulis asuv muutuja.
3.141592653589793
Moodulis defineeritud funktsiooni kasutamine:
x1, x2 = moodulFunkid.ruutvorrand(1, 2, 3)
# ^ ^ ^ ^ ^
# moodul funktsioon argument
print('Ruutvorrandi lahend x1 =', x1)
print('Ruutvorrandi lahend x2 =', x2)
Ruutvorrandi lahend x1 = (-0.9999999999999999+1.4142135623730951j) Ruutvorrandi lahend x2 = (-1-1.4142135623730951j)
Keegi ei keela meil imporditud funktsioonile uut nime andmast:
rv = moodulFunkid.ruutvorrand # Uus nimi.
print('Nimetasin ümber', rv(-3, 5, 7))
Nimetasin ümber (-0.9067177514850918, 2.5733844181517584)
Tihti on kasulik mooduli nime lühendada. Impordime mooduli moodulFunkid
uuesti:
import moodulFunkid as mF # Lühendan mooduli nime.
x1, x2 = mF.ruutvorrand(1, 2, 3) # Väljakutse uue nimega.
print('Ruutvorrandi lahend x2 =', x2)
Ruutvorrandi lahend x2 = (-1-1.4142135623730951j)
from moodulFunkid import ruutvorrand # Kindla funktsiooni import.
x1, x2 = ruutvorrand(-1, -2, 33) # Nüüd pole vaja viidata moodulile.
print('Ruutvorrandi lahend x2 =', x2)
Ruutvorrandi lahend x2 = 4.830951894845301
from moodulFunkid import ruutvorrand as rv # Lühendame kindla funktsiooni nime.
x1, x2 = rv(-11, -2, 3) # pole vaja viidata moodulile
print('Ruutvorrandi lahend x2 =', x2)
Ruutvorrandi lahend x2 = 0.4391774449859364
Kõikide objektide import:
from moodulFunkid import * # Impordib nimed kohalikku nimeruumi.
x1, x2 = ruutvorrand(-3, 2, 3) # Pole vaja viidata mooduli nimele.
print('Ruutvorrandi lahend x2 =', x2)
print('Pi =', pi) # Pole vaja viidata mooduli nimele.
Ruutvorrandi lahend x2 = 1.3874258867227933 Pi = 3.141592653589793
Pakett¶
Pythoni pakett on failisüsteemi kataloogi puu, mille iga kataloog (kaust) sisaldab vähemalt ühte Pythoni moodulit nimega __init__
(__init__.py
). Fail __init__.py
on tühi või sisaldab suhtelisi viiteid.
- Pythoni paketi nimi on kataloogi (kausta) nimi.
- Pythoni paketi kasutus sarnaneb moodulil kasutusele.
Joonis 2 kujutab Pythoni paketi struktuuri ja faile.
Joonis 2. Pythoni paketti struktuur koos __init__.py failiga. |
Tühja faili __init__.py
kasutamine¶
Kui __init__.py
fail on tühi siis impordime paketti kasutades järgmist süntaksit. Nii nagu eespool moodulite puhul toetatakse ka siin viite erinevat importimise viisi:
import <pakett>.<moodul>
import <pakett>.<moodul> as <nimi>
from <pakett>.<moodul> import <atribuut1>, <atribuut2>, <funktsioon1>, <klass1>
from <pakett>.<moodul> import <funktsioon1> as <nimi>
from <pakett>.<moodul> import * # Impordib kõik, mooduli nimele viitamine pole vajalik.
Alampaketi mooduli import:
import <pakett>.<alampakett>.<moodul>
Mittetühja faili __init__.py
kasutamine¶
Kui fail __init__.py
sisaldab suhtelisi viiteid kõikidele objektidele mida projektis on plaanitud kasudada. Viidete kasutamine lühendab importimise süntaksit. Tüüpiline __init__.py
faili sisu:
from .<moodul1> import <funktsioon1>
from .<moodul2> import *
Punktide kasutus: .
tähistab paketti, ..
alampaketti ja ...
alamalampaketti jne. Kui moodulis __init__
(__init__.py
) on eelmainitud suhtelised viited toimub import järgmiselt:
import <pakett> # Juurdepääs kõikide moodulite kõikidele objektidele.
import <pakett> as <nimi>
from <pakett> import <funktsioon1 (moodul1)>, <atribuut1 (moodul2)> # Näiteks.
from <pakett> import <funktsioon2> as <nimi>
from <pakett> import *
Vajadus viidata kindlale kaustas asuvale moodulile pole enam vajalik. Relatiivsed viited lubavad pakettil käituda sarnaselt moodulile. Imporditud moodulite ja pakketide sisu järgivaatamiseks kasutame endiselt funktsiooni dir
.
Paketi kasutamise näited¶
Näited praktikumis või kodus uurimiseks: Avame ja uurime mooduleid Loeng10_2
ja Loeng10_3
ning nendega kaasaskäivat paketti nimega pakettFunkid
:
- Failis 2.zip on salvestatud Pythoni projekt mis sisaldab paketti koos tühja
__init__.py
failiga - Failis 3.zip on salvestatud Pythoni projekt mis sisaldab paketti koos mittetühja viidetega
__init__.py
faili.
Praktikumi 10 ülesanded sisaldavad lisajuhiseid mis aitavad edukalt eelmainitud paketti kasutada.
Spetsiaalne muutuja __name__
¶
Peaprotseduur ja konstruktsiooni if __name__ == '__main__':
kasutamine¶
Oletame, et me töötame mitme mooduliga. Kuna Pythonis saab programmi käivitada meelevaldselt valitud moodulist (pole määratud interpreteerimise alguspunkti) siis on tekkinud tava kirjutada moodulit ja selles olevat peaprotseduuri mida nimetatakse tavaliselt main järgmiselt:
from <moodul> import func1
def func2(): # Seda funktsiooni saad kasutada kolmandas moodulis.
pass
def main(): # Siin teed funktsioonidega midagi...
func1()
func2()
if __name__ == '__main__':
main()
Kasutatud if
-tingimus tagab selle, et programmi peaprotseduur main
käivitub ainult juhul kui interpreteerime algab sellest moodulist kus mainitud if
-blokk asub. Eelöeldu töötab kuna moodulile kust alustatakse interpreteerimine omistatakse taustal spetsiaalsele muutujale __name__
väärtuseks '__main__'
(andmetüübis sõne).
Peaprotseduur on protseduur millele pannakse tavaliselt nimeks main
ning mis on kaitstud eelmises lõigus mainitus nimevalvuriga või nimehalduriga. Peaprotseduuri tööks on juhtida kogu programmi töövoogu ja kutsuda välja kõik teised kasutatavad protseduurid, funktsioonid, muutujad ja klassid.
Imporditud mooduli puhul määratakse selle mooduliga seotud spetsiaalsele muutujale __name__
väärtuseks mooduli enda nimi sõne formaadis.
Näide: Spetsiaalse muutuja __name__
väärtuse järelvaatamiseks saame kasutada näiteks eelnevalt kasutatud moodulit moodulFunkid
mis sisaldab protseduuri namefunc
. Protseduur namefunc
on sisu on järgmine:
def namefunc():
'''Protseduur mis prindib lause
print('\nmoodulFunkid spetsiaalne nimi __name__ =', __name__)
'''
print('\nmoodulFunkid spetsiaalne nimi __name__ =', __name__)
import moodulFunkid
print(moodulFunkid.__name__) # Muutuja __name__ järgivaatamine
moodulFunkid.namefunc()
print('\nSelle mooduli spetsiaalne nimi __name__ =', __name__)
moodulFunkid moodulFunkid spetsiaalne nimi __name__ = moodulFunkid Selle mooduli spetsiaalne nimi __name__ = __main__
Küsimus: Mis juhtub kui me alustame interpreteerimist moodulist moodulFunkid
?
Mooduli DocString ja mooduli dokumenteerimine¶
Vastavalt PEP 8-le (Style Guide for Python Code) peab ka mooduleid dokumenteerima. Kasuta PEP 257 (Docstring Conventions) soovitusi. Mooduli DocString kirjutatakse mooduli algusesse. Kasuta mitmerealist sõnet (multiline string) kujul:
"""Pealkiri
Optional plotz says to frobnicate the bizbaz first.
Sisu kirjuta siia. Eelmine rida on näide.
"""
Mooduli DocStringi järgivaatamiseks kasuta sama lähenemist mis muude objektide abiinfo ja dokumentatsiooni lugemisel:
import moodulFunkid
#help(moodulFunkid) # vt. iseseisvalt, liiga pikk väljatrükk.
# Konsoolis töötab ka nii:
moodulFunkid?
Type: module String form: <module 'moodulFunkid' from '/Users/dmitrikartofelev/Documents/WORK Teaching/YFX0500/Loeng 10/moodulFunkid.py'> File: ~/Documents/WORK Teaching/YFX0500/Loeng 10/moodulFunkid.py Docstring: Moodul moodulFunkid Moodul sisaldab kursuse jooksul loodud funktsioone mis on loodud õpilaste poolt. Funktsiooni on kokku kogutud ja dokumenteeritud tudengite abiga, seega ei pruugi korralikult töötada.
Taustal on DocStringi sisu seotud spetsiaalse muutujanimega __doc__
, seega:
print(moodulFunkid.__doc__)
Moodul moodulFunkid Moodul sisaldab kursuse jooksul loodud funktsioone mis on loodud õpilaste poolt. Funktsiooni on kokku kogutud ja dokumenteeritud tudengite abiga, seega ei pruugi korralikult töötada.
Näide heast ja halvast modulaarsusest¶
Allolevate koodinäidete taga on programm mis kodeerib "poomise" mängu inglisekeelse versiooni. Mängus "poomine" püüab kasjutaja talle teadmata sõna täht tähe haaval ära arvata. Programmis valib kasutaja mängu keerukuse ning üritab ennustada sõna, mis on valitud juhuslikult ette antud nimekirjast.
Halb näide programmist "poomine":
import random as r
words_easy = ["table", "star", "root", "flat", "turtle", "fish", "goat"]
words_hard = ["dragonborn", "monosaccharide", "disposable", "environment"]
difficulty = input("Please select difficulty('h' for 'hard' and 'e' for 'easy'): ")
if difficulty == 'h':
words = words_hard
elif difficulty == 'e':
words = words_easy
else:
raise ValueError("Not allowed input.")
word = r.choice(words)
guesses = len(word)
guess_word = ["-"] * len(word)
while True:
print(f"\nGuesses left: {guesses}")
print(f"The word is: {''.join(guess_word)}")
char = input("Please enter single character: ")
if char in word:
for i in range(len(word)):
guess_word[i] = char if word[i] == char else guess_word[i]
else:
print(f"The character {char} is not in the word!")
guesses -= 1
if "".join(guess_word) == word:
print("\nYou won!")
print(f"The word is: {word}")
break
if guesses == 0:
print("\nYou lost!")
print(f"The word was {word}")
break
Hea näide programmist "poomine":
import random as r
def select_difficulty(words_hard, words_easy):
difficulty = input("Please select difficulty ('h' for 'hard' and 'e' for 'easy'): ")
if difficulty == 'h':
words = words_hard
elif difficulty == 'e':
words = words_easy
else:
raise ValueError("Not allowed input.")
return words
def update_word(word, guess_word, char):
for i in range(len(word)):
if word[i] == char:
guess_word[i] = char
def track_game_state(word, guess_word, guesses):
if "".join(guess_word) == word:
print("\nYou won!")
print(f"The word is: '{word}'")
return True
if guesses == 0:
print("\nYou lost!")
print(f"The word was '{word}'")
return True
return False
def game(words_hard, words_easy):
words = select_difficulty(words_hard, words_easy)
word = r.choice(words)
guesses = len(word)
guess_word = ["-"] * len(word)
finished = False
while not finished:
print(f"\nGuesses left: {guesses}")
print(f"The word is: {''.join(guess_word)}")
char = input("Please enter single character: ")
if char not in word:
print(f"The character '{char}' is not in the word!")
guesses -= 1
else:
update_word(word, guess_word, char)
finished = track_game_state(word, guess_word, guesses)
def main():
easy = [
"table", "star", "root", "flat",
"turtle", "fish", "goat"
]
hard = [
"dragonborn", "monosaccharide",
"disposable", "environment"
]
game(words_hard=hard, words_easy=easy)
if __name__ == '__main__':
main()
Pythoni standardteegid¶
Lausendit import
kasutame ka Pythoni standardteekide ja moodulite ning kolmandate osapoolte teekide ja moodulite importimiseks.
Info Pythoni standardteekide kohta: https://docs.python.org/3/library/index.html
Valik olulisemaid Pythoni standardteeke:
- string, re
- datetime
- collections
- types
- pprint
- math
- decimal
- random, fractions
- itertools
- os.path
- glob
- shutil
- pickle, shelve
- zlib, zipfile, ...
- csv
- os
- time
- argparse
- getpass
- ctypes
- threading
- multiprocessing, subprocess
- html, xml
- urllib
- sys
import math
print(dir(math))
['__doc__', '__file__', '__loader__', '__name__', '__package__', '__spec__', 'acos', 'acosh', 'asin', 'asinh', 'atan', 'atan2', 'atanh', 'cbrt', 'ceil', 'comb', 'copysign', 'cos', 'cosh', 'degrees', 'dist', 'e', 'erf', 'erfc', 'exp', 'exp2', 'expm1', 'fabs', 'factorial', 'floor', 'fmod', 'frexp', 'fsum', 'gamma', 'gcd', 'hypot', 'inf', 'isclose', 'isfinite', 'isinf', 'isnan', 'isqrt', 'lcm', 'ldexp', 'lgamma', 'log', 'log10', 'log1p', 'log2', 'modf', 'nan', 'nextafter', 'perm', 'pi', 'pow', 'prod', 'radians', 'remainder', 'sin', 'sinh', 'sqrt', 'tan', 'tanh', 'tau', 'trunc', 'ulp']
type(math)
module
print(math.pi)
print(math.e)
3.141592653589793 2.718281828459045
math.exp(1) == math.e
True
math.cos(0)
1.0
math.sin(2 * math.pi) # NB! Numbrilist määramatust, sin(2pi) = 0.
-2.4492935982947064e-16
Kolmandate osapoolte teegid ja moodulid¶
Avalikuks kasutamiseks loodud moodulid on indekseeritud ja vabalt kautatamiseks allalaetavad. Veebisaidilt "Python Package Index" (PyPI) https://pypi.org/ leiab eelmainitud tarkvara. Lisaks sisaldab see sait infot ja juhiseid ka enda kirjutatud tarkvara avalikustamise ja jagamise kohta.
Joonis 4. Loetletud teekide ja projektide logod. |
Valik olulisemaid teeke (Anacondaga vaikimisi kaasas) millede logod on kujutatud Joonisel 4:
- NumPy — andmete esitus massiividena, https://numpy.org
- SciPy — teadusarvutuste algoritmid, https://www.scipy.org
- matplotlib — andmete analüüs ja visualiseerimine, https://matplotlib.org
- pandas — andmete salvestus ja analüüs, https://pandas.pydata.org
- IPython — interaktiivne andmete analüüs, https://ipython.org
- SymPy — sümbolarvutus, computer algebra system (CAS) või symbolic algebra system (SAS), https://www.sympy.org/en/index.html
import numpy as np
type(np)
module
print(np.pi)
print(np.e)
3.141592653589793 2.718281828459045
np.exp(1) == np.e
True
np.cos(0)
1.0
np.sin(2 * np.pi) # NB! Numbrilist määramatust, sin(2pi) = 0.
-2.4492935982947064e-16
Pythoni distributsiooni haldamine, pip
, conda
¶
Eelmises lõigus mainitud tarkvara allalaadimiseks ja installeermiseks kasutame spetsiaalselt selleks loodud Pythoni installatsiooniga kaasas olevat tarkvara.
Konsooli käsud:
PyPI tavalises konsoolis
pip
pip -h
, abimaterjal (help)pip list
, instaleeritud teekide ja moodulite nimistu ja versioonidpip install <nimi>
, instaleeri uus teek/moodulpip uninstall <nimi>
eemalda instaleeritud teek/moodul
Anaconda konsoolis
conda
conda -h
, abimaterjal (help)conda list
, instaleeritud teekide ja moodulite nimistu ja versioonidconda install <nimi>
, instaleeri uus teek/moodulconda uninstall <nimi>
võiconda remove <nimi>
, eemalda instaleeritud teek/moodul
Anaconda konsoolis töötavad ka pip
käsud. Kui arvutisse on installeeritud Anaconda distributsioon siis on soovitatav kasutada ainult conda
käske.