syntax-hightlight.css
Nemanja Mićović
28/6/2019
Do sada sam imao priliku da probam veliki broj jezika, biblioteka i tehnologija, ali mi je Python ekosistem i zajednica ostavila najprijatniji utisak kroz veliki broj različih domena primena kao što su mašinsko učenje, računarska vizija, veb programiranje(django i flask), desktop programiranje(PyQt5), izvlačenje informacija sa veba i automatsko slanje mejlova. Uprkos širokom domenu primene sa kojim sam se susreo, u većini slučajeva sam uspevao da jako brzo pronađem, instaliram i konfigurišem potrebne biblioteke i tehnologije, što nažalost nije uvek slučaj sa nekim jezicima i tehnologijama.
Sam jezik u poslednjih nekoliko godina doživljava veliku ekspanziju u IT svetu kao jedan od važnijih jezika u oblastima mašinskog učenja, računarske vizije, istraživanja podataka i veb programiranja. Razlozi za to su mnogi (jednostavnost, biblioteke, zajednica, robusnost, dokumentacija…) i diskusija o tome zaslužuje čitav jedan zaseban blog članak tako da ću tu temu ostaviti za budućnost. Do tada predlažem da pročitate zanimljivo istraživanje koje je sprovela kompanija Jetbrains koje možete pronaći ovde.
Kako se na Matematičkom fakultetu Python koristi na više kurseva (a verujem i na drugim fakultetima), deluje da ima smisla uložiti ozbiljnije vreme u upoznavanje sa nekim naprednijim ili korisnim delovima jezika.
Inicijalno sam planirao da ovaj članak bude skroman pregled nekih korisnih delova jezika Python, ali sam se usput zaigrao i zaključio da sam možda napisao nešto više teksta nego što ide u jedan običan članak :-)
Usled moje želje da od većine čitalaca ne dobijem tldr, objaviću nekoliko članaka za koje ću se potruditi da tematski budu povezani, a da sami primeri budu bogatiji i jasniji.
Nigde. Nema ih. Odnosno nema ih u nekoj zgodnoj Jupyter svesci ili kao niz skripti. Ostavljam vama da implementirate sami i isprobate stvari koje ću diskutovati. Glavni razlog je to da pokušate zaista da pokrenete neke od primera čime se nadam da ćete bolje zapamtiti stvari koje će biti prikazane. A osim toga, većina koda (ma skoro sve! :)) je vrlo blizu i da direktno proradi.
I mene zanima, ali minimalno 2! :)
I hajde da počnemo više!
Anonimne funkcije su funkcije kojima se ne dodeljuje ime (gle iznenađenja!). Razlog za to je potreba da se definiše funkcija na licu mesta kako bi se prosledila nekoj drugoj funkciji. Paradigma funkcionalnog programiranja često koristi funkcije višeg reda (funkcija koja prima funkciju) te je u praksi izuzetno korisno da jezik podržava anonimne funckije.
Sintaksa je sledeća: lambda ARGUMENTI: IZRAZ
.
add = lambda x, y: x + y
def add(x, y):
return x + y
Primetimo da lambda
funkcija nema naredbu return
, odnosno sintaksa za anonimne funkcije implicitno podrazumeva da vrednost izraza koji je prosleđen biva vraćena pri pozivu anonimne funkcije. Osim toga, telo anonimne funkcije prihvata izraz, a ne blok koda.
Jedna od jednostavnih, ali u praksi vrlo korisnih primena je sortiranje objekata.
Metod sort
prihvata argument key
pomoću kojeg izvlači informaciju o tome kako od objekta
da dobije vrednosti koje će koristiti za poređenje pri sortiranju. U primeru koji sledi,
prosleđujemo lambdu lambda element: element[1]
kojom definišemo da se od elementa koji
predstavlja uređeni par, dobije vrednost tako što se izvuče druge element koji predstavlja broj.
xs = [('python', 123), ('c++', 100), ('kotlin', 87), ('elixir', 111), ('visual basic', 50)
xs.sort(key=lambda element: element[1])
xs
>>> [('visual basic', 50), ('kotlin', 87), ('c++', 100), ('elixir', 111), ('python', 123)]
Lambda funkcije su takođe izuzetno korisne pri radu sa funkcijama map
, filter
i reduce
.
Funkcije map
, filter
i reduce
su funkcije višeg reda koje vrše neku transformaciju nad prosleđenom kolekcijom podataka. Potiču iz paradigme funkcionalnog programiranja i veliki broj jezika ih podržava.
Funkcija map
primenjuje funkciju f
nad svakim elementom kolekcije xs
i kao rezultat vraća iterator na kolekciju ys
gde važi da je ys[i] = f(xs[i])
.
ys_iterator = map(f, xs)
Na primer ukoliko želimo da kvadriramo sve elemente kolekcije.
xs = [1, 2, 3, 4, 5]
xs_kvadrirani = []
for x in xs:
xs_kvadrirani.append(x**2)
To možemo učiniti koristeći map
na sledeći način:
xs = [1, 2, 3, 4, 5]
xs_kvadrirani = map(lambda x: x**2, xs)
xs_kvadrirani_list = list(xs_kvadrirani)
Obratimo pažnju još jednom da map vraća iterator na rezultat i da je potrebno dobijeni iterator proslediti konstruktoru za potrebnu kolekciju podataka. Razlog za ovo je ušteda memorije i efikasnije izračunavanje kompozicije funkcija višeg reda.
Filter je funkcija koja prihvata predikat p
, kolekciju xs
i kao rezultat vraća iterator na rezultujuću kolekciju ys
u kojoj se nalaze svi elementi iz xs
za koje važi da zadovoljavaju predikat p
, odnosno važi p(x[i]) == True
.
ys_iterator = filter(p, xs)
Na primer, ukoliko želimo da zadržimo samo parne brojeve.
xs = [1, 2, 4, 5, 6]
ys = []
def paran(x):
return x % 2 == 0
for x in xs:
if paran(x):
ys.append(x)
Koristeći filter:
xs = [1, 2, 3, 4, 5, 6]
ys_iterator = filter(lambda x: x % 2 == 0, xs)
ys = list(ys_iterator)
Ukoliko je potrebno kolekciju podataka agregirati (ukrupniti) koristeći neku funkciju, može se koristiti reduce
.
vrednost = reduce(f, xs[, init])
Očekuje se da je f
binarna funkcija, odnosno da prihvata dva argumenta i vraća neku vrednost iz njihovog domena. Ukoliko se prosledi initializer
(inicijalna vrednost), onda će ona biti primenjena na samom početku kao f(initializer, x[0])
.
xs = [1, 2, 3, 4, 5]
suma = reduce(lambda x1, x2: x1+x2, xs)
suma = reduce(lambda x1, x2: x1+x2, xs, 0)
Navedene transformacije se simbolički mogu zapisati kao: ((((1+2)+3)+4)+5)
i (((((0+1)+2)+3)+4)+5)
.
Jedan primer upotrebe je sumirati objekte čiji metod vrati True
.
xs = [
ListItem('Item 1', True),
ListItem('Item 2', False),
ListItem('Item 3', True),
ListItem('Item 4', False),
]
agg = lambda prev_n, l_item: prev_n + 1 if l_item.is_selected() else prev_n)
'''
U Python-u aritmetika izmedju tipova `int` i `bool` funkcionise tako sto:
1 + True = 2
1 + False = 1
'''
agg_alt = lambda prev_n, l_item: prev_n + l_item.is_selected()
n_selected = reduce(agg, xs, 0)
n_selected_alt = reduce(agg_alt, xs, 0)
>>> print(n_selected)
2
>>> print(n_selected_alt)
2
Naravno, sličnu radnju smo mogli izvesti koristeći i filter.
n_selected = len(list(filter(lambda list_item: list_item.is_selected(), xs)))
>>> print(n_selected)
2
Takozvani MapReduce model programiranja se često koristi u radu sa velikim količinama podataka (eng. big data). Na primer radno okruženje Apache Spark opisuje većinu svojih transformacija na ovaj način.
Razumevanje listi (eng. list comprehension) predstavlja mogućnost jezika da generiše listu u zavisnosti od definicije koju prosledi korisnik. U nekoj meri dosta liči na definiciju skupa u matematici.
Na primer skup X:
X = {x | 1 ≤ x ≤ 5, x ∈ N}
definiše skup:
X = {1, 2, 3, 4, 5}
X
se u jeziku Python može zapisati:
X = [x for x in range(1, 6)]
>>> X
[1, 2, 3, 4, 5]
Ukoliko želimo da X
sadrži samo parne brojeve (odnosno da prihvatimo rezultat ako je zadovoljen predikat p
- filter):
def paran(x):
return x % 2 == 0
X = [x for x in range(0, 6) if paran(x)]]
>>> X
[0, 2, 4]
Ukoliko želimo da X
sadrži samo parne brojeve koji su kvadrirani (filter
i map
zajedno):
def paran(x):
return x % 2 == 0
def kvadriraj(x):
return x**2
X = [kvadriraj(x) for x in range(0, 6) if paran(x)]
>>> X
[0, 4, 16]
Od verzije 3.5, jezik Python
je dobio podršku za rad sa tipovima, odnosno proširena je sintaksa jezika tako da je moguće naznačiti kog je tipa promenljiva ili šta je povratna vrednost funkcije.
class Point:
def __init__(self, x, y):
self.x = x
self.y = y
def __str__(self):
return "({}, {})".format(self.x, self.y)
x: int = 5
y: int = 10
s1: str = "Tipovi u"
s2: str = " jeziku Python"
t: Point = Point(2, 3)
def add(a: int, b: int) -> int:
return a + b
def concat_str(s1: str, s2: str) -> str:
return s1 + s2
def show_point(t: Point = Point(0, 0)) -> None:
"""
Ukoliko funkcija nema povratnu vrednost,
onda se postavlja da vraca None.
Ukoliko je potrebno postaviti podrazumevanu vrednost za
argument, to se moze uciniti nakon tipa.
"""
print(str(t))
Ukoliko je neki tip previše kompleksan u svojem zapisu, može se uvesti novo ime za taj tip (eng. type alias). Ipak ovde treba biti pažljiv jer ukoliko imamo potrebu da uvodimo novi kompleksan tip, možda više ima smisla napraviti novu klasu.
from typing import List, Dict
PyFileLines = List[Dict[str, int]]
xs: PyFileLines = [
{'main.py': 127},
{'logger.py': 332},
{'meaning_of.py': 42},
]
mypy
Python interpreter za sada ignoriše prosleđene tipove, a postoji alat mypy koji se može koristiti za proveru tipova.
Alat mypy
se može instalirati koristeći pip nakon čega postaje dostupan za korišćenje iz konzole.
Na primer ako je ovo sadržaj skripte mypy_test.py
:
from typing import List
def perform_sum(xs: List[int]) -> int:
return sum(xs)
print(perform_sum([2.3, 3.3]))
print(perform_sum("Python"))
Onda se mypy
može pokrenuti sa:
mypy mypy_test.py
Nakon čega se dobija sledeći sadržaj kao rezultat rada:
typing_example.py:14: error: List item 0 has incompatible type "float"; expected "int"
typing_example.py:14: error: List item 1 has incompatible type "float"; expected "int"
typing_example.py:15: error: Argument 1 to "perform_sum" has incompatible type "str"; expected "List[int]"
Većina moćnijih editora koji podržavaju proširenja
imaju proširenje koje omogućava da se mypy
ugradi direktno u editor. Na primer evo
proširenja za neke od popularnijih kao što su Neovim,
Atom, i VSCode.
Takođe, PyCharm
ima ugrađen proveravač tipova koji tokom pisanja koda vrši provere i korisniku pokazuje informacije o urađenim analizama slično poput alata mypy
.
typing
Modul typing ima izdvojene već neke podrazumevane tipove kao što su List
, Set
i Dict
.
Uporedite čitljivost delova a)
i b)
.
from typing import List, Set, Dict
''' a) '''
x = {"a": 5, "b": 10}
y = {1, 2, 3}
z = [1, 2, 3]
''' b) '''
x: Dict[str, int] = {"a": 5, "b": 10}
y: Set[int] = {1, 2, 3}
z: List[int] = [1, 2, 3]
Doduše nije preterano korisno navoditi tip promenljive pri samoj inicijalizaciji
iz koje se može zaključiti šta je tip, ali ako funkcija prihvata nešto tipa elements: Dict[Point, str]
to je dosta informativnije u odnosu na elements
.
Šta ako funkcija vraća više vrednosti? Najpre, potrudite se da to ne radite, a ako već morate, razmislite još jednom. Ako bas odlučite da morate, razmislite da li ima smisla napraviti klasu. Ako i dalje ne želite klasu, možete napraviti uniju tipova.
from typing import Union
def f(x: int):
if x:
return x
else:
return False
def f_with_types(x: int) -> Union[int, bool]:
if x:
return x
else:
return False
Često se dešava situacija da neka radnja može da ne uspe i da je rezultat prazan. Na primer, otvaranje konekcije ka bazi podataka, pokušaj čitanja datoteke, otvaranje soketa i slično. U tim situacijama često funkcije vraćaju ili uspešan rezultat ili referencu na nešto prazno (null
, NULL
, None
…). Slično je i u jeziku Python i može se opisati tipom Union[T, None]
. Ipak, u većini jezika to se predstavlja tipom Optional<T>
(c++, java…) koji označava da je vrednost tipa T
opciona. Modul typing
poseduje tip Optional[T]
koji predstavlja u stvari tip Union[T, None]
.
from typing import Optional
def read_file(path: str) -> Optional[str]:
try:
with open(path, 'r') as f:
return f.read()
except IOError:
return None
U praksi se korišćenje tipova pokazuje kao izuzetno korisno, jer osim što eliminiše greške pri lošem prosleđivanju tipova funkciji, omogućava i udobniji rad jer anotiranje tipova pruža mogućnost editorima i okruženjima da programeru prikažu dobar autocomplete prozor. Ukoliko to možete, odnosno niste primorani da koristite starije verzije jezika, toplo preporučujem da uvedete sebi naviku pisanja tipova tokom rada.
Jezik Python podržava da se funkcijama prosledi nepoznat broj neimenovanih (args) i imenovanih (kwargs) argumenata.
Konvencija je da se u izvornom kodu te situacije označavaju sa args
i kwargs
, mada ne postoji formalno pravilo po kojem se promenljive moraju nazvati tako.
Ukoliko želimo da funkciji prosledimo nepoznat broj neimenovanih argumenata, funkciji treba proslediti jednu promenljivu sa karakterom *
ispred imena, a preporuka je da ime promenljive bude args
.
Na primer ukoliko želimo da sumiramo sve elemente koji su prosleđeni kao argumenti:
def saberi(*args):
# return sum(args)
s = 0
for arg in args:
s += arg
return s
>>> xs = [1, 2, 3, 4]
>>> saberi(*xs)
10
>>> saberi(1, 2, 3, 4)
10
>>> saberi(*[1, 2, 3, 4])
10
Ukoliko je potrebno, Python nudi mogućnost da se funkciji prosledi i nepoznat broj imenovanih argumenata. Notacija za promenljivu koja predstavlja rečnik argumenata je korišćenje operatora **
da se naznači da promenljiva koja sledi jeste rečnik gde su ključevi imena prosleđenih argumenata, a vrednosti upravo vrednosti tih argumenata.
def informacije(**kwargs):
""" Demonstracija za imenovane argumente. """
for k, v in kwargs.items():
print("{0} = {1}".format(k, v))
>>> informacije(ime="Petar", prezime="Petrovic", godine=21)
ime = Petar
prezime = Petrovic
godine = 21
>>> info = {"ime": "Petar", "prezime": "Petrovic", "godine": 21}
>>> informacije(**info)
ime = Petar
prezime = Petrovic
godine = 21
>>> informacije(**{"ime": "Petar", "prezime": "Petrovic", "godine": 21})
ime = Petar
prezime = Petrovic
godine = 21
Mnoge poznate biblioteke za Python intenzivno koriste ovu mogućnost jezika da korisnicima ponude udoban interfejs za korišćenje.
Operator **
se može iskoristiti i da se spoje dva rečnika:
x = {'a': 1, 'b': 2}
y = {'b': 3, 'c': 4}
z = {**x, **y}
>>> z
{'c': 4, 'a': 1, 'b': 3}
Nadam se da sam donekle uspeo da vas zainteresujem da malo više pažnje posvetite ovom zabavnom jeziku
i da je svako od vas barem nešto novo naučio.
Neke od tema koje će biti obrađene u narednom delu će biti moduli pathlib
i argparse
,
nabrojivi tipovi (enumi) i biblioteka tqdm
. Vidimo se i hvala na pažnji!