Функции и замыкание
Пространства имён: повторение
Функции
- Задание функции
Формальные и фактические параметры, 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 'Косортировка'
Написать функцию pattsort(pattern, seq), которой передаются две последовательности одинаковой длины pattern и seq, причём все элементы pattern различны. Функция возвращает список элементов seq в порядке возрастания элементов pattern. То есть если наименьший элемент стоит в pattern на k-й позиции, то в выводе на k-й позиции должен стоять наименьший элемент seq, если второй минимум стоит в pattern стоит на позиции i, то второй минимум seq должен в выводе оказаться на позиции i и т. д.
1 print(*pattsort((5, 4, 6, 3, 8, 1), (2, 2, 6, 1, 9, 6)))
6 2 6 2 9 1
EJudge: Without2Zeros 'Без двух нулей'
Написать функцию No_2Zero(N, K), которая вычисляет количество N-значных чисел в системе счисления с основанием K, таких что их запись не содержит двух подряд идущих нулей. Лидирующие нули не допускаются. Для EJudge N⩽33.
print(No_2Zero(6, 3))
328
EJudge: ArithFunct 'Арифметика функций'
Написать четыре функции (функционала): 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 (например, при умножении функции на константу).
-1.380426876732927 -1.380426876732927 0.5773502691896256 0.5773502691896257 0.7389056098930651 0.738905609893065 15
EJudge: AllProducts 'Разложения на сомножители'
Ввести произвольное натуральное число, не превосходящее 1000000000, и вывести (через «*») все его разложения на натуральные сомножители, превосходящие 1, без учёта перестановок. Сомножители в каждом разложении и сами разложения (как последовательности) при выводе должны быть упорядочены по возрастанию. Само число также считается разложением. Можно использовать рекурсию.
24
2*2*2*3 2*2*6 2*3*4 2*12 3*8 4*6 24