Функциональное программирование в Python
Сегодня поговорим об этом:
Вступление
Что такое функциональное программирование?
Функциональное программирование
Функциональное программирование — парадигма программирования, в которой процесс вычисления трактуется как вычисление значений функций в математическом понимании последних (в отличие от функций как подпрограмм в процедурном программировании).
Функциональное программирование — это набор идей, а не набор четких указаний.
Основана на лямбда-исчислении.
Функции используются почти для всего,
даже для простейших расчетов.
Даже переменные заменяются функциями.
Концепции: Функция - объект первого класса
Объектами первого класса в контексте конкретного языка программирования называются элементы, которые могут быть:
Функции в Python - это объекты (явл. функциями высшего порядка), с которыми можно производить разные действия:
Функции высших порядков
Функции высшего порядка - это функции, которые принимают другие функции в качестве аргументов или возвращают их как результат.
def apply_twice(func, arg):
'''
Принимает другую функцию в качестве аргумента
и вызывает ее дважды внутри своего тела
'''
return func(func(arg))
def add_five(x):
return x + 5
print(apply_twice(add_five, 10))
Концепции: Чистые функции
Чистыми называют функции, которые не имеют побочных эффектов (они зависят только от своих аргументов и возвращают только свой результат). Чистые функции обладают несколькими полезными свойствами:
def pure_function(x,y):
temp = x + 2*y
return temp / (2*x + y)
some_list = []
def impure_function(arg): # не чистая т.к. изменяет состояние списка
some_list.append(arg)
Чистые функции
Концепции: Рекурсия
В функциональных языках цикл обычно реализуется в виде рекурсии. Рекурсивные функции вызывают сами себя, позволяя операции выполняться снова и снова.
def rule_sequence(s, rules):
for rule in rules:
s = rule(s)
if s == None:
break
return s
def rule_sequence(s, rules):
if s == None or not rules:
return s
else:
return rule_sequence(
rules[0](s),
rules[1:]
)
Концепции: Анонимные функции (λ-выражения)
Определение функции def function_name(...): ... - это statement (оператор, утверждение/заявление), создающий одновременно и объект-функцию, и имя.
Лямбда - это expression (выражение), создающее функцию без имени - анонимную функцию. Далее мы эту функцию можем запомнить-передать-вызвать.
lambda arguments: expression
>>> print( (lambda: 1234) () ) # без параметров
1234
>>> print( (lambda x, y: x*y) (5, 6) ) # с несколькими параметрами
30
Разница выражения и оператора присваивания
Оператор присваивания NAME = expr является инструкцией, которая говорит записать в NAME ссылку на объект (значение) expr. Его нельзя применять внутри других выражений, он ничего не возвращает.
Выражение присваивания (NAME := expr) является выражением, которое кроме того, как записать в NAME ссылку на объект (значение) expr, еще и возвращает записанное значение expr. Таким образом мы можем использовать его внутри других выражений:
# Цикл, который не может быть тривиально переписан с помощью 2х аргументного iter()
while chunk := file.read(8192):
process(chunk)
Вернемся ближе к Python
Как программировать в стиле ФП на Python
В документации Python есть отдельная статья по этому поводу:
Пример на функции высших порядков
Задача: Протестировать три алгоритма сортировки.
Дано: функции sort1, sort2, sort3, принимают на вход список, возвращают отсортированный список.
Решение 1:
from sort_algs import sort1, sort2, sort3
if sort1(a) == ans:
print(True)
else:
print(False)
if sort2(a) == ans:
print(True)
#И т.д.
Минусы:
Решение
from sort_algs import sort1, sort2, sort3
def test_sort(sort_alg, list_to_sort, answer):
sorted_list = sort_alg(list_to_sort)
if sorted_list == answer:
return True
return False
l = [5, 4, 3, 2, 1]
answer = sorted(l)
for alg in [sort1, sort2, sort3]:
success = test_sort(alg, l, answer)
print(alg.__name__, ':', success)
Функция map() в Python:
map принимает два аргумента(больше): функцию и аргумент составного типа данных, например, список.
def add_five(x):
return x + 5
nums = [11, 22, 33, 44, 55]
result = list(map(add_five, nums))
print(result) # [16, 27, 38, 49, 60]
result1 = list(map(lambda x: x + 5, nums))
print(result1) # [16, 27, 38, 49, 60]
for x in map(lambda a, b: f'{a}+{b}', [1, 2, 3], [10, 20, 30, 40, 50]):
print(x, end=' ')
# 1+10 2+20 3+30
Функция filter() в Python:
filter используется чтобы определить, какие значения надо отдавать, а какие игнорировать. Использует в качестве параметра 1-местный предикат (функцию, возвращающую bool)
nums = [11, 22, 33, 44, 55]
res = list(filter(lambda x: x % 2 == 0, nums))
print(res) # [22, 44]
fizzbuzz = lambda x: (x % 3 == 0) or (x % 5 == 0) # плохой тон
print(list(filter(fizzbuzz, range(16)))) # [0, 3, 5, 6, 9, 10, 12, 15]
print(filter(fizzbuzz, range(16)))
# <filter object at 0x000002D710EB9F88>
Функция reduce() в Python:
from functools import reduce
a = [47, 11, 42, 13]
b = reduce(lambda a, b: a + b, a)
print(b) # 113
# import operator
# b = reduce(operator.add, a)
Функция zip() в Python:
Функция zip объединяет в кортежи элементы из последовательностей переданных в качестве аргументов.
a = [1,2,3]
b = "xyz"
c = (None, True)
res = list(zip(a, b, c))
print (res) # [(1, 'x', None), (2, 'y', True)]
А как вообще интерпретатор обрабатывает функции?
Когда Python выполняет код функций?
Все последовательно интерпретируется
C:\Users\Sergey\PycharmProjects\CourseAMCP20192020>type def.py
def foo():
"""I do nothing and return 92."""
return 92
foo()
C:\Users\Sergey\PycharmProjects\CourseAMCP20192020>python -m dis def.py
1 0 LOAD_CONST 0 (<code object foo at 0x000002146DF4F9C0, file "def.py", line 1>)
2 LOAD_CONST 1 ('foo')
4 MAKE_FUNCTION 0
6 STORE_NAME 0 (foo)
4 8 LOAD_NAME 0 (foo)
10 CALL_FUNCTION 0
12 POP_TOP
14 LOAD_CONST 2 (None)
16 RETURN_VALUE
Функция - это полноправный объект
>>> def foo():
... """I do nothing and return 92."""
... return 92
...
>>> foo.__name__
'foo'
>>> foo.__doc__
'I do nothing and return 92.'
>>> help(foo)
Help on function foo in module __main__:
foo()
I do nothing and return 92.
Как передаются значения в функцию
Единственный способ передачи параметров в Python - вызов по соиспользованию (call by sharing). Он используется в большинстве объектно-ориентированных языков, в том числе Ruby, SmallTalk и Java (это относится к ссылочным типам Java, параметры примитивных типов передаются по значению). Вызов по соиспользованию означает, что каждый формальный параметр функции получает копию ссылки на фактический аргумент. Иначе говоря, внутри функции параметры становятся синонимами фактических аргументов.
В результате функция получает возможность модифицировать любой изменяемый объект, переданный в качестве параметра, но не может заменить объект другим, не тождественным ему.
Передача аргументов по соиспользованию
def our_function(mutable, immutable):
mutable.append(1)
immutable += 1
print(f"Inside func:\timmutable={immutable},\tmutable={mutable}")
our_list, our_num = ['one'], 0
print(f"Before func:\timmutable={our_num},\tmutable={our_list}")
our_function(our_list, our_num)
print(f"After func: \timmutable={our_num},\tmutable={our_list}")
# Before func: immutable=0, mutable=['one']
# Inside func: immutable=1, mutable=['one', 1]
# After func: immutable=0, mutable=['one', 1]
Еще пример
def f(a, b):
a += b
return a
a1 = 1
b1 = 2
print(f"Before func:\ta1={a1}, b1={b1}") # a1=1, b1=2
print(f"f(a1, b1)={f(a1, b1)}") # f(a1, b1)=3
print(f"After func: \ta1={a1}, b1={b1}") # a1=1, b1=2
a2 = ['one']
b2 = ['two']
print(f"Before func:\ta2={a2}, b2={b2}") # a2=['one'], b2=['two']
print(f"f(a2, b2)={f(a2, b2)}") # f(a2, b2)=['one', 'two']
print(f"After func: \ta2={a2}, b2={b2}") # a2=['one', 'two'], b2=['two']
Поговорим про аргументы
def func(foo, bar=None): ...
def func(positional_only_parameters, /): ...
def func(arg, *, kw_only1, kw_only2): ...
def func(*args, **kwargs): ...
def func(*args, **kwargs): ...
Напишем функцию minimum
def minimum(x, y):
return x if x < y else y
# можно так
minimum(1, 2)
minimum(1, y=2)
minimum(x=1, y=2)
minimum(y=2, x=1)
Для двух аргументов - несерьезно
def minimum(*args):
# type(args) => <class 'tuple'>
res = float('inf')
for x in args:
res = x if x < res else res
return res
minimum(92, 10, 62) # 10
minimum() # inf
xs = [1, 2, 3]
minimum(*xs) # 1
inf - не очень правильный результат
def minimum(first, *rest):
res = first
for x in rest:
res = x if x < res else res
return res
>>> minimum("hello", ",", " ", "world", '!') # проявление утиной типизации
' '
>>> minimum()
Traceback (most recent call last):
File "<input>", line 1, in <module>
TypeError: minimum() missing 1 required positional argument: 'first'
Значение по умолчанию
def minimum(*args, default=None):
if not args:
return default
res, *rest = args
for x in rest:
res = x if x < res else res
return res
minimum(xs, default=0) # обязательно по имени
Что может пойти не так
Если значение изменяемое, могут быть "спецэффекты". С этим нужно быть очень осторожным, в примере ниже всё хорошо.
def foo(data = "a"): # str - неизменяемое
assert isinstance(data, str)
assert len(data) >= 1
data += chr(ord(data[-1])+1) # здесь будет копия: data = data + smth.
print(data)
foo() # ab
foo('bc') # bcd
foo() # ab
Что может пойти не так 2
Если значение изменяемое, могут быть "спецэффекты". С этим нужно быть очень осторожным
def foo(data=[0, 1]): # list - изменяемое
assert isinstance(data, list)
assert len(data) >= 2
data += [data[-1] + data[-2]] # list.+= изменяет сам объект
print(data)
foo() # [0, 1, 1]
foo() # [0, 1, 1, 2]
foo() # [0, 1, 1, 2, 3]
foo([100, 500]) # [100, 500, 600]
foo() # [0, 1, 1, 2, 3, 5]
Как исправить?
Альтернатива - использовать специальные значения (чаще всего - None)
def foo(data=None):
assert data is None or isinstance(data, list)
if data is None:
data = [0, 1] # каждый раз вычисляем значение по умолчанию
# data = data or [0,1]
data += [data[-1] + data[-2]]
print(data)
foo() # [0, 1, 1]
foo() # [0, 1, 1]
foo([100, 500]) # [100, 500, 600]
foo() # [0, 1, 1]
Время жизни дефолтных значений
Значения вычисляются единожды в момент определения функции (когда интерпретатор доберётся до неё).
C:\CourseAMCP>type default.py
def foo(x=[], y=92):
pass
C:\CourseAMCP>python -m dis default.py
1 0 BUILD_LIST 0
2 LOAD_CONST 0 (92)
4 BUILD_TUPLE 2
6 LOAD_CONST 1 (<code object foo at 0x000001F3FF3209C0, file "default.py", line 1>)
8 LOAD_CONST 2 ('foo')
10 MAKE_FUNCTION 1
12 STORE_NAME 0 (foo)
14 LOAD_CONST 3 (None)
16 RETURN_VALUE
Замечание: Для обязательности kword
flatten([0, 1, [2, [3]], 4], depth=1) # чтобы можно было только так
flatten([0, 1, [2, [3]], 4], 1) # не не так (будет TypeError)
def flatten(iterable, *, depth):
"""I flatten a given iterable up to a fixed depth."""
...
Произвольное количество аргументов
def you_gave_me(*args, **kwargs):
return args, kwargs
>>> args, kwargs = you_gave_me(1, 2, a=3, b=4)
>>> args
(1, 2)
>>> kwargs
{'a': 3, 'b': 4}
>>> type(kwargs)
<class 'dict'>
>>> kwargs.pop('b')
4
>>> kwargs
{'a': 3}
Что выведет эта программа?
def call_me(*args, **kwargs):
return args, kwargs
print(call_me({"a": 92}))
# ((), {'a': 92})
# (({'a': 92},), {})
print(call_me(**{"a": 92}))
Области видимости
Пример
def h():
print(12)
def f():
g(h)
def g(a):
a()
g(f) # в каком порядке будет работать данный скрипт?
# ['g', 'f', 'g', 'h', 'print']
Пространство имен
Пространство имен - множество всех ссылок переменных на объекты в памяти.
Примеры пространств имен:
Области видимости
Области видимости - текстовые фрагменты программы, в которых локальные namespace доступны напрямую
Любой вызов переменной в функции идет по стеку вызовов - сначала переменные ищутся в namespaсe функции, затем в enclosing, затем в global, затем в builtins.
Условные операторы и циклы не создают локальных namespace как функции - они работают на текущем уровне пространства имен.
Пример
def func (arg1, arg2):
var = 'var'
def print_var():
# при нахождении новой переменной ищем ее по правилу
# legn: local -> enclosing -> global -> built-in
inner_var = 1
print(inner_var) # локальная переменная # 1
print(var) # var
# охватывающая переменная, запросить информацию можно а изменить нельзя
print(global_var) # глобальная переменная # 0
print(list) # built-in # <class 'list'>
print_var()
func('one', 'two')
global
Иногда внутри функции хочется изменить переменную не из локального пространства имен. Для этого используются объявления nonlocal и global
Если есть объявление global smth - имя глобальное, ищется на самом верхнем уровне (текущего модуля/скрипта)
global_var = 0
def func():
global global_var # надо объявить чтобы можно было писать в нее
global_var = global_var + 1
print(global_var) # 0
func()
print(global_var) # 1
nonlocal
Если есть объявление nonlocal smth - имя заимствовано из охватывающей функции (и только из функции, и только на 1 уровень выше)
global_var = 0
def func():
var = 0
def inner():
nonlocal var
var += 1
inner() # вызвать можно только тут, либо в global через func
print(var)
func() # 1
Замыкание
Замыкание
Замыкание (closure) в программировании — это функция, в теле которой присутствуют ссылки (упоминания) на переменные, объявленные вне тела этой функции в окружающем коде и не являющиеся ее параметрами (такие переменные называются свободными).
Важно понять, как оно работает для последующих тем в функциональном программировании.
Пример
Замыкание в действии
def make_averager():
series = []
def averager(new_value):
series.append(new_value)
total = sum(series)
return total / len(series)
return averager
g = make_averager()
print(g(10)) # 10.0
print(g(10)) # 10.0
print(g(40)) # 20.0
print(g(10)) # 17.5
Посмотрим на локальные переменные
def make_averager():
series = []
print('locals of outer func:', locals())
def averager(new_value):
print('locals in:', locals())
series.append(new_value)
total = sum(series)
print('locals out:', locals())
return total / len(series)
return averager
g = make_averager()
print(g(1))
print(g(4))
print(g(7))
Выведет
Потому что locals() выводит и свободные переменные, если вызвана в блоке функции.
locals of outer func: {'series': []}
locals in: {'new_value': 1, 'series': []}
locals out: {'new_value': 1, 'series': [1], 'total': 1}
1.0
locals in: {'new_value': 4, 'series': [1]}
locals out: {'new_value': 4, 'series': [1, 4], 'total': 5}
2.5
locals in: {'new_value': 7, 'series': [1, 4]}
locals out: {'new_value': 7, 'series': [1, 4, 7], 'total': 12}
4.0
И с неизменяемыми объектами
def make_averager_without_list():
series = []
total, counter = 0, 0
print('locals of outer func:', locals())
def averager(new_value):
print('locals in:', locals())
nonlocal total, counter
total += new_value
counter += 1
print('locals out:', locals())
return total / counter
return averager
Результат
locals of outer func: {'series': [], 'counter': 0, 'total': 0}
locals in: {'new_value': 1, 'counter': 0, 'total': 0}
locals out: {'new_value': 1, 'counter': 1, 'total': 1}
1.0
locals in: {'new_value': 4, 'counter': 1, 'total': 1}
locals out: {'new_value': 4, 'counter': 2, 'total': 5}
2.5
locals in: {'new_value': 7, 'counter': 2, 'total': 5}
locals out: {'new_value': 7, 'counter': 3, 'total': 12}
4.0
ДЗ
Полезные ссылки