ВУЗ: Не указан
Категория: Не указан
Дисциплина: Не указана
Добавлен: 03.12.2023
Просмотров: 391
Скачиваний: 1
ВНИМАНИЕ! Если данный файл нарушает Ваши авторские права, то обязательно сообщите нам.
СОДЕРЖАНИЕ
Как полюбить значимые отступы
117
.open()
у webbrowser имеют одинаковые имена, но это разные функции. Импор- тирование webbrowser не замещает встроенную функцию open()
, потому что две функции существуют в разных пространствах имен: встроенном пространстве имен и пространстве имен модуля webbrowser соответственно. Однако не забывайте, что плоское лучше вложенного: какими бы замечательными ни были пространства имен, создавать их следует только для предотвращения конфликтов имен, а не для добавления лишней систематизации.
Как и в отношении всех мнений в области программирования, вы можете возра- зить мне, что мои советы могут быть просто неактуальными для вашей ситуации.
Споры о том, как следует писать код или какой код считать питоническим, редко бывают такими плодотворными, как это кажется на первый взгляд. (Если только вы не собираете в книгу одни лишь субъективные мнения.)
Как полюбить значимые отступы
Самая распространенная претензия к Python, которую я слышу от программистов с опытом работы на других языках, — необычность и непривычность значимых
отступов (которые часто по ошибке называют значимыми пробелами). Количество отступов в начале строки кода имеет смысл в Python, потому что оно определяет, какие строки кода принадлежат тому же программному блоку.
Группировка блоков кода в Python при помощи отступов может показаться стран- ной, потому что в других языках блоки начинаются и завершаются фигурными скобками:
{
and
}
. Однако программисты с опытом работы на других языках тоже обычно снабжают свои блоки отступами, как и программисты на Python, чтобы их код лучше читался. Например, в Java значимых отступов нет. Программистам Java не нужно использовать отступы в блоках, но они обычно все равно делают это ради удобочитаемости. В следующем примере функция Java main()
содержит только один вызов функции println()
:
// Пример на Java public static void main(String[] args) {
System.out.println("Hello, world!");
}
Этот код Java вполне нормально работал бы и без отступов в строке println()
, потому что начало и конец блоков в Java отмечаются фигурными скобками, а не отступами. Python не разрешает применять отступы по вашему усмотрению, а за- ставляет программиста последовательно обеспечивать удобочитаемость кода.
Однако следует заметить, что в Python нет значимых пробелов, потому что Python не ограничивает использование пробельных символов вне отступов (и
2
+
2
, и
2+2
являются допустимыми выражениями Python).
118
Глава 6.Написание питонического кода
Некоторые программисты считают, что открывающая фигурная скобка должна быть в одной строке с открывающей командой, а другие полагают, что ее следует разме- щать в следующей строке. Программисты могут без конца спорить о преимуществах выбранного стиля. Python изящно обходит эту проблему, вообще отказавшись от фигурных скобок, чтобы программисты занимались более продуктивной работой.
Я бы предпочел, чтобы все языки программирования последовали примеру Python в области группировки блоков.
Но некоторые люди все равно привыкли к фигурным скобкам и требуют добавить их в будущую версию Python — при всей их непитоничности. Модуль Python
__future__
выполняет обратное импортирование функциональности в более ран- ние версии Python, но при попытке импортировать фигурные скобки в Python обнаруживается пасхалка — скрытое послание:
>>> from __future__ import braces
SyntaxError: not a chance
Я бы не рассчитывал на то, что фигурные скобки будут добавлены в Python в обо- зримом будущем.
Использование модуля timeit для оценки
быстродействия
«Предварительная оптимизация — корень всех зол», — говорил Дональд Кнут, зна- менитый ученый из Стэнфорда. Предварительная оптимизация, или оптимизация быстродействия программы до того, как вы будете располагать реальными данными о ее быстродействии, часто встречается тогда, когда программисты пытаются приме- нять хитроумные программные трюки для экономии памяти или ускорения работы кода. Например, некоторые программисты используют алгоритм XOR (исключающее
ИЛИ), для того чтобы поменять местами два целых числа в памяти без использо- вания третьей временной переменной (как предполагается, для экономии памяти):
>>> a, b = 42, 101 # Создание двух переменных.
>>> print(a, b)
42 101
>>> a = a ^ b
>>> b = a ^ b
>>> a = a ^ b
>>> print(a, b) # Значения поменялись местами.
101 42
Если вы не знакомы с алгоритмом XOR (использующим поразрядный оператор
XOR
^
; о нем можно узнать по адресу https://ru.wikipedia.org/wiki/XOR-обмен), этот код выглядит загадочно. Проблема хитроумных трюков в том, что они приводят
Использование модуля timeit для оценки быстродействия
119
к созданию запутанного, нечитаемого кода. Как говорится в тезисах «Дзен Python»,
«удобочитаемость важна».
Что еще хуже, ваш хитроумный трюк может оказаться не таким уж хитрым и умным.
Нельзя бездоказательно полагать, что он поможет; единственный способ это узнать — измерить и сравнить время выполнения. Вспомните принцип «Дзена Python» —
«столкнувшись с неоднозначностью, боритесь с искушением угадать». Профилирова-
нием называется систематический анализ скорости, использования памяти и других характеристик программы. Модуль timeit стандартной библиотеки Python может профилировать скорость выполнения способом, управляющим основными искажа- ющими факторами, из-за чего данные получаются более точными, чем при простой регистрации времени запуска и завершения программы. Не пишите собственный непитонический код профилирования, который может выглядеть примерно так:
import time startTime = time.time() # Сохранение времени запуска.
for i in range(1000000): # Код выполняется 1 000 000 раз.
a, b = 42, 101
a = a ^ b b = a ^ b a = a ^ b print(time.time() - startTime, 'seconds') # Вычисление затраченного времени.
На моей машине при выполнении этого кода выводится следующий результат:
0.28080058097839355 seconds
Вместо этого можно передать код Python в строковом аргументе функции timeit.
timeit()
; функция сообщает среднее время, затраченное на выполнение строки кода 1 000 000 раз. Если вы хотите проверить фрагмент из нескольких строк кода, разделите их символом
;
:
>>> timeit.timeit('a, b = 42, 101; a = a ^ b; b = a ^ b; a = a ^ b')
0.19474047500000324
На моем компьютере выполнение этого кода заняло приблизительно 1/5 секунды.
Насколько это быстро? Сравним с кодом перестановки целых чисел, использующим третью временную переменную:
>>> timeit.timeit('a, b = 42, 101; temp = a; a = b; b = temp')
0.09632604099999753
Сюрприз! Код с третьей временной переменной не только лучше читается, но и вы- полняется почти вдвое быстрее метода XOR! «Умный» способ экономит несколько байтов памяти, но за счет скорости и удобочитаемости кода. Если только вы не пишете программы для крупного центра обработки данных, жертвовать удобочи- таемостью для экономии нескольких байтов памяти или наносекунд процессорного
120
Глава 6.Написание питонического кода времени не стоит. В конце концов, память стоит дешево, и только на чтение этого абзаца вы потратили миллиарды наносекунд.
Что еще лучше, значения двух переменных можно поменять местами при помощи трюка множественного присваивания, также называемого итерируемой распаковкой:
>>> timeit.timeit('a, b = 42, 101; a, b = b, a')
0.08467035000001033
Этот код получается не только самым удобочитаемым, но и самым быстрым! И мы знаем это не потому, что предположили, но и потому, что провели объективные измерения.
Функция time.time()
также может получить второй строковый аргумент с кодом настройки. Код настройки выполняется однократно до кода первой строки для выполнения любой необходимой подготовки. Этот код может импортировать какой-нибудь модуль, присвоить начальное значение переменной или выполнить другое необходимое действие. Также можно изменить количество испытаний по умолчанию; для этого следует передать целое число в ключевом аргументе number
Например, следующий тест измеряет время, необходимое модулю Python random для генерирования 10 000 000 случайных чисел от 1 до 100. (На моей машине для этого требуется около 10 секунд.)
>>> timeit.timeit('random.randint(1, 100)', 'import random', number=10000000)
10.020913950999784
Хорошее правило для программистов: сначала заставьте свой код работать, а потом переходите к оптимизации. Когда у вас уже имеется работоспособная программа, тогда и стоит браться за повышение ее эффективности.
Неправильное использование синтаксиса
Если Python — не первый ваш язык программирования, то при написании кода
Python вы можете использовать стратегии, знакомые вам по другим языкам.
А может, вы изобрели необычный способ написания кода Python, потому что не знали, что существуют общепринятые практики. Ваш неуклюжий код работает, но изучение стандартных способов написания питонического кода сэкономит время и усилия. В этом разделе я объясняю неправильные решения, используемые про- граммистами, и то, как должен выглядеть правильно написанный код.
Использование enumerate() вместо range()
При переборе списка или другой последовательности некоторые программисты используют функции range()
и len()
для генерирования целых индексов от 0 до длины последовательности (не включая последнее значение). В таких циклах for
Неправильное использование синтаксиса
121
часто используется переменная с именем i
(сокращение от index
). Например, вве- дите следующий непитонический пример в интерактивной оболочке:
>>> animals = ['cat', 'dog', 'moose']
>>> for i in range(len(animals)):
... print(i, animals[i])
0 cat
1 dog
2 moose
Идиома range(len())
прямолинейна, но далеко не идеальна, потому что она плохо читается. Вместо этого лучше передать список или последовательность встроенной функции enumerate()
, которая возвращает целочисленный индекс и элемент с этим индексом. Например, можно написать питонический код следующего вида:
>>> # Пример питонического кода
>>>
1 ... 7 8 9 10 11 12 13 14 ... 40
animals = ['cat', 'dog', 'moose']
>>> for i, animal in enumerate(animals):
... print(i, animal)
0 cat
1 dog
2 moose
Код с enumerate()
получается чуть более чистым, чем код с range(len())
. Если вам нужны только элементы, но не индексы, все равно можно напрямую перебрать список питоническим способом:
>>> # Пример питонического кода
>>> animals = ['cat', 'dog', 'moose']
>>> for animal in animals:
... print(animal)
cat dog moose
Вызов enumerate()
с прямым перебором последовательности предпочтительнее использования устаревшей схемы range(len())
Использование команды with вместо open() и close()
Функция open()
возвращает объект файла, содержащий методы для чтения и запи си в файл. После завершения работы метод close()
объекта файла делает файл до- ступным для чтения и записи со стороны других программ. Эти функции можно использовать по отдельности, но такой подход не соответствует питоническому стилю. Например, введите следующий фрагмент в интерактивной оболочке, чтобы вывести текст «Hello, world!» в файл с именем spam.txt
:
122
Глава 6.Написание питонического кода
>>> # Пример непитонического кода
>>> fileObj = open('spam.txt', 'w')
>>> fileObj.write('Hello, world!')
13
>>> fileObj.close()
Такой код может привести к тому, что файл останется открытым — скажем, если в блоке try возникнет ошибка и вызов close()
будет пропущен. Пример:
>>> # Пример непитонического кода
>>> try:
... fileObj = open('spam.txt', 'w')
... eggs = 42 / 0 # Здесь происходит ошибка деления на нуль.
... fileObj.close() # Эта строка никогда не выполняется.
... except:
... print('Some error occurred.')
Some error occurred.
При возникновении ошибки деления на нуль выполнение передается в блок except
, вызов close()
пропускается, а файл останется открытым. Позднее это может при- вести к повреждению структуры файла, причины которой будет трудно связать с блоком try
Но можно воспользоваться командой with
, чтобы функция close()
автоматически вызывалась при выходе управления из блока with
. Следующий питонический при- мер делает то же самое, что и первый пример в этом разделе:
>>> # Пример питонического кода.
>>> with open('spam.txt', 'w') as fileObj:
... fileObj.write('Hello, world!')
Несмотря на отсутствие явного вызова close()
, команда with обязательно вызовет ее при выходе управления за пределы блока.
Использование is для сравнения с None вместо ==
Оператор
==
сравнивает значения двух объектов, тогда как оператор is сравнивает два объекта на тождественность. Различия между равенством и тождественно- стью объясняются в главе 7. Два объекта могут хранить одинаковые значения, но если это два разных объекта, они не тождественны. Однако при сравнении значения с
None практически всегда следует использовать оператор is вместо оператора
==
В некоторых случаях выражение spam
==
None может дать результат
True
, даже если spam всего лишь содержит
None
. Это может произойти из-за перегрузки оператора
==
Форматирование строк
123
(эта тема более подробно рассматривается в главе 17). Но выражение spam is
None про- верит, является ли значение, хранящееся в переменной spam
, буквальным значением
None
. Так как
None является единственным значением типа данных
NoneType
, в любой программе Python существует только один объект
None
. Если переменной присвоено значение
None
, сравнение is
None всегда дает результат
True
. Особенности перегрузки оператора
==
рассматриваются в главе 17, но ниже приведен пример такого кода:
>>> class SomeClass:
... def __eq__(self, other):
... if other is None:
... return True
>>> spam = SomeClass()
>>> spam == None
True
>>> spam is None
False
Вероятность того, что класс перегружает оператор
==
подобным образом, невелика, но в Python стало идиоматичным всегда использовать is
None вместо
==
None про- сто на всякий случай.
Наконец, оператор is не следует использовать со значениями
True и
False
. Оператор проверки равенства
==
может использоваться для сравнения значения с
True или
False
(например, spam
==
True или spam
==
False
). Еще чаще оператор и логическое значение полностью опускаются, а код записывается в виде if spam:
или if not spam:
вместо if spam
==
True:
или if spam
==
False:
Форматирование строк
Строки встречаются почти во всех компьютерных программах независимо от языка.
Это весьма распространенный тип данных, и не приходится удивляться тому, что существует множество подходов к выполнению строковых операций и формати- рованию строк. В этом разделе я показываю пару приемов.
Использование необработанных строк, если строка содержит
много символов \ (обратный слэш)
Escape-символы позволяют вставлять в строковые литералы текст, который в про- тивном случае было бы невозможно включить
1
. Например, в строку 'Zophie\'s
1
Escape-последовательности, то есть последовательности, которые начинаются с симво- ла «\», за которым следует один или более символов, также называют экранированными последовательностями. — Примеч. ред.
124
Глава 6.Написание питонического кода chair'
необходимо включить символ
\
, чтобы Python интерпретировал вторую кавычку как часть строки, а не как символ, завершающий конец строки. Так как символ
\
является escape-символом, если вы хотите включить литеральный сим- вол
\
в строку, его необходимо ввести в виде
\\
Необработанные (raw) строки представляют собой строковые литералы с префик- сом r
; они не интерпретируют символы
\
как escape-символы. Вместо этого в строку просто включается символ
\
. Например, следующая строка с путем к файлу Windows должна содержать множество экранированных символов
\
, что не соответствует питоническому стилю:
>>> # Пример непитонического кода
>>> print('The file is in C:\\Users\\Al\\Desktop\\Info\\Archive\\Spam')
The file is in C:\Users\Al\Desktop\Info\Archive\Spam
Необработанная строка генерирует то же строковое значение, но читается гораздо лучше:
>>> # Пример питонического кода
>>> print(r'The file is in C:\Users\Al\Desktop\Info\Archive\Spam')
The file is in C:\Users\Al\Desktop\Info\Archive\Spam
Необработанные строки не определяют другой тип строковых данных; это все- го лишь удобный способ записи строковых литералов, содержащих несколько внутренних символов
\
. Необработанные строки часто используются для записи регулярных выражений или путей к файлам Windows с внутренними символами
\
, которые было бы слишком неудобно экранировать по отдельности символами
\\
Форматирование с использованием F-строк
Строковой интерполяцией называется процесс создания строк, включающих другие строки; интерполяция имеет долгую историю в Python. Сначала для конкатенации строк можно было использовать оператор
+
, но это приводило к появлению кода с множеством кавычек и плюсов:
'Hello,
'
+
name
+
'.
Today is
'
+
day
+
'
and it is
'
+
weather
+
'.'
. Спецификатор преобразования
%
несколько упростил этот синтак- сис:
'Hello,
%s.
Today is
%s and it is
%s.'
%
(name,
day,
weather)
. В обоих случаях строки из переменных name,
day и weather вставляются в строковые литералы для генерирования нового строкового значения:
'Hello,
Al.
Today is
Sunday and it is sunny.'
Строковый метод format()
добавляет мини-язык форматных спецификаций
(https://docs.python.org/3/library/string.html#formatspec), в котором пары фигурных скобок
{}
используются способом, напоминающим спецификатор формата
%s
. Тем не менее это довольно запутанный способ, который может привести к созданию нечитаемого кода, поэтому использовать его я не рекомендую.