Функционално програмиране#
План на лекцията:
Какво е функционално програмиране ?
Immutability & side-effects
Функции като обекти
Декоратори
Анонимни (lambda) функции
Lazy evaluation
Generators
Map
Filter
Reduce
Zip
List comprehension
Скорост
Dict comprehension
Pattern matching
Примери
Какво е функционално програмиране ?#
Функционалното програмиране е парадигма в програмирането, фокусираща се върху използването на функции. В езиците за функционално програмиране, функциите могат да се третират като обекти. Друга особенност на функционалното програмиране (или поне в чистата му форма), е липсата на състояние (а и от там, липсата на променливи в традиционния смисъл). В повечето езици за функционално програмиране липсват и класове.
Основни концепции във функционалното програмиране:
Функции като обекти
Функции от по-висок ред
“Чисти” функции
Рекурсия
Примери за езици, които следват единствено функционалната парадигма са Scheme, Haskell, Lisp.
В повечето модерни езици за програмиране могат да се открият елементи от функционалното програмиране - Python не е изключение.
Immutability и странични ефекти#
Накратко за mutable/immutable#
По дефиниция, една променлива е mutable
, ако можем да променим стойността ѝ, след като е създадена. Съответно immutable
променлива, е променлива, на която не можем да променяме стойността. На практика, това означава, че стойността на една immutable
променлива може да бъде задедена, само при създаването на променливата.
В езици като C++ или Java, това е еквивалентно на дефинирането на променлива като константа. Тогава стойността ѝ е ясна по време на компилация.
Накратко за страничните ефекти#
Страничен ефект е когато променим състоянието на някоя променлива от дадена функция. Например нека разгледаме следната функция
def add_to_list(l):
l.append(5)
l = []
add_to_list(l)
print(l)
add_to_list(l)
print(l)
[5]
[5, 5]
Тази функция, променя стойността на подадения аргумент l
. Казваме, че тази функция съдържа страничен ефект.
Страничните ефекти са силно използвани в обектно-ориентираното и процедурното програмиране, за разлика от функционалното програмиране. Както по-горе споменахме, в класическите езици за функционално програмиране, липсват променливи в познатия ни вид. Там можем да присвоим име на дадена стойност, без да я променяме повече.
Функции, които не променят стойностите на променливи, се наричат “чисти”. Във функционалното програмиране (а и не само), е прието всички функции да са чисти.
Можем да променим нашата функция add
по следния начин, за да стане чиста:
def add_to_list(l):
return l + [5]
l = []
l1 = add_to_list(l)
print(l1)
l2 = add_to_list(l)
print(l2)
print(l)
[5]
[5]
Функции, като обекти#
В Python, функциите са т.нар. “first-class objects” - което на български може да се преведе като “първокласни обекти”. А ако трябва да го обясним с прости думи, функциите могат да се третират като “нормалните” типове - числа, низове и т.н. Това означава, че можем да ги използваме като променливи - да им даваме имена, да ги подаваме към аргументи на други функции, да ги връщаме като резултат от функции и да ги пазим в колекции.
def multiply(a, b):
return a * b
f = multiply
print(f(2, 3))
6
Важно е да направим разлика между f = multiply
и f = multiply()
. Макар и разликата да е малка синтактично, поведението на двата реда е различно. Първото извикване присвоява функцията към променливата f
, докато второто присвоява резултата от извикването на функцията.
Когато присвоим цялата функция (може да си го представяме като function pointer в C++), можем да ползваме функцията като нормална променлива:
def multiply(a, b):
return a * b
def sum_(a, b):
return a + b
def apply_to_numbers(a, b, f):
print(f'Applying a function to {a} and {b}')
return f(a, b)
print(apply_to_numbers(2, 3, sum_))
print(apply_to_numbers(2, 3, multiply))
Applying a function to 2 and 3
5
Applying a function to 2 and 3
6
А можем и да връщаме функция като резултат от друга функция:
def foo():
print("Foo called")
def bar():
return foo
f = bar()
f()
Foo called
В тялото на една функция, можем да дефинираме друга функция, която да върнем:
def foo():
def bar():
print("Bar called")
return bar
f = foo()
f()
Bar called
Това, че функциите са обекти, ни позволява да правим интересни неща - например, можем да направим проста функция, която да използваме за logging.
def log_and_run(f, args=(), kwargs={}):
print(f'Calling {f} with {args} and {kwargs}')
return f(*args, **kwargs)
def add(a, b):
return a + b
print(log_and_run(add, (2, 3)))
Calling <function add at 0x7f2c9d108320> with (2, 3) and {}
5
Припомням, че args
съдържа позиционните аргументи и е от тип Tuple
, а kwargs
съдържа keyword аргументите и е от тип dict
.
Освен като аргументи и резултат, функциите могат да се пазят и в колекции (списъци, речници, т.н.):
def add(a, b):
return a + b
def subtract(a, b):
return a - b
def multiply(a, b):
return a * b
def divide(a, b):
return a / b
actions = {
'+': add,
'-': subtract,
'*': multiply,
'/': divide
}
first = input('Enter a number: ')
operation = input('Enter an operation (+, -, * or /)')
second = input('Enter a number: ')
if operation not in actions:
print('Operation not supported')
else:
if not first.isnumeric() or not second.isnumeric():
print('Input is not a number')
else:
first = int(first)
second = int(second)
result = actions[operation](first, second)
print(f'{first} {operation} {second} = {result}')
Enter a number: 2
Enter an operation (+, -, * or /)+
Enter a number: 3
2 + 3 = 5
Декоратори#
Една от по-интересните функционалности на Python са т.нар. декоратори (decorators). Всъщност, ние вече видяхме как се пише декоратор и как работи - нека се върнем на примера от по-горе с log_and_run
:
def log_and_run(f, args=(), kwargs={}):
print(f'Calling {f} with {args} and {kwargs}')
return f(*args, **kwargs)
def add(a, b):
return a + b
print(log_and_run(add, (2, 3)))
Calling <function add at 0x7f2c9d0b7170> with (2, 3) and {}
5
Това изглежда леко грозно, да трябва да извикваме log_and_run
вместо функцията, която всъщност искаме да извикаме. Ако добавим функции за изваждане, умножение и деление, всяко извикване ще трябва да преминава през log_and_run
.
А няма ли по-лесен начин с който да кажем - нека преди изпълнението на нашата функция f1
, да се изпълни функция f
(Както преди add
се изпълнява log_and_run
) ?
Преди да разгледаме как точно се прави декоратор функция в Python, нека всъщност разберем какво искаме да постигнем:
Искаме да приемем функция (нека я кръстим
g
), която да изпълнимПреди да изпълним
g
, искаме да изпълним някакви други действия (нека те са събрани във функциятаf
Искаме да върнем нова функция (нека да я кръстим
h
), която първо да изпълниf
, а послеg
.
Нека първо се фокусираме върху 1 и 2 :
def pseudo_decorator(f, g):
f()
g()
def print_hello():
print("Hello")
def print_bye():
print("Bye")
pseudo_decorator(print_hello, print_bye)
Hello
bye
Нека променим поведението на pseudo_decorator
от това просто да изпълнява f
и g
към това да връща нова функция, която да изпълнява f
и g
def pseudo_decorator(f, g):
def inner():
f()
g()
return inner
def print_hello():
print("Hello")
def print_bye():
print("bye")
f = pseudo_decorator(print_hello, print_bye)
f()
Hello
bye
Почти сме до истинските декоратори. За съжаление, следващата част е възможна заради малко синтактична захар.
Основната идея е, че искаме да вземем нашата функция g
и да я заместим с резултата от pseudo_decorator
, без да се налага реално да извикваме pseudo_decorator
. С цел опростяване, ще приемем, че f
е просто принтиране.
def print_decorator(g):
def inner():
print("Hello")
g()
return inner
@print_decorator
def print_bye():
print("bye")
print_bye()
Hello
bye
С помощта на @
, последвано от функция, която връща друга функция, ние можем да “декорираме” всяка функция в Python.
Това, което се случва в случая е, че взимаме print_bye
като първи аргумент на print_decorator
и изпълняваме print_decorator
. Тя от своя страна ни връща нова функция, която принтира “Hello” и след това изпълнява подадената от нас функция.
Обяснено още по-просто: При извикване на print_bye
, всъщност ще се извика print_decorator
с аргумент print_bye
.
Нека използваме този нов подход върху примера с log_and_run
(която ще преименуваме само на log
)
def log(f):
def inner(*args, **kwargs):
print(f'Calling {f} with {args} and {kwargs}')
return f(*args, **kwargs)
return inner
@log
def add(a, b):
return a + b
print(add(2, 3))
Calling <function add at 0x7f2c9d0be440> with (2, 3) and {}
5
Сега ако добавим нови методи за изваждане, умножение и деление, можем лесно да се използваме от log
, без да се налага да променяме начина им на извикване:
def log(f):
def inner(*args, **kwargs):
print(f'Calling {f} with {args} and {kwargs}')
return f(*args, **kwargs)
return inner
@log
def add(a, b):
return a + b
@log
def subtract(a, b):
return a - b
@log
def multiply(a, b):
return a * b
@log
def divide(a, b):
return a / b
print(add(2, 3))
print(subtract(3, 1))
print(multiply(1.5, b=3))
print(divide(a=12, b=6))
Calling <function add at 0x7f2c9d0be170> with (2, 3) and {}
5
Calling <function subtract at 0x7f2c9d0be440> with (3, 1) and {}
2
Calling <function multiply at 0x7f2c9d0bedd0> with (1.5,) and {'b': 3}
4.5
Calling <function divide at 0x7f2c9d0bef80> with () and {'a': 12, 'b': 6}
2.0
Друг пример за декоратор може да бъде декоратор, който измерва времето за изпълнение на функция:
import time
def time_it(f):
def inner(*args, **kwargs):
start = time.time()
result = f(*args, **kwargs)
end = time.time()
print(f'{f} took {end - start:.2f} second')
return inner
@time_it
def slow_function(a):
return (a ** a) ** a
slow_function(1000)
<function slow_function at 0x7f2c9d0c4950> took 1.56 second
Анонимни (lambda) функции#
Lambda функции идват от т.нар ламбда смятане (lambda calculus) и изразяват изпълнението на дадени изчисления върху дадени стойности. Няма да навлизаме в математическите доказателства и формализми. Lambda функциите са анонимни функции - т.е. не се дефинират с def
и без да е необходимо да им се дава име.
Всяка ламбда функция има две основни части - входни променливи и един израз (expression). В Python, ламбда функциите се дефинират с ключовата дума lambda
lambda x: x + 1
<function __main__.<lambda>(x)>
По-горната функция приема един аргумент (на име x
), и изпълнява израза x + 1
. Важно е да се отбележи, че една ламбда функция може да съдържа само един израз.
Припомням, че израз в Python е всичко, което съдържа някакъв индетификатор (напр. променлива), литерал (напр. 'hello'
) или оператор (напр. извикване на функция, оператор []
)
Или казано по-просто: не може да създадем ламбда, която да присвоява стойност на някаква променлива:
lambda x: a = x
File "<ipython-input-65-6a40442b8e41>", line 1
lambda x: a = x
^
SyntaxError: can't assign to lambda
Не можем също и да имаме два израза един след друг (Това чисто синтактично няма как да ни се позволи - ламбдите са на един ред, а в Python нямаме символ за край на израз.
Можем да извикваме дадена lambda функция, като просто използваме ()
след нея
(lambda x: print(x))(5)
5
Макар и анонимни, можем да присвоим ламбда функция към дадена променлива
f = lambda x: x + 1
print(f(5))
6
Горното парче код изпълнява следните стъпки:
Създава анонимна функция, която приема един аргумент (
x
) и връща стойноста наx
събрана с 1Присвояваме новосъздадената ни функция към променливата
f
Принтираме резултата от извикването на
f
със стойност 5В тялото на ламбда функцията, стойноста на
x
се замества с 5От там, 5 + 1 = 6
Едно от местата, където често се ползват lambda функции, са вградените функции за сортиране. Както знаем, метода .sort()
може да приема функция, която да сравнява обектите.
l = [5, 2, 7, 3]
l.sort(key=lambda x: -x)
print(l)
[7, 5, 3, 2]
С помощта на тази ламбда, можем да сортираме числата в обратен ред.
Можем да използваме ламбди за да сортираме и по-сложни списъци - например списък от наредени n-торки.
Нека е даден списък, съставен от имена на хора, и тяхната възраст.
people_data = [('Иван', 22), ('Георги', 71), ('Мария', 35), ('Митко', 51), ('Любо', 35)]
people_data.sort(key=lambda person: (person[1], person[0]))
print(people_data)
[('Иван', 22), ('Любо', 35), ('Мария', 35), ('Митко', 51), ('Георги', 71)]
Тук първо сортираме хората по тяхната възраст, а после лексикографкси, по тяхното име.
Можем да използваме и функцията sorted
с ламбда функция:
people_data = [('Иван', 22), ('Георги', 71), ('Мария', 35), ('Митко', 51), ('Любо', 35)]
print(sorted(people_data, key=lambda person: (person[1], person[0])))
[('Иван', 22), ('Любо', 35), ('Мария', 35), ('Митко', 51), ('Георги', 71)]
Lazy evaluation#
Съществуват различни стратегии за оценяване (или изчисляване) на изрази - най-позната е т.нар. “нетърпеливо оценяване” или “оценяване на момента” - целия израз се изчислява на момента.
Нека е даден израза a + b + c
. Първо, ще бъде оценена стойността на a
, после на b
. Следващата стъпка е да се извърши операцията събиране. Резултатът от първата операция ще бъде използван за следващата операция събиране, заедно с c
.
Пример за оценяване на момента може да бъде следната функция:
def f(x):
print(f'x = {x}')
return x
print(f(1) + f(2))
x = 1
x = 2
3
При оценяването на момента (eager evaluation), всички необходими данни за извършване на изчислението (променливи, функции, др.) се зареждат наведнъж в паметта. Освен това, всички изчисления трябва да бъда извършени точно на момента.
Тук се намесва т.нар. “мързеливо” оценяване - изчисленията се извършват само при необходимост. Това ни позволява да си спестим някои изчисления (както и да работим с т.нар. “безкрайни” колекции).
Най-близкото до “мързеливо” оценяване, което познаваме е изчисляването на логическите изрази при if
блокове и други булеви изрази - там, всеки елемент от израза се изчислява, при условие че предишния израз не е достатъчен за оценяване на целия израз (т.е. ако имаме един израз който е със стойност на True
при изрази съдържащи or
, тези след него не се оценяват. Ако имаме израз, който съдържа and
, трябва да оценим всички стойности).
По подобен начин работи и мързеливото оценяване, с разликата, че изразите се оценяват при поискване.
Generators#
Генераторите са специални функции, които позволяват “мързеливо” итериране на дадена (дори и безкрайна) поредица. Генераторите не държат всичките си стойности в паметта, а изчисляват стойността при поискване (това позволява работата с “безкрайни” поредици).
Нека разгледаме следния пример - искаме да създадем генератор, който да ни връща числата, които са точни квадрати (число, получено чрез повдигане на друго цяло число на квадрат - 1, 4, 9, 16, 25, т.н.).
Ако за момент се абстрахираме от идеята за генератори, как бихме могли да напишем функция, която ни връща n
-тия точен квадрат ? Първия точен квардрат е 0 (\(0^2 = 0\), защото са неотрицателни), втория е 1 (\(1^2 = 1\)), третия е 4 (\(2^2 = 4\)) и т.н.
def generate_nth_perfect_square(n):
return (n-1) ** 2
print(generate_nth_perfect_square(3))
4
Можем да променим нашата функция, която да ни връща списък от всички точни квадрати до n
-тия.
def generate_all_perfect_squares_until(n):
result = []
for i in range(1, n+1):
result.append(generate_nth_perfect_square(i))
return result
print(generate_all_perfect_squares_until(5))
[0, 1, 4, 9, 16]
Проблемът с горната функция, е че изчислява всички стойности до n
едновременно - ние искаме те да се изчисляват една по една, при поискване. По-важното е, че искаме да можем да итерираме по тях - т.е. да ги вкараме в един for
цикъл, и да ги обходим.
За да постигнем тази цел, ни трябват две неща - функция, която да “спре” изпълнението до дадено място и да ни върне някаква стойност - функцията, която извършва това, е yield
.
def perfect_squares(n):
for i in range(1, n+1):
yield generate_nth_perfect_square(i)
print(perfect_squares(5))
<generator object perfect_squares at 0x7f2c9d0b8350>
Тук виждаме, че нашата функция вече ни връща generator
обект.
За да работим с нашия генератор обект, е необходимо да го подадем като обект на функцията next
- тя ни връща следващия елемент от генератор/итератор.
perfect_square_generator = perfect_squares(5)
print(next(perfect_square_generator))
print(next(perfect_square_generator))
print(next(perfect_square_generator))
print(next(perfect_square_generator))
0
1
4
9
Както се вижда, next
изпълнява нашия код, до следващото срещане на yield
, и връща стойността, която yield
връща.
Благодарение на факта, че получваме генератор, може да използваме нашата функция perfect_squares
директно във for
цикъл. Начина по който for
цикъла работи е, че извиква next
метода на подадения от нас обект.
for i in perfect_squares(5):
print(i)
0
1
4
9
16
Нека направим нещата една идея по-сложни. Вместо да създаваме генератор на точни квадрати до дадено число, нека променим нашия генератор, да връща точни квадрати до безкрайност (понеже точните квадрати са дефинирани върху всички неотрицателни числа, е възможно до безкрайност да генерираме такива).
def perfect_square():
n = 1
while True:
yield generate_nth_perfect_square(n)
n += 1
perfect_square_generator = perfect_square()
print(next(perfect_square_generator))
print(next(perfect_square_generator))
print(next(perfect_square_generator))
print(next(perfect_square_generator))
0
1
4
9
Използването на yield
не е единствения начин да бъде създаден генератор. Python поддържа т.нар. “generator expressions”. Generator expressions е синтактична конструкция, която ни позволява да създаваме генератор на базата на друга колекция/генератор. Синтактично, generator expression следва математическата нотация за дефиниране на множество:
Това математическо заклинание ни дава целите числа в диапазона от 1 до 100, които са четни, като всяко от тях е повдигнато на квадрат.
В общият случай, един generator expression изглежда по следния начин: (f(x) for x in X if p(x))
, където f
е функция, която прилагаме върху x
, а p
е филтрираща функция (повече за прилагането на функции върху елементи и филтрирането му по-долу)
Ако искаме да запишем нашата функция perfect_squares(n)
, която ни връща точните квадрати до n
като generator expression, тя би изглеждала по следния начин.
perfect_squares_generator_exp = (generate_nth_perfect_square(i) for i in range(1, 6))
print(perfect_squares_generator_exp)
print(next(perfect_squares_generator_exp))
print(next(perfect_squares_generator_exp))
print(next(perfect_squares_generator_exp))
<generator object <genexpr> at 0x7f2ca22f6950>
0
1
4
В горния пример, нашия генератор ще ни върне първите 5 точни квадрата, с помощта на generator expression. Как би изглеждал кода обаче, ако искаме да можем да приемаме n
, с което да заместим 5 ?
perfect_squares_n_generator_exp = lambda n: (generate_nth_perfect_square(i) for i in range(1, n + 1))
for num in perfect_squares_n_generator_exp(3):
print(num)
print('--')
for num in perfect_squares_n_generator_exp(10):
print(num)
0
1
4
--
0
1
4
9
16
25
36
49
64
81
Едно последно нещо - един от най-използваните генератори е range
. range
приема три аргумента - начало, край и стъпка. В резултат, получаваме генератор, който генерира числата от това множество.
for i in range(2, 7, 2):
print(i)
2
4
6
Няма значение колко е голям размера на range
-а за представянето му - това е защото range
изчислява следващия елемент на момента.
Map#
Map е специална функция, която има една-единствена цел - да приложи друга функция към всеки елемент от дадена колекция. Нека е даден списък с числа и функция, която приема число, и го умножава по 2.
numbers = [2, 7, 3, 9, -1, 12]
def multiply(number):
return number * 2
Ако трябва сами да разпишем map функцията, тя би изглеждала по следния начин:
def my_map(map_function, collection):
result = []
for item in collection:
result.append(map_function(item))
return result
Нека извикаме нашата map функция, с дефинираните по-горе числа и функция, която да бъде приложена
print(f'Squared numbers: {my_map(multiply, numbers)}')
Squared numbers: [4, 14, 6, 18, -2, 24]
Можем да подадем и функция, която променя типа на обектите - нека разгледаме функция, която приема цяло число от 1 до 7 и връща съответния ден от седмицата (1 - Понеделник, 2 - Вторник, т.н.)
def number_to_day(number):
days = ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"]
return days[number - 1]
days = [2, 3, 1, 4, 6]
print(f'Days of the week: {my_map(number_to_day, days)}')
Days of the week: ['Tuesday', 'Wednesday', 'Monday', 'Thursday', 'Saturday']
Нека сега разгледаме и вградена функция map
в Python - тя приема като първи аргумент функцията, която ще бъде приложена, а като втори - колекцията, върху която да се приложи.
numbers = [1, 2, 3, 4]
def square(number):
return number ** 2
print(f'Appyling map directly returns: {map(square, numbers)}')
Appyling map directly returns: <map object at 0x7f2c9d1100d0>
Важно е да се отбележи, че резултатът от map функцията е map
обект - или поне така изглежда. Всъщност това, което се връща е генератор, който ни дава новите стойности. Съвсем лесно можем да превърнем този генератор в списък, с едно просто извикване на list()
функцията.
numbers = [1, 2, 3, 4]
def square(number):
return number ** 2
print(f'Converting the map object into a list: {list(map(square, numbers))}')
Converting the map object into a list: [1, 4, 9, 16]
Освен да подаваме нормална функция, можем да използваме и lambda
.
numbers = [1, 2, 3, 4]
mapped_numbers = map(lambda number: number ** 2, numbers)
result = list(mapped_numbers)
print(result)
[1, 4, 9, 16]
Нека усложим нещата - ако се върнем на първоначалната ни дефиниция за map
(Функция, която прилага друга функция върху колекция), си задаваме въпроса - какво става, ако имаме повече от една колекция, върху която искаме да приложим функция ? Възможно ли е въобще ?
Отговора е да - map
приема една или повече колекции. Единствената особенност е, че функцията която подаваме трябва да може да приеме повече от един аргумент. На по-прост език - ако подадем два списъка на map
, функцията която ще се приложи върху тях трябва да приема два аргумента.
first_numbers = [1, 2, 3, 4]
second_numbers = [5, 6, 7, 8]
def multiply(a, b):
return a * b
print(list(map(multiply, first_numbers, second_numbers)))
[5, 12, 21, 32]
Важно е да се отбележи, че резултата от map
операция винаги е една поредица от обекти - дори и да приемем 10 колекции, винаги на изхода ще имаме една.
Въпрос: Как може да “измамим” системата, и да върнем повече от една колекция ?
Отговор#
Няма как да върнем повече от една колекция - това което можем да направим, е да върнем колекция, която съдържа наредени n-торки (tuples).
first_numbers = [1, 2, 3, 4]
second_numbers = [5, 6, 7, 8]
def return_as_tuple(a, b):
return a, b
print(list(map(return_as_tuple, first_numbers, second_numbers)))
[(1, 5), (2, 6), (3, 7), (4, 8)]
Zip#
Нека се върнем за кратко към примера за използването на map
, при който от два списъка, ние направихме един, който съдържаше елементите на двата, групирани по позицията им:
first_numbers = [1, 2, 3, 4]
second_numbers = [5, 6, 7, 8]
def return_as_tuple(a, b):
return a, b
print(list(map(return_as_tuple, first_numbers, second_numbers)))
[(1, 5), (2, 6), (3, 7), (4, 8)]
В Python има готова функция, която прави това обединение за нас - zip
first_numbers = [1, 2, 3, 4]
second_numbers = [5, 6, 7, 8]
print(zip(first_numbers, second_numbers))
print(list(zip(first_numbers, second_numbers)))
<zip object at 0x7f2c9d130f00>
[(1, 5), (2, 6), (3, 7), (4, 8)]
Не е нужно списъците да са от еднакви типове елементи.
names = ['Иван', 'Любо', 'Алекс']
favorite_number = [3, 20, 8]
print(list(zip(names, favorite_number)))
[('Иван', 3), ('Любо', 20), ('Алекс', 8)]
Ако единият списък е по-дълъг от другия, по-дългия списък ще бъде отрязан:
names = ['Иван', 'Любо', 'Алекс', 'Иво']
favorite_number = [3, 20, 7]
print(list(zip(names, favorite_number)))
[('Иван', 3), ('Любо', 20), ('Алекс', 7)]
names = ['Иван', 'Любо', 'Алекс']
favorite_number = [3, 20, 7, -1]
print(list(zip(names, favorite_number)))
[('Иван', 3), ('Любо', 20), ('Алекс', 7)]
Или поне това е поведението по подразбиране. zip
предлага още два метода на работа. С помощта на аргумента strict=True
, ако подадените списъци са с различен размер, zip
ще ни хвърли грешка (това е въведено в Python3.10).
names = ['Иван', 'Любо', 'Алекс']
favorite_number = [3, 20, 7, -1]
zip(names, favorite_number, strict=True)
---------------------------------------------------------------------------
TypeError Traceback (most recent call last)
<ipython-input-64-acdcfbd62260> in <module>
2 favorite_number = [3, 20, 7, -1]
3
----> 4 zip(names, favorite_number, strict=True)
TypeError: zip() takes no keyword arguments
А ако искаме да добавим някакви “празни” стойности към по-късия списък, може да използваме
from itertools import zip_longest
names = ['Иван', 'Любо', 'Алекс']
favorite_number = [3, 20, 7, -1, -2]
print(list(zip_longest(names, favorite_number, fillvalue='Dummy')))
[('Иван', 3), ('Любо', 20), ('Алекс', 7), ('Dummy', -1), ('Dummy', -2)]
Ако трябва да сме на 100% коректни, zip
приема каквато и да е колекция:
numbers = [1, 2, 3]
letters = ['a', 'b', 'c']
print(list(zip(map(lambda x: x ** 2, numbers), letters)))
[(1, 'a'), (4, 'b'), (9, 'c')]
Горният пример, малко по-културно разписан, изглежда по следния начин:
numbers = [1, 2, 3]
letters = ['a', 'b', 'c']
squared_numbers = map(lambda x: x ** 2, numbers)
zipped_together = zip(squared_numbers, letters)
zipped_together_as_a_list = list(zipped_together)
print(zipped_together_as_a_list)
[(1, 'a'), (4, 'b'), (9, 'c')]
Filter#
Както името подсказва, filter
е функция която ни връща елементите от дадена колекция, удолетворяващи дадено условие.
Нека отново имаме някакви числа, и функция която връща дали дадено число е четно или не.
numbers = [2, 7, 11, 12]
def is_even(number):
return number % 2 == 0
Ако трябва сами да разпишем функцията, тя би изглеждала по следния начин:
def my_filter(filter_fn, collection):
result = []
for item in collection:
if filter_fn(item):
result.append(item)
return result
print(my_filter(is_even, numbers))
[2, 12]
Извикването на вградената функция filter
изглежда по следния начин
print(filter(is_even, numbers))
<filter object at 0x7f2c9d0c7510>
Както map
, така и filter
връщат генератор обекти - с едно извикване на list
можем да превърнем резултата в списък
print(list(filter(is_even, numbers)))
[2, 12]
Може и с lambda
print(list(filter(lambda number: number % 2 == 0, numbers)))
[2, 12]
Reduce#
Тук нещата стават една идея по-сложни. Нека започнем с един пример: трябва да намерим произведението на всички числа в даден списък.
Нека за момент се абстрахираме от програмирането - как бихме намерили произведението на числа 3, 5 и 8 ? Първо бихме умножили 3 и 5, което прави 15. След това, получения резултат от предишната операция (в нашия случай 15) и ще го умножим с 8.
Тази процедура може да бъде използвана за списъци с произволен брой числа - всяко следващо число бива умножено с резултата до момента.
Ако трябва да представим тази идея с код, тя би изглеждала по следния начин:
def get_product_of_all_nums(nums):
result = nums[0]
for num in nums[1:]:
result = result * num
return result
numbers = [3, 5, 8]
print(get_product_of_all_nums(numbers))
120
Нека генерализираме нашата функция - вместо умножение, тя да може да работи с подадена функция, приемаща два аргумента - резултата до момента и текущото число
def my_reduce(function, collection):
result = collection[0]
for item in collection[1:]:
result = function(result, item)
return result
numbers = [3, 5, 8]
def multiply(result_so_far, current_number):
return result_so_far * current_number
print(my_reduce(multiply, numbers))
120
Една промяна, която можем да направим е следната - вместо за начало да взимаме първия елемент от колекцията, може да приемаме началната стойност като аргумент на функцията. Така нашата функция би работела и с празни колекции.
def my_reduce_with_start(function, collection, start):
result = start
for item in collection:
result = function(result, item)
return result
numbers = [3, 5, 8]
def multiply(result_so_far, current_number):
return result_so_far * current_number
print(my_reduce_with_start(multiply, numbers, 1))
120
Ето че стигнахме и до reduce
- функция, която приема функция и колекция. Функцията бива приложена върху резултата, който имаме до момента и всеки елемент от колекцията.
Важно е да уточним терминологията тук - в някои езици може да срещенете тази функция като accumulate
, fold
или пък foldl
.
Тук е важно да спомена за наличието на т.нар “ляв” и “десен” reduce
. В други функционални езици (като Haskell), имаме концепцията за ляв и десен fold
- в каква посока се изпълнява функцията - отляво надясно или отдясно наляво. За някои операции (например умножение) реда на изпълнение няма значение, но например при деление би имало значение дали започваме от ляво или от дясно. Повече информация за fold функцията в другите езици, може да намерите тук.
Вградената функция reduce в Python се намира в библиотеката functools
. За да я използваме, първо трябва да я заредим (import
)
from functools import reduce
numbers = [3, 5, 8]
def multiply(result_so_far, current_number):
return result_so_far * current_number
print(reduce(multiply, numbers, 1))
120
Както може би сте се досетили, тук не получаваме генератор - резултатът от reduce
е една стойност (в нашия случай това е число).
reduce
, също както map
и filter
работи и с lambda
функции. Тук е добро място да се отбележи, че първия аргумент, който бива подаден на функцията е стойността до момента (или стойността, която се е акумулирала (accumulate).
numbers = [3, 5, 8]
print(reduce(lambda acc, number: acc * number, numbers))
120
А можем и да подадем начална стойност:
numbers = [3, 5, 8]
print(reduce(lambda acc, number: acc * number, numbers, 1))
print(reduce(lambda acc, number: acc * number, numbers, 2))
120
240
А сега един по-сложен пример: Нека имаме списък, чийто елементи са списъци с числа. С помощта на reduce
, подходяща функция и подходяща начална стойност, бихме могли да получим един списък, който съдържа числата, без те да са в отделни списъци.
numbers_2d = [[1, 2, 3], [4, 5, 6], [7, 8, 9, 10]]
print(reduce(lambda acc, item: acc + item, numbers_2d, []))
[1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
Припомням, че операция събиране (+
) приложена върху два списъка ни връща конкатенацията на двата списъка.
В горната операция, ние взехме един списък, и го “изравнихме” (flatten). Операцията “flatten” върху списъци е често срещата в езиците за функционално програмиране, макар и в Python да нямаме готова функция за това.
List comprehension#
В Python съществува функционалност, наречена “list comprehension”. List comprehension е синтактична конструкция, която ни позволява да създаваме списъци на базата на други колекции/итератори/генератори. Синтактично, list comprehension прилича на generator expressions:
Можем да изразим тази идея в Python като използваме вече познатите ни map
и filter
:
numbers = range(1, 101)
even_numbers = filter(lambda x: x % 2 == 0, numbers)
squared_numbers = map(lambda x: x ** 2, even_numbers)
print(f'Squared even numbers: {list(squared_numbers)}')
Squared even numbers: [4, 16, 36, 64, 100, 144, 196, 256, 324, 400, 484, 576, 676, 784, 900, 1024, 1156, 1296, 1444, 1600, 1764, 1936, 2116, 2304, 2500, 2704, 2916, 3136, 3364, 3600, 3844, 4096, 4356, 4624, 4900, 5184, 5476, 5776, 6084, 6400, 6724, 7056, 7396, 7744, 8100, 8464, 8836, 9216, 9604, 10000]
В общия случай, синтаксиса на list comprehension е следния: [f(item) for item in collection if p(item)]
- т.е. кода по-горе би изглеждал по следния начин като list comprehension:
print(f'Squared even numbers: {[x ** 2 for x in range(1, 101) if x % 2 == 0]}')
Squared even numbers: [4, 16, 36, 64, 100, 144, 196, 256, 324, 400, 484, 576, 676, 784, 900, 1024, 1156, 1296, 1444, 1600, 1764, 1936, 2116, 2304, 2500, 2704, 2916, 3136, 3364, 3600, 3844, 4096, 4356, 4624, 4900, 5184, 5476, 5776, 6084, 6400, 6724, 7056, 7396, 7744, 8100, 8464, 8836, 9216, 9604, 10000]
if
часта или функцията, която се прилага върху всяка променливата могат да бъдат изпуснати:
numbers = [1, 2, 3]
print(f'Odd numbers: {[number for number in numbers if number % 2 != 0]}')
print(f'Multiplying all numbers by 2: {[number * 2 for number in numbers]}')
Odd numbers: [1, 3]
Multiplying all numbers by 2: [2, 4, 6]
Тук е добре да отбележем, че основната синтактична разлика между list comprehension и generator comprehension е скобите ([]
за list comprehension и ()
за generator comprehension)
def calculate_area_of_rectangle(a, b):
return a * b
rectangles = [(1, 2), (5, 4), (3, 2.5), (-2, 3)]
areas = [calculate_area_of_rectangle(x, y) for x, y in rectangles if x >= 0 and y >= 0]
print(f'Areas of valid rectangles: {areas}')
Areas of valid rectangles: [2, 20, 7.5]
Може би забелязвате, че когато функцията която изпълняваме не е външна, не използваме lambda
, а директно пишем операцията която искаме да приложим - това е част от опростяването на синтаксиса на list comprehension-ите.
Нека имаме дадени числа - искаме да върнем списък, в който за всяко число да пише “Yes” ако се дели на 3 или 5 и “No” в противен случай. Това може да стане лесно, чрез използването на тернарния оператор в Python.
numbers = [7, 12, 5, 6, 9, 15, 1, 11]
results = ["Yes" if number % 3 == 0 or number % 5 == 0 else "No" for number in numbers]
print(f'Results are: {results}')
Results are: ['No', 'Yes', 'Yes', 'Yes', 'Yes', 'Yes', 'No', 'No']
Скорост#
Едно от най-“шокиращите” неща в Python е скоростта, която получаваме при използването на обикновенни for
цикли, map
или list comprehension.
За да демонстрираме разликата, ще създадем функция, която повдига число на квадрат, и ще я приложим върху списък от 40 000 000 числа.
from time import time
def power_up(a):
return a ** 2
numbers = list(range(1, 40000000))
for_loop_start = time()
for_loop_result = []
for number in numbers:
for_loop_result.append(power_up(number))
for_loop_end = time()
print(f'For-looping took {for_loop_end - for_loop_start:.2f} seconds')
map_start = time()
map_result = list(map(power_up, numbers))
map_end = time()
print(f'map() took {map_end - map_start:.2f} seconds')
list_comprehension_start = time()
list_comprehension_result = [power_up(x) for x in numbers]
list_comprehension_end = time()
print(f'List comprehension took {list_comprehension_end - list_comprehension_start:.2f} seconds')
For-looping took 19.72 seconds
map() took 13.60 seconds
List comprehension took 15.08 seconds
Основната разлика във времето идва от факта, че append()
операцията е бавна. map
и list comprehension-ите използва хитрини при заделянето на паметта, затова може да се каже че са с около 30% по-бързи. Няма да се впускам повече в темата, за по-любопитните, тук нещата са обяснение в повече детайли.
На въпросът, кой от трите подхода се използват, моят отговор е следния: list comprehension и map
дават доста по-добро представяне спрямо for
loop + append
, затова и са за предпочитане. Синтаксисът на list comprehension-ите е по-прост спрямо този на map
(особенно ако трябва и да филтрираме елементите), което води до това, че list comprehension-ите са за предпочитане.
Dict comprehension#
Подобно на list comprehension, можем да използваме подобен запис да създадем и речник (dictionary, dict).
Синтаксисът е същият, само че вместо []
, използваме {}
. Също така, вместо един елемент, използваме key:value
- т.е. общия вид би изглеждал по следния начин: {f(item):g(value) for item in collection if p(item)}
Нека като пример разглеждаме dict, който съдържа число (измежду 1 и 10), и дали е четно или не
my_dict = {num:num%2 == 0 for num in range(1, 11)}
print(my_dict)
{1: False, 2: True, 3: False, 4: True, 5: False, 6: True, 7: False, 8: True, 9: False, 10: True}
Както при generator изразите и list comprehension, можем да добавим if
част.
Нека създадем речник, който съдържа четните числа от 1 до 20, както и тяхната репрезентация като низ.
even_numbers_as_strs = {num:str(num) for num in range(1, 21) if num % 2 == 0}
print(even_numbers_as_strs)
{2: '2', 4: '4', 6: '6', 8: '8', 10: '10', 12: '12', 14: '14', 16: '16', 18: '18', 20: '20'}
Pattern matching и match
израза#
Pattern matching-а (или шаблонното съпоствяне на български) е една от най-полезните функционалности в езиците за функционално програмиране. Тя ни позволява да проверяваме дали даден обект отговаря на даден шаблон, и да извличаме данни от него.
В Python3.10 за първи път се появява възможността на pattern matching. Тя е реализирана чрез match
израза.
Общият вид на match
конструкцията изглежда по следния начин:
match <обект>:
case <условие> <guard>:
тяло
case <условие2>:
тяло
case <условие3> <guard>:
тяло
...
Прости примери#
Нека напишем кратка програма калкулатор. Потребителят ще въведе две числа от клавиатурата и операция, която да се извърши.
first_number = int(input('Enter the first number: '))
second_number = int(input('Enter the second number: '))
operation = input('Enter an operation (+, -, * or /)')
match operation:
case '+':
result = first_number + second_number
case '-':
result = first_number - second_number
case '*':
result = first_number * second_number
case '/':
result = first_number / second_number
case _:
print('Operation not supported')
print(f'{first_number} {operation} {second_number} = {result}')
1 + 2 = 3
Какво се случва тук ? Прочитаме две числа и операция която да се извърши върху тях. След това подаваме операцията на match
израза.
Оттам, започваме да сравняваме operation
с различните възможности. Ако operation
съвпада с някой от case
-овете, изпълняваме съответното тяло.
Важна забележка - _
винаги съвпада с всичко. Това ни позволява да си направим “default” клауза, която да се изпълни, ако няма друга която да се съвпада.
На мястото на <обект>
можем да поставим и обект от тип tuple
:
numbers = (5, 3)
match numbers:
case (1, 2):
print('First case')
case (3, 3):
print('Second case')
case (5, 3):
print('Third case')
case _:
print('Default case')
Third case
Тук numbers
се оценява спрямо всички възможни случаи. Първият който съвпада, е (5, 3)
, и съответно се изпълнява тялото на съответния case
.
С помощта на _
можем да укажем че не ни интересува конкретната стойност на даден елемент от tuple-а.
numbers = (5, 3)
match numbers:
case (5, _):
print('First case')
case (5, 3):
print('Second case')
case _:
print('Default case')
First case
В тази ситуация, match
изразът ще изпълни първия случай, защото той е първия който съвпада.
По-сложни примери#
Силата на match
израза идва в това какви изрази поддържа. Ще разгледаме някои от тях:
Or pattern#
numbers = (5, 3)
match numbers:
case (5, 3) | (2, 4):
print('First case')
case (5, 3):
print('Second case')
case _:
print('Default case')
First case
numbers = (2, 4)
match numbers:
case (5, 3) | (2, 4):
print('First case')
case (5, 3):
print('Second case')
case _:
print('Default case')
First case
or
pattern-ът ни позволява да сравняваме даден обект с няколко условия. Той се използва с |
оператора.
При сравнението се проверява дали подадения израз съвпада с първата или втората част от case
-а.
As pattern#
Ако искаме да проверим кое точно от pattern-ите съвпада, можем да използваме конструкцията as
:
numbers = (5, 3)
match numbers:
case (5, 3) | (2, 4) as x:
print(f'First case: {x}')
case (5, 3):
print('Second case')
case _:
print('Default case')
First case: (5, 3)
Тук, след като case
-а съвпадне, стойността на numbers ще бъде присвоена на x
.
numbers = (2, 4)
match numbers:
case (5, 3) | (2, 4) as x:
print(f'First case: {x}')
case (5, 3):
print('Second case')
case _:
print('Default case')
First case: (2, 4)
numbers = (2, 4)
match numbers:
case (2 as x, 4 as y):
print(f'First case: {x}')
case (5, 3):
print('Second case')
case _:
print('Default case')
First case: 2
Value pattern#
def divide(x, y):
if y == 0:
return None
return x / y
match divide(10, 2):
case None:
print('Division by zero')
case result:
print(f'Result is {result}')
Result is 5.0
Можем да сравняваме и спрямо конкретни стойности - можем да проверяваме дали даден обект е None, или някакъв друг литерал
type
pattern-ът ни позволява да сравняваме спрямо типа на дадения обект. В примера, функцията divide(10, 2)
ще върне int``, което ще се присвои на
result`.
Sequence pattern#
numbers = [1, 2, 3, 4, 5]
match numbers:
case [1, 2, 3]:
print('First case')
case [1, 2, 3, 4, 5]:
print('Second case')
Second case
numbers = [1, 2, 3, 4, 5]
match numbers:
case [1, 2, 3, *rest]:
print('First case')
case [1, 2, 3, 4, 5]:
print('Second case')
First case
Guard-ове#
match
израза ни позволява да добавяме и условия, които да се проверяват след като съвпадне pattern-а (т.нар guard-ове).
Те се добавят след шаблона в case частта.
numbers = (5, 3)
some_other_value = 42
match numbers:
case (5, 3) if some_other_value % 2 != 0:
print('First case')
case _:
print('Default case')
Default case
numbers = (5, 3)
match numbers:
case (x, y) if x == 5:
print('First case')
case _:
print('Default case')
First case
Примери#
Нека разгледаме някои примери за функционално програмиране от живия живот
Пример 1#
Нека е даден клас Student
с полета name
, student_id_number
, year
, courses
, където courses
е списък с курсове и оценки към тях, представени от класа Course
.
Да се напише функция, която приема списък от студенти, и връща списък, съдържащ номерата на студентите, които имат среден успех над 4.50.
class Course:
def __init__(self, name='', grade=2.0):
self.__name = name
self.__grade = grade
@property
def name(self):
return self.__name
@property
def grade(self):
return self.__grade
@grade.setter
def grade(self, new_grade):
self.__grade = new_grade
class Student:
def __init__(self, name, student_id_number, year):
self.__name = name
self.__student_id_number = student_id_number
self.__year = year
self.__courses = []
@property
def name(self):
return self.__name
@property
def student_id_number(self):
return self.__student_id_number
@property
def year(self):
return self.__year
@property
def courses(self):
return self.__courses
def start_new_year(self):
self.__year += 1
self.__courses = []
def add_new_course(self, course):
self.__courses.append(course)
student_1 = Student('Ivan the Programmer', '12345', 1)
student_1.add_new_course(Course('Algebra', 4.50))
student_1.add_new_course(Course('Intro to programming', 6.00))
student_1.add_new_course(Course('Geometry', 3.00))
student_1.add_new_course(Course('Calculus', 2.00))
student_2 = Student('Maria', '12346', 1)
student_2.add_new_course(Course('Algebra', 5.75))
student_2.add_new_course(Course('Intro to programming', 6.00))
student_2.add_new_course(Course('Geometry', 6.00))
student_2.add_new_course(Course('Calculus', 6.00))
student_3 = Student('Gosho from break', '12347', 1)
student_3.add_new_course(Course('Algebra', 2.00))
student_3.add_new_course(Course('Intro to programming', 2.00))
student_3.add_new_course(Course('Geometry', 2.00))
student_3.add_new_course(Course('Calculus', 3.00))
Решение на Пример 1#
def average_grade(student):
grades = [course.grade for course in student.courses]
return sum(grades) / len(grades)
def get_students_with_high_enough_grades(students):
target_grade = 4.50
return [student.name for student in students if average_grade(student) >= target_grade]
all_students = [student_1, student_2, student_3]
print(get_students_with_high_enough_grades(all_students))
['Maria']
Пример 2#
Напишете декоратор, който “защитава” изпълнението на функция със специална супер секретна парола (например ‘super_safe_password`). Нашият декоратор трябва да пита потребителя за парола. Ако въведената парола съвпада, изпълнява подадената функция. В противен случай, връща ‘Invalid password’.
Решение на пример 2#
def login_required(f):
def inner():
password = input('Please enter a password')
if password == 'super_safe_password':
result = f()
else:
result = 'Invalid password'
return result
return inner
@login_required
def get_super_secret_message(name):
return f'Hi, {name}. Your mission is to learn Python.'
@login_required
def foo(a, b, c, d):
return a + b + c + d
print(foo(2, 3, 4, d=5))
---------------------------------------------------------------------------
TypeError Traceback (most recent call last)
Cell In [8], line 9
5 @login_required
6 def foo(a, b, c, d):
7 return a + b + c + d
----> 9 print(foo(2, 3, 4, 5))
TypeError: inner() takes 0 positional arguments but 4 were given
Пример 3#
range
ни връща генератор, който ни дава числата в даден интервал. Нека разширим тази идея, и напишем генератор, който ни дава координатите в двумерно пространство при зададено начало и край.
Пример: начало - (1, 1), край - (3, 4)
Резултат: (1, 1), (1, 2), (1, 3), (2, 1), (2, 2), (2, 3)
Решение на Пример 3#
def range_2d(start, end):
for i in range(start[0], end[0]):
for j in range(start[1], end[1]):
yield (i, j)
# for coord in range_2d((1, 1), (3, 4)):
# print(coord)
save = range_2d((1, 1), (3, 4))
current = range_2d((1, 1), (3, 4))
print(next(current))
print(next(current))
print(next(save))
(1, 1)
(1, 2)
(1, 1)
Пример 4#
Напишете функция bind_countries_to_capitals
, която приема два списъка - списък с имена на държави и списък с имена на столици.
Функцията трябва да върне речник, който съдържа държавите като ключове и съответните им столици като стойности. (приемаме, че на същия индекс на който е държавата стои и столицата ѝ в другия списък)
Решение на пример 4#
def bind_countries_to_capitals(countries, capitals):
return dict(zip(countries, capitals))
countries = ['Bulgaria', 'Italy', 'Turkey']
capitals = ['Sofia', 'Rome', 'Ankara']
result = bind_countries_to_capitals(countries, capitals)
print(result)
# {'Bulgaria': 'Sofia', 'Italy': 'Rome', 'Turkey': 'Ankara'}
{'Bulgaria': 'Sofia', 'Italy': 'Rome', 'Turkey': 'Ankara'}
Пример 5#
Условие 1#
Даден е следният клас RGB
, който моделира цвят в RGB формат:
class RGB:
def __init__(self, r, g, b):
"""Initialize the RGB class with the given values for red, green and blue.
Each value should be an integer between 0 and 255.
"""
if r < 0 or r > 255:
raise ValueError('Invalid red value')
if g < 0 or g > 255:
raise ValueError('Invalid green value')
if b < 0 or b > 255:
raise ValueError('Invalid blue value')
self._r = r
self._g = g
self._b = b
@property
def r(self):
return self._r
@property
def g(self):
return self._g
@property
def b(self):
return self._b
def to_hex(self):
return f'#{self.r:02X}{self.g:02X}{self.b:02X}'
@classmethod
def from_hex(cls, hex):
r = int(hex[1:3], base=16)
g = int(hex[3:5], base=16)
b = int(hex[5:7], base=16)
return cls(r, g, b)
def __repr__(self):
return f'RGB({self.r}, {self.g}, {self.b})'
def __eq__(self, other):
return self.r == other.r and self.g == other.g and self.b == other.b
def __hash__(self):
return hash((self.r, self.g, self.b))
Да се напишат функции rgb_pallette_to_hex_str
и hex_str_pallette_to_rgb
, които:
rgb_pallette_to_hex_str
: конвертира лист от цветове катоRGB
до лист отstr
(цветовете във формат#RRGGBB
, къдетоRR
,GG
,BB
са сътответно стойностите на червеното, зеленото и синьото в шестнадесетична бройна система)hex_str_pallete_to_rgb
: конвертира лист отstr
в горепосочения hex формат (case-insensitive!) и връща лист отRGB
обекти (цветовете в RGB формат)
Hint 1: съществуват начини в str.format
или f-string
-овете да се специфицират колко цифри и в каква бройна система да се използват при форматиране на числа
Hint 2: разгледайте всички варианти на конструиране на int
по даден str
def rgb_pallette_to_hex_str(colors):
return [color.to_hex() for color in colors]
colors = [RGB(255, 0, 255)]
def hex_pallette_to_rgb(hex_colors):
return [RGB.from_hex(hex_color) for hex_color in hex_colors]
result = rgb_pallette_to_hex_str(colors)
print(result)
print(hex_pallette_to_rgb(result))
['#FF00FF']
[RGB(255, 0, 255)]
Условие 2#
Нека имаме и имена за някои от цветовете:
PALLETTE = {
RGB(255, 255, 255): 'white',
RGB(0, 0, 0): 'black',
RGB(255, 0, 0): 'red',
RGB(0, 255, 0): 'green',
RGB(0, 0, 255): 'blue',
RGB(69, 69, 69): "the 51st shade of grey",
}
hex_colors = ["#696969", "#00FF00", "#AAAAAA"]
def extract_only_named_colors(hex_colors):
# 1. Convert hex to RGB objects
# 2. Check if each RGB object is in Pallette
# 3. Return the names those who are in Pallette
rgb_objects = hex_pallette_to_rgb(hex_colors)
filtered_objects = [rgb_object for rgb_object in rgb_objects if rgb_object in PALLETTE]
names_of_filtered_objects = [PALLETTE[filtered_object] for filtered_object in filtered_objects]
return names_of_filtered_objects
Да се напише функция extract_only_named_colors
по подаден списък от цветове във формат #RRGGBB
връща списък от имената им (само на тези, които присъстват в PALLETTE
- другите се игнорират).
Пример 6#
Рефакторирайте долния код така, че да съответства на функционалния стил на програмиране:
class PrettyPrint:
def __init__(self):
self.__border = '|'
self.__content = ''
self.__prefix = '***'
self.__suffix = '***'
def pretty_print(self):
return f'{self.__border} {self.__prefix} {self.__content} {self.__suffix} {self.__border}'
@property
def border(self):
return self.__border
@border.setter
def border(self, new_border):
self.__border = new_border
@property
def content(self):
return self.__content
@content.setter
def content(self, new_content):
self.__content = new_content
@property
def prefix(self):
return self.__prefix
@prefix.setter
def prefix(self, new_prefix):
self.__prefix = new_prefix
@property
def suffix(self):
return self.__suffix
@suffix.setter
def suffix(self, new_suffix):
self.__suffix = new_suffix
pretty_printer = PrettyPrint()
pretty_printer.content = 'Hello world'
print(pretty_printer.pretty_print())
pretty_printer.prefix = '<<<'
pretty_printer.suffix = '>>>'
print(pretty_printer.pretty_print())
| *** Hello world *** |
| <<< Hello world >>> |
def pretty_print(content, border='|', prefix='666', suffix='666'):
return f'{border} {prefix} {content} {suffix} {border}'
print(pretty_print('Hell world'))
| 666 Hell world 666 |
Пример 7#
Направете преводач от кирилица към “шльокавица”.
Нека приемем, че азбуката на шльокавицата e A, B, V, G, D, E, J, Z, I, Y, K, L, M, N, O, P, R, S, T, U, F, H, C, 4, 6, 6T, U, Y, YU, Q.