Функция — часть программы, которую можно вызвать из другого места программы.
Все в Python объекты. И даже функции. Это значит, что у функций есть
От остальных объектов функции отличаются тем, что их можно вызвать*. Объекты, которые можно вызвать, называют Callable
-объектами. У них есть метод __call__()
.
* С точки зрения синтаксиса еще можно вызывать классы
# Функция определяется таким синтаксисом
def plus_one(x: int) -> int:
"""Функция возвращает увеличенное на 1 целое число"""
return x+1
Это избыточное определение. Из избыточного здесь использованы:
docstring
,На самом деле можно описать эту же функцию компактней.
# Функция plus_one без анотаций и документации
def plus_one_simple(x): return x+1
Как у любого объекта в python, у функции есть:
# У функции plus_one эти параметры выглядят так
id(plus_one), type(plus_one)
В CPython идентификатор — адрес объекта в виртуальной памяти
# Идентификатор в шестнадцатиричном формате — адрес функции plus_one
hex(id(plus_one))
Все атрибуты и методы функции как объекта можно посмотреть:
import inspect
list(filter(lambda x: x[0] != "__globals__", sorted(inspect.getmembers(plus_one))))
# Здесь мы выбросили поле "__globals__", чтобы не засорять вывод
# Вызов функции, ожидаем ответ 2
plus_one(1)
# Можно явно вызвать метод call, ожидаем ответ 2
plus_one.__call__(1)
# Байт-код функции function_name
plus_one.__code__.co_code
# Дизассемблированное тело функции function_name
import dis
dis.dis(plus_one)
Если заглянуть во внутренности интерпретатора (CPython), то функция описывается следующей струтурой: https://github.com/python/cpython/blob/3.7/Include/funcobject.h
Вложенная функция — функция, которая определена внутри другой функции.
При работе с вложенными функциями надо учитывать области видимости.
В Python есть 4 области видимости. Расположены они как показано на рисунке.
Стрелки на рисунке показывают в какой последовательности Python обходит области видимости. Следующий код показывает как распределены области относительно вложенной функции inner.
# (built-in) — область системных имен
# global — область модуля
def outer():
# enclosed — область функции-обёртки
def inner():
# local — область внутри функции
pass
Зачем это может быть нужно? Можно выделить 3 примера:
# Вложенная функция inner внутри plus_one_outer
def plus_one_outer(x):
"""Функция возвращает увеличенное на 1 целое число"""
def inner(y): return y+1
return inner(x)
# Вызов функции, ожидаем ответ 2
plus_one_outer(1)
# Вложенная функция недоступна (должна быть ошибка)
inner(1)
Вложенные функции дают накладные расходы
import dis
dis.dis(plus_one_outer)
def factorial(x):
"""Функция вычисляет факториал целого числа"""
def calc_factorial(y): return y * calc_factorial(y-1) if y!=0 else 1
if x<0:
return -1
return calc_factorial(x)
factorial(4)
Замыкание — вложенная функция, которая запоминает значения окружения, с которым она была вызвана. Говорят, что функция «замыкается» на значения переменных окружения. По сути это техника параметризированной генерации функций.
Рассмотрим простой пример замыкания
# Функция-обёртка принимает возвращает внутренную функцию, которая «замкнута» на значение a
def gen_mul(a):
def inner(b):
return a*b
return inner
# gen_mul возвращает функцию, которая будет всегда умножать на 2
double = gen_mul(2)
# Проверим (должно быть 6)
double(3)
# Можно возвращаемую функцию не сохранять
gen_mul(2)(3)
Функции, которые возвращают другие функции, называются «фабриками функций».
В замыканиях все переменные внутри вложенной функции вычисляются в момент её вызова, а не создания. Это называется позднее связывание (англ. late binding). Из-за него могут возникнуть проблемы, как в примере ниже.
# Проблема с late binding
powers = []
for i in (1,2):
def inner(x):
return x**i
powers.append(inner)
# Хотим получить список степеней 5-ки
for p in powers:
print(p(5))
Вместо ожидаемых двух степеней 5-ки получи два раза возведение в последнюю степень из списка. Это происходит из-за того, что в момент вызова p(5)
в теле функции переменная i «смотрит» на последнее своё значение, т. е. 2.
Это можно исправить, если создавать копии i
при создании замыканий. Это можно сделать с помощью обёртки. При каждом вызове функции параметры указывают на значения аргументов функции. Поэтому замыкания «замыкаются» на текущие значения счётчика.
# Решение проблемы late binding через обёртку (здесь make_inner)
powers = []
for i in (1,2):
def make_inner(j):
def inner(x):
return x**j
return inner
powers.append(make_inner(i))
for p in powers:
print(p(5))
Другое решение — использовать тот факт, что значения параметров по-умолчанию вычисляются при создании функции.
# Решение проблемы late binding через стандарное знаечение аргумента функции
powers = []
for i in (1,2):
def inner(x, i=i):
return x**i
powers.append(inner)
for p in powers:
print(p(5))
Декоратор — «синтаксический сахар» для функции-обёртки вокруг другой функции. Обычно декоратор используют, чтобы добавить новое поведение другой функции без изменения ее тела.
# Возьмем простую функцию возведения в квадрат
def sqr(x): return x*x
sqr(5)
Мы хотим обёрнуть функцию возведения в квадрат другой функцией, чтобы добавить новые возможности. Ниже приведен пример фабрики функций, которая возвращает функцию-обёртку. Эта функция-обёртка выполняет новый код и вызывает оборачивемую функцию.
from time import perf_counter_ns
# Фабрика функций, которая генерирует обернутые функции func для отладки вызова и результата
def make_debugable(func):
def wrapper(x):
print(f"[DEBUG] Launch function {func} with x={x}")
start_time_ns = perf_counter_ns()
result = func(x)
stop_time_ns = perf_counter_ns()
duration_ns = stop_time_ns-start_time_ns
print(f"[DEBUG] Time: {duration_ns}ns")
return result
return wrapper
Обернём функцию sqr, сгенерированное значение будем хранить в sqrt2.
sqrt2 = make_debugable(sqr)
sqrt2(5)
Синтаксический сахар декораторов позволяет описать такое поведение короче. При этом декоратор генерирует обёрнутую функцию с таким же именем как у обораичиваемой.
# Следующий код эквивалентен: sqr3 = make_debugable(sqr3)
@make_debugable
def sqr3(x): return x*x
sqr3(4)
make_debugable
¶Сравним скорость работы встроенной функции sum
с написанной «руками» с помощью декоратора make_debugable
.
@make_debugable
def sum_1(n):
"""Суммирование чисел от 1 до n в цикле for"""
s = 0
for i in range(n):
s += i
return s
@make_debugable
def sum_2(n):
"""Суммирование чисел от 1 до n встроенной функцией"""
return sum(range(n))
sum_1(10000)
sum_2(10000)
Наглядно видно, что встроенная функция производительней.
По сути декоратор возвращает другую функцию. Если проверить документацию к функции:
# Проверка справки (не должен вернуть строку)
help(sum_1)
Чтобы возвращаемая функция была похожа на оборачиваемую надо скопировать внутренние атрибуты.
from functools import wraps
from time import perf_counter_ns
# Улучшенная фабрика функций, которая генерирует обернутые функции func для отладки вызова и результата
def make_debugable_real(func):
@wraps(func) # Декоратор из библиотеки для копирования внутренних атрибутов
def wrapper(x):
print(f"[DEBUG] Launch function {func} with x={x}")
start_time_ns = perf_counter_ns()
result = func(x)
stop_time_ns = perf_counter_ns()
duration_ns = stop_time_ns-start_time_ns
print(f"[DEBUG] Time: {duration_ns}ns")
return result
return wrapper
@make_debugable_real
def sum_3(n):
"""Суммирование чисел от 1 до n встроенной функцией"""
return sum(range(n))
# Проверка справки (теперь должен вернуть строку)
help(sum_3)
Чтобы декоратор принимал аргументы, надо сформировать замыкание фабрики обёрток с параметром декоратора.
from functools import wraps
# Фабрика генераторов функций, которая позволяет использовать параметры, функция умножает результат функции на число.
def mul(p):
def decorator(func):
@wraps(func)
def wrapper(*args): # Упаковали параметры (см. ниже)
return p*func(*args) # Распаковали параметры обратно (см. ниже)
return wrapper
return decorator
Объявим функцию с двумя декораторами. Декораторы применяются последовательнос снизу вверх.
@mul(2)
@mul(4)
def f(x: int, y: int):
return x+y
# Должно быть 24 так как 2(4(1+2)) = 24
f(1,2)
При работе с последовательностями можно собирать значения в переменные. Это называется упаковка. Синтаксис такой.
a, *b, c = [2, 7, 5, 6, 3, 4, 1]
В b
теперь список всего того, что не попало в a
и c
print(b)
Эти значения можно подставить используя распаковку.
print(*b)
Результат вывода разный. В первом случае вызов эквивалентен print([b1, b2, b3])
, во втором — print(b1, b2, b3)
.
NB! Упаковка и распоковка доступна и для словарей. Для этого используется **.
%load_ext watermark
%watermark -d -u -v -iv