# Содержание

Содержание;
Нормальные имена переменных;
Функции в языке Python;
Возвращаем из функции и принимаем снаружи наборы чисел;
Как что выглядит и как называется;
Ретурнобулофобия;
Кладём результат проверки в переменную;
Локальные и глобальные переменные;
Дополнительная информация со звёздочкой;
nonlocal-переменные;
Полный список объявленных переменных;
Лямбда-функции;
Замыкание;

# Нормальные имена переменных

Умение давать хорошие имена переменным гораздо более важно, чем обычно кажется учащимся. Кроме того, это гораздо менее тривиально, чем кажется! Этому правда нужно учиться! Посмотрите, как выглядит реальный код: раз, два, три.

Итак, имена переменных, функций, типов должны быть осмысленными и отражать назначение. Слишком длинные имена, однако, не рекомендуются. В именах допускаются сокращения, которые не затрудняют понимание имени и назначения функций и переменных.

Допускается использование односложных имен переменных для счётчиков (i, j, k) в циклах for.

В именах должны использоваться только английские слова. Транслитерации русских слов не допускаются.

Если имя переменной или функции состоит из нескольких слов, то они должны разделяться символом подчеркивания. Например:
calc_sqrt – божественно
calculate_square_root – допустимо, но длинновато
vychislenie_kornya – недопустимо (используются русские имена)
f – недопустимо (непонятно назначение функции)

Обычно в качестве имён функций и переменных используются одно или несколько слов (или их сокращений) маленькими буквами с разделением символом подчёркивания. Например, factorial, calc_factorial(n), calc_fact(n), iter, find_next_value(), num_apples, apple_num и т.д. Для перевода слов можно использовать deepl.

Обычно имена переменных для чисел — это какое-то существительное или пара существительных: total, tot_sum, cur_elem, iter, prev_elem, next_elem, item_sq, num_apples, amount, tot_amt, seq_len, str_len, average, fib1, fib2, result, floor_num, hall_num, area, numerator, denominator, denom, quot, ratio.

Если переменная указывает на список, кортеж, словарь, множество или другую коллекцию, то используют множественное число: pupils, edges и т.д.

Хорошие имена для функций часто одного из трёх типов:

Имена констант записываются полностью заглавными буквами. Если имя константы состоит из нескольких слов, для их разделения используются подчеркивания. Например, EPSILON, MAX_SIZE.

# Функции в языке Python

Ранее была задача вычисления числа сочетаний из n элементов по k, для чего необходимо вычисление факториалов трех величин: n, k и n-k. Для этого можно сделать три цикла, что приводит к увеличению размера программы за счет трехкратного повторения похожего кода. Вместо этого лучше сделать одну функцию, вычисляющую факториал любого данного числа n и трижды использовать эту функцию в своей программе. Соответствующая функция может выглядеть так:

def factorial(n):
    f = 1
    for i in range(2, n + 1):
        f *= i
    return f

Этот текст должен идти в начале программы, вернее, до того места, где мы захотим воспользоваться функцией factorial. Первая строчка этого примера является описанием нашей функции. factorial — идентификатор, то есть имя нашей функции. После идентификатора в круглых скобках идет список параметров, которые получает наша функция. Список состоит из перечисленных через запятую идентификаторов параметров. В нашем случае список состоит из одной величины n. В конце строки ставится двоеточие.

Далее идет тело функции, оформленное в виде блока, то есть с отступом. Внутри функции вычисляется значение факториала числа n и оно сохраняется в переменной f. Функция завершается инструкцией return f, которая завершает работу функции и возвращает значение переменной f. Инструкция return может встречаться в произвольном месте функции, ее исполнение завершает работу функции и возвращает указанное значение в место вызова. Если функция не возвращает значения, то инструкция return используется без возвращаемого значения, также в функциях, не возвращающих значения, инструкция return может отсутствовать.

Теперь мы можем использовать нашу функцию несколько раз. В этом примере мы трижды вызываем функцию factorial для вычисления трех факториалов: factorial(n), factorial(k), factorial(n-k).

n = int(input())
k = int(input())
print(factorial(n) // (factorial(k) * factorial(n - k)))

Мы также можем, например, объявить функцию binomial, которая принимает два целочисленных параметра n и k и вычисляет число сочетаний из n по k:

def binomial(n, k):
    return factorial(n) // (factorial(k) * factorial(n - k))

Тогда в нашей основной программе мы можем вызвать функцию binomial для нахождения числа сочетаний:

print(binomial(n, k))

Вернемся к задаче нахождения наибольшего из двух или трех чисел. Функцию нахождения максимума из двух чисел можно написать так:

def max(a, b):
    if a > b:
        return a
    else:
        return b

Теперь мы можем реализовать функцию max3, находящую максимум трех чисел:

def max3(a, b, c):
    return max(max(a, b), c)

Функция max3 дважды вызывает функцию max для двух чисел: сначала, чтобы найти максимум из a и b, потом чтобы найти максимум из этой величины и c.

# Возвращаем из функции и принимаем снаружи наборы чисел

Если нужно, чтобы функция вернула не одно значение, а два или более, то для этого функция может вернуть кортеж из двух или нескольких значений:

return (a, b)

Тогда результат вызова функции тоже нужно присваивать кортежу:

(n, m) = f(a, b)

При этом можно не писать скобки у кортежей:

return a, b

n, m = f(a, b)

# Как что выглядит и как называется

Обратите внимание, не в каждой функции должен быть вывод на экран, ввод с клавиаутры или возвращаемое значение. Читайте условие, обычно в каждой функции есть что-то одно из этого!

# Ретурнобулофобия

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

def f():
    ...
    if expression:
       return True
    else:
       return False

Лечение: использование следующей конструкции:

def f():
    ...
    return expression

Другие примеры проявления этого недуга:

if expression:
   return False
else:
   return True
#
if expression:
   var = True
else:
   var = False

# Кладём результат проверки в переменную

Напомним, что логические выражения в духе 1 < x <= 10 or x > 20 можно «класть» в переменные, чтобы использовать потом. Часто это позволяет сделать код куда более «контролируемым» и читаемым. Пример:

x_between_or_greater = 1 < x <= 10 or x > 20
if (x_between_or_greater and y > 0) or (not x_between_or_greater and y < 0):
    print('В реальных примерах условия сложнее')

Кстати, скобки в условиях можно было не ставить, у and выше приоритет.

# Локальные и глобальные переменные

Внутри функции можно использовать переменные, объявленные вне этой функции

def f():
    print(a)
a = 1
f()

Здесь переменной a присваивается значение 1, и функция f печатает это значение, несмотря на то, что выше функции f эта переменная не инициализируется. Но в момент вызова функции f переменной a уже присвоено значение, поэтому функция f может вывести его на экран.

Такие переменные (объявленные вне функции, но доступные внутри функции) называются глобальными.

Но если инициализировать какую-то переменную внутри функции, использовать эту переменную вне функции не удастся. Например:

def f():
    a = 1
f()
print(a)

Получим NameError: name 'a' is not defined. Такие переменные, объявленные внутри функции, называются локальными. Эти переменные становятся недоступными после выхода из функции.

Интересным получится результат, если попробовать изменить значение глобальной переменной внутри функции:

def f():
    a = 1
    print(a)
a = 0
f()
print(a)

Будут выведены числа 1 и 0. То есть несмотря на то, что значение переменной a изменилось внутри функции, то вне функции оно осталось прежним! Это сделано в целях «защиты» глобальных переменных от случайного изменения из функции (например, если функция будет вызвана из цикла по переменной i, а в этой функции будет использована переменная i также для организации цикла, то эти переменные должны быть различными). То есть если внутри функции модифицируется значение некоторой переменной, то переменная с таким именем становится локальной переменной, и ее модификация не приведет к изменению глобальной переменной с таким же именем.

Более формально: интерпретатор Питон считает переменную локальной, если внутри нее есть хотя бы одна инструкция, модифицирующая значение переменной (это может быть оператор =, += и т.д., или использование этой переменной в качестве параметра цикла for, то эта переменная считается локальной и не может быть использована до инициализации. При этом даже если инструкция, модицифицирующая переменную никогда не будет выполнена: интерпретатор это проверить не может, и переменная все равно считается локальной. Пример:

def f():
    print(a)
    if False:
        a = 0
a = 1
f()

Возникает ошибка: UnboundLocalError: local variable 'a' referenced before assignment. А именно, в функции f идентификатор a становится локальной переменной, т.к. в функции есть команда, модифицирующая переменную a, пусть даже никогда и не выполняющийся (но интерпретатор не может это отследить). Поэтому вывод переменной a приводит к обращению к неинициализированной локальной переменной.

Чтобы функция могла изменить значение глобальной переменной, необходимо объявить эту переменную внутри функции, как глобальную, при помощи ключевого слова global:

def f():
    global a
    a = 1
    print(a)
a = 0
f()
print(a)

В этом примере на экран будет выведено 1 1, так как переменная a объявлена, как глобальная, и ее изменение внутри функции приводит к тому, что и вне функции переменная будет доступна.

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

# Дополнительная информация со звёздочкой

# nonlocal-переменные

Кроме глобальных переменных бывают nonlocal-переменные. Они очень похожи на глобальные. При ссылке переменную, которая не определена в текущей функции, будет использована "ближайшая" из локальных переменных в объемлющих функциях, либо в глобальной области видимости. Если же переменную пометить как nonlocal, то поиск переменной не будет производиться за пределами инструкций def ни в глобальной области видимости объемлющего модуля, ни во встроенной области видимости, даже если переменные с такими именами там существуют. Переменные, помеченные как nonlocal, можно менять внутри функции, при этом будет изменяться её значение в соответствующей объемлющей функции. В отличие от global декларация nonlocal не позволяет создать переменную во внешней области видимости.

def tester():
    state = 1
    print(state)
    def nested():
        state = 2
        print(state)
    nested()

    print(state)
tester()
>>> 1
>>> 2
>>> 1
def tester():
    state = 1
    print(state)
    def nested():
        nonlocal state
        state = 2
        print(state)
    nested()

    print(state)
tester()
>>> 1
>>> 2
>>> 2

# Полный список объявленных переменных

В питоне есть специальная функция, которые позволяют получить полный список объявленных глобальных переменных: globals(), а также функция, позволяющая получить список локальных переменных: locals(). В частности они позволяют проверить, объявлена ли в данный момент переменная.

def remove_magic(it):
    return [name for name in it if not name.startswith('__')]
a = 123
print('Сначала', remove_magic(globals()))
# Сначала ['remove_magic', 'a']
print('foo определена:', 'foo' in globals())
# foo определена: False
def foo(x, y):
    print('Внутри foo, локальные', remove_magic(locals()))
    # Внутри foo, локальные ['x', 'y']
    z = x + y
    print('Внутри foo, локальные', remove_magic(locals()))
    # Внутри foo, локальные ['x', 'y', 'z']
    return z
print('Потом', remove_magic(globals()))
# Потом ['remove_magic', 'a', 'foo']
print('foo определена:', 'foo' in globals())
# foo определена: True
foo(1, 2)

# Лямбда-функции

В тех случаях, когда у функции нет необязательных параметров, а её тело может быть записано в одну строчку, существует другой способ записи функции:
foo = lambda ([параметры]): ([выражение])
# Синоним
def foo([параметры]):
    return [выражение]

Однако приведённый пример является анти-паттерном. Лямбда функции предназначены для использования там, где функция передаётся в качестве параметра и более нигде не используется.

Вообще, синтаксис лямбд достаточно полон, чтобы в одну строчку можно было реализовать вообще любую программу. Вместе с тернаным оператором условия лямбды позволяют делать совершенно нечитаемые работающие однострочники.

# Числа Фибоначчи
print(list(map(lambda x,f=lambda x,f:(f(x-1,f)+f(x-2,f)) if x>1 else 1:f(x,f), range(20))))
>>> [1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144, 233, 377, 610, 987, 1597, 2584, 4181, 6765]

# Факториал
print((lambda n,f:f(n,1,f))(20,(lambda n,m,f:m if n<=1 else f(n-1,m*n,f))))
>>> 2432902008176640000

# В некоторых случаях даже функции не нужны. Первые простые числа
print([x for x in range(2, 101) if all(x % i for i in range(2, int(x**0.5)+1))])
>>> [2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37, 41, 43, 47, 53, 59, 61, 67, 71, 73, 79, 83, 89, 97]

# Замыкание

Одно из интересных понятий функционального программирования — это замыкания (closure).
В питоне не слишком часто используют замыкания явно, в то время, как, скажем, в javascript без замыканий обойтись практически невозможно.
Смысл замыкания состоит в том, что определение функции "замораживает" окружающий её контекст на момент определения. Это может делаться различными способами, например, за счёт параметризации создания функции. При замыкании будут использованы переменные из объемлющих функций. Для того, чтобы переменная попала в замыкание, она должна быть упомянута в коде функции:
def print_msg(msg):
    def fun():
        print(msg)
    return fun  # Помните, что в питоне всё - это объект. Функция тоже, её можно вернуть и воспользоваться потом.

printer = print_msg("Hello")
printer()
>>>Hello
def multiplier( n ):  # multiplier возвращает функцию умножения на n
    def mul( k ):
        return n * k
    return mul

mul3 = multiplier(3)  # mul3 - функция, умножающая на 3
print(mul3(3), mul3(5))
>>> 9 15

Правильно захваченные переменные при этом можно даже успешно модифицировать:

def tester(start):
    state = start  # В каждом вызове сохраняется свое значение state
    def nested(label):
        nonlocal state      # Объект state находится
        print(label, state) # в объемлющей области видимости
        state += 1 # Изменит значение переменной, объявленной как nonlocal
    return nested

>>> F = tester(0)
>>> F('spam')          # Будет увеличивать значение state при каждом вызове
spam 0
>>> F('ham')
ham 1
>>> F('eggs')
eggs 2

Но вообще тема эта достаточно сложная. В результате неаккуратного использования может быть отстрелено произвольное количество конечностей. Например, нужно иметь в виду, что в замыкание попадает имя переменной, а не её значение.

def gen_adders():
    i = 5
    def add5(x):
        return x + i
    i = 10
    def add10(x):
        return x + i
    i = 15
    return add5, add10

adder1, adder2 = gen_adders()
print(adder1(0))  # -> 15
print(adder2(0))  # -> 15

В данном случае обе функции используют одну и ту же захваченную из функции gen_adders переменную i, что и приводит к такому «неожиданному» поведению.