1. 1
  2. 2
  3. 3
  4. 4
  5. 5
  6. 6
  7. 7

# теория · шаг 4 из 7

Функции: аргументы, локальные имена и стек вызовов

Коротко

Функция задаётся def имя(параметры): и возвращает значение через return; без return она возвращает None. При вызове аргумент не копируется: параметр становится вторым именем того же значения. Поэтому spisok.append(x) внутри функции виден снаружи, а spisok = [0] - нет: это просто переклеенная наклейка. Локальная переменная - имя, которому присвоили значение внутри функции; она живёт до конца вызова. Изменяемое значение по умолчанию (def f(x, acc=[])) создаётся один раз на все вызовы. Каждый вызов получает свой набор локальных имён на стеке вызовов, а глубина стека в Python по умолчанию - около тысячи.

Если ты знаешь, почему a = a.sort() даёт None и чем опасен def f(x, acc=[]), листай до «Теперь сам» в конце.

Как думаешь, что напечатает программа

def dobavit(spisok, x):
    spisok.append(x)
    spisok = [0]
    spisok.append(x)

a = [1]
dobavit(a, 2)
print(a)

Ответь себе до того, как читать дальше. Вариантов три: [1] - если функция получила копию, [0, 2] - если функция может подменить список вызывающего, и [1, 2]. Языки отвечают на этот вопрос по-разному, а Python - третьим способом.

На что это похоже

Ты даёшь коллеге второй ключ от своей квартиры. Если он переставит мебель, ты это увидишь: квартира одна. Если он выбросит твой ключ и получит ключ от другой квартиры, в твоей ничего не изменится - он просто теперь ходит в другую. Параметр функции - такой второй ключ. Изменения ВНУТРИ значения видны всем, у кого есть ключ; замена ключа видна только тому, кто его заменил.

Механизм: вызов, имена и стек

Вызов dobavit(a, 2) делает три вещи. Создаёт новый набор локальных имён. Привязывает параметр spisok к тому же значению, на которое указывает a, а x - к числу 2. Выполняет тело функции, пока не встретит return или конец, и после этого набор имён исчезает, а выполнение продолжается с места вызова.

Наборы имён складываются друг на друга. Если функция вызывает другую функцию, новый набор кладётся сверху, а когда та закончит, снимается, и снова виден набор вызывающей. Такая конструкция - добавлять и снимать только сверху - называется стеком, а этот конкретный стек - стеком вызовов. Его глубина ограничена: Python по умолчанию разрешает около тысячи вложенных вызовов и потом падает с RecursionError. Для функции, которая вызывает саму себя, это предел глубины.

Правило

Аргумент не копируется: параметр - второе имя того же значения. Изменение значения (append, присваивание элементу) видно снаружи, переприсваивание параметра - нет. Имя, которому внутри функции что-то присвоено, локально во всей функции. Без return функция возвращает None. Значение по умолчанию вычисляется один раз, при создании функции.

Разбор: изменение против переприсваивания

ssylki.py
def dobavit(spisok, x):
    spisok.append(x)
    spisok = [0]
    spisok.append(x)


a = [1]
dobavit(a, 2)
print(a)


def uvelichit(n):
    n = n + 1


k = 5
uvelichit(k)
print(k)
↳  выводpython3 ssylki.py
[1, 2]
5

Первый append менял тот самый список, который снаружи зовётся a. Строка spisok = [0] переклеила локальное имя на новый список, и второй append ушёл в него - он исчез вместе с концом вызова. С числом n = n + 1 происходит ровно то же: создаётся новое число, локальное имя переклеивается на него, а k снаружи не тронут. Поэтому функция, которая должна вернуть результат, его возвращает, а не надеется изменить аргумент.

Разбор: значение по умолчанию, которое помнит прошлое

umolchanie.py
def zapisat(x, zhurnal=[]):
    zhurnal.append(x)
    return zhurnal


print(zapisat(1))
print(zapisat(2))


def zapisat2(x, zhurnal=None):
    if zhurnal is None:
        zhurnal = []
    zhurnal.append(x)
    return zhurnal


print(zapisat2(1))
print(zapisat2(2))
↳  выводpython3 umolchanie.py
[1]
[1, 2]
[1]
[2]

Пустой список [] в заголовке создаётся один раз, когда Python читает def, и дальше каждый вызов без второго аргумента получает ТОТ ЖЕ список. Второй вызов видит запись первого. Лечится так, как во второй функции: по умолчанию None, а новый список создаётся внутри тела, на каждом вызове свой. is None спрашивает «это то самое значение None?», а не «равно ли»: так None и проверяют.

Что ломается без этого

none.py
def kvadrat(x):
    x * x


print(kvadrat(3))
a = [3, 1, 2]
b = a.sort()
print(a, b)
print(sorted([3, 1, 2]))

schetchik = 0


def tik():
    schetchik = schetchik + 1


try:
    tik()
except UnboundLocalError as e:
    print("ошибка:", type(e).__name__)
↳  выводpython3 none.py
None
[1, 2, 3] None
[1, 2, 3]
ошибка: UnboundLocalError

Забытый return даёт None, и ошибка всплывает далеко от места, где её сделали. a.sort() сортирует список на месте и возвращает None, поэтому a = a.sort() теряет список целиком; новый отсортированный список возвращает sorted(a). Последняя функция падает, потому что присваивание schetchik = ... делает имя локальным во ВСЕЙ функции, и справа читается локальная переменная, у которой ещё нет значения. type(e).__name__ - имя типа ошибки, его и печатает последняя строка. Внешнее имя меняют, написав первой строкой тела функции global schetchik, но обычно правильнее вернуть новое значение.

Зачем это в работе

Функция обработки заказа добавила служебное поле в словарь-аргумент, и через три модуля отчёт показывает поле, которого никто не заказывал. Обработчик запросов с def handle(req, cache={}) годами работает как кеш на весь процесс и однажды отдаёт одному пользователю данные другого. Рекурсивный обход дерева каталогов падает на глубине в тысячу. Все три истории - это правила из этого урока, а не редкие особенности языка.

Теперь сам

Что напечатает программа?

def obrabotat(a):
    a[0] = 100
    a = a + [5]
    return len(a)


spisok = [1, 2]
n = obrabotat(spisok)
print(spisok, n)

Ответ. [100, 2] 3. a[0] = 100 меняет сам список, общий с spisok, - это видно снаружи. a = a + [5] создаёт НОВЫЙ список из трёх элементов и переклеивает на него локальное имя; spisok остаётся из двух. Длина нового списка, 3, возвращается через return.

Главное

Параметр функции - второе имя того же значения, а не копия: изменение значения видно вызывающему, переприсваивание имени - нет. Всё, что функция должна отдать, она отдаёт через return, а каждый вызов живёт в своём наборе локальных имён на стеке вызовов.

Комментарии

Пока нет комментариев. Будь первым!

Оставить комментарий

Комментарий появится после проверки. Email не публикуется. Войти, чтобы не вводить имя каждый раз.