Лекция 6, функции и модули

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

Начнём с простейшей функции, выводящей “Hello, world!”:

def print_hello():
    print('Hello, world!')

Определение функций выполняется с помощью оператора def, за которым через пробел должно идти имя определяемой функции (здесь print_hello), затем в круглых скобках аргументы функции (здесь аргументов нет и скобки пусты). Далее должно идти двоеточие, а все инструкции, относящиеся к функции, должны располагаться на увеличенном уровне отступов. Если происходит возврат к изначальному уровню отступов, на котором располагался оператор def, то инструкции на этом уровне больше не относятся к функции и будут выполнены после того, как определится функция:

def print_hello():
    print('Hello, world!')

print_hello()  # запустим функцию
print_hello()  # запустим функцию ещё раз

Функции могут принимать от вызывающего кода параметры:

def print_hello(name):
    print('Hello, world!')
    print('My name is', name)

print_hello('John')
username = input('Enter your name: ')
print_hello(username)

Такой код сначала поприветствует мир и представится Джоном, а потом попросит вас ввести своё имя и поприветствует мир уже от вашего лица. То есть, при каждом вызове функции параметр name будет получать новое значение, полученное из аргумента в вызове функции. Обратите внимание, что параметром называется имя, указанное в скобках после имени функции и используемое внутри неё, а аргументом – конкретное значение, которое попадает в функцию при вызове.

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

def square(x):
    return x * x

x = 2
x2 = square(x + 3)   # Сначала вычислится выражение x + 3, потом его результат станет
                     # аргументом. Результат функции 25.
y = square(10) + 23   # 123

Пример более сложной функции, вычисляющей факториал:

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

f0 = factorial(0)  # 1
f1 = factorial(1)  # 1
f2 = factorial(2)  # 2

f5 = factorial(5)  # 120

Как видно, параметры функции factorial каждый раз получают новые значения от констант, переменных или результатов выражений, стоящих в аргументе.

Написанная выше функция factorial отлично работает и правильно вычисляет факториалы чисел 0 и 1 (при этом цикл, выполняющий умножение, не выполнится ни разу, в переменной f останется значение 1, которое и будет возвращено как результат функции). Но давайте явно обработаем этот случай, чтобы увидеть, что return может находится не только в конце функции. Если же он, как в примере ниже, находится в середине функции, то при выполнении он сразу завершит всю функцию и вернёт вызывающему коду результат работы. Здесь это используется, чтобы сразу возвращать значение, если n равно 0 или 1.

def factorial(n):
    if n == 0 or n == 1:
        return 1  # выйти из функции с результатом 1
    f = 1
    for i in range(2, n + 1):
        f *= i
    return f

Подробнее об аргументах и возвращаемом значении

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

def add(x, y):
    return x + y

print(add(1, 2))  # 3, сумма чисел
print(add('abc', 'def'))  # abcdef, соединение строк

Функции, которые ничего не возвращают, либо используют оператор return без параметра просто для завершения работы, автоматически возвращают объект None:

def print_hello():
    print('Hello, world')

x = print_hello()  # выведет 'Hello, world!'
print(x)  # None

Так что не удивляйтесь, если увидите такой объект, хотя не использовали его. Скорее всего, вы просто забыли написать return или параметр после return.

В языке Python параметр – это ссылка на аргумент. Рассмотрим следующий код:

def change_list(L):
    L.append(123)

def no_change_in_list(L):
    L = []

L = ['a', 'b']
change_list(L)
print(L)  # ['a', 'b', 123]
no_change_in_list(L)
print(L)  # ['a', 'b', 123]

Вызывает интерес, почему функция change_list смогла изменить аргумент, а функция no_change_in_list не смогла, хотя они обе изменяли свой параметр. Ответ кроется в том, что change_list изменяла содержимое того объекта, на который ссылался параметр, и при завершении функции аргумент ссылался на уже изменённый объект.

_images/params_are_refs_1.png

А функция no_change_in_list просто поменяла ссылку в параметре, при этом аргумент продолжил ссылаться на тот же объект, что и раньше.

_images/params_are_refs_2.png

Если указать в операторе return несколько значений через запятую, их можно распаковать в несколько значений при вызове функции:

def indexmax(L):
    index = 0
    maximum = L[0]
    for i in range(1, len(L)):
        if L[i] > maximum:
            index = i
            maximum = L[i]
    return index, maximum

L = [10, 200, 30]
ind, m = indexmax(L)  # ind = 1  (индекс максимума)
                      # m = 200  (значение максимума)

Области видимости переменных

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

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

  1. Если мы считываем значение переменной, сначала переменная с таким именем ищется в области видимости функции, затем в области видимости файла (модуля).

  2. Переменные в области видимости функции (их ещё называют локальными переменными) создаются присваиванием им начального значения. Даже если уже есть глобальная переменная с таким именем, будет создана локальная переменная (кроме случая из следующего пункта).

    При этом, переменная считается локальной, если есть хотя бы одно присваивание переменной с таким именем в любом месте функции. То есть, если есть такое присваивание, но переменная ещё не была определения, вы получите ошибку Undefined Name (неопределённая переменная), даже если есть глобальная переменная с таким именем.

    a = 10
    b = 20
    
    def f1():
        print(a, b)
    
    
    def f2():
        a = 'hello'
        print(a, b)
    
    f1()    # выведет 10 20
    f2()    # выведет hello 20
    print(a, b)    # выведет 10 20
    

    Обратите внимание, что в этом примере присваивание значения 'hello' локальной переменной функции f2 не изменило глобальную переменную a в области видимости модуля.

  3. Если в функции есть конструкция global x, то переменная с именем x будет считаться в данной функции глобальной и при присвоении ей нового значения будет меняться переменная во области видимости модуля.

    c = 30
    d = 40
    def f3():
        global c
        c = 'world'
        print(c, d)
    prinnt(c, d)  # 30 40
    f3()          # world 40
    prinnt(c, d)  # world 40
    

    В этом примере функция f3 присваивает новое значение глобальной переменной c.

Модули

В языке Python существует простая и мощная система модулей. Каждый файл с расширением .py является модулем, и нет никакого разделения на два типа файлов (заголовочные файлы и файлы с исходным кодом), как в языках C и C++. Каждый модуль при запуске программы имеет возможность определить, запущен он как главным модуль программы (и выполнить в этом случае какие-то особые действия) или просто импортирован из другого модуля.

Небольшой пример. Напишем два модуля: один будет содержать только функцию факториала, другой только функцию экспоненты, \(e^x\) Для вычисления экспоненты будет применяться функция факториала из первого модуля.

\[e^x = \sum_{n=0}^{\infty}\frac{x^n}{n!} \approx \sum_{n=0}^{N_{max}}\frac{x^n}{n!}\]

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

Начнём с модуля, содержащего функцию для вычисления факториала.

# Файл fact.py

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

def main():
    n = int(input("n: "))
    print(n, "! = ", factorial(n), sep='')

if __name__ == "__main__":
    main()

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

Давайте подробнее рассмотрим оператор if, расположенный в конце модуля. Если рассказать о нём совсем кратко, то логическое условие __name__ == "__main__" выполняется, если модуль запущен как основная программа, и не выполняется, если основным модулем является другой, а данный модуль просто импортирован из него.

Как вы помните, у каждого модуля своё пространство имён, и поэтому переменная __name__ может иметь в различных модулях различные значения. У интерпретатора Python есть следующее правило: при старте программы в главном модуле создаётся переменная с именем __name__ и значением '__main__'. Во всех остальных модулях, которые импортируются из главного, интерпретатор поместит в переменную __name__ каждого модуля его имя. То есть в модуле math в переменной __name__ будет лежать строка "math". Это даже можно проверить с помощью кода

import math
print(math.__name__)

Таким образом, логическое выражение __name__ == "__main__" истинно, только если данный модуль запущен как главный. В этом случае, в нём выполнится функция main, как мы того и хотим, а при импорте его из других модулей main выполняться не будет, а мы просто будем получать доступ к функции factorial. Давайте воспользуемся ей из модуля вычисления экспоненты:

# Файл exp.py

import fact

def exp(x, n):
    s = 0
    for i in range(n + 1):
        s += x ** i / fact.factorial(i)
    return s

def main():
    x = float(input("x: "))
    n = int(input("n: "))
    print("exp = ", exp(x, n))

if __name__ == "__main__":
    main()

Во время запуска exp.py как главного модуля будет выполняться инструкция import fact. Но функция fact.main не будет выполнена, потому что fact.__name__ содержит строку "fact". Поэтому будет выполнена только функция main из модуля exp.

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