ВУЗ: Не указан
Категория: Не указан
Дисциплина: Не указана
Добавлен: 03.12.2023
Просмотров: 377
Скачиваний: 1
ВНИМАНИЕ! Если данный файл нарушает Ваши авторские права, то обязательно сообщите нам.
СОДЕРЖАНИЕ
Списковые включения внутри списковых включений
105
['1', '2', '3', '4', '6', '7', '8', '9', '11', '12', '13', '14', '16', '17',
'86', '87', '88', '89', '91', '92', '93', '94', '96', '97', '98', '99']
Также в Python существует синтаксис включений множеств и словарных вклю- чений:
>>> spam = {str(number) for number in range(100) if number % 5 != 0}
❶
>>> spam
{'39', '31', '96', '76', '91', '11', '71', '24', '2', '1', '22', '14', '62',
'4', '57', '49', '51', '9', '63', '78', '93', '6', '86', '92', '64', '37'}
>>> spam = {str(number): number for number in range(100) if number % 5 != 0}
❷
>>> spam
{'1': 1, '2': 2, '3': 3, '4': 4, '6': 6, '7': 7, '8': 8, '9': 9, '11': 11,
'92': 92, '93': 93, '94': 94, '96': 96, '97': 97, '98': 98, '99': 99}
Включение множества
❶
использует фигурные скобки вместо квадратных, а ге- нерируемое им значение представляет собой множество. Словарное включение
❷
создает значение-словарь и использует двоеточие для разделения ключа и значения во включении. Включения компактны, и они могут сделать код более удобочитае- мым. Но обратите внимание на то, что включения создают список, множество или словарь на основании итерируемого объекта (в данном примере — объекта диапа- зона, возвращаемого вызовом range(100)
). Списки, множества и словари являются итерируемыми объектами, это означает, что включения могут вкладываться во включения, как в следующем примере:
>>> nestedIntList = [[0, 1, 2, 3], [4], [5, 6], [7, 8, 9]]
>>> nestedStrList = [[str(i) for i in sublist] for sublist in nestedIntList]
>>> nestedStrList
[['0', '1', '2', '3'], ['4'], ['5', '6'], ['7', '8', '9']]
Но вложенные списковые включения (или вложенные включения множеств/
словарные включения) упаковывают значительную сложность в небольшой объем кода, что усложняет его чтение. Лучше развернуть списковое включение в один или несколько циклов for
:
>>> nestedIntList = [[0, 1, 2, 3], [4], [5, 6], [7, 8, 9]]
>>> nestedStrList = []
>>> for sublist in nestedIntList:
... nestedStrList.append([str(i) for i in sublist])
...
>>> nestedStrList
[['0', '1', '2', '3'], ['4'], ['5', '6'], ['7', '8', '9']]
Включения также могут содержать множественные выражения for
, хотя в та- ких ситуациях также часто появляется нечитаемый код. Например, следующее
106
Глава 5.Поиск запахов в коде списковое включение создает неструктурированный список на базе вложенного списка:
>>> nestedList = [[0, 1, 2, 3], [4], [5, 6], [7, 8, 9]]
1 ... 5 6 7 8 9 10 11 12 ... 40
>>> flatList = [num for sublist in nestedList for num in sublist]
>>> flatList
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
Это списковое включение содержит два выражения for
, но даже опытному разра- ботчику Python будет непросто понять его. Развернутая форма с двумя циклами for создает тот же деструктурированный список, но читается намного проще:
>>> nestedList = [[0, 1, 2, 3], [4], [5, 6], [7, 8, 9]]
>>> flatList = []
>>> for sublist in nestedList:
... for num in sublist:
... flatList.append(num)
...
>>> flatList
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
Включения представляют собой синтаксические сокращения, которые позволяют создавать компактный код. Тем не менее не увлекайтесь и не вкладывайте их друг в друга.
Пустые блоки except и плохие сообщения
об ошибках
Перехват исключений — один из основных способов восстановить работоспособ- ность программы даже при возникновении проблем. Если в программе возникает исключение, но нет блока except для его обработки, программа Python аварийно завершается. Это может привести к потере несохраненной работы или к сохранению полузавершенного кода, который в дальнейшем может стать причиной еще более серьезных ошибок.
Чтобы предотвратить фатальные ошибки, можно добавить блок except с кодом обработки ошибки. Но иногда бывает трудно решить, как должна обрабатываться ошибка, и у программиста возникает искушение оставить блок except пустым, с одной командой pass
. Например, в следующем коде команда pass используется для создания блока except
, который ничего не делает:
>>> try:
... num = input('Enter a number: ')
... num = int(num)
... except ValueError:
... pass
Мифы о запахах кода
107
...
Enter a number: forty two
>>> num
'forty two'
В этом коде не происходит сбой, когда функции int()
передается строка 'forty two'
, потому что исключение
ValueError
, выдаваемое int()
, обрабатывается командой except
. Однако если ошибка попросту игнорируется, это может быть хуже, чем ава- рийное завершение. Программы аварийно завершаются, чтобы они не продолжали работать с некорректными данными или в неполном состоянии, что может породить более серьезные ошибки в будущем. При вводе нецифровых символов в нашем примере аварийное завершение не происходит. Но теперь переменная num содержит строку вместо целого числа, что вызовет проблемы при использовании переменной num
. Наша команда except не столько обрабатывает ошибки, сколько скрывает их.
Обработка исключений с плохими сообщениями об ошибках также относится к категории запахов кода. Взгляните на следующий пример:
>>> try:
... num = input('Enter a number: ')
... num = int(num)
... except ValueError:
... print('An incorrect value was passed to int()')
Enter a number: forty two
An incorrect value was passed to int()
В этом коде не происходит фатального сбоя, и это хорошо, но он не предоставляет пользователю достаточной информации о том, как решить проблему. Сообщения об ошибках предназначены для пользователей, а не для программистов. В данном случае сообщение не только содержит технические подробности, непонятные пользователю (например, ссылку на функцию int()
), но и не сообщает пользова- телю, как можно решить проблему. Сообщения об ошибках должны объяснять, что случилось и что пользователь должен сделать.
Программисту проще быстро описать произошедшее (что бесполезно), вместо того чтобы подробно пояснить, что стоит сделать пользователю для решения проблемы.
Но помните, что, если ваша программа не обрабатывает все возможные исключения, эта программа еще не завершена.
Мифы о запахах кода
Некоторые запахи кода вообще не являются таковыми. В программировании полно полузабытых плохих советов, которые были вырваны из контекста или про- существовали так долго, что пережили свою полезность. Я виню в этом авторов
108
Глава 5.Поиск запахов в коде технических книг, которые пытаются выдать свои субъективные мнения за передо- вые практики. Возможно, вы слышали, что некоторые из них являются причинами ошибок в коде, но в основном в них нет ничего плохого. Я называю их мифами о за- пахах кода: это всего лишь предупреждения, которые можно и нужно игнорировать.
Рассмотрим несколько примеров.
Миф: функции должны содержать только одну команду return в самом конце
Идея «один вход, один выход» происходит из неправильно интерпретированного совета из эпохи программирования на языке ассемблера и FORTRAN. Эти языки позволяли войти в подпрограмму (структуру, сходную с функцией) в любой точ- ке, в том числе и в середине, из-за чего в ходе отладки было труднее определить, какие части подпрограммы уже были выполнены. У функций такой проблемы нет (выполнение всегда начинается с начала функции). Но совет продолжал существовать и в конце концов трансформировался в «функции и методы долж- ны содержать только одну команду return
, которая должна находиться в конце функции или метода».
Попытки добиться того, чтобы в функции или методе была только одна команда return
, часто приводят к появлению запутанных последовательностей команд if
- else
, которые создают гораздо больше проблем, чем несколько команд return
Функция или метод может содержать несколько команд return
, ничего страшного в этом нет.
Миф: функции должны содержать не более одной команды try
«Функции и методы должны делать что-то одно» — в большинстве случаев это хо- роший совет. Но требовать, чтобы обработка исключений выполнялась в отдельной функции, значит заходить слишком далеко. Для примера рассмотрим функцию, которая проверяет, существует ли удаляемый файл:
>>> import os
>>> def deleteWithConfirmation(filename):
... try:
... if (input('Delete ' + filename + ', are you sure? Y/N') == 'Y'):
... os.unlink(filename)
... except FileNotFoundError:
... print('That file already did not exist.')
Сторонники этого мифа возражают, что функции должны всегда иметь только одну обязанность. Обработка исключений — это обязанность, поэтому функцию нужно разбить на две. Они считают, что, если вы используете команду try
- except
, она должна быть первой командой и охватывать весь код функции:
Мифы о запахах кода
109
>>> import os
>>> def handleErrorForDeleteWithConfirmation(filename):
... try:
... _deleteWithConfirmation(filename)
... except FileNotFoundError:
... print('That file already did not exist.')
...
>>> def _deleteWithConfirmation(filename):
... if (input('Delete ' + filename + ', are you sure? Y/N') == 'Y'):
... os.unlink(filename)
Этот код излишне усложнен. Функция
_deleteWithConfirmation()
теперь помечена как приватная при помощи префикса
_
, который указывает, что функция никогда не должна вызываться напрямую — только косвенно, через вызов handleErrorFo rDeleteWithConfirmation()
. Имя новой функции получилось неудобным, потому что она вызывается для удаления файла, а не для обработки ошибки при удалении.
Ваши функции должны быть простыми и компактными, но это не значит, что они всегда должны делать что-то одно (как бы вы это ни определяли). Вполне нормаль- но, если ваши функции содержат несколько команд try
- except и эти команды не охватывают весь код функции.
Миф: аргументы-флаги нежелательны
Логические аргументы функций или методов иногда называются аргументами-фла- гами. В программировании флагом называется значение, включающее бинарный выбор «включено — выключено»; для представления флагов часто используются логические значения. Такие настройки можно описать как установленные (
True
) или сброшенные (
False
).
Ложная уверенность в том, что аргументы-флаги функций чем-то плохи, основана на утверждении, что в зависимости от значения флага функция решает две совер- шенно разные задачи, как в следующем примере:
def someFunction(flagArgument):
if flagArgument:
# Выполнить код...
else:
# Выполнить совершенно другой код...
Действительно, если ваша функция выглядит так, лучше создать две разные функ- ции, вместо того чтобы в зависимости от аргумента выбирать, какая половина кода функции должна выполняться. Но большинство функций с аргументами-флагами работает не так. Например, логическое значение может передаваться в ключевом ар- гументе reverse функции sorted()
для определения порядка сортировки. Разбиение
110
Глава 5.Поиск запахов в коде функции на две функции с именами sorted()
и reverseSorted()
не улучшит код
(а также удвоит объем необходимой документации). Таким образом, мнение о не- желательности аргументов-флагов является мифом.
Миф: глобальные переменные нежелательны
Функции и методы напоминают мини-программы внутри вашей программы: они содержат код, включая локальные переменные, которые теряются при выходе из функции (подобно тому как переменные программы теряются после ее завершения).
Функции существуют изолированно: либо их код выполняется правильно, либо содержит ошибку в зависимости от аргументов, переданных при вызове.
Но функции и методы, использующие глобальные переменные, отчасти утрачивают эту полезную изоляцию. Каждая глобальная переменная, используемая в функции, фактически становится дополнительным входным значением функции наряду с ар- гументами. Больше аргументов — больше сложности, что в свою очередь означает более высокую вероятность ошибок. Если ошибка проявляется в функции из-за неправильного значения глобальной переменной, это значение может быть задано в любой точке программы. Чтобы найти вероятную причину ошибочного значения, недостаточно проанализировать код функции или строку кода с вызовом функции; придется рассмотреть всю программу. Поэтому следует ограничить использование глобальных переменных.
Для примера возьмем функцию calculateSlicesPerGuest()
в воображаемой про- грамме partyPlanner.py
, содержащей тысячи строк. Я включил номера строк, чтобы дать представление о размере программы:
1504. def calculateSlicesPerGuest(numberOfCakeSlices):
1505. global numberOfPartyGuests
1506. return numberOfCakeSlices / numberOfPartyGuests
Допустим, при выполнении этой программы возникает следующее исключение:
Traceback (most recent call last):
File "partyPlanner.py", line 1898, in
print(calculateSlicesPerGuest(42))
File "partyPlanner.py", line 1506, in calculateSlicesPerGuest return numberOfCakeSlices / numberOfPartyGuests
ZeroDivisionError: division by zero
В программе возникает ошибка деления на 0, за которую ответственна строка return numberOfCakeSlices
/
numberOfPartyGuests
. Чтобы это произошло, пере- менная numberOfPartyGuests должна быть равна
0
, но где numberOfPartyGuests было присвоено это значение? Так как переменная является глобальной, это могло произойти в любой из тысяч строк программы! Из данных трассировки мы знаем,
Мифы о запахах кода
111
что функция calculateSlicesPerGuest()
вызывалась в строке 1898 нашей вымыш- ленной программы. Взглянув на строку 1898, можно узнать, какой аргумент пере- давался для параметра numberOfCakeSlices
. Но значение глобальной переменной numberOfPartyGuests могло быть присвоено где угодно до этого вызова функции.
Следует заметить, что применение глобальных констант не считается нежела- тельной практикой. Так как их значения никогда не изменяются, они не повы- шают сложность кода так, как это делают другие глобальные переменные. Когда программисты говорят о том, что глобальные переменные нежелательны, они не имеют в виду константы.
Глобальные переменные увеличивают объем работы по отладке — программист должен найти точку, в которой было присвоено значение, вызвавшее исключение.
Из-за этого чрезмерное использование глобальных переменных нежелательно.
Но сама идея о том, что все глобальные переменные плохи, неверна. Глобальные переменные часто используют в небольших программах и для хранения настроек, действующих во всей программе. Если без глобальной переменной можно обой- тись, вероятно, лучше это сделать. Но утверждение «все глобальные переменные плохи» — слишком упрощенное и субъективное.
Миф: комментарии излишни
Плохие комментарии хуже, чем отсутствие комментариев. Комментарий с уста- ревшей или ошибочной информацией не разъясняет программу, а только создает лишнюю работу для программиста. Но такая локальная проблема иногда порождает тезис, что все комментарии плохи. Его апологеты считают, что каждый комментарий должен заменяться более понятным кодом вплоть до момента, когда в программе вообще не останется комментариев.
Комментарии пишутся на английском (или другом языке, на котором общается программист), что дает возможность гораздо более полно и подробно передавать информацию, чем с помощью имен переменных, функций или классов. Тем не менее написать лаконичные и эффективные комментарии непросто. Комментарии, как и код, приходится неоднократно редактировать. Наш код нам абсолютно понятен после того, как он написан, поэтому комментарии могут показаться бессмысленной и лишней работой. Тут и возникает мнение: комментарии излишни.
Но чаще на практике в программах слишком мало комментариев (или их нет во- обще) или они так запутаны, что могут дезинформировать. Отказываться от коммен- тариев на этом основании все равно что заявлять: «Перелеты через Атлантический океан безопасны только на 99,999991%, поэтому я лучше поплыву на пароходе».
О том, как пишутся эффективные комментарии, более подробно я расскажу в гла- ве 10.