Функции и замыкание
Пространства имён: повторение
Функции
- Задание функции
- Формальные и фактические параметры, return 
- Duck typing: функция как формализация алгоритма
- вызов функции как локальное пространство имён 
- Использование Python Tutor 
- функция как объект: именование, передача в качестве параметра - работа sort()/sorted() (а заодно и max()/min()) 
 
- Лямбда-функции (функции-выражения) - Писать f = lambda … не рекомендуется, лучше def; лямбды нужны как раз для задания объектов-функций по месту (как в sorted()) 
 
- Распаковка (def fun(*args)) и запаковка (a = fun(*seq)) параметров — пока только позиционных - функция с произвольным числом параметров
- Передача параметров с распаковкой последовательности
- Лайф-хак вида print(*последовательность) 
 
- Параметры функции по умолчанию и именованные параметры 1 >>> def fun(a, b=1, c=2, d=3): 2 ... print(a, c, b, d) 3 >>> fun(9, 8, 7, 6) 4 9 7 8 6 5 >>> fun(9, 8) 6 9 2 8 3 7 >>> fun(9) 8 9 2 1 3 9 >>> fun() 10 Traceback (most recent call last): 11 File "<stdin>", line 1, in <module> 12 TypeError: fun() missing 1 required positional argument: 'a' 13 >>> fun(100, d=123, b=0) 14 100 2 0 123 15 
- Строго позиционные и строго именованные параметры, общее понятие «параметр» и полный вид описания функции - 1 >>> def fun(a, b=1, /, c=2, *, d=3): 2 ... print(a, c, b, d) 3 >>> fun(1) 4 1 2 1 3 5 >>> fun(1, b=3) 6 Traceback (most recent call last): 7 File "<stdin>", line 1, in <module> 8 TypeError: fun() got some positional-only arguments passed as keyword arguments: 'b' 9 10 >>> fun(1, 3, 5) 11 1 5 3 3 12 >>> fun(1, 3, 5, 8) 13 Traceback (most recent call last): 14 File "<stdin>", line 1, in <module> 15 TypeError: fun() takes from 1 to 3 positional arguments but 4 were given 
 
- Самодокументирование - 1 >>> def fun(a, b): 2 ... '''Calculate a formula on a and b 3 ... 4 ... :param a: First parameter 5 ... :param b: Second parameter 6 ... :return: Resulting formula''' 7 ... return a * 2 + b 8 >>> fun(12, 56) 9 80 10 >>> print(fun.__doc__) 11 Calculate a formula on a and b 12 13 :param a: First parameter 14 :param b: Second parameter 15 :return: Resulting formula 16 17 >>> help(fun) 18 Help on function fun in module __main__: 19 20 fun(a, b) 21 Calculate a formula on a and b 22 23 :param a: First parameter 24 :param b: Second parameter 25 :return: Resulting formula 26 (END) 27 
 
Про рекурсию
Рекурсия в Python — это стек вызовов, см. пример
- Достаточно тяжёлая операция (создание / удаление контестов вызова и пространств имён) - не стоит использовать вместо цикла 
- ⇒ максимальная глубина рекурсии по умолчанию всего 1000
- ⇒ логарифмический критерий уместности рекурсии
 
Гвидо, Python и хвостовой вызов
- («Тут меня упрекают: дескать, новый Бейсик написал. Лично я считаю это знаком почёта…»
Замещение рекурсии стеком (не успеем)
TL;DR: см. ссылку выше: рекурсия это и есть стек.
TODO вынести в отдельную статью:
- пример примитивной рекурсии
- стек контекстов, хранящих связанные переменные (на pythontutor)
- разбор примера ниже
Разбор задачи. Есть ли среди натуральных чисел seq такие, что в сумме дают req?
Рекурсивный вариант:
- Основание рекурсии: start == len(seq) 
- seq — свободная переменная (могла быть глобальной) 
- start и req — связанные переменные 
- Отсечение (причина не делать рекурсивный вызов, когда основание не достигнуто) — req <= 0 - Отсечение бывает локальное (переход к следующему шагу без рекурсивного вызова) и глобальное (выход из рекурсивного вызова)
- В данном примере оно… 
 
Рекурсия — это цикл до исчерпания стека, содержащий как минимум три действия:
- Выполняется шаг рекурсии
- Если основание рекурсии не достигнуто, вычисляется и кладётся на стек новое значение связанных переменных (аналог рекурсивного вызова)
- Если основание рекурсии достигнуто, со стека снимаются связанные переменные 1 def subsS(seq, req): 2 stack = [[req, -1]] # Инициализация 3 while stack: # Цикл до исчерпания стека 4 stack[-1][-1] += 1 # Шаг рекурсии 5 req, start = stack[-1] # (необязательное именование) 6 if req == 0: # Глобальное отсечение 7 return True 8 if start < len(seq) and req >= seq[start]: # Основание не достигнуто и отсечение не сработало 9 stack.append([req - seq[start], start]) # Рекурсивный вызов 10 else: # Основание рекурсии достигнуто 11 stack.pop() # Выход из рекурсивного вызова - Для удобства мы поименовали вершину стека как в рекурсивном варианте: req и start 
 
Замыкание
- Функция — это объект
- Её можно изготовить внутри другой функции и вернуть
- …причём в зависимости от параметров этой другой функции!
- …в процессе чего некоторые объекты из ПИ создающей функции «залипают» в ПИ создаваемой - только они там навсегда должны залипнуть, а не только на время вызова
- ⇒ .__closure__ 
 
- Это и есть замыкание!
Пример:
и
Also: nonlocal name — явное указание брать имя name из внешнего, но не глобального пространства имён
Модификатор nonlocal для доступа к пространству имён вызывающей функции:
- Без nonlocal имя x оказалось бы локальным, а с global — глобальным 
Замыкание и позднее связывание
Вот этот код не работает так, как может показаться:
Выясняется, что все adder-ы работают одинаково — прибавляют 9!
Что происходит?
- При определении очередного adder()-а выясняется, что i — нелокальное имя, потому что среди локальных его нет, зато оно локально для create_adders() 
- Значит, надо сформировать специальное пространство имён, в котором это имя «залипнет» после выхода из create_adders() 
- В adder-ах будет сформировано замыкание, куда попадёт i из этого пространства имён 
- На момент выхода из create_adders() имя i указывает на 9 
   1 >>> c = create_adders()
   2 >>> c[1]
   3 <function create_adders.<locals>.adder at 0x7f272d2f93b0>
   4 >>> c[1].__closure__
   5 (<cell at 0x7f272d1c1510: int object at 0x7f272db36660>,)
   6 >>> c[2].__closure__
   7 (<cell at 0x7f272d1c1510: int object at 0x7f272db36660>,)
   8 >>> c[2].__closure__[0].cell_contents
   9 9
  10 >>> c[1].__closure__[0].cell_contents
  11 9
  12 
По сути, в Python любое превращение имени в объект — это позднее связывание. Какой именно объект именуется i или x, каждый из adder-ов решает только когда его вызовут
- Когда формируется замыкание, нелокальное имя может быть даже ещё не определено! Главное, чтобы оно появилось до выхода из контекста, которому принадлежит:
- (Спасибо слушателю лекции 2023 года) Если имя i в функции create_adders() удалить непосредственно перед return adders, сформируется неработоспособное замыкание (проверьте!) 
Если мы хотели не этого, надо сделать так, чтобы при создании очередного adder-а его i именовало новый объект. Например, связывать это i отдельной переменной во время создания очередного adder-а:
При этом никакого замыкания не произойдёт, у каждого adder-а будет своё локальное j, инициализированное соответствующим значением i. (Если бы нам нужно было сильнее запутаться, мы могли бы написать i=i вместо j=i ☺ ).
Д/З
- Прочитать: - в Tutorial про функции 
- Про замыкания: Gabor Laszlo Hajba и Dmitry Soshnikov 
- Посмотреть, как оформлять задачи типа «написать функцию» - ВНИМАНИЕ! Первые три задания к этой лекции именно такого типа, последнее — обычное! 
 
 
- EJudge: PatternSort 'Косортировка' Input:- Написать функцию pattsort(pattern, seq), которой передаются две последовательности одинаковой длины pattern и seq, причём все элементы pattern различны. Функция возвращает список элементов seq в порядке возрастания элементов pattern. То есть если наименьший элемент стоит в pattern на k-й позиции, то в выводе на k-й позиции должен стоять наименьший элемент seq, если второй минимум стоит в pattern стоит на позиции i, то второй минимум seq должен в выводе оказаться на позиции i и т. д. Output:- 1 print(*pattsort((5, 4, 6, 3, 8, 1), (2, 2, 6, 1, 9, 6)))- 6 2 6 2 9 1 
- EJudge: Without2Zeros 'Без двух нулей' Input:- Написать функцию No_2Zero(N, K), которая вычисляет количество натуральных N-значных чисел в системе счисления с основанием K, таких что их запись не содержит двух подряд идущих нулей. Лидирующие нули не допускаются. Для EJudge N⩽33. Output:- print(No_2Zero(6, 3)) - 328 
- EJudge: ArithFunct 'Арифметика функций' Input:- Написать четыре функции (функционала): ADD(f, g), SUB(f, g), MUL(f, g) и DIV(f, g), параметрами которых могут быть как обычные объекты, так и функции от одной переменной (проверить, является ли объект функцией можно с помощью callable(объект)). Возвращать эти функционалы должны функцию от одной переменной h(x), которая выполняет соответствующее действие — f(x)+g(x), f(x)-g(x), f(x)*g(x) или f(x)/g(x) — над этими переменными. Если f или g не были функцией, вместо f(x) используется f, а вместо g(x) — g (например, при умножении функции на константу). Output:- -1.380426876732927 -1.380426876732927 0.5773502691896256 0.5773502691896257 0.7389056098930651 0.738905609893065 15 
- EJudge: AllProducts 'Разложения на сомножители' Input:- Ввести произвольное натуральное число, не превосходящее 1000000000, и вывести (через «*») все его разложения на натуральные сомножители, превосходящие 1, без учёта перестановок. Сомножители в каждом разложении и сами разложения (как последовательности) при выводе должны быть упорядочены по возрастанию. Само число также считается разложением. Можно использовать рекурсию. Output:- 24 - 2*2*2*3 2*2*6 2*3*4 2*12 3*8 4*6 24 

 в Python 3.13+ иногда можно менять
 в Python 3.13+ иногда можно менять