Курса си има 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.
Според операционната ви система е възможно да имате 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
kind_of_pi = 3.14
the_answer = 42
our_hero = 'Zaphod Beeblebrox'
a, b, c = 1, 2, 3
print(a, b, c)
# числа
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-а
a = 5 # цяло число `int`
b = 1.25 # число с десетична запетая `float`
Като цяло рядко ще се налага да мислите за разликата между двете
(3 + 5)/12
Могат да се пишат в единични (''
) или в довйни (""
) кавички.
Буквално няма никаква разлика, освен кои кавички ще се налага да екранирате в текста в тях.
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\"")
python поддържа комплексни числа като вграден тип в езика. Нотацията може да изглежда малко странна.
i = 1j
square_of_i = i * i
print(square_of_i)
print(square_of_i.real)
print(square_of_i.imag)
pi = 3.14
print(pi)
pi = 'approximately three point one four'
print(pi)
3 + 5 # събирне на цели числа
3 + 5.28 # събиране на числа с плаваща запетая (забележете резултата!)
3 - 8 # изваждане
1j ** 2 # повдигане на степен
28 / 5 # деление
28 // 5 # целичислено деление
28 % 5 # остатък при деление
'баба' + ' ' + 'готви' + ' ' + 'баница'
24 * 'na, '
24 * 'na, ' + 'BATMAN!'
True and False
True or False
if
/else
¶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')
while
¶counter = 0
while counter < 5:
print(counter)
counter += 1
for … in …:
¶for
циклите в python се използват само за обхождане на итеруеми обекти (мислете си за колекции от елементи).
Няма пряк аналог на for (int i = 0; i < n; i++) { stuff(i) }
конструкцията в C и C-подобните езици. По-късно ще видим как можем да го постигнем ако наистина ни трябва.
В python има няколко вградени видове колекции:
tuple
)list
)dict
)set
)Могат да бъдат обхождани с for ... in ...:
конструкцията.
Всички вградени колекции в python са хетерогенни, т.е. могат да съсъдржат обекти от различни типове
tuple
list
nil_vector = (0, 0, 0) # tuple с три елемента
a_vector = (3, 4, 5) # tuple с три елемента
vectors = [nil_vector, a_vector] # list с два елемента, горните tuple-и
for component in a_vector:
print(component)
for vector in vectors:
print(vector)
казахме, че не е нужно да са хомогенни
things = (nil_vector, vectors, [[(2, 5, 4, 3), 8], 'баба'], 42, False)
също така казахаме, че tuple
-ите не могат да се променят
things[3] = 0
за разлика от списъците, които могат да се променят
print(things[2])
things[2][1] = 42
print(things[2])
Можем лесно да взимаме „изрезки“ от наредени колекции като списъци и n-торки. Момже да взимаме всички елементи след определен индекс, до определен индекс, или между два индекса.
indexes = [0, 1, 2, 3, 4, 5, 6, 7, 8]
indexes[2:]
indexes[:3]
indexes[4:6]
при slice-овете може да дефинираме и стъпка
indexes[2:8:2]
можем да индексираме tuple
-и и list
-ове и на „наобратно“
indexes[-1]
можем да правим и slice-ове с отрицателни индекси
indexes[-5:-2]
очевидно ако „началние“ и „крайния“ индекс на slice-а го позволяват можем да имаме и отрицателна стъпка
indexes[9:2:-2]
ако вземем slice без индекси, просто копираме итеруемото
indexes[:] == indexes
is
проверява дали две променливи сочат към един и същи обект (буквално еидн и същи адрес в паметта)
indexes[:] is indexes
колко от вас знаят какво е HashMap
?
речниците са като HashMap
album_artists = {
'Deconstruction': 'Devin Townsend Project',
'Túlélő': 'Leander Kills',
'Malina': 'Leprous',
'Currents': 'Covet',
}
album_artists['Deconstruction']
album_artists['Whenever You Need Somebody'] = 'Rick Astley'
album_artists['Whenever You Need Somebody']
set
е вграден тип за множество. Мислете си точно за математическата дефиниция на понятието, съвкупност от елементи, без повторения и без дефиниран ред.
some_numbers = {1, 5, 9, 12, 3, 42}
for number in some_numbers:
print(number)
3 in some_numbers
3.14 in some_numbers
print(some_numbers)
some_numbers.add(1024)
print(some_numbers)
some_numbers.remove(9)
print(some_numbers)
{}
е синтаксис за създаване на празен речникset()
an_empty_dict = {}
an_empty_set = set()
print(an_empty_dict)
print(an_empty_set)
Това, което в много други езици се постига с map
/filter
/и т.н. в python обикновено се постига с comprehension-и. Вградените колекции могат да бъдат създавани чрез comprehension изрази, чрез прилагане на трансформации върху други колекции.
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)
type
¶всеки обект в python има точно определен тип, който можем да проверим с вградената функция type
beverage = 'tea'
type(beverage)
hour_of_the_day = 4
type(hour_of_the_day)
is_it_tea_time = hour_of_the_day == 4
type(is_it_tea_time)
type([2, 3, 'baba', [(8, False, True)]])
type(some_numbers)
дефинират се с ключовата дума def
последвана от името и списъка с аргументи, тялото е индентирано с 4 space спрямо реда с дефиницията, който трябва да завършва с двуеточие
def compute_answer(question):
if question == 'The ultimate question of life, the universe and everything':
return 42
else:
return None
compute_answer('The ultimate question of life, the universe and everything')
Аргументите на функциите могат да имат стойности по подразбиране, така че да не се налага да ги подаваме, освен ако не искаме да променим някакво базово поведение.
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
можем да не подадем никой от аргументите със стойност по подразбиране
bloat_sum(3, 4)
bloat_sum('оле', '-ле')
можем да ги подадем като позиционни, в който случай стойностите по подразбиране просто се игнорират
bloat_sum(3, 4, True, int)
bloat_sum('оле', '-ле', False, str)
можем да прескочим един или няколко и да подадем изрично други
bloat_sum(3, 4, required_type=str)
bloat_sum(a=3, b=4)
bloat_sum(b=4, invert_result=True, a=3)
възможността за подаване на именовани аргументи като позиционни е нож с две остриета, с който е тривиално да се застреляте в крака
По тази причина съществува възможността за keyword only аргументи, т.е. такива, които не могат да се подават като позиционни, а само като именовани. Такива са keyword аргументите след *
в списъка с аргументи в дефиницията на функцията.
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
bloat_sum(3, 4, True, int)
*args
и **kwargs
¶можем да дефинираме функции с произволен брой позиционни и именовани аргументи
def i_can_haz_argumentz(*args, **kwargs):
print('args = {} {}'.format(args, type(args)))
print('kwargs = {} {}'.format(kwargs, type(kwargs)))
Имената args
и kwargs
не са специални, но се използват по конвенция в почти всички ситуации, освен ако нямате много добра причина да искате да ползвате нещо друго. Специалната част са едната *
в началото на името, която указва, че това е името, през което ще се достъпват позиционните аргументи и двете **
, които указват името за именованите аргументи.
i_can_haz_argumentz(3, 4, 'сладолед',
animals=['pony', 'panda', 'koala'],
people={'Лъчо', 'Евгени', 'Стефан'})
можем да „разгънем“ списък или кортеж до позиционни аргументи когато извикваме функция
def add_arguments(a, b, c, d): return a + b + c + d
add_arguments(*[1, 2, 3, 4])
можем да направим същото с речник, но за именовани аргументи
add_arguments(**{'b': 4, 'c': 1, 'a': 2, 'd': 3})
badjokeeel.jpg
def factorial(n):
if n == 0:
return 1
return n * factorial(n-1)
factorial(10)
няма tail recursion оптимизация
преизползваеми парчета код, с които можем да „опаковаме“ други функции
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)
дълго и досадно, а може да е по-приятно
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)
python е обектно ориентиран език, всички стойности са обекти, инстанции на някой клас
Разбира се, можем да дефинираме наши собствени типове. Голяма част от структурите и дори видовете числа, които ще ползвате когато работите с numpy
са всъщност инстанции на класове, дефинирани в самата бибилиотека, а не стандартни структури от езика. Ползата от това е, че тези имплементации могат да бъдат оптимизирани за типа работа, за която обикновено се ползва numpy
, т.е. по-тежки изчисления, потенциално с много голям обем данни.
type(3)
import numpy as np
type(np.array([[0.25, 0.34, 0.72], [0, 0.98, 0.54], [0.16, 0.7, 1]]))
Класове се дефинират с ключовата дума class
, последвана от името на класа, последващия блок от код е тялото на класа. Всички променливи дефинирани в тялото на класа са класови променливи и могат да бъдат достъпвани чрез самия клас или чрез инстанциите му. Всички функции дефинирани в тялото на класа са методи и могат да бъдат достъпвани през класа или през инстанциите му (има разлика в резултата).
__init__
е специален метод, който се извиква при създаване на инстнация от класа. Може да си мислите за него като за конструктор, но по-правилната дума е „инциализатор“, тъй като заделянето на памет за обекта, което обикновено се случва в конструктора, вече е извършено преди да се извика __init__
.
Първия аргумент на всеки метод е инстанцията, върху която се извиква. Името няма синтактично значение, но по конвенция е прието винаги да се използва self
.
class Vector:
def __init__(self, x, y):
self.x = x
self.y = y
v = Vector(1.0, 1.0)
v.x
така създадохме наш собствен тип
type(v)
споменахме, че всичко в python е обект, инстнация на някакъв клас, значи...
type(Vector)
type(just_return)
ама ВСИЧКО ВСИЧКО... тогава, какво ще стане ако...
type(type)
Don't think about it
– Rick Sanchez
Методите на инстанциите се достъпват точно както другите им атрибути. Те просто са функции и могат да бъдат извиквани.
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()
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())
Имаме възможността да конторлираме доста по-детайлно поведението на инстанциите на класовете, които дефинираме, чрез така наречените „магически“ методи (magic methods/dunder methods). Това са определени методи с имена започващи и завършващи с две подчертавки: __some_method_name__
.
Така можем да предефинираме базови операции като събиране, изваждане, деление и т.н., както и по-интересни неща като как може да итерираме по инстанциите на класа си като колекции, да проверяваме за това дали обект е елемент в тях или не, да ги индексираме и прочее.
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)
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])
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)
v = Vector(3, 4)
for component in v:
print(component)
това обаче е дълго и грозно... на python всичко трябва да е красиво, кратко и елегантно... soooooooo...
Генераторите в python са механизъм за лесно описване на специфични видове итеруеми обекти. Друг начин, по който можете да гледате на генераторите е като на корутини (всъщност корутините в python се реализират чрез генератори).
Генератор-функция е такава фунцкия, в която се използва ключовата дума yield
. Докато семантиката на return
е приключване на изпълнение на функцията и връщане на съответната стойност, то семантиката на yield
е „паузиране“ на изпълнението на функцията и потенциално предаване на стойността към горния scope, от който се итерира по генератора.
Ако последните изречения ви объркват, не е проблем. Кода ще помогне.
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)
Тогава можем да пренапишем горната ни имплементация на итератор мнго по-просто и кратко с генератор.
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)
Генераторите могат да бъдат и „безкрайни“, т.е. да продължават да гененрират нови стойности при всяко ново извикване и никога да не хвърлят StopIteration
.
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))
Както по-рано дефинирахме списъци/множества/речници с comprehension-и, по подобен начин можем да използваме и генератор comprehension.
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))
Ако искаме да оперираме с много големи, или дори в конкретние случай с безкрайни данни, няма как да конструираме списък, тъй като нямаме достатъчно памет, в която да държим всички елементи на списъка.
# този ред буквално „забива“ докато операционната система умре заради липса на RAM
# или докато реши да убие процеса, защото е твърде лаком
multiples_of_six = [number for number in even_numbers if number % 3 == 0]
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))
gencho = Person('Евгени', 'Кунев')
luchko = Doctor('Лъчезар', 'Божков')
print(gencho.name())
print(luchko.name())
конструкцията за обработване на изключения в python е следната
try:
1/0
except Exception as e:
print(e)
finally:
print("we're through...")
след ключовата дума try
има блок код, в който очакваме, че може да се предизвика изключение
след него следва ключовата дума except
, след която можем да кажим какъв вид изключения конкретно искаме да обработваме в последващия блок код
на края всичко във finally
блока винаги се изпълнява без значение дали е било предзвикнао изключение или не
за разлика от повечето езици, в python try/except
конструкцията, освен finally
блок може да има и else
блок, който се изпълянва само ако не е било предизвикано изключение
else
блока трябва да е след всички except
блокове и преди finally
блока
try:
1/2
except Exception as e:
print(e)
else:
print('amazingly so, nothing bad actually happened')
finally:
print("we're through...")