Часто в программировании возникает проблема с неявными зависимостями и побочными эффектами. Они могут привести к неожиданным результатам или ошибкам в программе. Обычно с такими проблемами борются с помощью чистых функций.
В этом уроке мы изучим важные свойства функций. С их помощью можно более точно определять, как лучше разбивать код на функции и когда вообще их стоит выделять.
Что такое детерминированность
Детерминированность функций — это свойство, при котором функция всегда возвращает один и тот же результат для одних и тех же входных данных без побочных эффектов, связанных с состоянием программы. Детерминированная функция не зависит от контекста и внешних факторов. Она гарантированно возвращает предсказуемый результат.
Рассмотрим пример функции, которая не является детерминированной:
import random
def get_random_number():
return random.randint(1, 10)
Эта функция возвращает случайное число от 1 до 10 при каждом вызове. Если мы вызываем функцию несколько раз, то получим разные результаты:
get_random_number()
# 3
get_random_number()
# 7
get_random_number()
# 1
Теперь рассмотрим пример детерминированной функции:
def multiply(a, b):
return a * b
Функция multiply()
всегда возвращает результат умножения двух переданных ей аргументов. Если мы передадим одни и те же значения, то получим один и тот же результат:
multiply(2, 3)
# 6
multiply(2, 3)
# 6
multiply(2, 3)
# 6
Это делает функцию multiply()
детерминированной, так как она всегда возвращает один и тот же результат для одного и того же набора входных данных.
Нельзя однозначно сказать, что отсутствие детерминированности — это плохо. Для работы многих программ и сайтов нужна функция, которая будет возвращать случайное число или вычислять текущую дату. Но мы можем разделить код так, чтобы в нем было как можно больше детерминированных частей.
Если есть возможность написать функцию так, что она будет детерминированной, то так и нужно делать. При этом не стоит использовать глобальные переменные. Необходимо создавать функции, которые зависят только от своих аргументов.
Что такое побочные эффекты
Побочный эффект или side effects — это любые взаимодействия с внешней средой. К ним относятся файловые операции, такие как запись в файл, чтение файла, отправка или приём данных по сети и даже вывод в консоль.
Рассмотрим пример функции, которая имеет побочный эффект:
def print_hello():
print("Hello, world!")
Эта функция выводит сообщение в консоль, значит, она имеет побочный эффект. Если мы вызываем функцию print_hello
, то она изменяет состояние программы путем вывода сообщения в консоль. Но она не возвращает никакого значения:
print_hello()
# Hello, world!
Кроме того, побочными эффектами считаются изменения внешних переменных (например, глобальных) и входных параметров в случае изменяемых типов (списки, словари и прочие).
def func(dictionary):
dictionary['key'] = 42
Побочные эффекты составляют одну из самых больших сложностей при разработке. Они затрудняют логику кода и тестирование. Это приводит к большому числу ошибок. Например, только при работе с файлами количество возможных ошибок измеряется сотней. В этом случае может не хватить места на диске или будет попытка прочитать данные из несуществующего файла. Для предотвращения ошибок код обрастает большим числом проверок и защитных механизмов.
Но без побочных эффектов невозможно написать ни одной программы. Какие бы вычисления она ни делала, их результат должен быть как-то продемонстрирован. Даже у простой программы сложения двух чисел нам нужно увидеть результат, что автоматически приводит к побочным эффектам. В реальных же приложениях, обычно, все сводится к взаимодействию с базой данных или отправкой запросов по сети. Не существует способа избавиться от побочных эффектов совсем, но их влияние на программу можно минимизировать.
Как правило, в типичной программе побочных эффектов не так много по отношению к остальному коду, и происходят они лишь в самом начале и в конце. Например, программа, которая конвертирует файл из текстового формата в PDF, в идеале выполняет ровно два побочных эффекта:
- Читает файл в самом начале работы программы.
- Записывает результат работы программы в новый файл.
Между этими двумя пунктами и происходит основная работа, которая содержит чистую алгоритмическую часть. Побочные эффекты в таком случае будут находиться только в верхнем слое приложения, а ядро, выполняющее основную работу, останется чистым от них.
Чистые функции
Чистые функции или pure functions — это функции, которые при вызове не влияют на состояние программы и не имеют побочных эффектов. Они возвращают значения только на основе входных аргументов и не изменяют их.
Признаки чистых функций:
- Всегда возвращают одинаковые значения для одинаковых аргументов.
- Не имеют побочных эффектов — не изменяют состояние программы за пределами своей области видимости. Таким образом они не модифицируют глобальные переменные, не изменяют содержимое файлов, не выводят на экран и не изменяют содержимое баз данных.
- Не имеют побочных эффектов на аргументы, которые переданы им по ссылке — не изменяют их содержимое.
- Не зависят от состояния программы, поэтому не используют глобальные переменные или переменные, которые могут изменяться во время выполнения программы.
Примеры чистых функций:
def add_numbers(x, y):
return x + y
# создаем новый список вместо изменения существующего
def add_user(users, new_user):
return [*users, new_user]
# чистые функции могут быть и очень сложными
# например, парсинг html страниц
import html5lib
def parse_html(html):
return html5lib.parse(html)
Эти функции возвращают значение только на основе переданных им аргументов и не влияют на другие переменные в программе.
У чистых функций есть несколько преимуществ:
- Их просто тестировать. Достаточно передать на вход функции нужные параметры и посмотреть ожидаемый выход.
- Их безопасно запускать повторно, что особенно актуально в асинхронном коде или в случае многопоточного кода.
- Их легко комбинировать, получая новое поведение без необходимости переписывать программу.
Грязные функции
Не все функции могут быть чистыми, особенно, когда программы часто взаимодействуют с внешними ресурсами. В таких случаях функции могут изменять состояние программы или взаимодействовать с внешними системами, такими как базы данных или веб-сервисы. Такие функции называют грязными.
Один из интересных примеров это замыкание. Само по себе замыкание не делает функцию ни чистой, ни грязной. Все зависит от того, возможно ли изменять замкнутую переменную.
Если замыкание только читает замкнутые переменные, не изменяя их, функция остается чистой:
def outer(x):
def inner(y):
return x + y
return inner
add_5 = outer(5)
print(add_5(10)) # => 15
Если замыкание изменяет замкнутые переменные, функция становится грязной - нарушается требование детерминированности:
def outer():
count = 0
def inner():
nonlocal count
count += 1
return count
return inner
counter = outer()
print(counter()) # => 1
print(counter()) # => 2
Если замыкание использует изменяемые объекты, функция тоже может быть грязной - возникает побочный эффект:
def outer(lst):
def inner(item):
lst.append(item)
return lst
return inner
add_to_list = outer([])
print(add_to_list(1)) # => [1]
print(add_to_list(2)) # => [1, 2]
Выводы
Чистые и грязные функции имеют разные особенности и применения.
Чистые функции являются более предпочтительными. Они проще для понимания и тестирования, а также могут быть использованы в многопоточных приложениях без каких-либо проблем.
Грязные функции могут быть полезными в некоторых случаях, например, когда нужно изменить состояние программы или выполнить ввод-вывод. Но такие функции могут усложнить отладку и тестирование приложения.
Рекомендуем использовать чистые функции, когда это возможно. А использование грязных функций лучше ограничить. Их стоит применять только для тех случаев, когда это действительно необходимо.
Остались вопросы? Задайте их в разделе «Обсуждение»
Вам ответят команда поддержки Хекслета или другие студенты
Для полного доступа к курсу нужен базовый план
Базовый план откроет полный доступ ко всем курсам, упражнениям и урокам Хекслета, проектам и пожизненный доступ к теории пройденных уроков. Подписку можно отменить в любой момент.