syntax-hightlight.css Korisni delovi jezika Python - deo 02



Korisni delovi jezika Python - deo 02
Autorova slika

Nemanja Mićović

15/7/2019


Korisni delovi jezika Python - deo 02

poster

Dragi moji, hvala vam na puno za odlične povratne informacije i pohvale povodom prošlog dela ovog članka. Nastavljamo naš put kroz svet jezika Python nekim novim zanimljivim temama kao što su moduli pathlib i argparse, biblioteka tqdm uz još neke kratke zanimljivosti.

Svakako Vas ohrabrujem da ostavite povratnu informaciju o tome šta je to sledeće što bi želeli da naredni deo članka pokrije. Ako ne umete tačno da precizirate, možete napisati idejno.

  1. Funkcije all i any
  2. Modul pathlib
    1. Klasa Path
    2. Otvaranje i čitanje datoteka
    3. Komponente objekta klase Path
    4. Poseban komentar na rad sa putanjama
  3. Enum
  4. Biblioteka tqdm
    1. Biblioteka tqdm
  5. Interpreter ptpython
  6. Argumenti komandne linije
    1. Modul argparse
  7. Zaključak

Funkcije all i any

Prilikom rada jedna od čestih operacija je potreba da se nakon primene neke transformacije nad kolekcijom proveri da li postoji neki element (any) koji zadovoljava određeni predikat, ili da se proveri da li svi elementi (all) zadovoljavaju određeni predikat. Za te potrebe postoje funkcije any i all.

Obe funkcije prihvataju iterator na kolekciju podataka i vrše pretragu o postojanju vrednosti True - any traži prvu pojavu, a all proverava da li su svi elementi upravo True.

xs = [True, False, True,False]

if any(xs):
    print("Barem jedan je tacan!")
if all(xs):
    print("Svi su tacni!")
if any(xs) and not all(xs):
    print("Barem jedan je tacan i barem jedan je netacan.")

>>> Barem jedan je tacan!
>>> Barem jedan je tacan i barem jedan je netacan.

ys = range(1, 10)
even = filter(lambda x: x % 2 == 0, ys)

if any(even):
    print("Barem jedan je tacan!")
if all(even):
    print("Svi su tacni!")
if any(even) and not all(even):
    print("Barem jedan je tacan i barem jedan je netacan.")

>>> Barem jedan je tacan!
>>> Barem jedan je tacan i barem jedan je netacan.

Jedna moguća implentacija za ove funkcije bi mogla biti sledećeg oblika:

def any(iterable):
    for element in iterable:
        if element:
            return True
    return False

def all(iterable):
    for element in iterable:
        if not element:
            return False
    return True

Modul pathlib

Većina softvera u svom radu koristi neku vrstu datoteka da realizuje svoje aktivnosti. Na primer skladištenje korisničkih podešavanja, čitanje/čuvanje podataka ili pisanje dnevnika prilikom rada (eng. logging).

U modulu os postoji podrška za rad sa putanjama na fajl sistemu, no putanja se predstavlja stringom. Korišćenje stringova za reprezentaciju putanja suštinski jeste jednostavno rešenje, ali vodi nečitljivom kodu jer se većina operacija svodi na transformacije stringova.

Na primer:

path.rsplit('/', maxsplit=1)[0]

Osim toga, putanje na sistemu su apstraktniji pojam od stringova i nad njima se često vrše operacije kao:

  • spajanje putanja
  • traženje roditeljskog direktorijuma
  • dobijanje ekstenzije datoteke
  • provera da li datoteka postoji i slično.

Usled ovoga, funkcionalnosti za rad putanjama su razdvojene i u module os, glob i shutil.

Zbog navedenih problema u jezik je od verzije 3.4 dodat modul pathlib koji uvodi dublju apstrakciju putanja na fajl sistemu.

Klasa Path

import pathlib
pathlib.Path.cwd()
>>> PosixPath('/home/korisnik/')

U klasi Path postoji statički metod cwd() koja vraća trenutni radni direktorijum (current working directory).

Putanja se može napraviti koristeći konstruktor za Path.

documents = pathlib.Path(r'/home/korisnik/Documents')

Konkatenacija putanja se može učiniti koristeći operator /. Operator / se prevodi u odgovarajući sistemski separator za datoteke koji zavisi od operativnog sistema. Može se uočiti da je operator / u stvari binarna funkcija čiji je prvi argument objekat klase Path, a drugi argument string ili Path.

pathlib.Path.home() / 'Documents' / 'Pictures'
>>> PosixPath('/home/korisnik/Documents/Pictures')
# Alternativno:
pathlib.Path.home().joinpath('Documents', 'Pictures')
>>> PosixPath('/home/korisnik/Documents/Pictures')

Otvaranje i čitanje datoteka

Funkcija open dozvoljava da argument koji predstavlja putanju bude i objekat klase Path.

path = pathlib.Path.cwd() / 'main.py'
with open(path, mode='r') as fid:
    imports = [line.strip() for line in fid if line.startswith('import')]

Nad objektom klase Path dostupan je i metod open koji se takođe može koristiti za otvaranje datoteke.

path = pathlib.Path.cwd() / 'main.py'
with path.open(mode='r') as fid:
    imports = [line.strip() for line in fid if line.startswith('import')]

Komponente objekta klase Path

Objekat klase path sadrži nekoliko korisnih atributa:

  • .name: ime datoteke sa ekstenzijom
  • .parent: roditeljski direktorijum putanje
  • .stem: ime datoteke bez ekstenzije
  • .suffix: ekstenzija datoteke
  • .anchor: sadržaj putanje pre direktorijuma

Navedeni atributi objekta su često izuzetno potrebno u praksi i korisno je imati ih dostupne na ovako čitljiv i jednostavan način.

path = pathlib.Path.home() / 'Documents' / 'python_notes.md'
print(path)
>>> PosixPath('/home/korisnik/Documents/python_notes.md')

print(path.name)
>>> 'python_notes.md'

print(path.stem)
>>> 'python_notes'

print(path.suffix)
>>> '.md'

print(path.parent)
>>> PosixPath('/home/korisnik/Documents')

print(path.parent.parent)
>>> PosixPath('/home/korisnik')

print(path.anchor)
>>> '/'

Jedna korisna transformacija i funkcija je metod with_sufix koji omogućava sledeću lepo transformaciju:

path = pathlib.Path.home() / 'Documents' / 'python_notes.md'
print(path)
>>> PosixPath('/home/korisnik/Documents/python_notes.md')

with_extension_tex = path.with_sufix('.tex')
print(with_extension_tex)
>>> PosixPath('/home/korisnik/Documents/python_notes.tex')

Poseban komentar na rad sa putanjama

Imao sam priliku da skoro razvijan softver za labeliranje podataka na projektu za jednog važnog klijenta. Nažalost nisam na početku iskoristio modul pathlib misleći da nema potrebe da komplikujem svoj kod. Na kraju sam završio sa klasom koja u stvari implementira većinu osnovnih funkcionalnosti modula pathlib.

Na primer ovakve gluposti (izbacio sam dokumentacione stringove i anotirane tipove da dodatno uvedem konfuziju i istaknem nečitljivost):

def extract_file_info(file_path):
    file_name, base_path = None, None
    file_name = os.path.basename(file_path)
    base_path = os.path.dirname(file_path)
    return file_name, base_path

def split_into_name_and_extension(file_path):
    base = os.path.basename(file_path)
    return os.path.splitext(base)

def remove_file_extension(file_name):
    return os.path.splitext(file_name)[0]

def replace_extension_with(file_name, new_extension):
    file_name_without_extension = remove_file_extension(file_name)
    return file_name_without_extension + "." + new_extension

Hm ček ček šta beše radi os.path.basename i šta tačno vraća?

Ovaj…a os.path.dirname vraća…ime direktorijuma?

Oh da li je filename u pozivu os.path.splitext(file_name)[0] zapravo relativna ili apsolutna putanja? Da li funkcija radi za oba? Da li je korisniku jasno šta da prosledi?

Ovakvi kodovi zaista čine jezik Python vrlo lošim za razvoj iole ozbiljnijeg softvera. Naravno, navedeni primeri su jednostavno, ali nije teško zamisliti kompleksniji kod sa sličnim problemima.

Slava modulu pathlib!

Enum

Nabrojiv tip (eng. enum) je tip koji dozvoljava da vrednost bude dodeljena iz unapred određenog skupa dozvoljenih vrednosti. Jezik Python je podršku za nabrojive tipove dobio od verzije 3.4, a većina poznatih jezika koji podržavaju proceduralnu ili objektnu paradigmu takođe nude podršku za rad sa njima.

Kako bi se napravio nabrojivi tip potrebno je naslediti klasu Enum iz modula enum.

from enum import Enum

class Color(Enum):
    RED = 1
    GREEN = 2
    BLUE = 3

Obavezno je pisanje vrednosti koje se dodeljuju dozvoljenim vrednosti, a praksa je da se vrednosti nabrojivog tipa nazivaju velikim slovima. Koristan je i sledeći trik da se izbegne pisanje vrednosti:

from enum import Enum

class Color(Enum):
    RED, GREEN, BLUE = range(3)

c = Color.RED
print(c.name)
>>> RED
print(c.value)
>>> 1

Moguće je konvertovati enum u listu kao i iterirati kroz dozvoljene vrednosti.

list(Color)
>>> [<Color.RED: 1>, <Color.GREEN: 2>, <Color.BLUE: 3>]

for c in Color:
    print(c)
>>> Color.RED
>>> Color.GREEN
>>> Color.BLUE

Nabrojive tipove možemo i porediti.

c1 = Color.RED
c2 = Color.GREEN

if c1 == c2:
    print('c1 == c2')
else:
    print('c1 1= c2')

>>> c1 != c2

Korišćenje nabrojivih tipova je nekada jako dobra praksa jer vodi čistijem, čitljivijem i bezbednijem kodu. Osim toga, ukoliko je argument funkcije predstavljen tipom koji je nabrojiva vrednost, onda je korisniku funkcije lakše da razume šta je to neophodno da prosledi funkciji.

from enum import Enum

class Algorithm(Enum):
    BUBBLE_SORT = 1
    MERGE_SORT = 2
    HEAP_SORT = 3
    QUICK_SORT = 4
    BOGO_SORT = 999

Korisnik koristi funkciju koja ima sledeći potpis:

sort1(iterable, choosen_algorithm: Algorithn)
sort2(iterable, choosen_algorithm: str)

Usled definicije enuma Algorithm korisniku je skoro odmah jasno koje su dozvoljene vrednosti za funkciju sort1, dok je u funkciji sort2 nejasno na prvi pogled šta su dozvoljene vrednosti koje funkcija prihvata. Ne samo to, postavlja se pitanje da li je bitno korišćenje malih i velikih slova kao i koji se separator koristi, odnosno:

  • bogosort
  • bogo_sort
  • BOGOSORT
  • BOGO_SORT
  • bOgO_sOrT
  • BoGo_SoRt

Naspram:

  • Algorithn.BOGO_SORT

Biblioteka tqdm

Avaj iako danas imamo izuzetno jak dostupan hardver, i naša ljudska priroda nas tera da imamo i drastično veća očekivanja od naših računara, algoritama i programa. Usled toga česta je pojava nekih izačunavanja, algoritama i poslova koji traju

oh

Ipak ovde nekada možemo ući u nešto što podseća na halting problem. Odnosno, ukoliko učitavanje ili izvršavanje potraje, često ćemo početi da se pitamo koliko još treba da čekamo? Ako sačekamo 30 minuta, a program i dalje deluje kao da se nije pomerio, postavljaju se ozbiljna pitanja, koliko još da čekamo, da li da čekamo, i da li smo zapravo dobro implementirali logiku?

Većina ovih problema se rešava korišćenjem (pisanjem na standardni izlaz ili u log datoteku) neke kontrole koja prikazuje napredak u radu, odnosno progres bar (eng. progress bar).

25%

Pre nego što prikažem biblioteku tqdm, želim da vam pokažem ovu simpatičnu implementaciju progres bara.

import time, os

class ProgressBar():
    """ Klasa predstavlja implementaciju kontrole za prikazivanje progresa. """
    def __init__(self, width=50):
        """
        pointer - Celobrojna vrednost iz intervala [0, 100] koja oznacava koliki je progress postignut.
        width - Sirina kontrole koja ce biti iscrtana.
        """
        self.pointer = 0
        self.width = width

    def __call__(self, current_progress):
        """
        Funkcija predstavlja implementaciju operatora () koji se moze pozvati nad objektom klase 'ProgressBar'.
        Operator () vraca stringovnu reprezentaciju trenutnog progresa (current_progress)
        """
        self.pointer = int(self.width*(current_progress/100.0))
        return "|" + "#"*self.pointer + "-"*(self.width-self.pointer)+ "| %d%% done" % int(current_progress)


def main():
    pb = ProgressBar()
    for i in range(101):
        os.system("clear")
        print(pb(i))
        time.sleep(0.1)

if __name__ == "__main__":
    main()

Odnosno:

|--------------------------------------------------| 0% done
|--------------------------------------------------| 1% done
|#-------------------------------------------------| 2% done
|#-------------------------------------------------| 3% done
|##------------------------------------------------| 4% done
|##------------------------------------------------| 5% done
|###-----------------------------------------------| 6% done
|###-----------------------------------------------| 7% done
|####----------------------------------------------| 8% done
|####----------------------------------------------| 9% done
...
|#########################################---------| 82% done
|#########################################---------| 83% done
|##########################################--------| 84% done
|##########################################--------| 85% done
|###########################################-------| 86% done
|###########################################-------| 87% done
|############################################------| 88% done
|############################################------| 89% done
|#############################################-----| 90% done
|#############################################-----| 91% done
|##############################################----| 92% done
|##############################################----| 93% done
|###############################################---| 94% done
|###############################################---| 95% done
|################################################--| 96% done
|################################################--| 97% done
|#################################################-| 98% done
|#################################################-| 99% done
|##################################################| 100% done

Biblioteka tqdm

Biblioteka tqdm na arapskom tqdm označava progres - taqadum تقدّم i predstavlja skraćenicu za tq quiero demasiado na španskom jeziku koja ima značenje mnogo te volim. Nakon što smo izneli najvažnije informacije o biblioteci, možemo videti i kako se ona instalira. Proces je izuzetno komplikovan.

pip install tqdm

Nakon ovog komplikovanog procesa sledi korišćenje biblioteke تقدّم koje ilustruje sledeći gif.

Osnovna ideja je da se u pozivu petlje prosledi funkcija tqdm koja kao argument prihvata iterable na osnovu kojeg biblioteka može da dedukuje koliki je progres ostvaren i slično. Imenovani argumenti funkcije tqdm omogućavaju da dodatno prilagodimo rad.

from tqdm import tqdm
import time

text = ""
for char in tqdm(["a", "b", "c", "d"]):
    time.sleep(0.25)
    text = text + char

Ukoliko koristimo range, predlaže se korišenje trange funkcije iz biblioteke tqdm koja je optimizovana verzije oblika tqdm(range(n)).

for i in trange(100):
    time.sleep(0.01)

Instalacija biblioteke na sistem dodaje i naredbu tqdm koja se može pozvati iz konzole. Omogućava nam da prikažemo progres prilikom poziva neke naredbe.

Na primer, umesto ovoga:

$ time find . -name '*.py' -type f -exec cat \{} \; | wc -l
857365

real    0m3.458s
user    0m0.274s
sys     0m3.325s
Možemo koristiti `tqdm` da prikaže progres tokom brojanja linija.
$ time find . -name '*.py' -type f -exec cat \{} \; | tqdm | wc -l
857366it [00:03, 246471.31it/s]
857365

real    0m3.585s
user    0m0.862s
sys     0m3.358s

Interpreter ptpython

Jezik Python iako popularan u raznim domenima primene je nekada i koristan u interaktivnom radu. Jedna od čestih primena mu je da bude korišćen kao kalkulator koji zapravo i nije iritantan da se koristi. Podrazumevani Python interpreter (odnosno Repl) ima vrlo skromne mogućnosti i njegovo korišćenje za nešto iole ozbiljnije (na primer generisanje podataka za modele u radnom okviru Django) je neprijatno.

Iako postoji IPython koji nudi bogatiji Repl za rad, u poslednje vreme se posebno izdvojio alat ptpython koji toplo predlažem da instalirate i koristite umesto prethodnih Repl rešenja.

Alat ptpython nudi veliki broj ugodnih funkcionalnosti kao što je prikazivanje dokumentacije za klase, objekte i funkcije, Vi i Emacs režim rada, autocomplete, sintaksičko bojenje koda i jedna od važnijih, izmene višelinijskih naredbi - na primer definicija funkcije ili klase.

Alat se lako može konfigurisati kroz konfiguracioni prozor koji se pokreće na F2.

Čak popunjava i putanje na fajl sistemu!

Instalacija je jednostavna.

pip install ptpython

Argumenti komandne linije

Prilikom rada u komandnoj liniji izuzetno je korisno da pri pozivu raznih programa postoji mogućnost da se rad tih programa prilagoditi određenim slučajevima upotrebe ili da se programu proslede argumenti. Na primer:

cd risk

Argument risk predstavlja argument komandne linije za program cd koji na osnovu svog argumenta zna u koji direktorijum da se pozicionira.

Pristup argumentima komandne linije u Python-u se vrši na sledeći način:

# Ime skripte: args.py

import sys

print("Broj argumenata:", len(sys.argv))
print("Argumenti:", sys.argv)

Pozivanje skripte:

$ python args.py 10 --argument 30 risk
Broj argumenata: 5
Argumenti: ['args.py', '10', '--argument', '30', 'risk']

Iako vrlo jednostavno, ovo je vrlo nepraktično u realnim primenama jer zahteva od nas da vršimo parsiranje prosleđenih argumenata što postaje vrlo nepotreban i dosadan proces nakon što ga uradite barem jednom. Kako bi se to nadomestilo, dostupan je modul argparse.

Modul argparse

Modul argparse je deo standardne Python biblioteke, dostupan je od verzije jezika 3.2 i predstavlja preporučeni način za rad sa argumentima komandne linije.

Osnovnu funkcionalnost nudi klasa ArgumentParser koja se koristi da se izvrši parsiranje prosleđenih arguemanta po definisanim pravilima od strane korisnika.

# Ime skripte: 01.args.py
import argparse

parser = argparse.ArgumentParser()
parser.add_argument('echo', help='Tekst koji zelite da ispisete')
args = parser.parse_args()
print(args.echo)

Metod add_argument se koristi da se doda pozicioni argument echo sa prosleđenim tekstom koji će biti iskorišćen da se korisniku ispišu pomoćne informacije za argument.

Poziv $ python 01.args.py daje sledeći ispis:

usage: 01.args.py [-h] echo
args.py: error: the following arguments are required: echo

Što je i očekivano jer nije prosleđen argument echo. Biblioteka za nas generiše potrebnu poruku o grešci i prekida rad programa.

Osim toga, biblioteka generiše i akciju za argument -h pomoću koje korisnik može dobiti pomoćne informacije o radu skripte.

Poziv $ python 01.args.py -h daje sledeći ispis:

usage: args.py [-h] echo

positional arguments:
  echo        Tekst koji zelite da ispisete

optional arguments:
  -h, --help  show this help message and exit

Opcioni argumenti se takođe dodaju pomoću metoda add_argument uz dodatak da je potrebno proslediti njihov pun i skraćen naziv.

import argparse

parser = argparse.ArgumentParser()
parser.add_argument('--ime', '-i', help='Ime korisnika')
parser.add_argument('--prezime', '-p', help='Prezime korisnika')
args = parser.parse_args()

ime = args.ime if args.ime else '?'
prezime = args.prezime if args.prezime else '?'

print('Ime i prezime: {} {}'.format(ime, prezime))

Pozivi

  • $ python 02.args.py --ime Geralt -p 'of Rivia'
  • $ python 02.args.py --ime Geralt --prezime 'of Rivia'
  • $ python 02.args.py -i Geralt -p 'of Rivia'
  • $ python 02.args.py -i Geralt --prezime 'of Rivia'

daju sledeći ispis:

Ime i prezime: Geralt of Rivia

Dok izostavljanje nekog argumenta i dalje dopušta dalji rad programa.

$ python 02.args.py --ime Geralt
Ime i prezime: Geralt ?

$ python 02.args.py -p 'of Rivia'
Ime i prezime: ? of Rivia

$ python 02.args.py
Ime i prezime: ? ?

Modul naravno generiše i pomoćni tekst za argument -h (odnosno --help).

$ python 02.args.py --help
usage: 02.args.py [-h] [--ime IME] [--prezime PREZIME]

optional arguments:
  -h, --help            show this help message and exit
  --ime IME, -i IME     Ime korisnika
  --prezime PREZIME, -p PREZIME
                        Prezime korisnika

Modul nudi još mogućnosti, ali ostatak ostavljam vama da iskoristite i istražite po potrebi. Svakako je preporuka da ne pravite svoj parser argumenata već koristite modul argparse.

Zaključak

Oh, pa hvala vam opet na pažnji! Uskoro sledi nastavak koji će uključiti teme kao što su dekorateri, virtuelna okruženja za Python i jedinično testiranje koda.