2016. szeptember 27., kedd

10 - Modulok, csomagok

Jól kihagytam az érettségis blogbejegyzést. Nem is keresek kifogásokat, lusta voltam. Ellenben már készülőben van (konkrétan több mint a fele magvan) az ezt követő, tehát a 11. része a tutorialnak.

Modulok. Library-k. Függvénykönyvtárak.

Egyre hosszabb kódokat írogatunk és látható, hogy csak nem kéne egyetlen fájlba írni a komplett programot, mert átláthatatlanná válik a munkánk. A modulok ezt a problémát oldják meg: kirakhatjuk a forráskód egyes részeit különböző fájlokba, és ezekből a fájlokból akkor és ott töltünk be egy részt (konkrétabban: valami objektumot, függvényt, vagy csak egyszerű konstans értékeket) amikor arra szükség van.

Ahogy modulokra bontjuk a programunkat úgy a modulok hordozhatóvá is válnak: összegyűjtünk egy csokor objektumot, azok saját modulba kerülnek, és ha egy másik programban ugyan ezekre (vagy csak néhány) az objektumokra lenne szükség, akkor egyszerűen csak oda másoljuk a modult az új programhoz, és már használhatóak azok a kódok, amiket régen megírtunk.

Látható hogy így megoldható a hordozhatóság, és a kód újrahasznosítás problémája. De nem csak a saját magunknak írhatunk modulokat, hanem mások is írhatnak nekünk, és ezek tetszőlegesen terjeszthetőek. A modulok használata a szoftverfejlesztés szerver része, ugyanis rengeteg olyan modul érhető el, amik olyan funkciókat valósítanak meg, amit ha magunknak kéne implementálnunk akkor az igen sokáig tartana (de az is lehet, hogy egyáltalán nem vagyunk elég képzettek hogy kidolgozzuk a részleteket).

Tehát modulokat azért használunk, hogy a saját programunkat szétbontsuk, és azért, hogy új funkciókat érhessünk el a Pythonban, úgy, hogy azokat más programozók helyettünk már megírták.

A standard library

A Pythonra szokták azt is mondani, hogy batteries included (az elemeket tartalmazza), azaz nem csak a nyers szintaxist meg az interpretert kapjuk, hanem egy halom nagyon hasznos modul is beépítve részét képezi a Pythonnak. A beépített modulokat a standard library (stdlib) kifejezéssel szokták illetni.

Oké de milyen modulok állnak rendelkezésre? A The Python Standard Library reference oldalán egy igen hosszú listát láthatunk. Ezek közül egy pár, ami érdekesebb lehet:

  1. datetime - Dátum- és idő kezelő függvények
  2. decimal - Tetszőleges pontosságú tizedes törtek
  3. io - Műveletek be- és kimeneti adatokkal
  4. math - Matematikai műveletek
  5. multiprocessing és threading - Több szálon / processzor magon futó programok fejlesztéséhez
  6. random - Véletlenszerű értékek generálásához
  7. sqlite3 - SQLite SQL adatbázis eléréséhez
  8. subprocess - Programok indítása és vezérlése Python forráskódból
  9. tempfile - Átmeneti fájlok és könyvtárak létrehozásához
  10. time - Aktuális idő vagy program futásának szüneteltetése
  11. traceback - Python traceback-ek elérése str-ként vagy formázásuk
  12. urllib - Adatok letöltése az internetről
  13. unittest - Tesztek fejlesztéséhez
  14. zipfile - .zip kiterjesztésű fájlok írása, olvasása

Tudom hogy ezek a jellemzések igen rövidek. Általában 1-1 modulról 1-2 teljes bejegyzést lehetne mesélni. Valószínűleg ahogy véget ér ez a tutorial elkezdem a standard library részletes bemutatását. De ez még a jövő zenéje.

Ez most csak 14 modul amit így gyorsan összeszedtem, és viszonylag könnyen megfoghatónak találtam, csak a jéghegy csúcsa. Aki a Python telepítés című postom alapján telepítette a Python értelmezőjét, az installálta a pip nevű programot is, ami egy úgynevezett csomagkezelő. Ez a program képes az interneten fellelhető csomagok (modulok) telepítésére, amikből több tízezer található. De erre majd később térünk vissza.

Egy csomagban (package) több modul található, ezért ezek direkt különböző fogalmak.

Modulok használata

A modul az importálás után válik használhatóvá. Ez kétféleképp lehetséges, az import utasítással például így:

import random

E sor után a random modul elérhető lesz a forráskódban, aminek a tartalmára a random névvel hivatkozhatunk. A modulban található értékek (konstansok, függvények, osztályok) hasonlóan érhetőek el, mint ha azok a random objektum adattagjai lennének, azaz a . operátorral, például így:

import random

number = random.randint(0, 10)

A fenti sorral a random.randint() egy darab 0 és 10 közötti véletlenszerű számot generál, amit number néven tárolunk el.

Előfordulhat hogy nem akarjuk a teljes modult elérhetővé tenni, mert nem akarunk olyan sokat gépelni, vagy mert a modul betöltése túl sokáig tartana. Erre az esetre használhatjuk a from ... import ... kifejezést:

from random import randint

number = randint(0, 10)

Az 1. sorban álló kifejezés már-már egy komplett angol mondat: a random modulból importáld a randintet. Látható, hogy ilyenkor többé nem random.randint()-ként érjük el az importált metódust. A random név a forráskódban továbbra se lesz definiálva.

Az importálások általában a forráskód elejére kerülnek, néhány speciális eset kivételével, például a körkörös függőség (circular dependency) esetén.

Importált objektum átnevezése

Ez egy igen rövid történt, ezért leírom. Az as kulcsszóval át tudjuk nevezni az importált modult vagy objektumokat. Ez nagyon jól jön akkor, ha amúgy névütközés alakulna ki a forráskódban, vagy az importált objektum neve túl hosszú, vagy simán csak nem tetszik.

import random as rndm
from random import randint as random_integer

Gondoltam egy számra

Oké, lássuk ezt a gyakorlatban az ide vágó klasszikus feladattal: írjunk egy olyan játékot, hogy a számítógép gondol egy tetszőleges 0 <= x <= 100 számra és a játékosnak ki kell találnia hogy mire gondolt a gép. Ha a kipróbált szám kisebb a gondoltnál, akkor írjuk ki hogy “kisebb”, ha nagyobb akkor értelem szerűen azt hogy “nagyobb”. Addig fusson a játék amíg nincs találat.

Lássuk a kódot!

import random

secret = random.randint(0, 100)

while True:
    player_number = int(input('Your guess: '))

    if player_number < secret:
        print('Your guess is too low')
    elif player_number > secret:
        print('Your guess is too high')
    else:
        print('You guessed my number!')
        break

Igazából az érdekes részeket le is tudjuk az első 3 sorban: az 1. sorban importáljuk a random modult, a 3. sorban pedig előállítok egy véletlenszerű számot 0 és 100 között és eltárolom a secret változóban.

Ez után csak a szokásos dolgok jönnek: indítunk egy vételen ciklust, mivel nem tudjuk hogy mikor ér majd véget a játék. Bekérjük a kipróbált számot amit player_number néven tárolunk el. Ez után jön pár ellenőrzés: ha kisebb a játékos száma akkor kiírjuk hogy túl kicsi a szám; ha nagyobb a játékosé akkor kiírjuk hogy túl nagy; amúgy meg nyerés van. Pont ahogy a feladat leírásban volt.

Na akkor futtassunk egy menetet:

Your guess: 50
Your guess is too low
Your guess: 75
Your guess is too high
Your guess: 62
Your guess is too low
Your guess: 68
Your guess is too high
Your guess: 65
Your guess is too low
Your guess: 66
Your guess is too low
Your guess: 67
You guessed my number!

Tök fasza nem?

A programot megszakítani a CTRL+C billentyűkombinációval lehet (szokás szerint).
A kitalálás során fejben a bináris keresés algoritmusát alkalmaztam.

Dátum és óra

Írjunk egy egyszerű kis scriptet ami a számítógépen beállított dátumot és időt mutatja élőben, tehát minden másodpercben frissüljön:

import time

while True:
    print(time.strftime('%Y-%m-%d %H:%M:%S'), end='\r')
    time.sleep(1)

A forráskód futását a sleep() függvénnyel tudjuk megállítani, ami a time modulban található. Itt található többek közt az strftime() függvény is, amivel formázhatjuk az idő megjelenését.

Importáltuk tehát a time modult, és indul is a végtelen ciklus. Itt a print()-en belül meghívjuk a time.strftime() függvényt. Az strftime()-nak adott '%Y-%m-%d %H:%M:%S' paraméter szerint az időt úgy formázzuk, hogy az ehhez hasonlóan jelenjen meg: 2016-09-25 11:56:12.

Most nem mennék bele, hogy melyik hieroglifa mit jelent, de az strftime() leírásában megtalálható minden.

A print()-ben még beállítjuk az end='\r' kwarggal, hogy a sor végét lezáró karakter legyen a carriage return (erről itt lehet olvasni). Így tehát kiíródik a dátum és idő, majd a kurzor visszakerül a sor elejére, és a következő kiírás felülírja a meglevőt. Így az óra egy helyben marad.

Az utolsó sorban a time.sleep(1)-el 1 másodpercig áll a program. Itt tetszőleges törtet is megadhatunk (így kevesebb időt is várakozhatunk).

Megoldás másik modullal

A datetime modul is dátum és idő funkciókat tartalmaz. Ebben a modulban van egy olyan függvény, ami egyből szépen formázva adja vissza az időt. Próbáljuk ki ezt:

import datetime
import time

while True:
    print(datetime.datetime.now(), end='\r')
    time.sleep(1)

Importáltuk a datetime modutl, és a print()-be beírtuk a datetime.datetime.now() hívást. Ha futtatjuk a programot akkor egy kicsit pontosabb időt látunk, mert látszódnak a milliszekundumok, de ez most elhanyagolható. Valami ilyesmi jelenik meg: 2016-09-25 12:05:49.251479

Ez a fenti egy érdekes példa, amit nem is említettem külön: a modulok tartalmazhatnak modulokat, ezzel nincs semmi gond, csupán még egy .-ot kell kifejezésbe tenni.

Az is látszik a példában, hogy 2 km kódot kell írni a pontos időhöz. Írjuk át így a kódot:

import time
from datetime import datetime

while True:
    print(datetime.now(), end='\r')
    time.sleep(1)

A datetime csomag tartalmazza az azonos nevű datetime modult, de ott vannak még például a time, date vagy timedelta modulok is a csomagban.

Még két példa

A fenti példákból úgy tűnhet mintha csak függvények lennének elérhetőek a modulokból, de ez nem így van. Például ha a pi értéke elég pontosan elérhető a math modulból:

import math
print(math.pi)

Látszik, ez csak egy egyszerű tört.

Egy kicsit szeméylesbb példával zárnám ezt a blokkot. Anno azért kezdtem el komolyabban programozni, hogy tudjak írni egy olyan programot, amivel leelőzhetem a barátaimat egy hülye Facebookos játékban. Ehhez az kellett hogy a programom tudjon kommunikálni a távoli szerverrel, szóval hogy elérhessem az internetet. Az urllib segítségével ez megoldható.

Ez a kis script letölt egy weboldalt, ami az IP-címünket tartalmazza:

import urllib.request

response = urllib.request.urlopen('https://httpbin.org/ip')
print(response.read())

A kimeneten valami ehhez hasonló jelenik meg: b'{\n "origin": "10.0.0.1"\n}\n'. Ez amúgy ránézésre majdnem olyan mintha Python kód lenne, de valójából JSON objektumot látunk. A json modullal átalakíthatjuk egy dict-é ezt az eredményt:

import json
import urllib.request

response = urllib.request.urlopen('https://httpbin.org/ip')
my_ip = json.loads(response.read().decode())['origin']

print(my_ip)

Nem minden szerver válaszol ilyen szépen formázva, de a példa kedvéért kerestem egyet.

Honnan tudom hogy mi van a modulban?

Kérdés hogy mégis honnan a picsából kéne tudnom, hogy mi-merre van? Sehonnan. Senki se tudja magától, senki se tudja mit-hogy szoktak a programozók elnevezni. Így válik nyilvánvalóvá, hogy nem is magát a Pythont (szintaxist, kifejezéseket) tart sokáig megtanulni, hanem azt, hogy melyik csomagban és modulban mi van; azt hogy hívják, és hogyan lehet elérni. Általában elmondható, hogy a csomagok ismeretének elsajátítása tart a legtovább a programozói karrier építése során.

A tanulás során érdemes mindig kéznél tartani a Python Standard Library oldalát, illetve olyan fejlesztői környezetet (IDE) beszerezni, ami ki tudja egészíteni a parancsainkat, így nem kell mindenre pontosan emlékezni.

Igazából a Python nyelvtől teljesen független, hogy az ember milyen fejlesztői környezetet használ, ezért nem is foglalkoztam ezzel a kérdéssel eddig, és most se szánnék rá időt. Rövidre fogva, én a JetBrains PyCharm szoftverét használom évek óta.

Hogyan kell modult írni?

Azt hittem már hogy sose érkezünk meg ehhez a ponthoz. Modult szerencsére pofon egyszerű írni: másoljuk össze a kódjainkat egy különálló .py fájlba, és a fájl neve lesz a modul. Ilyen egyszerű.

Korábban a számkitatlálós példában írtam az int(input()) kifejezést, ezzel alakítottam át a játékos által beírt szöveget int-é. Ezt a kis kódocskát átalakítjuk egy függvénnyé, és kirakjuk a typed_inputs.py nevű fájlba.

# typed_inputs.py

def int_input(prompt):
    return int(input(prompt))

Mondjuk azt, hogy a typed_inputs modulban olyan szöveg beolvasó függvényeket gyűjtünk, amik megfelelő típusúvá alakítják a felhasználótól érkező szöveget. Így az int_input()-tól azt várjuk, hogy egy int-el tér vissza.

Ez után egy másik fájlból, amit most hívjunk program.py-nak, importáljuk a modult így:

# program.py

import typed_inputs

number = typed_inputs.int_input('Enter a number: ')

És kész is: az int-es inputunk mostantól független életet élhet.

Hogyan kell csomagot írni?

A csomag sem egy nagy mutatvány, ezért is nem írtam róla sokat idáig. Annyit kell róluk tudni, hogy több modult fognak össze. Roppant könnyű létrehozni egy sajátot: tegyük a moduljainkat egy könyvtárba, és ebben a könyvtárban készítsünk egy __init__.py elnevezésű fájlt is. A könyvtár neve lesz a csomag neve.

Véleményem szerint csak azért nem szükséges csomagot készíteni hogy legyen olyan is. Akkor érdemes csomagot írni ha van több különböző modulunk amik logikailag jól összetartoznak. A modulok nagyon jól elvannak magukban is a hétköznapokban.

Tegyük fel hogy mindenféle típus-specifikus modulokat írogattunk, úgyhogy ezeket belerakjuk a typed nevű csomagba. A következő könyvtárstruktúrát alakítsuk ki:

program.py
typed/
    __init__.py
    inputs.py

“Magyarul”: legyen egy program.py fájlunk és mellette egy typed nevű könyvtár. Ebben a könyvtárban legyen egy __init__.py és egy inputs.py nevű fájl.

Ezért a példáért nem akartam új forráskódot kitalálni, szóval az inputs.py tartalma egyezzen meg a feljebb bemutatott typed_inputs.py tartalmával. Ha ez megvan, akkor a program.py így módosul:

# program.py

from typed import inputs

number = inputs.int_input('Enter a number: ')

A lényeg a 3. sorban látható: a typed csomagból importáljuk az inputs modult. Ez a forma pont úgy néz ki, mint amikor egy modulból egy objektumot importáltunk. Akkor most hogyan tudjuk megkülönböztetni hogy csomagból vagy modulból történt az importálás? Szokás szerint nem tudjuk. De igazából érdekel ez akárkit is? Nem.

Egyébként ha már itt tartunk, még mindig lehetséges a typed csomag inputs moduljának egyetlen objektumát importálni ha ezt írjuk:

from typed.inputs import int_input

Viszont meglepő lehet, de ez nem fog működni:

# program.py

import typed

number = typed.inputs.int_input('Enter a number: ')

Mire való az __init__.py?

A csomagban helyet foglaló __init__.py, mint ahogy egyébként bármi más is, nem dísznek került oda a könyvtárba. Persze állhat üresen is, a csomag akkor is működni fog, de írhatunk ide mindenféle inicializáló kódokat. Ez azt jelenti, hogy amikor a csomagból importálunk valamit, akkor az __init__.py fájl tartalma automatikusan le fog futni!

Írjuk be ezt a typed/__init__.py fájlba:

# __init__.py

print('*typed package is in action*')

Írjuk vissza a program.py-t a működő verzióra és futtassuk így. Valami ilyesmi fog megjelenni a konzolban:

*typed package is in action*
Enter a number:

A *typed package is in action* szöveg varázslatosan megjelent abban a pillanatban, hogy az importálásra sorára került a program futása.

Remélem érezhető hogy ide akármilyen kódot írhatunk, ami segíthet például beállítani a csomag saját lelki világában valamit még mielőtt az őt használó program futhatna. Egy konkrét esetet mutatnék: írjuk be ezt az __init__.py-ba:

# __init__.py

from typed import inputs

Így a program.py-ban ez már lehetséges lesz:

# program.py

import typed

number = typed.inputs.int_input('Enter a number: ')

De ennyi trükközés egyenlőre elég lesz. Majd még visszatérünk erre.

A modul névtere

A névterekről volt már szó. Akkor azt mondtam, hogy minden Python fájl 1-1 névtér. Így, hogy már több fájlunk is van, ez végre értelmet is nyert.

Az egyszerűség kedvéért térjünk vissza erre a struktúrára:

program.py
typed_inputs.py

Ahogy a program.py elején megtörténik az importálás a typed_inputs.py tartalma lefut, a saját izolált névterében. Ebből a névtérből egy nevet (függvényt, konstanst, objektumot) az import utasítással tudtunk elérni. Más módszer viszont nincs az átjárásra.

A modulokat nem úgy kell elképelni, hogy azok összeállnak egyetlen nagy szöveggé amikor elindul a program, hanem mindegyik modul önálló és az importálás hatására kelnek életre. Éppen ezért azok a modulok amik soha se voltak importálva nem is futnak le sose; viszont ha fut az importálás, akkor minden sor végrehajtódik mint bármelyik szabványos Python program esetén.

Egyébként az izoláció nem azt jelenti, hogy csak olvashatóak az importált modulok. Tehát lehetséges a futó modulban található objektumokat módosítani, törölni. Ekkor magában a forráskódban nem történik változás, de a futó program máshogy működhet. De erről majd a második rész szól.

Zárás

Most hogy végre tudunk a modulok és csomagok létezéséről én végre színesebb példákat és ti izgalmasabb programokat írhatok. Alig várom hogy végre valami érdekes szoftvert írjunk. Addig is az objektumorientált programozással folytatjuk (már majdnem kész az 1. rész!)

Ez a bejegyzés a Python tutorialom egyik része. Az összes rész listája itt fellelhető.

-slp

Nincsenek megjegyzések:

Megjegyzés küldése