1 of 54

Функциональное программирование в Python

2 of 54

Сегодня поговорим об этом:

  • Что такое Функциональное программирование
  • Функции?
  • Чистые функции
  • Функции высших порядков
  • Рекурсия
  • Лямбда функции
  • В Python функция - полноправный объект
  • Как программировать в стиле ФП (docs FP HOWTO)
  • filter, map, reduce + zip
  • Немного про аргументы
  • Области видимости
  • Замыкания

3 of 54

Вступление

Что такое функциональное программирование?

4 of 54

Функциональное программирование

Функциональное программирование — парадигма программирования, в которой процесс вычисления трактуется как вычисление значений функций в математическом понимании последних (в отличие от функций как подпрограмм в процедурном программировании).

Функциональное программирование — это набор идей, а не набор четких указаний.

Основана на лямбда-исчислении.

Функции используются почти для всего,

даже для простейших расчетов.

Даже переменные заменяются функциями.

5 of 54

Концепции: Функция - объект первого класса

Объектами первого класса в контексте конкретного языка программирования называются элементы, которые могут быть:

  • переданы как параметр,
  • возвращены из функции,
  • присвоены переменной.

Функции в Python - это объекты (явл. функциями высшего порядка), с которыми можно производить разные действия:

  • запоминать в переменных
  • передавать внутрь функций как аргументы, возвращать из функций
  • создавать на лету
  • и, разумеется, вызывать

6 of 54

Функции высших порядков

Функции высшего порядка - это функции, которые принимают другие функции в качестве аргументов или возвращают их как результат.

def apply_twice(func, arg):

'''

Принимает другую функцию в качестве аргумента

и вызывает ее дважды внутри своего тела

'''

return func(func(arg))

def add_five(x):

return x + 5

print(apply_twice(add_five, 10))

7 of 54

Концепции: Чистые функции

Чистыми называют функции, которые не имеют побочных эффектов (они зависят только от своих аргументов и возвращают только свой результат). Чистые функции обладают несколькими полезными свойствами:

def pure_function(x,y):

temp = x + 2*y

return temp / (2*x + y)

some_list = []

def impure_function(arg): # не чистая т.к. изменяет состояние списка

some_list.append(arg)

8 of 54

Чистые функции

  • Легко применять, тестировать и анализировать
  • Мемоизация - чистые функции более эффективны после того, как функция обработала некоторый ввод, результат которого может быть сохранен и взят в следующий раз, когда функция вызывается для аналогичного ввода. Такую функцию не нужно вызывать снова
  • Легче обрабатывать параллельно
  • Сильно усложняют задачу ввода/вывода, которая требует дополнительных побочных эффектов

9 of 54

Концепции: Рекурсия

В функциональных языках цикл обычно реализуется в виде рекурсии. Рекурсивные функции вызывают сами себя, позволяя операции выполняться снова и снова.

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:]

)

10 of 54

Концепции: Анонимные функции (λ-выражения)

Определение функции def function_name(...): ... - это statement (оператор, утверждение/заявление), создающий одновременно и объект-функцию, и имя.

Лямбда - это expression (выражение), создающее функцию без имени - анонимную функцию. Далее мы эту функцию можем запомнить-передать-вызвать.

lambda arguments: expression

>>> print( (lambda: 1234) () ) # без параметров

1234

>>> print( (lambda x, y: x*y) (5, 6) ) # с несколькими параметрами

30

11 of 54

Разница выражения и оператора присваивания

Оператор присваивания NAME = expr является инструкцией, которая говорит записать в NAME ссылку на объект (значение) expr. Его нельзя применять внутри других выражений, он ничего не возвращает.

Выражение присваивания (NAME := expr) является выражением, которое кроме того, как записать в NAME ссылку на объект (значение) expr, еще и возвращает записанное значение expr. Таким образом мы можем использовать его внутри других выражений:

# Цикл, который не может быть тривиально переписан с помощью 2х аргументного iter()

while chunk := file.read(8192):

process(chunk)

12 of 54

Вернемся ближе к Python

13 of 54

Как программировать в стиле ФП на Python

В документации Python есть отдельная статья по этому поводу:

  • Разбирают разницу в парадигмах
  • Плюсы ФП
  • Применение вместе с итераторами и генераторами
  • Применение itertools и functools
  • Приводят решения базовых задач в этом стиле

14 of 54

Пример на функции высших порядков

Задача: Протестировать три алгоритма сортировки.

Дано: функции 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)

#И т.д.

Минусы:

  1. При добавлении новых алгоритмов - изменяем код программы
  2. Много повторяющегося кода
  3. Сложно читать

15 of 54

Решение

  1. При добавлении новых алгоритмов, мы добавим их в список на тестирование
  2. Избегаем повторений

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)

16 of 54

Функция 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

17 of 54

Функция 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>

18 of 54

Функция reduce() в Python:

  • reduce(function, iterable)
  • Находится в модуле functools
  • У function должно быть два аргумента
  • Работает так: f(f(f(a, b), c), d)

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)

19 of 54

Функция 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)]

20 of 54

А как вообще интерпретатор обрабатывает функции?

21 of 54

Когда 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

22 of 54

Функция - это полноправный объект

>>> 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.

23 of 54

Как передаются значения в функцию

Единственный способ передачи параметров в Python - вызов по соиспользованию (call by sharing). Он используется в большинстве объектно-ориентированных язы­ков, в том числе Ruby, SmallTalk и Java (это относится к ссылочным типам Java, параметры примитивных типов передаются по значению). Вызов по соиспользованию означает, что каждый формальный параметр функции получает копию ссылки на фактический аргумент. Иначе говоря, внутри функции параметры ста­новятся синонимами фактических аргументов.

В результате функция получает возможность модифицировать любой изменяемый объект, переданный в качестве параметра, но не может заменить объект другим, не тождественным ему.

24 of 54

Передача аргументов по соиспользованию

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]

25 of 54

Еще пример

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']

26 of 54

Поговорим про аргументы

  • positional-or-keyword:

def func(foo, bar=None): ...

  • positional-only: Синтаксиса в Python <= 3.7 для этого нет (в 3.8 есть)

def func(positional_only_parameters, /): ...

  • keyword-only:

def func(arg, *, kw_only1, kw_only2): ...

  • var-positional:

def func(*args, **kwargs): ...

  • var-keyword:

def func(*args, **kwargs): ...

27 of 54

Напишем функцию 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)

28 of 54

Для двух аргументов - несерьезно

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

29 of 54

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'

30 of 54

Значение по умолчанию

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) # обязательно по имени

31 of 54

Что может пойти не так

Если значение изменяемое, могут быть "спецэффекты". С этим нужно быть очень осторожным, в примере ниже всё хорошо.

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

32 of 54

Что может пойти не так 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]

33 of 54

Как исправить?

Альтернатива - использовать специальные значения (чаще всего - 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]

34 of 54

Время жизни дефолтных значений

Значения вычисляются единожды в момент определения функции (когда интерпретатор доберётся до неё).

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

35 of 54

Замечание: Для обязательности 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."""

...

36 of 54

Произвольное количество аргументов

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}

37 of 54

Что выведет эта программа?

def call_me(*args, **kwargs):

return args, kwargs

print(call_me({"a": 92}))

# ((), {'a': 92})

# (({'a': 92},), {})

print(call_me(**{"a": 92}))

38 of 54

Области видимости

39 of 54

Пример

def h():

print(12)

def f():

g(h)

def g(a):

a()

g(f) # в каком порядке будет работать данный скрипт?

# ['g', 'f', 'g', 'h', 'print']

40 of 54

Пространство имен

Пространство имен - множество всех ссылок переменных на объекты в памяти.

Примеры пространств имен:

  • builtins (int, str, float, abs, id, max, ...) - при запуске скрипта
  • global - то, что на самом верхнем уровне кода, над всеми функциями - в теле самого скрипта, который запускается (выполняется во время module в стеке вызовов)
  • namespace функции - то, что находится внутри функции (в отступе в 4 пробела), нужно для стека вызовов, локальных переменных и чистки мусора - переменные живут только в пределе функции. Локальное пространство имен для функции создается в момент вызова функции.

41 of 54

Области видимости

Области видимости - текстовые фрагменты программы, в которых локальные namespace доступны напрямую

Любой вызов переменной в функции идет по стеку вызовов - сначала переменные ищутся в namespaсe функции, затем в enclosing, затем в global, затем в builtins.

Условные операторы и циклы не создают локальных namespace как функции - они работают на текущем уровне пространства имен.

42 of 54

Пример

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')

43 of 54

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

44 of 54

nonlocal

Если есть объявление nonlocal smth - имя заимствовано из охватывающей функции (и только из функции, и только на 1 уровень выше)

global_var = 0

def func():

var = 0

def inner():

nonlocal var

var += 1

inner() # вызвать можно только тут, либо в global через func

print(var)

func() # 1

45 of 54

Замыкание

46 of 54

Замыкание

Замыкание (closure) в программировании — это функция, в теле которой присутствуют ссылки (упоминания) на переменные, объявленные вне тела этой функции в окружающем коде и не являющиеся ее параметрами (такие переменные называются свободными).

Важно понять, как оно работает для последующих тем в функциональном программировании.

47 of 54

Пример

48 of 54

Замыкание в действии

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

49 of 54

Посмотрим на локальные переменные

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))

50 of 54

Выведет

Потому что 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

51 of 54

И с неизменяемыми объектами

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

52 of 54

Результат

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

53 of 54

ДЗ

  • Разобраться с областями видимости на этом примере
  • Понять, что такое замыкания тут, и по полезным ссылкам.
  • Сделать задания, которые мы выложим в тимсе.

54 of 54

Полезные ссылки