Python

but first!

Курса си има slack! Вижте как да се присъедините към групата в новината в сайта.

За целите на курса (и в machine learinng света като цяло) стандартния език е python

колко от вас имат инсталиран python?

сигурни ли сте, че е python3?

Разликите между python 2.7 и 3.x са достатъчни, за да направят живота ви далеч по-лесен ако ползвате 3.x, но също и достатъчни, за да направят мигрирането на стар код доста тежко. Макар че версия 3.0 се появи на бял свят през 2008, до преди 3-4 години беше относително нормално да се срещат огромни тежки проекти, които все още използват 2.7. До голяма степен това беше така, поради факта, че трудността за мигриране към 3.x означаваше, че и много библиотеки продължаваха да не работят с python 3. За целите на този курс и като цяло в machine learning света, python 3 вече е много добре поддържан и е стандартната версия, котяо се очаква да използвате.

[kunev@r_lyeh machine-learning-lectures]$ python
Python 3.6.2 (default, Jul 20 2017, 03:52:27)
[GCC 7.1.1 20170630] on linux
Type "help", "copyright", "credits" or "license" for more information.

Инсталирайте си python

os x
brew install python3
ubuntu/debian
apt-get install python3
other linux

обикновено пакета се казва python3

windows

Свалете инсталатора от тук

Според операционната ви система е възможно да имате standartno инсталиран python интерпретатор. При OS X/macOS и Debian е 2.7. За повечето linux дистрибуции (особено не базирани на Debian) е по-вероятно да е 3.x. Определено искате да проверите версията си като просто пуснете python. Ако се окаже, че версията е 2.7, пробвайте дали не можете да изпълните python3. Ако не, просто инстлаирайте по някой от описаните по-горе начини.

променливи

Имената се състоят от букви, цифри и _, като не могат да започват с цифра

  • a
  • baba
  • larodi_3
  • _spam_42
  • _A_h0Rr1bbl3_Nam3_U_c0UlD_uSE_buT_pL3Ase_DONT

присвояване на стойности

In [1]:
kind_of_pi = 3.14
the_answer = 42
our_hero = 'Zaphod Beeblebrox'
присвояване на много имена
In [2]:
a, b, c = 1, 2, 3
print(a, b, c)
1 2 3

вградени скаларни типове

In [3]:
# числа
one = 1
one_and_a_half = 1.5
# текстови низове
cheese_1 = 'brie'
cheese_2 = "gauda"
# булеви променливи
we_love_cheese = True
we_dislike_koalas = False

# 🤔
i = 1j
числа

Числата в python са ужасно лесни за ползване, което помага за огромната му популярност в научните среди, включително Machine Learning-а

In [4]:
a = 5  # цяло число `int`
b = 1.25  # число с десетична запетая `float`

Като цяло рядко ще се налага да мислите за разликата между двете

In [5]:
(3 + 5)/12
Out[5]:
0.6666666666666666
текстови низове

Могат да се пишат в единични ('') или в довйни ("") кавички.

Буквално няма никаква разлика, освен кои кавички ще се налага да екранирате в текста в тях.

In [6]:
print('This is a string')
print("This is also a string")
print("Single quotes shouldn't be escaped inside double quote and vice versa")
print('A string with single quotes must\'ve single quotes within it escaped')
print("It is said \"We need to escape double quotes withing strings surrounded in double quotes\"")
This is a string
This is also a string
Single quotes shouldn't be escaped inside double quote and vice versa
A string with single quotes must've single quotes within it escaped
It is said "We need to escape double quotes withing strings surrounded in double quotes"
комплексни числа

python поддържа комплексни числа като вграден тип в езика. Нотацията може да изглежда малко странна.

In [7]:
i = 1j
square_of_i = i * i
print(square_of_i)
print(square_of_i.real)
print(square_of_i.imag)
(-1+0j)
-1.0
0.0
типизация на променливите
  • обектите в python са силно типизирани
  • променливите нямат тип, т.е. могат да сочат към различни типове обекти в различни моменти от време
In [8]:
pi = 3.14
print(pi)
pi = 'approximately three point one four'
print(pi)
3.14
approximately three point one four

oперации с базови типове

аритметични операции с числа
In [9]:
3 + 5  # събирне на цели числа
Out[9]:
8
In [10]:
3 + 5.28  # събиране на числа с плаваща запетая (забележете резултата!)
Out[10]:
8.280000000000001
In [11]:
3 - 8  # изваждане
Out[11]:
-5
аритметични операции с числа
In [12]:
1j ** 2  # повдигане на степен
Out[12]:
(-1+0j)
In [13]:
28 / 5  # деление
Out[13]:
5.6
In [14]:
28 // 5  # целичислено деление
Out[14]:
5
In [15]:
28 % 5  # остатък при деление
Out[15]:
3
операции с текстови низове
операции с текстови низове
In [16]:
'баба' + ' ' + 'готви' + ' ' + 'баница'
Out[16]:
'баба готви баница'
операции с текстови низове
In [17]:
24 * 'na, '
Out[17]:
'na, na, na, na, na, na, na, na, na, na, na, na, na, na, na, na, na, na, na, na, na, na, na, na, '
In [18]:
24 * 'na, ' + 'BATMAN!'
Out[18]:
'na, na, na, na, na, na, na, na, na, na, na, na, na, na, na, na, na, na, na, na, na, na, na, na, BATMAN!'
булеви операции
In [19]:
True and False
Out[19]:
False
In [20]:
True or False
Out[20]:
True

структури за управление

if/else
In [21]:
a = 15
if a < 10:
    print('a is less than 10')
elif a < 20:
    print('a is less than 20, but more than 10')
else:
    print('a is more than 20')
a is less than 20, but more than 10
while
In [22]:
counter = 0
while counter < 5:
    print(counter)
    counter += 1
0
1
2
3
4
for … in …:

for циклите в python се използват само за обхождане на итеруеми обекти (мислете си за колекции от елементи).

Няма пряк аналог на for (int i = 0; i < n; i++) { stuff(i) } конструкцията в C и C-подобните езици. По-късно ще видим как можем да го постигнем ако наистина ни трябва.

колекции

В python има няколко вградени видове колекции:

  • n-торки/кортежи (tuple)
  • списъци (list)
  • речници (dict)
  • множества (set)

Могат да бъдат обхождани с for ... in ...: конструкцията. Всички вградени колекции в python са хетерогенни, т.е. могат да съсъдржат обекти от различни типове

наредени колекции
  • tuple
    • вграден тип за наредени n–торки
    • не могат да бъдат променяни
  • list
    • вграден тип за наредени списъци
    • могат да бъдат променяни
In [23]:
nil_vector = (0, 0, 0)  # tuple с три елемента
a_vector = (3, 4, 5)  # tuple с три елемента
vectors = [nil_vector, a_vector]  # list с два елемента, горните tuple-и
In [24]:
for component in a_vector:
    print(component)
3
4
5
In [25]:
for vector in vectors:
    print(vector)
(0, 0, 0)
(3, 4, 5)

казахме, че не е нужно да са хомогенни

In [26]:
things = (nil_vector, vectors, [[(2, 5, 4, 3), 8], 'баба'], 42, False)

също така казахаме, че tuple-ите не могат да се променят

In [27]:
things[3] = 0
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
<ipython-input-27-c67097f23028> in <module>()
----> 1 things[3] = 0

TypeError: 'tuple' object does not support item assignment

за разлика от списъците, които могат да се променят

In [28]:
print(things[2])
things[2][1] = 42
print(things[2])
[[(2, 5, 4, 3), 8], 'баба']
[[(2, 5, 4, 3), 8], 42]
slice-ове

Можем лесно да взимаме „изрезки“ от наредени колекции като списъци и n-торки. Момже да взимаме всички елементи след определен индекс, до определен индекс, или между два индекса.

In [3]:
indexes = [0, 1, 2, 3, 4, 5, 6, 7, 8]
In [4]:
indexes[2:]
Out[4]:
[2, 3, 4, 5, 6, 7, 8]
In [5]:
indexes[:3]
Out[5]:
[0, 1, 2]
In [6]:
indexes[4:6]
Out[6]:
[4, 5]

при slice-овете може да дефинираме и стъпка

In [11]:
indexes[2:8:2]
Out[11]:
[2, 4, 6]

можем да индексираме tuple-и и list-ове и на „наобратно“

In [7]:
indexes[-1]
Out[7]:
8

можем да правим и slice-ове с отрицателни индекси

In [8]:
indexes[-5:-2]
Out[8]:
[4, 5, 6]

очевидно ако „началние“ и „крайния“ индекс на slice-а го позволяват можем да имаме и отрицателна стъпка

In [14]:
indexes[9:2:-2]
Out[14]:
[8, 6, 4]

ако вземем slice без индекси, просто копираме итеруемото

In [15]:
indexes[:] == indexes
Out[15]:
True

is проверява дали две променливи сочат към един и същи обект (буквално еидн и същи адрес в паметта)

In [16]:
indexes[:] is indexes
Out[16]:
False
речници

колко от вас знаят какво е HashMap?

речниците са като HashMap

  • съпоставят точно една стойност на ключ
  • дават константен достъп до стойността за даден ключ
In [33]:
album_artists = {
    'Deconstruction': 'Devin Townsend Project',
    'Túlélő': 'Leander Kills',
    'Malina': 'Leprous',
    'Currents': 'Covet',
}
In [34]:
album_artists['Deconstruction']
Out[34]:
'Devin Townsend Project'
In [35]:
album_artists['Whenever You Need Somebody'] = 'Rick Astley'
In [36]:
album_artists['Whenever You Need Somebody']
Out[36]:
'Rick Astley'
множества

set е вграден тип за множество. Мислете си точно за математическата дефиниция на понятието, съвкупност от елементи, без повторения и без дефиниран ред.

In [37]:
some_numbers = {1, 5, 9, 12, 3, 42}
In [38]:
for number in some_numbers:
    print(number)
1
3
5
9
42
12
In [39]:
3 in some_numbers
Out[39]:
True
In [40]:
3.14 in some_numbers
Out[40]:
False
In [41]:
print(some_numbers)
{1, 3, 5, 9, 42, 12}
In [42]:
some_numbers.add(1024)
print(some_numbers)
{1024, 1, 3, 5, 9, 42, 12}
In [43]:
some_numbers.remove(9)
print(some_numbers)
{1024, 1, 3, 5, 42, 12}
обърнете внимание!
  • {} е синтаксис за създаване на празен речник
  • празно множество се създава със set()
In [44]:
an_empty_dict = {}
an_empty_set = set()
print(an_empty_dict)
print(an_empty_set)
{}
set()

схващания

comprehensions

Това, което в много други езици се постига с map/filter/и т.н. в python обикновено се постига с comprehension-и. Вградените колекции могат да бъдат създавани чрез comprehension изрази, чрез прилагане на трансформации върху други колекции.

In [45]:
numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]

even_numbers = [number for number in numbers if number % 2  == 0]
print(even_numbers)

squares = {number: number ** 2 for number in numbers}
print(squares)

twice_the_numbers = numbers + numbers
print(twice_the_numbers)

odd_over_five = {number for number in twice_the_numbers if number > 5}
print(odd_over_five)
[2, 4, 6, 8, 10]
{1: 1, 2: 4, 3: 9, 4: 16, 5: 25, 6: 36, 7: 49, 8: 64, 9: 81, 10: 100}
[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
{6, 7, 8, 9, 10}

type

всеки обект в python има точно определен тип, който можем да проверим с вградената функция type

In [46]:
beverage = 'tea'
type(beverage)
Out[46]:
str
In [47]:
hour_of_the_day = 4
type(hour_of_the_day)
Out[47]:
int
In [48]:
is_it_tea_time = hour_of_the_day == 4
type(is_it_tea_time)
Out[48]:
bool
In [49]:
type([2, 3, 'baba', [(8, False, True)]])
Out[49]:
list
In [50]:
type(some_numbers)
Out[50]:
set

функции

дефинират се с ключовата дума def последвана от името и списъка с аргументи, тялото е индентирано с 4 space спрямо реда с дефиницията, който трябва да завършва с двуеточие

In [51]:
def compute_answer(question):
    if question == 'The ultimate question of life, the universe and everything':
        return 42
    else:
        return None
In [52]:
compute_answer('The ultimate question of life, the universe and everything')
Out[52]:
42

стойности по подразбиране и именовани аргументи

keyword arguments

Аргументите на функциите могат да имат стойности по подразбиране, така че да не се налага да ги подаваме, освен ако не искаме да променим някакво базово поведение.

In [53]:
def bloat_sum(a, b, invert_result=False, required_type=None):
    if required_type:
        if not isinstance(a, required_type) or not isinstance(b, required_type):
            print('Arguments must be of type {}'.format(required_type))
            return None
    result = a + b
    return -result if invert_result else result

можем да не подадем никой от аргументите със стойност по подразбиране

In [54]:
bloat_sum(3, 4)
Out[54]:
7
In [55]:
bloat_sum('оле', '-ле')
Out[55]:
'оле-ле'

можем да ги подадем като позиционни, в който случай стойностите по подразбиране просто се игнорират

In [56]:
bloat_sum(3, 4, True, int)
Out[56]:
-7
In [57]:
bloat_sum('оле', '-ле', False, str)
Out[57]:
'оле-ле'

можем да прескочим един или няколко и да подадем изрично други

In [58]:
bloat_sum(3, 4, required_type=str)
Arguments must be of type <class 'str'>
In [59]:
bloat_sum(a=3, b=4)
Out[59]:
7
In [60]:
bloat_sum(b=4, invert_result=True, a=3)
Out[60]:
-7

keyword only

възможността за подаване на именовани аргументи като позиционни е нож с две остриета, с който е тривиално да се застреляте в крака

По тази причина съществува възможността за keyword only аргументи, т.е. такива, които не могат да се подават като позиционни, а само като именовани. Такива са keyword аргументите след * в списъка с аргументи в дефиницията на функцията.

In [61]:
def bloat_sum(a, b, *, invert_result=False, required_type=None):
    if required_type:
        if not isinstance(a, required_type) or not isinstance(b, required_type):
            print('Arguments must be of type {}'.format(required_type))
            return None
    result = a + b
    return -result if invert_result else result
In [62]:
bloat_sum(3, 4, True, int)
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
<ipython-input-62-42ebb9084de7> in <module>()
----> 1 bloat_sum(3, 4, True, int)

TypeError: bloat_sum() takes 2 positional arguments but 4 were given

*args и **kwargs

можем да дефинираме функции с произволен брой позиционни и именовани аргументи

In [63]:
def i_can_haz_argumentz(*args, **kwargs):
    print('args = {} {}'.format(args, type(args)))
    print('kwargs = {} {}'.format(kwargs, type(kwargs)))

Имената args и kwargs не са специални, но се използват по конвенция в почти всички ситуации, освен ако нямате много добра причина да искате да ползвате нещо друго. Специалната част са едната * в началото на името, която указва, че това е името, през което ще се достъпват позиционните аргументи и двете **, които указват името за именованите аргументи.

In [64]:
i_can_haz_argumentz(3, 4, 'сладолед',
                    animals=['pony', 'panda', 'koala'],
                    people={'Лъчо', 'Евгени', 'Стефан'})
args = (3, 4, 'сладолед') <class 'tuple'>
kwargs = {'animals': ['pony', 'panda', 'koala'], 'people': {'Стефан', 'Евгени', 'Лъчо'}} <class 'dict'>

разгъване на аргументи

можем да „разгънем“ списък или кортеж до позиционни аргументи когато извикваме функция

In [26]:
def add_arguments(a, b, c, d): return a + b + c + d
add_arguments(*[1, 2, 3, 4])
Out[26]:
10

можем да направим същото с речник, но за именовани аргументи

In [27]:
add_arguments(**{'b': 4, 'c': 1, 'a': 2, 'd': 3})
Out[27]:
10

рекурсия

рекурсия

рекурсия

рекурсия

рекурсия

badjokeeel.jpg

рекурсия

In [65]:
def factorial(n):
    if n == 0:
        return 1
    return n * factorial(n-1)

factorial(10)
Out[65]:
3628800

няма tail recursion оптимизация

декоратори

преизползваеми парчета код, с които можем да „опаковаме“ други функции

In [66]:
def just_return(result):
    return result

def call_tracker(function):
    def tracked(*args, **kwargs):
        print('{function} was called with args={args}, kwargs={kwargs}'.format(
            function=function,
            args=args,
            kwargs=kwargs))
        return function(*args, **kwargs)
    
    return tracked

track_and_return = call_tracker(just_return)
track_and_return(10)
<function just_return at 0x7fad786596a8> was called with args=(10,), kwargs={}
Out[66]:
10

дълго и досадно, а може да е по-приятно

In [67]:
def call_tracker(function):
    def tracked(*args, **kwargs):
        print('{function} was called with args={args}, kwargs={kwargs}'.format(
            function=function,
            args=args,
            kwargs=kwargs))
        return function(*args, **kwargs)
    
    return tracked

@call_tracker
def just_return(result):
    return result

just_return(42)
<function just_return at 0x7fad78659ae8> was called with args=(42,), kwargs={}
Out[67]:
42

ООП

класове/типове

python е обектно ориентиран език, всички стойности са обекти, инстанции на някой клас

Разбира се, можем да дефинираме наши собствени типове. Голяма част от структурите и дори видовете числа, които ще ползвате когато работите с numpy са всъщност инстанции на класове, дефинирани в самата бибилиотека, а не стандартни структури от езика. Ползата от това е, че тези имплементации могат да бъдат оптимизирани за типа работа, за която обикновено се ползва numpy, т.е. по-тежки изчисления, потенциално с много голям обем данни.

In [68]:
type(3)
Out[68]:
int
In [69]:
import numpy as np
type(np.array([[0.25, 0.34, 0.72], [0, 0.98, 0.54], [0.16, 0.7, 1]]))
Out[69]:
numpy.ndarray

Класове се дефинират с ключовата дума class, последвана от името на класа, последващия блок от код е тялото на класа. Всички променливи дефинирани в тялото на класа са класови променливи и могат да бъдат достъпвани чрез самия клас или чрез инстанциите му. Всички функции дефинирани в тялото на класа са методи и могат да бъдат достъпвани през класа или през инстанциите му (има разлика в резултата).

__init__ е специален метод, който се извиква при създаване на инстнация от класа. Може да си мислите за него като за конструктор, но по-правилната дума е „инциализатор“, тъй като заделянето на памет за обекта, което обикновено се случва в конструктора, вече е извършено преди да се извика __init__.

Първия аргумент на всеки метод е инстанцията, върху която се извиква. Името няма синтактично значение, но по конвенция е прието винаги да се използва self.

In [70]:
class Vector:
    def __init__(self, x, y):
        self.x = x
        self.y = y

v = Vector(1.0, 1.0)
v.x
Out[70]:
1.0

така създадохме наш собствен тип

In [71]:
type(v)
Out[71]:
__main__.Vector
лирическо отклонение

споменахме, че всичко в python е обект, инстнация на някакъв клас, значи...

In [72]:
type(Vector)
Out[72]:
type
In [73]:
type(just_return)
Out[73]:
function

ама ВСИЧКО ВСИЧКО... тогава, какво ще стане ако...

In [74]:
type(type)
Out[74]:
type
...

Don't think about it

     – Rick Sanchez

методи

Методите на инстанциите се достъпват точно както другите им атрибути. Те просто са функции и могат да бъдат извиквани.

In [75]:
import math

class Vector:
    def __init__(self, x, y):
        self.x = x
        self.y = y
        
    def length(self):
        return math.sqrt(self.x**2 + self.y**2)
    
v = Vector(3, 4)
v.length()
Out[75]:
5.0
In [76]:
import math

class Vector:
    def __init__(self, x, y):
        self.x = x
        self.y = y
        
    def length(self):
        return math.sqrt(self.x**2 + self.y**2)
    
    def scale(self, coeficient):
        self.x = self.x * coeficient
        self.y = self.y * coeficient
        
v = Vector(3, 4)
print(v.length())
v.scale(2)
print(v.x, v.y, v.length())
5.0
6 8 10.0
„магически“ методи

Имаме възможността да конторлираме доста по-детайлно поведението на инстанциите на класовете, които дефинираме, чрез така наречените „магически“ методи (magic methods/dunder methods). Това са определени методи с имена започващи и завършващи с две подчертавки: __some_method_name__.

Така можем да предефинираме базови операции като събиране, изваждане, деление и т.н., както и по-интересни неща като как може да итерираме по инстанциите на класа си като колекции, да проверяваме за това дали обект е елемент в тях или не, да ги индексираме и прочее.

аритметични операции
  • __add__
  • __sub__
  • __mul__
  • __lt__
  • __gt__
  • __eq__
  • … Повече може да намерите тук
In [77]:
class Vector:
    def __init__(self, x, y):
        self.x = x
        self.y = y
        
    def __add__(self, other):
        return Vector(self.x + other.x, self.y + other.y)
    
    def __mul__(self, other):
        return Vector(self.x * other.x, self.y * other.y)
    
    def __sub__(self, other):
        return self + Vector(-other.x, -other.y)
    
    def __str__(self):
        return '<Vector x={x}, y={y}>'.format(x=self.x, y=self.y)
    
v1, v2 = Vector(3, 4), Vector(1, 2)
print(v1 - v2)
print(v1 + v2)
print(v1 * v2)
<Vector x=2, y=2>
<Vector x=4, y=6>
<Vector x=3, y=8>
да си направим наши колекции
In [78]:
class Vector:
    def __init__(self, x, y):
        self.x = x
        self.y = y

    def __getitem__(self, index):
        if index == 0:
            return self.x
        if index == 1:
            return self.y
        
        raise IndexError('Vector only has two components')
        
v1 = Vector(3, 4)
print(v1[0])
print(v1[1])
print(v1[15])
3
4
---------------------------------------------------------------------------
IndexError                                Traceback (most recent call last)
<ipython-input-78-d77f648fbbb6> in <module>()
     15 print(v1[0])
     16 print(v1[1])
---> 17 print(v1[15])

<ipython-input-78-d77f648fbbb6> in __getitem__(self, index)
     10             return self.y
     11 
---> 12         raise IndexError('Vector only has two components')
     13 
     14 v1 = Vector(3, 4)

IndexError: Vector only has two components
In [79]:
class Vector:
    class VectorIterator:
        def __init__(self, vector):
            self.vector = vector
            self.went_over_x = False
            self.went_over_y = False
            
        def __next__(self):
            if not self.went_over_x:
                self.went_over_x = True
                return self.vector.x
            elif not self.went_over_y:
                self.went_over_y = True
                return self.vector.y
            else:
                raise StopIteration
    
    def __init__(self, x, y):
        self.x = x
        self.y = y

    def __iter__(self):
        return self.VectorIterator(self)
In [80]:
v = Vector(3, 4)
for component in v:
    print(component)
3
4

това обаче е дълго и грозно... на python всичко трябва да е красиво, кратко и елегантно... soooooooo...

генератори

Генераторите в python са механизъм за лесно описване на специфични видове итеруеми обекти. Друг начин, по който можете да гледате на генераторите е като на корутини (всъщност корутините в python се реализират чрез генератори).

Генератор-функция е такава фунцкия, в която се използва ключовата дума yield. Докато семантиката на return е приключване на изпълнение на функцията и връщане на съответната стойност, то семантиката на yield е „паузиране“ на изпълнението на функцията и потенциално предаване на стойността към горния scope, от който се итерира по генератора.

Ако последните изречения ви объркват, не е проблем. Кода ще помогне.

In [81]:
def actors_generator():
    yield 'Graham Chapman'
    yield 'John Cleese'
    yield 'Terry Gilliam'
    yield 'Eric Idle'
    yield 'Terry Jones'
    yield 'Michael Palin'
    
actors = actors_generator()
for actor in actors:
    print(actor)
Graham Chapman
John Cleese
Terry Gilliam
Eric Idle
Terry Jones
Michael Palin

Тогава можем да пренапишем горната ни имплементация на итератор мнго по-просто и кратко с генератор.

In [82]:
class Vector:
    def __init__(self, x, y):
        self.x = x
        self.y = y

    def __iter__(self):
        yield self.x
        yield self.y
        
v = Vector(3, 4)
for component in v:
    print(component)
3
4

Генераторите могат да бъдат и „безкрайни“, т.е. да продължават да гененрират нови стойности при всяко ново извикване и никога да не хвърлят StopIteration.

In [22]:
def even_number_generator(start=2):
    if start % 2 == 0:
        number = start
    else:
        number = start + 1
    while True:
        yield number
        number += 2
        
even_numbers = even_number_generator()
print(next(even_numbers))
print(next(even_numbers))

even_numbers_from_nine = even_number_generator(9)
print(next(even_numbers_from_nine))
print(next(even_numbers_from_nine))
print(next(even_numbers_from_nine))
print(next(even_numbers_from_nine))
2
4
10
12
14
16

Както по-рано дефинирахме списъци/множества/речници с comprehension-и, по подобен начин можем да използваме и генератор comprehension.

In [23]:
multiples_of_six = (number for number in even_numbers if number % 3 ==0)
print(next(multiples_of_six))
print(next(multiples_of_six))
print(next(multiples_of_six))
print(next(multiples_of_six))
6
12
18
24

Ако искаме да оперираме с много големи, или дори в конкретние случай с безкрайни данни, няма как да конструираме списък, тъй като нямаме достатъчно памет, в която да държим всички елементи на списъка.

In [25]:
# този ред буквално „забива“ докато операционната система умре заради липса на RAM
# или докато реши да убие процеса, защото е твърде лаком
multiples_of_six = [number for number in even_numbers if number % 3 == 0] 

наследяване

In [95]:
class Person:
    def __init__(self, first_name, last_name):
        self.first_name, self.last_name = first_name, last_name

    def name(self):
        return "{0} {1}".format(self.first_name, self.last_name)

class Doctor(Person):
    def name(self):
        return "Ph.D. {0}".format(Person.name(self))
In [96]:
gencho = Person('Евгени', 'Кунев')
luchko = Doctor('Лъчезар', 'Божков')
In [97]:
print(gencho.name())
print(luchko.name())
Евгени Кунев
Ph.D. Лъчезар Божков

изключения

конструкцията за обработване на изключения в python е следната

In [29]:
try:
    1/0
except Exception as e:
    print(e)
finally:
    print("we're through...")
division by zero
we're through...

след ключовата дума try има блок код, в който очакваме, че може да се предизвика изключение след него следва ключовата дума except, след която можем да кажим какъв вид изключения конкретно искаме да обработваме в последващия блок код на края всичко във finally блока винаги се изпълнява без значение дали е било предзвикнао изключение или не

за разлика от повечето езици, в python try/except конструкцията, освен finally блок може да има и else блок, който се изпълянва само ако не е било предизвикано изключение else блока трябва да е след всички except блокове и преди finally блока

In [31]:
try:
    1/2
except Exception as e:
    print(e)
else:
    print('amazingly so, nothing bad actually happened')
finally:
    print("we're through...")
amazingly so, nothing bad actually happened
we're through...