ВУЗ: Не указан
Категория: Не указан
Дисциплина: Не указана
Добавлен: 03.12.2023
Просмотров: 380
Скачиваний: 1
ВНИМАНИЕ! Если данный файл нарушает Ваши авторские права, то обязательно сообщите нам.
СОДЕРЖАНИЕ
96
Глава 5.Поиск запахов в коде код. Обратите внимание: она трижды задает пользователю вопрос о том, как тот себя чувствует (“How are you feeling?”):
print('Good morning!')
print('How are you feeling?')
feeling = input()
print('I am happy to hear that you are feeling ' + feeling + '.')
print('Good afternoon!')
print('How are you feeling?')
feeling = input()
print('I am happy to hear that you are feeling ' + feeling + '.')
print('Good evening!')
print('How are you feeling?')
feeling = input()
print('I am happy to hear that you are feeling ' + feeling + '.')
Дублирование кода создает проблемы, потому что оно усложняет редактирование кода: изменение в одной копии должно быть внесено во все его дубли в программе.
Если вы забудете где-то внести изменение или вы внесете разные изменения в раз- ных копиях, в вашей программе, скорее всего, возникнет ошибка.
Проблема дублирования кода решается дедупликацией — код преобразуют так, чтобы он встречался только в одном месте программы — обычно в функции или цикле. В следующем примере я переместил дубликат в функцию, а затем повторно вызывал эту функцию:
def askFeeling():
print('How are you feeling?')
feeling = input()
print('I am happy to hear that you are feeling ' + feeling + '.')
print('Good morning!')
askFeeling()
print('Good afternoon!')
askFeeling()
print('Good evening!')
askFeeling()
В следующем примере дублирующийся код был перемещен в цикл:
for timeOfDay in ['morning', 'afternoon', 'evening']:
print('Good ' + timeOfDay + '!')
print('How are you feeling?')
feeling = input()
print('I am happy to hear that you are feeling ' + feeling + '.')
Также можно объединить два приема и совместить функцию с циклом:
def askFeeling(timeOfDay):
print('Good ' + timeOfDay + '!')
«Магические» числа
97
print('How are you feeling?')
feeling = input()
print('I am happy to hear that you are feeling ' + feeling + '.')
for timeOfDay in ['morning', 'afternoon', 'evening']:
askFeeling(timeOfDay)
Обратите внимание: код, который выдает сообщения «Good morning/afternoon/
evening!», похож, но не идентичен. В третьей версии программы я параметризовал код, чтобы исключить дублирование идентичных частей.
Параметр timeOfDay и переменная цикла timeOfDay заменяют различающиеся части.
Теперь, когда из кода были устранены дубликаты (лишние копии), необходимые изменения достаточно внести только в одном месте.
Как и со всеми запахами кода, исключение дублирования не является неукосни- тельным правилом, которое следует соблюдать всегда. Как правило, чем длиннее фрагмент дубля или чем больше копий присутствует в вашей программе, тем се- рьезнее стоит задуматься об их устранении. Я не против того, чтобы скопировать код один и даже два раза. Но если в программе присутствуют три или четыре дубля, я обычно серьезно задумываюсь об их устранении.
Иногда дедупликация не стоит затраченных усилий. Сравните первый пример кода в этом разделе с последним. Хотя код с дубликатами длиннее, он проще и прямо- линейнее. Пример без дубликатов делает то же самое, но с добавлением цикла, новой переменной цикла timeOfDay и новой функции с параметром, которому также присвоено имя timeOfDay
Дублирование кода способно вызывать ошибки, потому что оно усложняет целост- ное изменение кода. Если программа содержит несколько одинаковых фрагментов, стоит поместить код в функцию или цикл, чтобы он вызывался только один раз.
1 ... 4 5 6 7 8 9 10 11 ... 40
«Магические» числа
Не приходится удивляться тому, что в программировании используются числа.
Но некоторые числа, встречающиеся в исходном коде, могут сбить с толку других программистов (или вас через пару недель после написания программы). Для при- мера возьмем число
604800
в следующей строке:
expiration = time.time() + 604800
Функция time.time()
возвращает целое число, представляющее текущее время.
Можно предположить, что переменная expiration представляет некий будущий момент, который наступит через 604 800 секунд. Но число
604800
выглядит зага- дочно: что оно означает? Комментарий поможет прояснить ситуацию:
expiration = time.time() + 604800 # Срок действия истекает через неделю.
98
Глава 5.Поиск запахов в коде
Это хорошее решение, но еще лучше заменить такие «магические» числа кон- стантами. Константы представляют собой переменные, имена которых записаны в верхнем регистре, это означает, что они не должны изменяться после исходного присваивания. Обычно константы определяются как глобальные переменные в на- чале файла с исходным кодом:
# Константы для разных промежутков времени:
SECONDS_PER_MINUTE = 60
SECONDS_PER_HOUR = 60 * SECONDS_PER_MINUTE
SECONDS_PER_DAY = 24 * SECONDS_PER_HOUR
SECONDS_PER_WEEK = 7 * SECONDS_PER_DAY
expiration = time.time() + SECONDS_PER_WEEK # Срок действия истекает
# через неделю.
Используйте отдельные константы для «магических» чисел, предназначенных для разных целей, даже если их числовые значения совпадают. Например, в колоде
52 карты, а в году 52 недели. Но если в вашей программе используются обе вели- чины, нужно поступить примерно так:
NUM_CARDS_IN_DECK = 52
NUM_WEEKS_IN_YEAR = 52
print('This deck contains', NUM_CARDS_IN_DECK, 'cards.')
print('The 2-year contract lasts for', 2 * NUM_WEEKS_IN_YEAR, 'weeks.')
При выполнении этого кода результат будет выглядеть так:
This deck contains 52 cards.
The 2-year contract lasts for 104 weeks.
(Колода содержит 52 карты.
Двухлетний контракт длится 104 недели.)
Использование разных констант позволяет независимо изменять их в будущем. Ко- нечно, значения констант не должны изменяться во время выполнения кода. Но это не означает, что программист никогда не обновит их в исходном коде. Например, если в будущей версии кода появится дополнительная карта-джокер, константу cards можно изменить независимо от weeks
:
NUM_CARDS_IN_DECK = 53
NUM_WEEKS_IN_YEAR = 52
«Магическими» числами также иногда называют нечисловые значения. Например, строковые значения могут использоваться как константы. Возьмем следующую программу, которая предлагает пользователю указать направление и выводит
«Магические» числа
99
преду преждение, если пользователь выбрал 'north'
. Из-за опечатки 'nrth'
воз- никает ошибка, вследствие чего предупреждение не выводится:
while True:
print('Set solar panel direction:')
direction = input().lower()
if direction in ('north', 'south', 'east', 'west'):
break print('Solar panel heading set to:', direction)
if direction == 'nrth':
❶
print('Warning: Facing north is inefficient for this panel.')
Найти такую ошибку нелегко: хотя в строке 'nrth'
совершена опечатка, она остается синтаксически правильным кодом Python. Программа не завершается аварийно, а предупреждение легко упустить из виду. Но если вы допустите ту же опечатку при использовании констант, ошибка будет обнаружена, потому что Python заметит, что константа
NRTH
не существует:
# Константы для разных промежутков времени:
NORTH = 'north'
SOUTH = 'south'
EAST = 'east'
WEST = 'west'
while True:
print('Set solar panel direction:')
direction = input().lower()
if direction in (NORTH, SOUTH, EAST, WEST):
break print('Solar panel heading set to:', direction)
if direction == NRTH:
❶
print('Warning: Facing north is inefficient for this panel.')
Из-за исключения
NameError
, выдаваемого в строке кода с опечаткой
NRTH
❶
, ошибка становится очевидной при запуске программы:
Set solar panel direction:
west
Solar panel heading set to: west
Traceback (most recent call last):
File "panelset.py", line 14, in
if direction == NRTH:
NameError: name 'NRTH' is not defined
«Магические» числа свидетельствуют о наличии запаха кода, потому что они не выполняют свое предназначение, затрудняют чтение и обновление кода и повышают риск опечаток, которые так трудно обнаружить. Проблема решается использова- нием констант.
100
Глава 5.Поиск запахов в коде
Закомментированный и мертвый код
Поместить код в комментарий, чтобы он не выполнялся, — временная мера. Воз- можно, вы хотите пропустить часть строк, чтобы протестировать другую функцио- нальность; закомментированные строки вы легко вернете позднее. Но если заком- ментированный код так и останется на месте, для читателя останется абсолютной тайной, почему он был удален и при каких условиях он может опять стать частью программы. Рассмотрим следующий пример:
doSomething()
#doAnotherThing()
doSomeImportantTask()
doAnotherThing()
Возникает целый ряд вопросов: почему вызов doAnotherThing()
был закоммен- тирован? Будет ли он снова включен в программу? Почему не был закоммен- тирован второй вызов doAnotherThing()
? Изначально в коде было два вызова doAnotherThing()
или только один вызов, который переместился в точку после вызова doSomeImportantTask()
? Почему закомментированный код не был удален, для этого есть какая-то причина? На все эти вопросы нет очевидных ответов.
Мертвым называется код, который недоступен или никогда не может быть выпол- нен на логическом уровне. Код внутри функции после команды return
, команды if
, условие которой всегда равно
False
, или код функции, которая никогда не вызы- вается в программе, — все это примеры мертвого кода. Чтобы увидеть пример на практике, введите следующую команду в интерактивной оболочке:
>>> import random
>>> def coinFlip():
... if random.randint(0, 1):
... return 'Heads!'
... else:
... return 'Tails!'
... return 'The coin landed on its edge!'
...
>>> print(coinFlip())
Tails!
1
Строка 'The coin landed on its edge!'
является мертвым кодом, потому что код в блоках if и else возвращает управление до того, как программа сможет достичь этой строки. Мертвый код дезинформирует, поскольку читающие его программисты предполагают, что он составляет активную часть программы, тогда как по сути это закомментированный код.
1
Heads — орел; Tails — решка; The coin landed on its edge — Монета встала ребром (англ.). —
Примеч. пер.
Отладочный вывод
101
Заглушки (stubs) являются исключением из этих правил. Они представляют со- бой заменители для будущего кода (например, функции и классы, которые еще не были реализованы). Вместо реального кода заглушка содержит команду pass
, которая ничего не делает. (Она также называется пустой операцией.) Команда pass существует как раз для того, чтобы вы могли создавать заглушки в тех местах, где с точки зрения синтаксиса языка должен присутствовать какой-то код:
>>> def exampleFunction():
... pass
Если вызвать эту функцию, она не сделает ничего. Вместо этого она лишь показы- вает, что когда-нибудь в нее будет добавлен код.
Также возможен другой вариант: чтобы предотвратить случайный вызов не- реализованной функции, можно использовать заглушку из команды raise
NotImplementedError
. Тем самым вы покажете, что функция еще не готова к вызову:
>>> def exampleFunction():
... raise NotImplementedError
>>> exampleFunction()
Traceback (most recent call last):
File "
File "
NotImplementedError
Исключение
NotImplementedError предупредит вас о том, что в программе была случайно вызвана заглушка (функция или метод).
Закомментированный код и мертвый код считаются запахами кода, потому что они могут создать у программиста ошибочное впечатление, будто код является испол- няемой частью программы. Вместо этого следует удалить их и использовать систему контроля версий (например, Git или Subversion) для отслеживания изменений. Кон- тролю версий я посвятил главу 12. С системой контроля версий вы можете удалить код из своей программы, а при необходимости позднее вернуть его обратно.
Отладочный вывод
Отладочный вывод — это практика включения в программу временных вызовов print()
для вывода значений переменных и повторного запуска программы.
Последовательность процесса, как правило, такова.
1. Обнаружение ошибки в программе.
2. Включение вызовов print()
для некоторых переменных, чтобы узнать их текущие значения.
102
Глава 5.Поиск запахов в коде
3. Перезапуск программы.
4. Включение новых вызовов print()
, потому что предыдущие вызовы не предоставили достаточной информации.
5. Перезапуск программы.
6. Предыдущие этапы следует повторять несколько раз, пока вы наконец не найдете причину ошибки.
7. Перезапуск программы.
8. Вы понимаете, что забыли удалить какие-то вызовы print()
, и удаляете их.
Отладочный вывод обманчиво прост и быстр. Но часто он требует многих циклов перезапуска программы, пока не будет выведена информация, необходимая для исправления ошибки. Проблема решается при помощи отладки или организации журнального вывода в программе. При использовании отладчика вы можете вы- полнять свои программы по одной строке и проверять значения любых переменных.
Иногда кажется, что работа с отладчиком занимает больше времени, чем простая вставка вызова print()
, но в долгосрочной перспективе она экономит время.
Журнальные файлы сохраняют большие объемы информации из вашей программы, чтобы вы могли сравнить результаты одного запуска с другим. В Python встроенный модуль logging предоставляет функциональность, необходимую для простого со- хранения журнальной информации, всего в трех строках кода:
import logging logging.basicConfig(filename='log_filename.txt', level=logging.DEBUG, format='%(asctime)s - %(levelname)s - %(message)s')
logging.debug('This is a log message.')
После импортирования модуля logging и настройки его базовой конфигурации можно вызвать метод logging.debug()
для записи информации в текстовый файл — в отличие от использования print()
для вывода ее на экран. В отличие от отладоч- ного вывода вызов logging.debug()
очевидно показывает, какой вывод содержит отладочную информацию, а какой является результатом нормального выполнения программы. Подробнее об отладке вы можете прочитать в главе 11 книги «Automate the Boring Stuff with Python», 2nd edition (No Starch, 2019), доступной по адресу
https://autbor.com/2e/c11/.
1
Переменные с числовыми суффиксами
При написании программ вам может понадобиться набор переменных для хране- ния однотипных данных. И здесь возникает искушение повторно использовать
1
Свейгарт Э. Автоматизация рутинных задач с помощью Python. 2-е изд.
Классы, которые должны быть функциями или модулями
103
имя переменной, добавив к нему числовой суффикс. Например, если вы обраба- тываете форму ввода регистрационных данных, на которой пользователю пред- лагается дважды ввести пароль для предотвращения опечаток, две введенные строки можно сохранить в переменных с именами password1
и password2
. Эти числовые суффиксы не описывают, что содержат переменные и чем они отлича- ются. Также они не показывают, сколько всего таких переменных: существуют ли также переменные password3
или password4
? Попробуйте создавать разные имена, вместо того чтобы бездумно добавлять числовые суффиксы. В примере с паролями лучше использовать имена password и confirm_password
("пароль"
и "подтвердить пароль")
Другой пример: если у вас есть функция, которая получает две пары координат на плоскости, она может иметь параметры x1
, y1
, x2
и y2
. Но имена с числовыми суффиксами не передают такой информации, как имена start_x
, start_y
, end_x и end_y
. Также очевидно, что переменные start_x и start_y связаны друг с другом, чего не скажешь о x1
и y1
Если количество числовых суффиксов больше двух, стоит подумать об использо- вании структур: списка или множества для хранения данных в виде коллекции.
Например, значения pet1Name
, pet2Name
, pet3Name и т. д. можно хранить в списке с именем petNames
Эти замечания относятся не ко всем переменным, которые заканчиваются цифрой.
Например, вполне нормально иметь переменную с именем enableIPv6
, потому что 6 является частью имени собственного IPv6, а не числовым суффиксом. Но если вы используете числовые суффиксы для серии переменных, подумайте о том, чтобы заменить их структурой данных — например, списком или словарем.
Классы, которые должны быть функциями
или модулями
Программисты, работающие на таких языках, как Java, привыкли создавать классы для организации кода их программ. Например, возьмем класс
Dice с методом roll()
:
>>> import random
>>> class Dice:
... def __init__(self, sides=6):
... self.sides = sides
... def roll(self):
... return random.randint(1, self.sides)
...
>>> d = Dice()
>>> print('You rolled a', d.roll())
You rolled a 1
104
Глава 5.Поиск запахов в коде
Может показаться, что перед вами хорошо организованный код, но подумайте, что нам здесь действительно нужно: случайное число от 1 до 6. Стоит заменить весь класс простым вызовом функции:
>>> print('You rolled a', random.randint(1, 6))
You rolled a 6
По сравнению с другими языками Python использует свободный подход к органи- зации кода, потому что код не обязан существовать в классе или другой шаблонной структуре. Если вы обнаруживаете, что создаете объекты только для того, чтобы вызвать одну функцию, или пишете классы, содержащие только статические ме- тоды, возможно, стоит лучше написать функции.
В Python для группировки функций используются модули вместо классов. Так как классы все равно должны находиться в модуле, размещение этого кода в классах только добавляет в ваш код лишний организационный уровень. В главах 15–17 я рассмотрю принципы объектно-ориентированного проектирования более по- дробно. Джек Дидерих (Jack Diederich) в своем докладе «Перестаньте писать классы» на конференции PyCon 2012 рассказывает и о других возможностях из- быточного усложнения кода Python.
Списковые включения внутри
списковых включений
Списковые включения (list comprehensions) предоставляют компактный механизм создания сложных списковых значений. Например, чтобы создать список цифр в числах от 0 до 100, из которого исключены все числа, кратные 5, обычно исполь- зуют цикл for
:
>>> spam = []
>>> for number in range(100):
... if number % 5 != 0:
... spam.append(str(number))
...
>>> spam
['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']
Однако тот же список можно создать всего одной строкой кода с использованием синтаксиса спискового включения:
>>> spam = [str(number) for number in range(100) if number % 5 != 0]
>>> spam