Поскольку очень не хотелось оставлять в тексте важный термин латиницей, мы позволили себе перевести слово «docstring» как «докстрока», обнаружив этот термин в нескольких русскоязычных источниках.
В Python, как и в большинстве современных языков программирования, функция – это основной метод абстрагирования и инкапсуляции. Вы, будучи разработчиком, вероятно, написали уже сотни функций. Но функции функциям – рознь. Причем, если писать «плохие» функции, это немедленно скажется на удобочитаемости и поддержке вашего кода. Итак, что же такое «плохая» функция, а еще важнее – как сделать из нее «хорошую»?
Освежим тему
Математика изобилует функциями, правда, припомнить их сложно. Так что давайте вернемся к нашей излюбленной дисциплине: анализу. Вероятно, вам доводилось видеть формулы вроде
f(x) = 2x + 3
. Это функция под названием f
, принимающая аргумент x
, а затем «возвращающая» дважды x + 3
. Хотя, она и не слишком похожа на те функции, к которым мы привыкли в Python, она совершенно аналогична следующему коду:def f(x):
return 2*x + 3
Функции издавна существуют в математике, но в информатике совершенно преображаются. Однако, эта сила не дается даром: приходится миновать различные подводные камни. Давайте же обсудим, какова должна быть «хорошая» функция, и какие «звоночки» характерны для функций, возможно, требующих рефакторинга.
Секреты хорошей функции
Что отличает «хорошую» функцию Python от посредственной? Вы удивитесь, как много трактовок допускает слово «хорошая». В рамках этой статьи я буду считать функцию Python «хорошей», если она удовлетворяет большинству пунктов из следующего списка (выполнить все пункты для конкретной функции порой невозможно):
- Она внятно названа
- Соответствует принципу единственной обязанности
- Содержит докстроку
- Возвращает значение
- Состоит не более чем из 50 строк
- Она идемпотентная и, если это возможно, чистая
Многим из вас эти требования могут показаться чрезмерно суровыми. Однако, обещаю: если ваши функции будут соответствовать этим правилам, то получатся настолько прекрасны, что пробьют на слезу даже единорога. Ниже я посвящу по разделу каждому из элементов вышеприведенного списка, а затем завершу повествование, рассказав, как они гармонируют друг с другом и помогают создавать хорошие функции.
Именование
Вот моя любимая цитата на эту тему, часто ошибочно приписываемая Дональду, а на самом деле принадлежащая Филу Карлтону:
В компьютерных науках есть две сложности: инвалидация кэша и именование.Как бы глупо это ни звучало, именование – действительно сложная штука. Вот пример «плохого» названия функции:
def get_knn_from_df(df):
Теперь плохие названия попадаются мне практически повсюду, но данный пример взят из области Data Science (точнее, машинного обучения), где практикующие специалисты обычно пишут код в блокноте Jupyter, а потом пытаются собрать из этих ячеек удобоваримую программу.
Первая проблема с названием этой функции – в нем используются аббревиатуры. Лучше использовать полные английские слова, а не аббревиатуры и не малоизвестные сокращения. Единственная причина, по которой хочется сокращать слова — не тратить сил на набор лишнего текста, но в любом современном редакторе есть функция автозавершения, поэтому вам придется набрать полное название функции всего один раз. Аббревиатура – это проблема, поскольку зачастую она специфична для предметной области. В вышеприведенном коде
knn
означает «K-ближайшие соседи», а df
означает «DataFrame», структуру данных, повсеместно используемую в библиотеке pandas. Если код будет читать программист, не знающий этих сокращений, то он практически ничего не поймет в названии функции.Еще в названии этой функции есть два более мелких недочета. Во-первых, слово
"get"
избыточно. В большинстве грамотно поименованных функций сразу понятно, что данная функция что-то возвращает, что конкретно – отражено в имени. Элемент from_d
f также не нужен. Либо в докстроке функции, либо (если она находится на периферии) в аннотации типа будет описан тип параметра, если эта информация и так не очевидна из названия параметра.Так как же нам переименовать эту функцию? Просто:
def k_nearest_neighbors(dataframe):
Теперь даже неспециалисту понятно, что вычисляется в этой функции, а имя параметра
(dataframe)
не оставляет сомнений, какой аргумент ей следует передавать.Единственная ответственность
Развивая мысль Боба Мартина, скажу, что Принцип единственной ответственности касается функций не меньше, чем классов и модулей (о которых изначально и писал господин Мартин). Согласно этому принципу (в нашем случае) у функции должна быть единственная ответственность. То есть, она должна делать одну и только одну вещь. Один из самых веских доводов в пользу этого: если функция делает всего одну вещь, то и переписывать ее придется в единственном случае: если эту самую вещь придется делать по-новому. Также становится ясно, когда функцию можно удалить; если, внеся изменения где-то в другом месте, мы поймем, что единственная обязанность функции более не актуальна, то мы от нее просто избавимся.
Здесь лучше привести пример. Вот функция, делающая более одной «вещи»:
def calculate_and print_stats(list_of_numbers):
sum = sum(list_of_numbers)
mean = statistics.mean(list_of_numbers)
median = statistics.median(list_of_numbers)
mode = statistics.mode(list_of_numbers)
print('-----------------Stats-----------------')
print('SUM: {}'.format(sum)
print('MEAN: {}'.format(mean)
print('MEDIAN: {}'.format(median)
print('MODE: {}'.format(mode)
А именно две: вычисляет набор статистических данных о списке чисел и выводит их в
STDOUT
. Функция нарушает правило: должна быть единственная конкретная причина, по которой ее, возможно, потребовалось бы изменить. В данном случае просматриваются две очевидные причины, по которым это понадобится: либо потребуется вычислять новую или иную статистику, либо потребуется изменить формат вывода. Поэтому данную функцию лучше переписать в виде двух отдельных функций: одна будет выполнять вычисления и возвращать их результаты, а другая – принимать эти результаты и выводить их в консоль. Функцию (вернее, наличие у нее двух обязанностей) с потрохами выдает слово and в ее названии.Такое разделение также серьезно упрощает тестирование функции, а еще позволяет не только разбить ее на две функции в рамках одного и того же модуля, но даже разнести две эти функции в совершенно разные модули, если это уместно. Это дополнительно способствует более чистому тестированию и упрощает поддержку кода.
На самом деле, функции, выполняющие ровно две вещи, встречаются редко. Гораздо чаще натыкаешься на функции, делающие намного, намного больше операций. Опять же, из соображений удобочитаемости и тестируемости такие «многостаночные» функции следует дробить на однозадачные, в каждой из которых заключен единственный аспект работы.
Докстроки
Казалось бы, все в курсе, что есть документ PEP-8, где даются рекомендации по стилю кода на Python, но гораздо меньше среди нас тех, кто знает PEP-257, в котором такие же рекомендации даются по поводу докстрок. Чтобы не пересказывать содержание PEP-257, отсылаю вас самих к этому документу – почитайте в свободное время. Однако, основные его идеи таковы:
- Для каждой функции нужна докстрока
- В ней следует соблюдать грамматику и пунктуацию; писать законченными предложениями
- Докстрока начинается с краткого (в одно предложение) описания того, что делает функция
- Докстрока формулируется в предписывающем, а не в описательном стиле
Все эти пункты легко соблюсти, когда пишешь функции. Просто написание докстрок должно войти в привычку, причем, старайтесь писать их прежде, чем приступать к коду самой функции. Если у вас не получается написать четкую докстроку, характеризующую функцию – это хороший повод задуматься, зачем вы вообще пишете эту функцию.
Возвращаемые значения
Функции можно (и следует) трактовать как маленькие самодостаточные программы. Они принимают некоторый ввод в форме параметров и возвращают результат. Параметры, конечно, опциональны. А вот возвращаемые значения обязательны с точки зрения внутреннего устройства Python. Если вы даже попытаетесь написать функцию, которая не возвращает значения – не сможете. Если функция даже не станет возвращать значения, то интерпретатор Python «принудит» ее возвращать
None
. Не верите? Попробуйте сами:? python3
Python 3.7.0 (default, Jul 23 2018, 20:22:55)
[Clang 9.1.0 (clang-902.0.39.2)] on darwin
Type "help", "copyright", "credits" or "license" for more information.
>>> def add(a, b):
... print(a + b)
...
>>> b = add(1, 2)
3
>>> b
>>> b is None
True
Как видите, значение
b
– по сути None
. Итак, даже если вы напишете функцию без инструкции return, она все равно будет что-то возвращать. И должна. В конце концов, это ведь маленькая программа, верно? Насколько полезны программы, от которых нет никакого вывода – и поэтому невозможно судить, верно ли выполнилась данная программа? Но самое важное – как вы собираетесь тестировать такую программу? Я даже не побоюсь утверждать следующее: каждая функция должна возвращать полезное значение, хотя бы ради тестируемости. Код, который я пишу, должен быть протестирован (это не обсуждается). Только представьте, каким корявым может получится тестирование вышеприведенной функции
add
(подсказка: вам придется перенаправлять ввод/вывод, после чего вскоре все пойдет наперекосяк). Кроме того, возвращая значение, мы можем выполнять сцепление методов и, следовательно, писать код вот так: with open('foo.txt', 'r') as input_file:
for line in input_file:
if line.strip().lower().endswith('cat'):
# ... делаем с этими строками что-нибудь полезное
Строка
if line.strip().lower().endswith('cat'):
работает, поскольку каждый из строковых методов (strip()
, lower()
, endswith()
) в результате вызова функции возвращает строку.Вот несколько распространенных доводов, которые вам может привести программист, объясняя, почему написанная им функция не возвращает значения:
«Она всего лишь [какая-то операция, связанная с вводом/выводом, например, сохранение значения в базе данных]. Здесь я не могу вернуть ничего полезного.»Не соглашусь. Функция может вернуть True, если операция завершилась успешно.
«Здесь мы изменяем один из имеющихся параметров, используем его как ссылочный параметр.»""Здесь – два замечания. Во-первых, всеми силами старайтесь так не делать. Во-вторых, снабжать функцию каким-либо аргументом лишь для того, чтобы узнать, что она изменилась – в лучшем случае удивительно, а в худшем – попросту опасно. Вместо этого, как и при работе со строковыми методами, старайтесь возвращать новый экземпляр параметра, в котором уже отражены примененные к нему изменения. Даже если это не получается делать, поскольку создание копии какого-то параметра сопряжено с чрезмерными издержками, все равно можно откатываться к предложенному выше варианту «Вернуть
True
, если операция завершилась успешно».«Мне нужно возвращать несколько значений. Нет такого единственного значения, которое в данном случае было бы целесообразно возвращать.»Этот аргумент немного надуманный, но мне доводилось его слышать. Ответ, разумеется, как раз в том, что автор и хотел сделать – но не знал как: для возврата нескольких значений используйте кортеж.
Наконец, самый сильный аргумент в пользу того, что полезное значение лучше возвращать в любом случае – в том, что вызывающая сторона всегда может с полным правом эти значения игнорировать. Короче говоря, возврат значения от функции – практически наверняка здравая идея, и крайне маловероятно, что мы таким образом что-нибудь повредим, даже в сложившихся базах кода.
Длина функции
Я не раз признавался, что довольно туп. Могу одновременно держать в голове примерно три вещи. Если вы дадите мне прочесть 200-строчную функцию и спросите, что она делает, я, вероятно, буду таращиться на нее не менее 10 секунд. Длина функции прямо сказывается на ее удобочитаемости и, следовательно, на поддержке. Поэтому старайтесь, чтобы ваши функции оставались короткими. 50 строк – величина, взятая совершенно с потолка, но мне она кажется разумной. (Надеюсь), что большинство функций, которые вам доведется писать, будут значительно короче.
Если функция соответствует Принципу единственной ответственности, то, вероятно, она будет достаточно краткой. Если она читая или идемпотентная (об этом мы поговорим) ниже – то, наверное, она также получится короткой. Все эти идеи гармонично сочетаются друг с другом и помогают писать хороший, чистый код.
Итак, что же делать, если ваша функция получилась слишком длинной? РЕФАКТОРИТЬ! Вероятно, вам приходится заниматься рефакторингом постоянно, даже если вы не знаете этого термина. Рефакторинг – это попросту изменение структуры программы, без изменения ее поведения. Поэтому, извлечение нескольких строк кода из длинной функции и превращение их в самостоятельную функцию – это один из типов рефакторинга. Оказывается, это еще и наиболее распространенный, и самый быстрый способ продуктивного укорачивания длинных функций. Поскольку вы даете этим новым функциям подходящие имена, получающийся у вас код гораздо проще читать. Я написал целую книгу о рефакторинге (на самом деле, я им постоянно занимаюсь), так что здесь вдаваться в детали не буду. Просто знайте, что, если у вас есть слишком длинная функция – то ее следует рефакторить.
Идемпотентность и функциональная чистота
Заголовок этого раздела может показаться слегка устрашающим, но концептуально раздел прост. Идемпотентная функция при одинаковом наборе аргументов всегда возвращает одно и то же значение, независимо от того, сколько раз ее вызывают. Результат не зависит от нелокальных переменных, изменяемости аргументов или от любых данных, поступающих из потоков ввода/вывода. Следующая функция
add_three(number)
идемпотентна:def add_three(number):
"""вернуть *число* + 3."""
return number + 3
Независимо от того, сколько раз мы вызовем
add_three(7)
, ответ всегда будет равен 10. А вот другой случай – функция, не являющаяся идемпотентной:def add_three():
"""Вернуть 3 + число, введенное пользователем."""
number = int(input('Enter a number: '))
return number + 3
Эта откровенно надуманная функция не идемпотентна, поскольку возвращаемое значение функции зависит от ввода/вывода, а именно – от числа, введенного пользователем. Разумеется, при разных вызовах
add_three()
возвращаемые значения будут отличаться. Если мы дважды вызовем эту функцию, то пользователь в первом случае может ввести 3, а во втором – 7, и тогда два вызова add_three()
вернут 6 и 10 соответственно.Вне программирования также встречаются примеры идемпотентности – например, по такому принципу устроена кнопка «вверх» у лифта. Нажимая ее в первый раз, мы «уведомляем» лифт, что хотим подняться. Поскольку кнопка идемпотентна, то сколько ее потом ни нажимать – ничего страшного не произойдет. Результат будет всегда одинаков.
Почему идемпотентность так важна
Тестируемость и удобство в поддержке. Идемпотентные функции легко тестировать, поскольку они гарантированно, в любом случае вернут одинаковый результат, если вызвать их с одними и теми же аргументами. Тестирование сводится к проверке того, что при разнообразных вызовах функция всегда возвращает ожидаемое значение. Более того, эти тесты будут быстрыми: скорость тестов – важная проблема, которую часто обходят вниманием при модульном тестировании. А рефакторинг при работе с идемпотентными функциями – вообще легкая прогулка. Не важно, как вы измените код вне функции – результат ее вызова с одними и теми же аргументами всегда будет один и тот же.
Что такое «чистая» функция?
В функциональном программировании функция считается чистой, если она, во-первых, идемпотентна, а во-вторых – не вызывает наблюдаемых побочных эффектов. Не забывайте: функция идемпотентна, если всегда возвращает один и тот же результат при конкретном наборе аргументов. Однако, это не означает, что функция не может влиять на другие компоненты – например, на нелокальные переменные или потоки ввода/вывода. Например, если бы идемпотентная версия вышеприведенной функции
add_three(number)
выводила результат в консоль, а лишь затем возвращала бы его, она все равно считалась бы идемпотентной, поскольку при ее обращении к потоку ввода/вывода эта операция доступа никак не влияет на значение, возвращаемое от функции. Вызов print()
– это просто побочный эффект: взаимодействие с остальной программой или системой как таковой, происходящее наряду с возвратом значения.Давайте немного разовьем наш пример с
add_three(number)
. Можно написать следующий код, чтобы определить, сколько раз была вызвана add_three(number)
:add_three_calls = 0
def add_three(number):
"""Вернуть *число* + 3."""
global add_three_calls
print(f'Returning {number + 3}')
add_three_calls += 1
return number + 3
def num_calls():
"""Вернуть, сколько раз была вызвана *add_three*."""
return add_three_calls
Теперь мы выполняем вывод в консоль (это побочный эффект) и изменяем нелокальную переменную (другой побочный эффект), но, поскольку ни то, ни другое не влияет на значение, возвращаемое функцией, она все равно идемпотентна.
Чистая функция не оказывает побочных эффектов. Она не только не использует никаких «внешних данных» при расчете значения, но и не взаимодействует с остальной программой/системой, только вычисляет и возвращает указанное значение. Следовательно, хотя наше новое определение
add_three(number)
остается идемпотентным, эта функция уже не чистая.В чистых функциях нет инструкций логирования или вызовов
print()
. При работе они не обращаются к базе данных и не используют соединений с интернетом. Не обращаются к нелокальным переменным и не изменяют их. И не вызывают других не-чистых функций. Короче говоря, они не оказывают «жуткого дальнодействия», выражаясь словами Эйнштейна (но в контексте информатики, а не физики). Они не изменяют каким-либо образом остальные части программы или системы. В императивном программировании (а именно им вы и занимаетесь, когда пишете код на Python), такие функции – самые безопасные. Они известны своей тестируемостью и удобством в поддержке; более того, поскольку они идемпотентны, тестирование таких функций гарантированно будет столь же быстрым, как и выполнение. Сами тесты также просты: не приходится подключаться к базе данных либо имитировать какие-либо внешние ресурсы, готовить стартовую конфигурацию кода, а по окончании работы не нужно ничего подчищать.
Честно говоря, идемпотентность и чистота очень желательны, но не обязательны. То есть, нам бы хотелось писать только чистые или идемпотентные функции, учитывая все вышеупомянутые их преимущества, но это не всегда возможно. Суть, однако, в том, чтобы приучиться писать код, естественным образом не допуская побочных эффектов и внешних зависимостей. Таким образом, каждую написанную нами строку кода станет проще тестировать, даже если не удастся обойтись только лишь чистыми или идемпотентными функциями.
Заключение
Вот и все. Оказывается, секрет создания новых функций – никакой не секрет. Просто нужно придерживаться некоторых выверенных наилучших практик и железных правил. Надеюсь, статья вам понравилась. А теперь идите – и перескажите ее друзьям! Давайте договоримся везде и во всех случаях писать отличный код. Или, как минимум, прилагать максимум усилий, чтобы не плодить в этом мире «плохой код». С этим я смогу жить.
Комментарии (22)
samsergey
15.10.2018 01:18Всё-таки, идемпотентность, не очень удачный термин в данном контексте. Речь идет о чистоте функции или прозрачности по ссылкам, но слово "идемпотентность" кроме труднопроизносимости имеет совсем другой смысл. Это когда f(f(x))=f(x).
bogolt
15.10.2018 01:29> Во-первых, слово «get» избыточно. В большинстве грамотно поименованных функций сразу понятно, что данная функция что-то возвращает, что конкретно – отражено в имени.
Вот уж нет. Классический пример это STL с его функциями 'clear' и 'empty'. Одна из этих функций очищает контейнер а другая говорит пуст ли он.
>Поэтому данную функцию лучше переписать в виде двух отдельных функций: одна будет выполнять вычисления и возвращать их результаты, а другая – принимать эти результаты и выводить их в консоль.
А потом написать третью функцию которая вызовет обе эти функции ( потому что у меня в коде они используются очень часто и всегда одна следом за другой), и хммм как бы мне ее назвать…
Sklott
15.10.2018 09:42Вернуть True, если операция завершилась успешно
Какой-то маниакальный бред мне кажется. Зачем вообще что-то возвращать и потом 90% времени это даже не проверять???
Ну т.е. для функций где ничто не может пойти не так (не считая исключений), например:
def calcTotal(price): self.total += price
Предлагается добавлять «return True» только на том основании что «ну Python всё равно уже что-то возвращает, давайте это „что-то“ сделаем „полезным“». Мне кажется это уже какой-то маразм…
Мне казалось что вообще проверять возвращаемое значение на предмет ошибки в ООП уже давно моветон… Ну возможно за исключением случаев ввода/вывода.
ЗЫ: Хотя судя по остальным «добрым советам», автор текста просто перепутал парадигму языка и искренне считает python функциональным. Я конечно не против такого использования, но говорить что оно «единственно верное», это всё-же перебор…
maslyaev
15.10.2018 16:24Поэтому, извлечение нескольких строк кода из длинной функции и превращение их в самостоятельную функцию – это один из типов рефакторинга.
В результате программа рискует превратиться из набора длинных спагетти (что, безусловно, зло) в маловразумительную кастрюлю вермишели. Особенно это доставляет, когда эта вермишель ещё и разбросана по десятку-другому модулей.
Кроме того, когда функций много, всё сложнее становится придумывать им вразумительные имена. Такие, из которых понятно не только что функция делает, но и зачем она это делает.
Barafu_Albino_Cheetah
15.10.2018 17:16Понятие чистоты функции полезно в функциональных и процедуных подходах. В ООП оно — зло. Объект — это по определению состояние, и функции, изменяющие объект, меняют его состояние. Когда логически объект тот-же, но с изменениями, а физически — каждый раз новый, чтобы добится чистоты — то на этот объект очень сложно ссылаться, да и рефакторинг его затруднён.
samsergey
16.10.2018 01:18Гигиеническую роль чистоты в ООП играет принцип инкапсуляции, так что он и там есть и используется.
vobo
15.10.2018 18:28+2Аббревиатура – это проблема, поскольку зачастую она специфична для предметной области
Думаю, почти любой небиблиотечный код специфичен для предметной области и это нормально, если человек с улицы его не понимает. Разворачивать все аббревиатуры тоже зло. get_knn_from_df — на мой взгляд, очень понятно, даже для меня (я не data scientist)lagranzh
15.10.2018 21:35+1Согласен с предидущим оратором. Если человек не знает что такое кнн, то зачем ему эта функция?
Про откидывание 'from_df' тоже не очень удачная идея. Обычно такие имена дают, когда рядом есть get_knn_from_csv(filename) и get_knn_from_rawdata(mtx).lostmsu
15.10.2018 22:07Последнее — плохой аргумент. Лучше бы это были
knn(read_csv(filename)) и knn(get_raw_data(source))trapwalker
16.10.2018 11:27Этот вариант, конечно, лучше, но объективно же возможна ситуация, когда преобразовывать из разных входных форматов к единому для вычисления или вовсе невозможно или сложнее, чем сделать два отдельных вычисления на основе разных представлений.
К примеру мы делаем функцию, которая формирует специального вида хеш, вроде контент-id. И специфика алгоритма хеширования сильно зависит от формата входных данных, хотя сам результат — это просто GUID. Этот хеш нужно считать одним способом для звука, другим для изображений и третьим для видео. Если делать тут специальный слой абстрации нецелесообразно (не все же пишут на java, там лишний слой абстракции никогда не лишний=), то сделать три реализации вполне логично.
В питоне же, кстати, нет сигнатурной перегрузки функций, если опираться на названия входных параметров, то код функции может неоправданно усложниться.
Да простят мой тонкий троллинг те, кого он затронул. Хочу лишь добавить, что все правила важны и нужны, а с опытом мы понимаем мета-правила и приобретаем знания где какие правила актуальны.
ksenofobius
16.10.2018 11:18Непонятно при чем тут питон. Тот же Боб Мартин писал про чистый код в целом (на примере java). Также из названия k_nearest_neighbors я не могу понять что делается с ближайшим соседом. Значит мне придется смотреть в код. Если бы тут присутствовал get мне бы этого делать не пришлось.
trapwalker
16.10.2018 11:31Понятно что хотел сказать автор, понятно что хотел сказать Боб. Залезть же в бутылку всегда можно ещё глубже и дно тут не станет ограничением. Возможно кому-то покажется необходимым написать целое эссе в названии функции. Аббревиатуры полезны, если они устоявшиеся; 'get_' может оказаться полезным, если реально убирает какую-то неоднозначность… хотя если у вас есть какая-то неоднозначность, это незначит, что её следует решать только и именно неймингом. Может быть что-то пошло не так раньше, на более ранних этапах проектирования.
budda
16.10.2018 13:30Я тут как начинающий и непрофесионал, пишущий для себя, хочу спросить: а куда же запихивать обращение к файловой системе, бд, сети, если в функцию нельзя. Только вот вчера в одном классе запихал в метод открытия на чтение бд и в нее же закрытие, то же самое с чтением файла. До прочтение этой статьи думал, что это нормально.
ksenofobius
17.10.2018 08:12Если это не класс коннектор то стоит его таким сделать. И использовать либо наследованием либо композицией с DI в классе где вы используете коннект к базе. ФП безусловно пресутствует в языке, но в таких случаях вы прям сильно теряете, выбирая его вместо ООП
kAIST
Это разве соответствует идеологии питона? На это есть исключения, код возврата в питоне не особо то применяют.
Areldar
Исключение должны бросаться в исключительной ситуации. Тут ИМХО автор пытался продвинуть код стайл:
1. Если в функции допустим провал и она ничего не возвращает верни True в случае успеха и False в случае провала.
2. Если нечего вернуть отрапортуй об успехе
kAIST
Ну так если функция допустила провал, это как раз таки исключительная ситуация и надо что то делать с этим. Или мы немного о разном говорим?!
Areldar
Провал не всегда исключительная ситуация. Функция например отправляет метрики на удаленный сервер. В случае недоступности оного отбрасывает их так как они все равно на момент того как оный подымется будут неактуальны. Мы не хотим тут бросать исключение так как в целом мы ожидаем, что сервер может быть недоступен и мы можем продолжить работу и без него. Или ситауция мы хотим почистить свои временные файлы и не смогли их удалить т.к. была перезагрузка и tmpfs пуст. Функция чистки завершилась провалом, но он ожидаемый и исключительной ситуацией не является и исключение не покинет функцию очистки временных файлов.
trapwalker
Не очень хороший пример. Получается, что у функции две «ответственности», как выражается автор. Она отправляет метрики и как-то реагирует на невозможность их отправки (игнорирует, логирует warning, считает попытки прежде чем дропнуть исключение?)
При более правильном дизайне наша функция должна ТОЛЬКО отправлять метрики на удалённый сервер и валиться с исключением, если это не удалось. Если вдруг нужно делать несколько попыток или считать неудачи прежде чем бить тревогу, то этим займётся специальная обёртка, декоратор, или какая-то конструкция в вызывающем коде.
Да, автор не затронул тему ООП, мктодов, инкапсулированного состояния и прочее. Все эти правила нужно знать и понимать, но всё было бы слишком просто, если бы это были железные правила и у них бы не было исключений.
Areldar
Она нарушит single responsibility только если реализует
некоторую функциональность отличную от контракта не бросать эксепшены
Я писал примерно про такой шаблон:
Areldar
Предвосхищая замечание. Да это по хорошему декоратор