syntax-hightlight.css
Nemanja Mićović
15/7/2019
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.
all
i any
pathlib
tqdm
ptpython
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
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:
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.
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')
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')]
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 direktorijumaNavedeni 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')
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
!
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
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…
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).
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
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
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
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
.
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
.
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.