
Определение функций
Создание функций. Параметры функций. Аннотации типов. Окружение
Штана Альберт Игоревич
Определение функций
С помощью определения собственных функций писать и поддерживать программы проще. Они позволяют объединять составные операции в одну.
Создание функции
Допустим, мы хотим отправить письма на сайте — это достаточно сложный процесс, который включает взаимодействие с внешними системами. Но если определить функцию, вся сложность скроется за одной простой функцией. Представим такой код:
# Место, откуда берется функция
from emails import send
email = 'support@admin.ru'
title = 'Помогите'
body = 'Я написал историю, как я могу получить скидку?'
# Один вызов — много логики внутри
send(email, title, body)
Внутри себя этот вызов выполняет много логики: соединяется с почтовым сервером, формирует правильный запрос на основе заголовка и тела сообщения, а затем все отправляет, не забыв закрыть соединение. Создадим нашу первую функцию. Ее задача — вывести на экран приветствие: Hello, World!
# Определение функции не вызывает и не выполняет код функции
def show_greeting():
# Внутри тела — отступ четыре пробела
text = 'Hello, World!'
print(text)
# Вызов функции
show_greeting() # => Hello, World!
Важным моментом при определении функции является то, что внутри тела функции обязательно должен быть отступ в четыре пробела. В отличие от обычных данных, функции выполняют действия. Поэтому их имена нужно указывать через глаголы: «построить что-то», «нарисовать что-то», «открыть что-то». Описание с отступом, которое находится ниже имени функции, называется телом функции. Внутри тела можно писать любой код. Это как небольшая самостоятельная программа — набор произвольных инструкций. Тело выполняется в тот момент, когда запускается функция. При этом каждый вызов функции запускает тело независимо от других вызовов. Тело функции может быть пустым, тогда внутри него используется ключевое слово pass(используется как "заглушка", чтобы интерпретатор Python не выдавал ошибку, позднее можно будет написать код):
# Минимальное определение функции
# Функция ничего не делает
def noop():
pass
noop()
У понятия «создать функцию» много синонимов: «реализовать», «определить» и даже «заимплементить». Они часто встречаются на практике. Создавая свои функции, вы облегчите сложные операции и сделаете разработку проще.
Возврат значений
Когда мы определяем функцию, она печатает на экране какие-то данные:
def show_greeting():
print('Hello, World!')
Пользы от таких функций немного, так как результатом нельзя воспользоваться внутри программы. Рассмотрим на примере. Возьмем задачу обработки электронной почты. Когда пользователь регистрируется на сайте, он может ввести email любым способом:
- Добавить случайно пробелы в начале или в конце: support@admin.ru_
- Использовать буквы в разных регистрах: SUPPORT@admin.ru Если мы сохраним его в таком виде в базе данных, то пользователь не войдет на сайт. Чтобы этого не произошло, email нужно подготовить к записи в базу: привести его к нижнему регистру и обрезать пробелы по краям строки. Такая задача решается в пару строчек:
def save_email():
# Email приходит из формы
email = ' SuppORT@admin.ru'
# Обрезаем пробельные символы
trimmed_email = email.strip()
prepared_email = trimmed_email.lower()
print(prepared_email)
# Здесь будет запись в базу данных
Этот код стал возможен благодаря тому, что значение вернулось. Методы strip() и lower() ничего не печатают на экране, они возвращают результат своей работы. Поэтому мы можем записать его в переменные. Если бы они печатали на экране, мы бы не могли присвоить результат переменной. Например, так мы не можем сделать с функцией show_greeting():
message = show_greeting()
# в действительности функция print() возвращает None
# None — специальный объект, используемый для представления отсутствия значения
print(message) # => None
Теперь переименуем и изменим функцию show_greeting() так, чтобы она возвращала данные. Для этого выполним возврат вместо печати на экране:
def greeting():
return 'Hello World!'
return — это инструкция. Она берет записанное справа выражение и отдает его тому коду, который вызвал метод. Здесь выполнение функции завершается.
# Теперь мы можем использовать результат работы функции
message = greeting()
print(message) # => Hello World!
# И даже выполнить какие-то действия над результатом
print(message.upper()) # => HELLO WORLD!
Любой код после return не выполняется:
def greeting_with_code_after_return():
return 'Hello World!'
print('Я никогда не выполнюсь')
Даже если функция возвращает данные, это не ограничивает ее в том, что она печатает. Кроме возврата данных мы можем и печатать:
def greeting_with_return_and_printing():
print('Я появлюсь в консоли')
return 'Hello World!'
# И напечатает текст на экране, и вернет значение
message = greeting_with_return_and_printing()
Возвращать можно не только конкретное значение. Так как return работает с выражениями, то справа от него может быть что угодно. Здесь нужно руководствоваться принципами читаемости кода:
def greeting():
message = 'Hello World!'
return message
Здесь мы не возвращаем переменную. Возвращается всегда значение, которое находится в этой переменной. Ниже пример с вычислениями:
def double_four():
# или return 4 + 4
result = 4 + 4
return result
Определить функцию мало. Еще важно, чтобы она была полезна, и результатом можно было воспользоваться. А теперь подумайте, что вернет вызов функции run(), определенной ниже:
# Определение
def run():
return 1
return 2
# Что будет выведено на экран?
print(run())
Параметры функций
Функции могут не только возвращать значения, но и принимать параметры. Напомним, что с параметрами функций мы уже сталкивались:
# Принимает на вход один параметр любого типа
print('я параметр')
# Принимает на вход два строковых параметра
# первый — что ищем, второй — на что меняем
'google'.replace('go', 'mo') # moogle
# Принимает на вход два числовых параметра
# первый — округляемое число, второй — число знаков после запятой, которые нужно оставить
round(10.23456, 3) # 10.235
А теперь представим, что нам нужно реализовать функцию get_last_char(), которая возвращает последний символ в строке, переданной ей на вход как параметр. Вот как будет выглядеть использование этой функции:
# Передача параметров напрямую без переменных
get_last_char("Python") # n
# Передача параметров через переменные
name1 = 'Python'
get_last_char(name1) # n
name2 = 'Go'
get_last_char(name2) # o
Из этого примера можно сделать следующие выводы:
- Нужно определить функцию get_last_char()
- Функция должна принимать на вход один параметр строкового типа
- Функция должна возвращать значение строкового типа
Определяем функцию:
def get_last_char(text):
return text[-1]
В скобках указывается имя переменной text, которая служит параметром. Имя параметра может быть любым. Главное, чтобы оно отражало смысл значения, которое содержится внутри. Например:
def get_last_char(string):
return string[-1]
Значение параметра будет зависеть от вызова этой функции:
# Внутри функции string будет равна 'python'
get_last_char('python') # n
# Внутри функции string будет равна 'code'
get_last_char('code') # e
# Внутри функции string будет равна 'Summer'
# Имя переменной снаружи не связано с именем переменной в определении функции
text = 'Summer'
get_last_char(text) # r
Параметр нужно обязательно указывать. Если вызвать функцию без него, то интерпретатор выдаст ошибку:
get_last_char() # У такого кода нет смысла
TypeError: get_last_char() missing 1 required positional argument: 'string'
Многие функции работают одновременно с несколькими параметрами. Например, чтобы округлить число, нужно указать не только само число, но и количество знаков после запятой:
round(10.23456, 3) # 10.235
То же самое относится и к методам. Они могут требовать на вход любое количество параметров, которое им нужно для работы:
# Первый параметр — что ищем
# Второй параметр — на что меняем
'google'.replace('go', 'do') # doogle
Чтобы создать такие функции и методы, в определении нужно указать необходимое количество параметров через запятую. Еще им нужно дать понятные имена. Ниже пример определения функции replace(), которая заменяет в слове одну часть строки на другую:
def replace(text, from_, to):
# Здесь тело функции
replace('google', 'go', 'do') # doogle
Когда параметров два и более, то практически для всех функций важен порядок передачи этих параметров. Если его поменять, то функция отработает по-другому:
# Ничего не заменилось, так как внутри google нет do
replace('google', 'do', 'go') # google
Необязательные параметры функций
В программировании у многих функций и методов есть параметры, которые редко меняются. В таких случаях этим параметрам задают значения по умолчанию, которые можно поменять по необходимости. С помощью этого сокращается количество одинакового кода. Рассмотрим, как это выглядит на практике. Разберем пример:
# Функция возведения в степень
# Второй параметр имеет значение по умолчанию два
def pow(x, base=2):
return x ** base
# Три во второй степени (двойка задана по умолчанию)
pow(3) # 3 * 3 = 9
# Три в третьей степени
pow(3, 3) # 3 * 3 * 3 = 27
Значение по умолчанию выглядит как обычное присваивание в определении. Оно срабатывает только в том случае, если параметр не передали. Представьте, что вы не привезли с собой в автосервис запчасти для вашего автомобиля. Тогда автомеханик предложит вам поставить те, которые есть у него — в наличии, по умолчанию. Значение по умолчанию может быть даже в том случае, когда параметр один:
def my_print(text='nothing'):
print(text)
my_print() # => "nothing"
my_print("Python") # => "Python"
Параметров со значениями по умолчанию может быть любое количество:
def print_params(a=5, b=10, c=100):
...
У значений по умолчанию есть одно ограничение. Они должны идти в самом конце списка параметров. С точки зрения синтаксиса, невозможно создать функцию, у которой после необязательного параметра идет обязательный:
# Такой код завершится с ошибкой
def print_params(a=5, b=10, c=100, x):
# И такой
def print_params(a=5, b=10, x, c=100):
# Этот код сработает
def print_params(x, a=5, b=10, c=100):
# Этот тоже сработает
def print_params(x, y, a=5, b=10, c=100):
Значения по умолчанию могут быть как у нескольких параметров, так и у одного. И помните, что значения по умолчанию должны быть в самом конце списка параметров. Эти знания помогут сократить количество одинакового кода.
Функции с переменным числом параметров
У некоторых функций есть особенность — они принимают переменное число параметров. И мы говорим не о значениях по умолчанию. Посмотрите на этот пример:
max(1, 10, 3) # 10
В примере выше функция max() находит максимальное значение среди переданных параметров. Чтобы узнать, сколько параметров можно передавать на вход, нужно изучить документацию этой функции. Там мы увидим такую конструкцию:
max(arg1, arg2, *args[, key])
Это значит, что max() принимает на вход два параметра и больше:
max(1, 3, 2, 4, 2) # 4
Если функция найдет несколько параметров с максимальным значением, значит, она вернет самый первый из них.
Именованные аргументы
Разберем, какие параметры существуют, чем они отличаются и в каких случаях их применять.
Какие параметры существуют
Аргументы — это данные, которые передаются в вызов функции. Они бывают двух типов:
Первый тип — позиционные аргументы. Они передаются в том же порядке, в котором определены параметры функции:
# (text, length)
truncate('My Text', 3)
Второй тип — именованные аргументы. Они передаются не просто как значения, а как пары «имя=значение». Поэтому их можно передавать в любом порядке:
# Аргументы переданы в другом порядке
truncate(length=3, text='My Text')
Если внимательно посмотреть на два примера выше, то можно понять, что это две одинаковые функции.
Теперь разберемся, в каких случаях нужно применять эти типы аргументов.
Какие параметры использовать
Выбор типа параметра зависит от того, кто вызывает функцию.
Есть две причины использовать именованные аргументы:
- Они повышают читаемость, так как сразу видно имена.
- Можно не указывать все промежуточные параметры, которые нам сейчас не нужны.
Последнее полезно, если у функции много необязательных параметров. Посмотрим на примере:
def print_params(a=1, b=2, c=None, d=4):
print(a, b, c, d)
# Нужно передать только d, но приходится передавать все
print_params(1, 2, None, 8)
# Именованные аргументы позволяют передавать только d
# Для остальных аргументов используются значения по умолчанию
print_params(d=8)
Именованные аргументы можно передавать одновременно с позиционными. Тогда позиционные должны идти в самом начале:
# Передаем только a (позиционно) и d (как именованный)
print_params(3, d=3)
Итак, мы ограничились только базовыми знаниями, которые помогут вам читать примеры кода с именованными аргументами. В дальнейшем необходимо изучить эту тему подробнее.
Аннотации типов
Аннотации типов — это возможность указать типы параметров и возвращаемого значения у функции в Python. Это не является обязательным требованием языка, но может помочь программистам в дальнейшей разработке, улучшить читаемость кода и повысить его надежность.
Давайте рассмотрим простой пример функции без аннотаций типов:
def concat(first, second):
return first + second
Эта функция конкатенирует две строки в одну. При этом с первого взгляда на код сложно понять, что происходит в нем: какие типы у аргументов, почему функция работает со строками, а не складывает, например, два числа. Если в дальнейшем использовать эту функцию в коде, то может возникнуть необходимость проверять типы аргументов перед передачей их в функцию, что увеличивает объем кода и затрудняет его понимание.
Теперь давайте добавим аннотации типов к функции:
def concat(first: str, second: str) -> str:
return first + second
Здесь указали, что аргументы first и second должны быть строкового типа (str). С помощью стрелки -> указали тип возвращаемого значения функции, оно также будет строковым (-> str). Когда мы будем использовать эту функцию в коде, нам будет проще понять, какие типы аргументов можно передавать и какой тип возвращаемого значения ожидается. Аннотации типов также могут быть использованы для определения типов переменных внутри функции. Например:
def double(n: int) -> int:
result: int = n * 2
return result
В этом примере мы определили тип переменной result как int, используя аннотацию типа. Аннотации типов — это нестрогая проверка типов в Python. Их использование не гарантирует, что функция будет вызвана с аргументами и возвращаемым значением указанных типов. Python остаётся динамически типизированным языком. В нем аннотации типов не влияют на возможность передачи аргументов различных типов и возвращения значений других типов. Тем не менее их использование упрощает чтение и понимание кода и помогает отслеживать ошибки.
Окружение
Поработаем с функцией generate() и переменными result и age. Также разберем, что такое локальная переменная и как она работает. Возьмем для примера следующий код:
age = 18
def generate():
return age + 3
result = generate()
Когда код выполнится, внутри переменной result окажется значение 21. Хоть переменная age — это не аргумент функции generate(), ее все равно видно в теле функции. Так происходит, потому что переменную age определили ранее вызова функции, а интерпретатор Python читает файл сверху вниз. Это правило относится и к другим переменным. Рассмотрим другой пример:
age = 18
def generate():
age = 20
return age + 3
result = generate()
В данном случае результатом будет число 23. Внешнее значение age = 18 не влияет на код функции, потому что в теле функции определили свою переменную age — локальную переменную. Ее не видно за пределами функции. И последний пример:
age = 20
def generate():
age = 18
generate()
result = age
Результат будет 20. Локальная переменная, которую создали внутри функции generate(), не влияет на внешнюю переменную age. Поэтому, когда функцию вызвали, значение внешней age не изменилось и осталось 5.
Попробуйте сами запустить код в окне ниже с интерпретатором Python и повторите примеры из статьи чтобы самим увидеть и понять как всё это работает. Для этого в ячейке с кодом нажмите клавиши на клавиатуре Shift+Enter или запустите код через кнопку Run по значку ▶.