<jupyter_start><jupyter_text>[GitHub Python Bar Review](https://github.com/ckorikov/python-bar-review)   Вложенные функции, замыкания и декораторы  1. Функции **Функция** — часть программы, которую можно вызвать из другого места программы.Все в Python объекты. И даже функции. Это значит, что у функций есть- атрибуты- и методы.От остальных объектов функции отличаются тем, что их можно вызвать*. Объекты, которые можно вызвать, называют `Callable`-объектами. У них есть метод `__call__()`.\* С точки зрения синтаксиса еще можно вызывать классы  Как определить функцию<jupyter_code># Функция определяется таким синтаксисом
def plus_one(x: int) -> int:
    """Функция возвращает увеличенное на 1 целое число"""
    return x+1<jupyter_output><empty_output><jupyter_text>Это избыточное определение. Из избыточного здесь использованы:- строка документирования — `docstring`,- и анотация функции.На самом деле можно описать эту же функцию компактней.<jupyter_code># Функция plus_one без анотаций и документации
def plus_one_simple(x): return x+1<jupyter_output><empty_output><jupyter_text>Функция как объект Как у любого объекта в python, у функции есть:- идентификатор,- тип.<jupyter_code># У функции plus_one эти параметры выглядят так
id(plus_one), type(plus_one)<jupyter_output><empty_output><jupyter_text>В CPython идентификатор — **адрес объекта** в виртуальной памяти<jupyter_code># Идентификатор в шестнадцатиричном формате — адрес функции plus_one
hex(id(plus_one))<jupyter_output><empty_output><jupyter_text>Все атрибуты и методы функции как объекта можно посмотреть:<jupyter_code>import inspect
list(filter(lambda x: x[0] != "__globals__", sorted(inspect.getmembers(plus_one))))
# Здесь мы выбросили поле "__globals__", чтобы не засорять вывод<jupyter_output><empty_output><jupyter_text>Как вызвать функцию<jupyter_code>#  Вызов функции, ожидаем ответ 2
plus_one(1)
#  Можно явно вызвать метод call, ожидаем ответ 2
plus_one.__call__(1)<jupyter_output><empty_output><jupyter_text>Как функции устроены<jupyter_code># Байт-код функции function_name
plus_one.__code__.co_code
# Дизассемблированное тело функции function_name
import dis
dis.dis(plus_one)<jupyter_output>4           0 LOAD_FAST                0 (x)
              2 LOAD_CONST               1 (1)
              4 BINARY_ADD
              6 RETURN_VALUE<jupyter_text>Если заглянуть во внутренности интерпретатора (CPython), то функция описывается следующей струтурой: https://github.com/python/cpython/blob/3.7/Include/funcobject.h  Почитать1. [The Python Language Reference. Data model](https://docs.python.org/3/reference/datamodel.htmlobjects-values-and-types)2. [The Python Language Reference. Inspect live objects](https://docs.python.org/3/library/inspect.html)3. [PEP 3107 -- Function Annotations](https://www.python.org/dev/peps/pep-3107/)4. [PEP 257 -- Docstring Conventions](https://www.python.org/dev/peps/pep-0257/)  2. Вложенные функции **Вложенная функция** — функция, которая определена внутри другой функции.При работе с вложенными функциями надо учитывать области видимости.  Область видимости в Python — LEGB В Python есть 4 области видимости. Расположены они как показано на рисунке.![title](img/LEGB.png)Стрелки на рисунке показывают в какой последовательности Python обходит области видимости. Следующий код показывает как распределены области относительно вложенной функции inner.<jupyter_code># (built-in) — область системных имен

# global — область модуля
def outer():
    # enclosed — область функции-обёртки 
    def inner():
        # local — область внутри функции
        pass<jupyter_output><empty_output><jupyter_text>Зачем нужны вложенные функции? Зачем это может быть нужно? Можно выделить 3 примера:1. чтобы скрыть функцию в глобальной области видимости,2. чтобы вынести «лишний» код из функцию в обёртку,3. чтобы реализовать замыкания (см. следующий раздел).  Пример 1. Чтобы скрыть функцию — инкапсуляция<jupyter_code># Вложенная функция 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)<jupyter_output><empty_output><jupyter_text>Вложенные функции дают накладные расходы<jupyter_code>import dis
dis.dis(plus_one_outer)<jupyter_output>4           0 LOAD_CONST               1 (<code object inner at 0x7f75e851fc90, file "<ipython-input-11-c887b5072665>", line 4>)
              2 LOAD_CONST               2 ('plus_one_outer.<locals>.inner')
              4 MAKE_FUNCTION            0
              6 STORE_FAST               1 (inner)

  5           8 LOAD_FAST                1 (inner)
             10 LOAD_FAST                0 (x)
             12 CALL_FUNCTION            1
             14 RETURN_VALUE

Disassembly of <code object inner at 0x7f75e851fc90, file "<ipython-input-11-c887b5072665>", line 4>:
  4           0 LOAD_FAST                0 (y)
              2 LOAD_CONST               1 (1)
              4 BINARY_ADD
              6 RETURN_VALUE<jupyter_text>Пример 2. Чтобы вынести «лишний» код из функции в обёртку<jupyter_code>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)<jupyter_output><empty_output><jupyter_text>3. Замыкания **Замыкание** — вложенная функция, которая запоминает значения окружения, с которым она была вызвана. Говорят, что функция «замыкается» на значения переменных окружения. По сути это техника параметризированной генерации функций.Рассмотрим простой пример замыкания<jupyter_code># Функция-обёртка принимает возвращает внутренную функцию, которая «замкнута» на значение 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)<jupyter_output><empty_output><jupyter_text>Функции, которые возвращают другие функции, называются **«фабриками функций»**.  Проблема late bindingВ замыканиях все переменные внутри вложенной функции вычисляются в момент её вызова, а не создания. Это называется позднее связывание (англ. [late binding](https://en.wikipedia.org/wiki/Late_binding)). Из-за него могут возникнуть проблемы, как в примере ниже.<jupyter_code># Проблема с 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))<jupyter_output>25
25<jupyter_text>Вместо ожидаемых двух степеней 5-ки получи два раза возведение в последнюю степень из списка. Это происходит из-за того, что в момент вызова `p(5)` в теле функции переменная i «смотрит» на последнее своё значение, т. е. 2.Это можно исправить, если создавать копии `i` при создании замыканий. Это можно сделать с помощью обёртки. При каждом вызове функции параметры указывают на значения аргументов функции. Поэтому замыкания «замыкаются» на текущие значения счётчика.<jupyter_code># Решение проблемы 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))<jupyter_output>5
25<jupyter_text>Другое решение — использовать тот факт, что значения параметров по-умолчанию вычисляются при создании функции.<jupyter_code># Решение проблемы 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))<jupyter_output>5
25<jupyter_text>4. Декораторы **Декоратор** — «синтаксический сахар» для функции-обёртки вокруг другой функции. Обычно декоратор используют, чтобы добавить новое поведение другой функции без изменения ее тела.<jupyter_code># Возьмем простую функцию возведения в квадрат
def sqr(x): return x*x
sqr(5)<jupyter_output><empty_output><jupyter_text>Пример элементарного декоратора Мы хотим обёрнуть функцию возведения в квадрат другой функцией, чтобы добавить новые возможности.Ниже приведен пример фабрики функций, которая возвращает функцию-обёртку. Эта функция-обёртка выполняет новый коди вызывает оборачивемую функцию.<jupyter_code>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<jupyter_output><empty_output><jupyter_text>Обернём функцию sqr, сгенерированное значение будем хранить в sqrt2.<jupyter_code>sqrt2 = make_debugable(sqr)
sqrt2(5)<jupyter_output>[DEBUG] Launch function <function sqr at 0x7f75e84a69d0> with x=5
[DEBUG] Time: 11200ns<jupyter_text>Синтаксический сахар декораторов позволяет описать такое поведение короче. При этом декоратор генерирует обёрнутую функцию с таким же именем как у обораичиваемой.<jupyter_code># Следующий код эквивалентен: sqr3 = make_debugable(sqr3)
@make_debugable
def sqr3(x): return x*x

sqr3(4)<jupyter_output>[DEBUG] Launch function <function sqr3 at 0x7f75e8539820> with x=4
[DEBUG] Time: 12900ns<jupyter_text>Практичный пример применения декоратора `make_debugable` Сравним скорость работы встроенной функции `sum` с написанной «руками» с помощью декоратора `make_debugable`.<jupyter_code>@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)<jupyter_output>[DEBUG] Launch function <function sum_2 at 0x7f75e844b310> with x=10000
[DEBUG] Time: 437100ns<jupyter_text>Наглядно видно, что встроенная функция производительней.  Пример рабочего декоратора По сути декоратор возвращает другую функцию. Если проверить документацию к функции:<jupyter_code># Проверка справки (не должен вернуть строку)
help(sum_1)<jupyter_output>Help on function wrapper in module __main__:

wrapper(x)<jupyter_text>Чтобы возвращаемая функция была похожа на оборачиваемую надо скопировать внутренние атрибуты.<jupyter_code>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)<jupyter_output>Help on function sum_3 in module __main__:

sum_3(n)
    Суммирование чисел от 1 до n встроенной функцией<jupyter_text>Пример рабочего декоратора с параметрами Чтобы декоратор принимал аргументы, надо сформировать замыкание фабрики обёрток с параметром декоратора.<jupyter_code>from functools import wraps

# Фабрика генераторов функций, которая позволяет использовать параметры, функция умножает результат функции на число.
def mul(p):
    def decorator(func):
        @wraps(func)
        def wrapper(*args): # Упаковали параметры (см. ниже)
            return p*func(*args) # Распаковали параметры обратно (см. ниже)
        return wrapper
    return decorator<jupyter_output><empty_output><jupyter_text>Объявим функцию с двумя декораторами. Декораторы применяются последовательнос снизу вверх.<jupyter_code>@mul(2)
@mul(4)
def f(x: int, y: int):
    return x+y
# Должно быть 24 так как 2(4(1+2)) = 24
f(1,2)<jupyter_output><empty_output><jupyter_text>Упаковка и распаковка параметров При работе с последовательностями можно собирать значения в переменные. Это называется **упаковка**. Синтаксис такой.<jupyter_code>a, *b, c = [2, 7, 5, 6, 3, 4, 1]<jupyter_output><empty_output><jupyter_text>В `b` теперь список всего того, что не попало в `a` и `c`<jupyter_code>print(b)<jupyter_output>[7, 5, 6, 3, 4]<jupyter_text>Эти значения можно подставить используя **распаковку**.<jupyter_code>print(*b)<jupyter_output>7 5 6 3 4<jupyter_text>Результат вывода разный. В первом случае вызов эквивалентен `print([b1, b2, b3])`, во втором — `print(b1, b2, b3)`.**NB!** Упаковка и распоковка доступна и для словарей. Для этого используется **.  Watermark<jupyter_code>%load_ext watermark
%watermark -d -u -v -iv<jupyter_output>last updated: 2019-10-21 

CPython 3.8.0
IPython 7.8.0
{'fraction_non_alphanumeric': 0.08445050367340756, 'fraction_numerical': 0.022722108611679163, 'mean_word_length': 4.618723404255319, 'pattern_counts': {'":': 0, '<': 109, '<?xml version=': 0, '>': 109, 'https://': 7, 'lorem ipsum': 0, 'www.': 2, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'chain_length': '28', 'dir': 'jupyter-structured-clean-dedup', 'id': '596669', 'license': 'MIT', 'n_tokens_mistral': 5248, 'n_tokens_neox': 5949, 'n_words': 1325, 'path': '01-nested_functions-closures-decorators.ipynb', 'repo_name': 'ckorikov/python-bar-review', 'size': '13203'}