Программирование на языке Python для сбора и анализа данных

Текст лекции: Щуров И.В., НИУ ВШЭ

Данный notebook является конспектом лекции по курсу «Программирование на языке Python для сбора и анализа данных» (НИУ ВШЭ, 2015-16). Он распространяется на условиях лицензии Creative Commons Attribution-Share Alike 4.0. При использовании обязательно упоминание автора курса и аффилиации. При наличии технической возможности необходимо также указать активную гиперссылку на страницу курса. Фрагменты кода, включенные в этот notebook, публикуются как общественное достояние.

Другие материалы курса, включая конспекты и видеозаписи лекций, а также наборы задач, можно найти на странице курса.

Лекция №4: Функции

Пример определения функции

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

In [1]:
from math import sqrt
sqrt(25)+sqrt(9)
Out[1]:
8.0

Что означает этот код? В первой строчке мы импортировали функцию sqrt из пакета math, то есть загрузили в память (или «выписали на бумажку») инструкцию о том, как считать квадратные корни (её написали раньше умные люди, разработчики Python). Эта инструкция — такая маленькая программка (когда-то давно функций в современном понимании не было, но были подпрограммы).

Во второй строчке необходимо вычислить сумму двух выражений, каждое из которых, в свою очередь, содержит вызов функции sqrt. В тот момент, когда мы написали sqrt(25), Python посмотрел на бумажку и пользуясь этой маленькой программкой посчитал корень из того числа, которое мы этой функции передали (то есть из 25). После этого функция «вернула» значение 5.0. Это означает, что в строчке sqrt(25)+sqrt(9) фрагмент sqrt(25) превратился в результат выполнения функции, то есть в 5.0. Затем то же самое произошло с sqrt(9), этот фрагмент превратился в число 3.0. Затем мы сложили два числа и получили число 8.0.

Функции в программировании похожи на функции в математике, хотя имеют и свою специфику. Давайте напишем какую-нибудь функцию. Для примера рассмотрим факториал. Напомним, что факториал от натурального числа $n$ это произведение всех натуральных чисел (начиная с 1) до $n$.

$$\mathop{\mathrm{factorial}}(n)=1\times 2\times \ldots \times n$$

Обычно в математике факториал обозначается восклицательным знаком (например, $3!=1\times 2\times 3=6$).

Давайте для начала напишем программу для вычисления факториала какого-нибудь числа. Она будет выглядеть так:

In [1]:
n = 5

f = 1
for i in range(2, n+1):
    f = f * i
    # эквивалентный синтаксис: f *= i
print(f)
120

Здесь инициализируется переменная f, в ней помещается значение 1, затем переменная f последовательно умножается на все числа от 2 до n включительно (нам пришлось прибавить единицу при вызове range(), потому что правый конец никогда не включается в интервал, см. конспект лекции №2 про циклы и range()), каждый раз записывая результат умножения снова в f. Таким образом в f оказалось искомое произведение.

Если бы нам пришлось вычислять факториалы в разных частях программы, то можно было бы просто скопировать в них этот короткий фрагмент кода. Однако, так почти никогда делать не следует: если вам приходится копировать какие-то строчки кода в своей программе, почти наверняка это значит, что вы делаете что-то не то. (В программировании это называется «принципом DRY» — Don't Repeat Yourself.) Например, представьте себе, что вы скопируете этот код в десять мест программы, а потом придумаете, как сделать вычисление факториала более эффективным. Вам придётся тогда вносить изменение в десять разных мест!

В данном случае нам нужно написать функцию, вычисляющую факториал. Она выглядит так:

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

При выполнении этой ячейки вроде бы ничего не произошло — по крайней мере, Python не выдал никакого вывода. Так и должно быть. На самом деле, в этот момент Python достал большую чёрную записную книжку на странице на букву f (это метафора, на самом деле нет никаких страниц) и записал в неё: «если меня попросят выполнить функцию factorial, нужно сделать вот такие действия». Теперь мы можем вызывать функцию factorial, передавая ей параметр.

In [4]:
factorial(6)
Out[4]:
720

И даже использовать её в более сложных выражениях:

In [6]:
factorial(5)+factorial(6)
Out[6]:
840

Посмотрим более внимательно на то, что происходит, когда Python вычисляет значение выражения factorial(6). В первую очередь он открывает свою записную книжку и ищет там функцию factorial. Находит (поскольку раньше мы её туда записали). Дальше он смотрит на первую строчку определения функции (это так называемая сигнатура):

def factorial(n):

Здесь он видит, что функция factorial() имеет аргумент, который называется n. Python помнит, что мы вызвали factorial(6), то есть значение аргумента должно быть равно 6. Таким образом, дальше он выполняет строчку (которую мы не писали)

n = 6

После чего выполняет остальные строчки из тела функции:

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

Наконец он доходит до строчки

return f

В этот момент переменная f имеет значение 24. Слово return означает, что Python должен вернуться к строчке, в которой был вызов factorial(6), и заменить там factorial(6) на 24 (то, что написано после return). На этом вызов функции завершён.

Возвращаемые значения и побочные эффекты

Давайте а нарисую такую схему:

Схема работы функции

Функция — это такая машинка, которая что-то получает на вход (аргументы), что-то выдаёт на выход (возвращаемое значение), а ещё может попутно что-то сделать с окружающей реальностью (побочные эффекты). Например, функция может что-то напечатать на экране, записать в файл или отправить e-mail. Это всё будут побочные эффекты.

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

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

In [13]:
def hello(name):
    return "Hello, "+name+"!"
In [14]:
s = hello("World")
In [15]:
s
Out[15]:
'Hello, World!'

В переменной s сейчас — результат выполнения функции hello(), которой на вход передали аргумент name, равный "World".

А теперь давайте напишем другую функцию, которая не возвращает строчку, а печатает её.

In [16]:
def say_hello(name):
    print("Hello, "+name+"!")

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

In [17]:
s = say_hello("Harry")
Hello, Harry!

Обратите внимание: теперь при выполнении s = say_hello("Harry") строчка выводится на печать. Это и есть побочный эффект выполнения функции say_hello. Что лежит в переменной s?

In [18]:
s
In [19]:
print(s)
None

В ней лежит ничего. Это специальный объект None, который испоьзуется, когда какое-то значение нужно присвоить переменной, но никакого значения нет. В данном случае его нет, потому что функция ничего не вернула. Возвращаемого значения у say_hello() нет. Так бывает.

Более сложные ситуации с функциями

Функции могут вызывать другие функции. Например, вместо того, чтобы копировать строчку "Hello, "+name+"!" из функции hello() в функцию say_hello(), просто вызовем hello() из say_hello().

In [18]:
def new_say_hello(name):
    print(hello(name))
In [19]:
new_say_hello("Harry")
Hello, Harry!

Современные программы обычно так и выглядят — это набор из множества функций, каждая из которых вызывает другие функции. В обычной жизни происходит примерно то же самое: мы разбиваем задачу, которую нужно решить, на более простых задач, которые нужно решить последовательно, потом каждую из этих задач разбиваем на ещё более простые и так далее. Например, чтобы решить задачу «доехать до университета», нужно решить задачи «выйти из дома», «дойти до остановки общественного транспорта», «сесть на общественный транспорт», «доехать до нужной остановки» и т.д. Чтобы решить задачу «выйти из дома», нужно решить задачу «встать с кровати», «позавтракать», «одеться», «открыть дверь» и т.д. В программистских терминах мы бы написали функцию go_to_university(), которая бы в какой-то момент вызывала функции exit_from_home(), go_to_stop(), get_transport() и т.д., а функция exit_from_home() вызывала бы wake_up(), breakfast() и т.д.

Например, давайте напишем функцию, которая вычисляет биномиальные коэффициенты. Напомним, что биномиальным коэффициентом $C_n^k$ (читается «це из эн по ка») называется число, показывающее, сколькими способами можно выбрать $k$ объектов из $n$. Великая наука комбинаторика учит нас, что это число может быть вычислено следующим образом:

$$ C_n^k=\frac{n!}{k!(n-k)!} $$

Здесь восклительными знаками обозначаются факториалы. Напишем функцию, которая вычисляет биномиальный коэффициент. Используем для этого написанную ранее функцию factorial.

In [20]:
def binom(k, n):
    """
    calculates binomial coeffs: k from n
    k, n are integers
    retuns C_n^k
    """
    return factorial(n)//(factorial(k)*factorial(n-k))
    # можно использовать целочисленное деление, поскольку результат гарантированно целый

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

In [22]:
binom(2,3) 
Out[22]:
3

В тройных кавычках сразу после сигнатуры функции обычно приводится её описание (так называемая docstring). Это комментарий для людей, которые будут использовать вашу функцию в будущем. Даже если это будете вы сами, скорее всего, через месяц вы уже забудете, что именно делает эта функция, какие аргументы принимает и какие значения возвращает. Написать всё это сразу бывает очень полезно. Чтобы посмотреть на эту справку, можно набрать название вашей функции, открывающую скобку и нажать Shift+Tab+Tab.

После выполнения строчки return выполнение функции прекращается. Давайте рассмотрим ещё один пример: вычислим модуль некоторого числа. Код для этой функции может иметь такой вид (я назвал её my_abs(), потому что в Python есть встроенная функция abs, которая решает ту же задачу):

In [22]:
def my_abs(x):
    if x > 0:
        return x
    else:
        return -x
In [23]:
my_abs(-5)
Out[23]:
5

Это самое простое решение: если число положительное, то возвращается оно само, а если отрицательное, то возвращается оно с обратным знаком (-x). Подробнее об условном операторе if написано в конспекте лекции №3.

Можно было бы написать эту функцию и таким образом:

In [24]:
def my_abs(x):
    print("New my_abs")
    # если функция с таким названием уже была, то Python про неё забудет и запишет вместо этого новую функция
    # чтобы убедиться в этом я поставил здесь этот print
    if x > 0:
        return x
    return -x
In [25]:
my_abs(-6)
New my_abs
Out[25]:
6

Здесь происходит следующее: если число положительное, то срабатывает return x и после этого выполнение функции прекращается, до строчки return -x дело не доходит. А если число отрицательное, то наборот, срабатывает только строчка return -x (из-за оператора if).

In [32]:
%load_ext tutormagic
The tutormagic extension is already loaded. To reload it, use:
  %reload_ext tutormagic
The tutormagic extension is already loaded. To reload it, use:
  %reload_ext tutormagic

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

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

In [41]:
f = 10

def factorial(n):
    f = 1
    for i in range(2, n+1):
        f = f * i
    print("In the function, f =", f)
    return f

f = 10
print(f)
print(factorial(8))
print("Out of function")
print(f)
10
In the function, f = 40320
40320
Out of function
10

Тот же код, запущенный в визуализаторе:

In [40]:
%%tutor lang='python3'
f = 10

def factorial(n):
    f = 1
    for i in range(2, n+1):
        f = f * i
    print("In the function, f =", f)
    return f

f = 10
print(f)
print(factorial(8))
print("Out of function")
print(f)

Как видно из результата выполнения этого кода, переменная f в основной программе и переменная f внутри функции — это совсем разные переменные (визуализатор pythontutor рисует их в разных фреймах). От того, что мы как-то меняем f внутри функции, значение переменной f вне её не поменялось, и наоборот. Это очень удобно: если бы функция меняла значение «внешней» переменной, то она могла бы сделать это случайно и это привело бы к непредсказуемым последствиям.

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

In [36]:
def hello_i18n(name, lang):
    if lang == 'ru':
        print("Привет,",name)
    else:
        print("Hello,",name)
In [37]:
hello_i18n("Ivan", 'ru')
Привет, Ivan
In [38]:
hello_i18n("Ivan", 'en')
Hello, Ivan

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

In [39]:
def hello_i18n(name):
    if lang == 'ru':
        print("Привет,",name)
    else:
        print("Hello,",name)
        
lang = 'ru'
print("Hello world")
hello_i18n('Ivan')

lang = 'en'
hello_i18n('John')
Hello world
Привет, Ivan
Hello, John

Как видите, сейчс поведение функции зависит от того, чему равняется переменная lang, определенная вне функции. Может быть и в функции factorial() можно было обратиться к переменной f до того момента, как мы положили в неё число 1? Давайте попробуем:

In [43]:
def factorial(n):
    print("In the function, before assignment, f =", f)
    f = 1
    for i in range(2, n+1):
        f = f * i
    print("In the function, f =", f)
    return f
In [44]:
factorial(2)
---------------------------------------------------------------------------
UnboundLocalError                         Traceback (most recent call last)
<ipython-input-44-21ad3053ce9b> in <module>()
----> 1 factorial(2)

<ipython-input-43-d4a84f87351c> in factorial(n)
      1 def factorial(n):
----> 2     print("In the function, before assignment, f =", f)
      3     f = 1
      4     for i in range(2, n+1):
      5         f = f * i

UnboundLocalError: local variable 'f' referenced before assignment

В этом случае Python выдаёт ошибку: локальная переменная f использовалась до присвоения значения. В чём разница между этим кодом и предыдущим?

Оказывается, Python очень умный: прежде, чем выполнить функцию, он анализирует её код и определяет, какая из переменных является локальной, а какая глобальной. В качестве глобальных переменных по умолчанию используются те, которые не меняются в теле функции (то есть такие, к которым не применяются операторы типа приравнивания или +=). Иными словами, по умолчанию глобальные переменные доступны только для чтения, но не для модификации изнутри функции.

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

In [77]:
def set_lang():
    useRussian = input("Would you like to speak Russian (Y/N): ")
    if useRussian == 'Y':
        lang = 'ru'
    else:
        lang = 'en'
In [78]:
lang = 'en'
print(lang)
set_lang()
print(lang)
en
Would you like to speak Russian (Y/N): Y
en

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

In [79]:
def set_lang():
    global lang
    useRussian = input("Would you like to speak Russian (Y/N): ")
    if useRussian == 'Y':
        lang = 'ru'
    else:
        lang = 'en'
In [80]:
lang = 'en'
print(lang)
set_lang()
print(lang)
en
Would you like to speak Russian (Y/N): Y
ru

Теперь всё работает!

Передача аргументов

Есть разные способы передавать аргументы функции. С одним из них мы уже знакомы:

In [81]:
def hello(name, title):
    print("Hello", title, name)
In [82]:
hello("Potter", "Mr.")
Hello Mr. Potter

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

In [83]:
hello("Harry")
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
<ipython-input-83-dd83cc2cf4d3> in <module>()
----> 1 hello("Harry")

TypeError: hello() missing 1 required positional argument: 'title'

Это неудивительно: мы сказали, что функция hello() должна использовать аргумент title, но не передали его — какое же значение тогда использовать? Для преодоления этой трудности используютя значения по умолчанию (default values).

In [84]:
def hello(name, title=""):
    print("Hello", title, name)
hello("Harry")
Hello  Harry
In [85]:
hello("Smith", "Mrs.")
Hello Mrs. Smith

Аргументы можно передавать, указывая их имена.

In [86]:
hello("Smith", title = "Mr.")
Hello Mr. Smith
In [87]:
hello(name = "Smith", title= "Mr.")
Hello Mr. Smith

В этом случае порядок будет неважен.

In [89]:
hello(title= "Mr.", name = "Smith")
Hello Mr. Smith

Ещё бывают функции, которые принимают неограниченное число аргументов. Например, так ведёт себя функция print().

In [61]:
print(8, 7, 5, 'hello', 8)
8 7 5 hello 8

Как она устроена? Примерно вот так:

In [75]:
def my_print(*args):
    for x in args:
        print(x)
In [78]:
my_print(6, 8, 9, 'hello', 88, 55)
6
8
9
hello
88
55

Обратите внимание на звёздочку перед args в сигнатуре функции. Давайте посмотрим повнимательнее, как работает этот код:

In [91]:
def test(*args):
    print(args)
test(1,2,3, 'hello')
(1, 2, 3, 'hello')

Оказывается, что в args теперь лежит так называемые кортеж, состоящий из элементов, которые мы передали функции.

Отступление: кортежи

Кортеж (tuple) — это почти то же самое, что и список, только его элементы неизменяемы. Обозначается он круглыми скобками.

In [92]:
t = (2,3, 5, 1)
print(t[1])
print(t[0:2])
3
(2, 3)
In [97]:
for x in t:
    print(x)
2
3
5
1
In [93]:
t[0] = 10
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
<ipython-input-93-eaf187d38884> in <module>()
----> 1 t[0] = 10

TypeError: 'tuple' object does not support item assignment
In [94]:
t.append(1)
---------------------------------------------------------------------------
AttributeError                            Traceback (most recent call last)
<ipython-input-94-2337caf868e4> in <module>()
----> 1 t.append(1)

AttributeError: 'tuple' object has no attribute 'append'

Тот факт, что нельзя изменять кортеж, не означает, что нельзя переопределить переменную t:

In [99]:
t = (8, 1, 2, 3)
In [100]:
t
Out[100]:
(8, 1, 2, 3)

Можно думать о списке как о меловой доске, расчерченной на отдельные ячейки, в которые можно записывать разные значения. Тогда кортеж — это глинянная (или каменная) дощечка, на которой выбиты какие-то значения. Вы не можете изменить значения, которые на ней выбиты, но вы можете разбить старую дощечку и изготовить новую.

Можно также конвертировать списки в кортежи и наоборот.

In [101]:
print( list( (1, 2, 3) ) )
print( tuple( [1, 2, 3] ) )
[1, 2, 3]
(1, 2, 3)

Возвращаясь к функциям

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

In [95]:
def test1(*args):
    print(args)
test1(1, 2, 3)

def test2(args):
    print(args)
test2( (1,2,3) )
(1, 2, 3)
(1, 2, 3)

Можно комбинировать списочные переменные и обычные (при условии, что переменная со звёздочкой только одна):

In [96]:
def my_print(sep, *args):
    for x in args:
        print(x, end = sep)
In [102]:
my_print('----', 7, 8, 9, 'hello')
7----8----9----hello----

На сегодня всё :)