Часто задаваемые вопросы о проектировании и истории
- Почему в Python для группировки инструкций используются отступы?
- Почему простые арифметические операции дают странные результаты?
- Почему вычисления с плавающей точкой так неточны?
- Почему строки Python неизменяемы?
- Почему в определениях и вызовах методов нужно явно использовать ‘self’?
- Почему нельзя использовать присваивание в выражении?
- Почему в Python для одних операций используются методы (например, list.index()), а для других — функции (например, len(list))?
- Почему join() — это метод строки, а не списка или кортежа?
- Насколько быстры исключения?
- Почему в Python нет инструкции switch или case?
- Нельзя ли эмулировать потоки в интерпретаторе вместо того, чтобы полагаться на специфичную для ОС реализацию потоков?
- Почему лямбда-выражения не могут содержать инструкции?
- Можно ли компилировать Python в машинный код, C или какой-либо другой язык?
- Как Python управляет памятью?
- Почему CPython не использует более традиционную схему сборки мусора?
- Почему при завершении работы CPython освобождается не вся память?
- Почему для кортежей и списков существуют отдельные типы данных?
- Как реализованы списки в CPython?
- Как реализованы словари в CPython?
- Почему ключи словарей должны быть неизменяемыми?
- Почему list.sort() не возвращает отсортированный список?
- Как в Python задать и обеспечить соблюдение спецификации интерфейса?
- Почему нет goto?
- Почему необработанные строки (r-строки) не могут заканчиваться обратной косой чертой?
- Почему в Python нет инструкции «with» для присваивания атрибутам?
- Почему генераторы не поддерживают инструкцию with?
- Почему после инструкций if/while/def/class обязательно ставить двоеточие?
- Почему Python разрешает ставить запятые в конце списков и кортежей?
Почему в Python для группировки инструкций используются отступы?
Гвидо ван Россум считает, что использование отступов для группировки — чрезвычайно элегантное решение, которое значительно повышает ясность типичной программы на Python. Со временем большинству людей начинает нравиться эта особенность.
Поскольку в языке нет открывающих и закрывающих скобок, между группировкой, предполагаемой синтаксическим анализатором, и той, которую видит читатель, не может возникнуть разногласий. Иногда программисты на C встречают фрагмент кода вроде такого:
if (x <= y)
x++;
y--;
z++;
Если условие истинно, выполняется только инструкция x++, но отступы заставляют многих думать иначе. Даже опытные программисты на C иногда подолгу смотрят на этот код, недоумевая, почему y уменьшается даже для x > y.
Поскольку в языке нет открывающих и закрывающих скобок, в Python гораздо реже возникают конфликты из-за стиля написания кода. В C существует множество способов расставить фигурные скобки. Привыкнув читать и писать код в определённом стиле, вполне естественно испытывать некоторую неловкость, читая код в другом стиле (или будучи вынужденным писать в нём).
Во многих стилях открывающие и закрывающие скобки ставят на отдельной строке. Это значительно удлиняет программы и расходует ценное пространство экрана, затрудняя получение общего представления о программе. В идеале функция должна помещаться на одном экране (скажем, занимать 20–30 строк). 20 строк Python могут выполнить гораздо больше работы, чем 20 строк C. Причина не только в отсутствии открывающих и закрывающих скобок — свою роль также играют отсутствие объявлений и высокоуровневые типы данных, — но синтаксис, основанный на отступах, безусловно, помогает.
Почему простые арифметические операции дают странные результаты?
См. следующий вопрос.
Почему вычисления с плавающей точкой так неточны?
Пользователей часто удивляют такие результаты:
>>> 1.2 - 1.0 0.19999999999999996
и они думают, что это ошибка Python. Но это не так. Дело почти не в Python, а в том, как базовая платформа обрабатывает числа с плавающей точкой.
Тип float в CPython использует для хранения тип C double. Значение объекта float хранится в двоичном формате с плавающей точкой и фиксированной точностью (обычно 53 бита), а для выполнения операций с плавающей точкой Python использует операции C, которые, в свою очередь, опираются на аппаратную реализацию процессора. Это означает, что в отношении операций с плавающей точкой Python ведёт себя так же, как многие популярные языки, в том числе C и Java.
Многие числа, которые легко записать в десятичной системе счисления, невозможно точно представить в двоичном формате с плавающей точкой. Например, после выполнения:
>>> x = 1.2
хранящееся значение x является (очень хорошим) приближением десятичного значения 1.2, но не равно ему точно. На типичной машине фактически хранящееся значение имеет вид:
1.0011001100110011001100110011001100110011001100110011 (binary)
что в точности равно:
1.1999999999999999555910790149937383830547332763671875 (decimal)
Типичная точность в 53 бита обеспечивает числам с плавающей точкой Python точность в 15–16 десятичных знаков.
Более подробное объяснение см. в главе арифметика с плавающей точкой руководства по Python.
Почему строки Python неизменяемы?
У этого есть несколько преимуществ.
Одно из них — производительность: если строка неизменяема, для неё можно выделить память при создании, а требования к объёму хранилища будут фиксированными и неизменными. Это также одна из причин различия между кортежами и списками.
Другое преимущество состоит в том, что строки в Python считаются такими же «элементарными», как числа. Никакие действия не изменят значение 8 на что-либо другое, и в Python никакие действия не изменят строку «eight» на что-либо другое.
Почему в определениях и вызовах методов нужно явно использовать ‘self’?
Эта идея заимствована из Modula-3. Она оказалась очень полезной по целому ряду причин.
Во-первых, так очевиднее, что вы используете метод или атрибут экземпляра, а не локальную переменную. Читая self.x или self.meth(), вы совершенно точно понимаете, что используется переменная экземпляра или метод, даже если не знаете наизусть определение класса. В C++ это можно определить по отсутствию объявления локальной переменной (если предположить, что глобальные переменные встречаются редко или их легко распознать), но в Python объявлений локальных переменных нет, поэтому для уверенности пришлось бы искать определение класса. Некоторые стандарты написания кода на C++ и Java требуют, чтобы перед атрибутами экземпляра стоял префикс m_, поэтому такая явная запись полезна и в этих языках.
Во-вторых, благодаря этому не нужен специальный синтаксис, если вы хотите явно обратиться к методу конкретного класса или вызвать его. В C++, если вы хотите использовать метод базового класса, переопределённый в производном классе, нужно применить оператор :: — в Python можно написать baseclass.methodname(self, <argument list>). Это особенно полезно для методов __init__() и вообще в случаях, когда метод производного класса должен расширить метод базового класса с тем же именем и поэтому каким-либо образом вызвать метод базового класса.
Наконец, в случае переменных экземпляра это решает синтаксическую проблему, связанную с присваиванием: поскольку локальными переменными в Python по определению являются переменные, которым присваивается значение в теле функции (и которые явно не объявлены глобальными), нужен способ сообщить интерпретатору, что присваивание должно изменить переменную экземпляра, а не локальную переменную; желательно, чтобы этот способ был синтаксическим (из соображений эффективности). В C++ это обеспечивается объявлениями, но в Python объявлений нет, и было бы жаль вводить их только ради этой цели. Явное использование self.var хорошо решает эту задачу. Аналогично, при обращении к переменным экземпляра необходимость писать self.var означает, что при поиске неквалифицированных имён внутри метода не нужно просматривать пространства имён экземпляра. Иными словами, локальные переменные и переменные экземпляра находятся в двух разных пространствах имён, и нужно указать Python, какое из них использовать.
Почему нельзя использовать присваивание в выражении?
Начиная с Python 3.8 — можно!
Выражения присваивания с оператором моржа := присваивают значение переменной внутри выражения:
while chunk := fp.read(200): print(chunk)
Дополнительные сведения см. в PEP 572.
Почему в Python для одних операций используются методы (например, list.index()), а для других — функции (например, len(list))?
Вот что сказал Гвидо:
(a) Для некоторых операций префиксная запись просто читается лучше постфиксной: префиксные (и инфиксные!) операции имеют давнюю традицию в математике, где предпочитают обозначения, наглядность которых помогает математику обдумывать задачу. Сравните, насколько легко преобразовать формулу x*(a+b) в x*a + x*b и насколько неудобно сделать то же самое с помощью обычной объектно-ориентированной записи.
(b) Читая код len(x), я знаю, что он запрашивает длину чего-либо. Это сообщает мне две вещи: результат — целое число, а аргумент — контейнер некоторого типа. Напротив, читая x.len(), я должен заранее знать, что x — это какой-то контейнер, реализующий интерфейс или наследующий класс, в котором есть стандартный len(). Вспомните, какое недоумение у нас иногда возникает, когда у класса, не реализующего отображение, есть метод get() или keys(), или когда у объекта, который не является файлом, есть метод write().
—https://mail.python.org/pipermail/python-3000/2006-November/004643.html
Почему join() — это метод строки, а не списка или кортежа?
Начиная с Python 1.6 строки стали гораздо больше похожи на другие стандартные типы: тогда были добавлены методы с той же функциональностью, которая прежде всегда была доступна через функции модуля string. Большинство этих новых методов получили широкое признание, но один из них, похоже, вызывает дискомфорт у некоторых программистов:
", ".join(['1', '2', '4', '8', '16'])
в результате получается:
"1, 2, 4, 8, 16"
Против такого использования обычно приводят два аргумента.
Первый звучит примерно так: «Выглядит очень некрасиво — вызывать метод у строкового литерала (строковой константы)». Ответ на это: возможно, но строковый литерал — всего лишь фиксированное значение. Если разрешить вызов методов у имён, связанных со строками, нет никаких логических причин запрещать его для литералов.
Второе возражение обычно формулируют так: «На самом деле я прошу последовательность объединить её элементы с помощью строковой константы». К сожалению, это не так. По какой-то причине гораздо меньше затруднений вызывает split() как метод строки, поскольку в этом случае легко понять, что
"1, 2, 4, 8, 16".split(", ")
— это инструкция строковому литералу вернуть подстроки, разделённые указанным разделителем (или, по умолчанию, произвольными последовательностями пробельных символов).
join() — метод строки, потому что при его использовании вы просите строку-разделитель перебрать последовательность строк и вставить себя между соседними элементами. Этот метод можно использовать с любым аргументом, который соответствует правилам для объектов-последовательностей, в том числе с любыми классами, которые вы определите сами. Аналогичные методы есть у объектов bytes и bytearray.
Насколько быстры исключения?
Блок try/except работает чрезвычайно эффективно, если исключения не возникают. А вот перехват исключения обходится дорого. В версиях Python до 2.0 часто использовался такой идиоматический приём:
try:
value = mydict[key]
except KeyError:
mydict[key] = getvalue(key)
value = mydict[key]
Это имело смысл только тогда, когда вы почти всегда ожидали, что в словаре будет нужный ключ. Если это было не так, код писали следующим образом:
if key in mydict:
value = mydict[key]
else:
value = mydict[key] = getvalue(key)
В этом конкретном случае можно также использовать value = dict.setdefault(key,
getvalue(key)), но только если вызов getvalue() достаточно дешёвый, поскольку он выполняется в любом случае.
Почему в Python нет инструкции switch или case?
Как правило, структурированные инструкции switch выполняют один блок кода, когда выражение имеет определённое значение или одно из заданных значений. Начиная с Python 3.10 можно легко сопоставлять литеральные значения или константы в пространстве имён с помощью инструкции match ... case. Более старый способ — последовательность инструкций if... elif... elif... else.
Если нужно выбирать из очень большого количества вариантов, можно создать словарь, сопоставляющий значения вариантов с вызываемыми функциями. Например:
functions = {'a': function_1,
'b': function_2,
'c': self.method_1}
func = functions[value]
func()
Для вызова методов объектов можно сделать ещё проще, используя встроенную функцию getattr(), которая позволяет получить методы с заданным именем:
class MyVisitor:
def visit_a(self):
...
def dispatch(self, value):
method_name = 'visit_' + str(value)
method = getattr(self, method_name)
method()
Рекомендуется использовать префикс в именах методов, например visit_ в этом примере. Без такого префикса, если значения поступают из ненадёжного источника, злоумышленник сможет вызвать любой метод вашего объекта.
Имитация switch с проваливанием к следующему случаю, как в конструкции switch-case-default языка C, возможна, но гораздо сложнее и требуется реже.
Нельзя ли эмулировать потоки в интерпретаторе вместо того, чтобы полагаться на специфичную для ОС реализацию потоков?
Ответ 1: К сожалению, для каждого кадра стека Python интерпретатор добавляет как минимум один кадр стека C. Кроме того, расширения могут вызывать Python обратно почти в любой момент. Поэтому для полноценной реализации потоков необходима поддержка потоков в C.
Ответ 2: К счастью, существует Stackless Python — его цикл интерпретатора полностью переработан и не использует стек C.
Почему лямбда-выражения не могут содержать инструкции?
Лямбда-выражения Python не могут содержать инструкции, потому что синтаксическая система Python не поддерживает вложенные в выражения инструкции. Однако в Python это не представляет серьёзной проблемы. В отличие от лямбда-форм в других языках, которые добавляют функциональность, лямбда-выражения Python — это лишь сокращённая запись для тех, кому лень определять функцию.
Функции в Python и так являются объектами первого класса, и их можно объявлять в локальной области видимости. Поэтому единственное преимущество использования лямбда-выражения вместо локально определённой функции состоит в том, что не нужно придумывать для функции имя. Но это всего лишь локальная переменная, которой присваивается объект-функция (точно такой же тип объекта, какой возвращает лямбда-выражение)!
Можно ли компилировать Python в машинный код, C или какой-либо другой язык?
Cython компилирует модифицированную версию Python с необязательными аннотациями в расширения C. Nuitka — перспективный компилятор Python в код C++, который стремится поддерживать весь язык Python.
Как Python управляет памятью?
Подробности управления памятью в Python зависят от реализации. Стандартная реализация Python — CPython — использует подсчёт ссылок для обнаружения недоступных объектов и ещё один механизм для сбора циклических ссылок: периодически выполняется алгоритм обнаружения циклов, который ищет недоступные циклы и удаляет входящие в них объекты. Модуль gc предоставляет функции для запуска сборки мусора, получения статистики отладки и настройки параметров сборщика.
Однако другие реализации (например, Jython или PyPy) могут использовать другой механизм, например полноценный сборщик мусора. Это различие может привести к тонким проблемам при переносе, если ваш код Python зависит от поведения реализации с подсчётом ссылок.
В некоторых реализациях Python следующий код (который корректно работает в CPython), вероятно, исчерпает файловые дескрипторы:
for file in very_long_list_of_files:
f = open(file)
c = f.read(1)
Действительно, благодаря подсчёту ссылок и схеме деструкторов CPython каждое новое присваивание переменной f закрывает предыдущий файл. Однако при традиционном сборщике мусора эти файловые объекты будут собраны (и закрыты) лишь через разные, возможно длительные промежутки времени.
Если вы хотите писать код, который будет работать в любой реализации Python, явно закрывайте файл или используйте инструкцию with; это будет работать независимо от схемы управления памятью:
for file in very_long_list_of_files:
with open(file) as f:
c = f.read(1)
Почему CPython не использует более традиционную схему сборки мусора?
Во-первых, это не является частью стандарта C и потому непереносимо. (Да, мы знаем о библиотеке сборки мусора Boehm. Она содержит фрагменты кода на ассемблере для большинства распространённых платформ, но не для всех; и, хотя она в основном прозрачна, полной прозрачности нет — чтобы Python мог с ней работать, требуются исправления.)
Традиционный сборщик мусора также создаёт проблемы, когда Python встраивают в другие приложения. В автономном Python заменить стандартные malloc() и free() версиями из библиотеки сборщика мусора вполне допустимо, но приложению, в которое встроен Python, может потребоваться собственная замена malloc() и free(), и оно может не захотеть использовать замену из Python. Сейчас CPython работает с любыми реализациями, которые корректно реализуют malloc() и free().
Почему при завершении работы CPython освобождается не вся память?
Объекты, на которые ссылаются глобальные пространства имён модулей Python, не всегда освобождаются при завершении работы Python. Это может происходить при наличии циклических ссылок. Кроме того, некоторые области памяти выделяются библиотекой C и не могут быть освобождены (например, на них будет жаловаться такой инструмент, как Purify). Тем не менее Python активно очищает память при завершении работы и пытается уничтожить каждый объект.
Если вы хотите, чтобы Python гарантированно удалял определённые объекты при освобождении памяти, воспользуйтесь модулем atexit, чтобы запустить функцию, которая принудительно выполнит это удаление.
Почему для кортежей и списков существуют отдельные типы данных?
Хотя списки и кортежи во многом похожи, обычно их используют совершенно по-разному. Кортежи можно считать аналогами записей Pascal или структур C: это небольшие наборы связанных данных, которые могут иметь разные типы и обрабатываются как единое целое. Например, декартовы координаты уместно представить кортежем из двух или трёх чисел.
Списки, напротив, больше похожи на массивы в других языках. Обычно они содержат переменное количество объектов одного типа, которые обрабатываются по одному. Например, os.listdir('.') возвращает список строк с именами файлов в текущем каталоге. Функции, работающие с этим результатом, обычно продолжат работать корректно, даже если в каталог добавить ещё один или два файла.
Кортежи неизменяемы: после создания кортежа нельзя заменить ни один из его элементов новым значением. Списки изменяемы, то есть их элементы можно менять в любое время. Ключами словарей могут быть только неизменяемые элементы, поэтому в качестве ключей можно использовать кортежи, но не списки.
Как реализованы списки в CPython?
Списки CPython на самом деле представляют собой массивы переменной длины, а не связные списки в стиле Lisp. Реализация использует непрерывный массив ссылок на другие объекты и хранит указатель на этот массив и его длину в структуре заголовка списка.
Благодаря этому индексирование списка a[i] выполняется за время, не зависящее от размера списка или значения индекса.
При добавлении или вставке элементов массив ссылок изменяет размер. Для повышения производительности при многократном добавлении элементов применяются некоторые хитрости: когда массив приходится увеличивать, выделяется дополнительное пространство, чтобы следующие несколько операций не требовали фактического изменения размера.
Как реализованы словари в CPython?
Словари CPython реализованы в виде хеш-таблиц с изменяемым размером. По сравнению с B-деревьями это обеспечивает более высокую производительность поиска (безусловно, самой распространённой операции) в большинстве случаев, а сама реализация проще.
Для работы словарей каждому хранящемуся в них ключу вычисляется хеш-код с помощью встроенной функции hash(). Хеш-код сильно зависит от ключа и от начального значения, задаваемого для каждого процесса; например, для «Python» хеш может быть равен -539294296, а для «python» — строки, отличающейся всего одним битом, — 1142331976. Затем хеш-код используется для вычисления позиции во внутреннем массиве, где будет храниться значение. Если у всех сохраняемых ключей разные хеш-значения, то для поиска ключа словарю требуется постоянное время — O(1) в нотации «О большое».
Почему ключи словарей должны быть неизменяемыми?
Реализация словарей на основе хеш-таблиц использует хеш-значение, вычисленное по значению ключа, чтобы найти этот ключ. Если бы ключ был изменяемым объектом, его значение могло бы измениться, а вместе с ним изменился бы и его хеш. Но тот, кто изменил объект-ключ, не знает, что он используется в качестве ключа словаря, поэтому не может переместить соответствующую запись в другое место словаря. В результате при попытке найти тот же объект в словаре он не будет найден, поскольку его хеш-значение изменилось. Старое значение тоже не найдётся, так как значение объекта, находящегося в этой корзине хеш-таблицы, будет другим.
Если вы хотите использовать список в качестве ключа словаря, просто сначала преобразуйте его в кортеж: функция tuple(L) создаёт кортеж с теми же элементами, что и список L. Кортежи неизменяемы, поэтому их можно использовать в качестве ключей словаря.
Некоторые неприемлемые решения, которые предлагались:
-
Вычислять хеш списков по их адресу (идентификатору объекта). Это не работает, потому что новый список с тем же значением не будет найден; например:
mydict = {[1, 2]: '12'} print(mydict[[1, 2]])вызовет исключение
KeyError, поскольку идентификатор[1, 2], использованного во второй строке, отличается от идентификатора в первой строке. Другими словами, ключи словаря нужно сравнивать с помощью==, а не с помощьюis. - Создавать копию списка, если он используется в качестве ключа. Это не работает, потому что список, будучи изменяемым объектом, может содержать ссылку на самого себя, и тогда код копирования зациклится.
- Разрешить использовать списки в качестве ключей, но предупредить пользователя, чтобы он их не изменял. Это привело бы к трудноотслеживаемым ошибкам в программах, если бы вы забыли об этом или случайно изменили список. Кроме того, это нарушило бы важный инвариант словарей: каждое значение в
d.keys()можно использовать в качестве ключа словаря. - Помечать списки как доступные только для чтения после использования в качестве ключа словаря. Проблема в том, что измениться может не только значение объекта верхнего уровня: в качестве ключа можно использовать кортеж, содержащий список. Добавление чего-либо в словарь в качестве ключа потребовало бы пометки всех достижимых из него объектов как доступных только для чтения; и, опять же, самоссылочные объекты могли бы привести к бесконечному циклу.
Если вам это необходимо, можно применить одну хитрость, но на свой страх и риск: оберните изменяемую структуру в экземпляр класса, у которого есть методы __eq__() и __hash__(). Затем необходимо следить за тем, чтобы хеш-значение всех таких объектов-обёрток, находящихся в словаре (или другой структуре на основе хеширования), оставалось неизменным, пока объект находится в словаре (или другой структуре).
class ListWrapper:
def __init__(self, the_list):
self.the_list = the_list
def __eq__(self, other):
return self.the_list == other.the_list
def __hash__(self):
l = self.the_list
result = 98767 - len(l)*555
for i, el in enumerate(l):
try:
result = result + (hash(el) % 9999999) * 1001 + i
except Exception:
result = (result % 7777777) + i * 333
return result
Обратите внимание, что вычисление хеша усложняется вероятностью того, что некоторые элементы списка могут быть нехешируемыми, а также возможностью арифметического переполнения.
Кроме того, всегда должно выполняться условие: если o1 == o2 (то есть o1.__eq__(o2)
is True), то hash(o1) == hash(o2) (то есть o1.__hash__() == o2.__hash__()), независимо от того, находится объект в словаре или нет. Если не соблюдать эти ограничения, словари и другие структуры на основе хеширования будут работать некорректно.
В случае ListWrapper, пока объект-обёртка находится в словаре, обёрнутый список не должен изменяться — иначе возникнут аномалии. Не делайте этого, если не готовы тщательно обдумать требования и последствия их несоблюдения. Считайте, что мы вас предупредили.
Почему list.sort() не возвращает отсортированный список?
В ситуациях, когда важна производительность, создание копии списка только ради его сортировки было бы расточительным. Поэтому list.sort() сортирует список на месте. Чтобы напомнить об этом, метод не возвращает отсортированный список. Так вы не перезапишете случайно список, если вам нужна отсортированная копия, но при этом необходимо сохранить исходный вариант.
Если нужно вернуть новый список, воспользуйтесь встроенной функцией sorted(). Эта функция создаёт новый список из передаваемого итерируемого объекта, сортирует его и возвращает результат. Например, вот как можно перебрать ключи словаря в отсортированном порядке:
for key in sorted(mydict):
... # do whatever with mydict[key]...
Как задать и обеспечить соблюдение спецификации интерфейса в Python?
Спецификация интерфейса модуля, предоставляемая такими языками, как C++ и Java, описывает прототипы методов и функций модуля. Многие считают, что проверка спецификаций интерфейсов во время компиляции помогает при создании больших программ.
В Python 2.6 появился модуль abc, позволяющий определять абстрактные базовые классы (ABC). Затем можно использовать isinstance() и issubclass(), чтобы проверить, реализует ли экземпляр или класс определённый ABC. Модуль collections.abc определяет набор полезных ABC, таких как Iterable, Container и MutableMapping.
В Python многих преимуществ спецификаций интерфейсов можно добиться с помощью подходящей методики тестирования компонентов.
Хороший набор тестов для модуля может одновременно служить регрессионным тестом, спецификацией интерфейса модуля и набором примеров. Многие модули Python можно запустить как скрипт для выполнения простой «самопроверки». Даже модули, использующие сложные внешние интерфейсы, часто можно тестировать изолированно с помощью простых эмуляций-заглушек внешнего интерфейса. Модули doctest и unittest или сторонние платформы тестирования можно использовать для создания исчерпывающих наборов тестов, проверяющих каждую строку кода в модуле.
Подходящая методика тестирования может помочь создавать крупные сложные приложения на Python не хуже, чем спецификации интерфейсов. Более того, она может оказаться лучше, поскольку спецификация интерфейса не способна проверить некоторые свойства программы. Например, предполагается, что метод list.append() добавляет новые элементы в конец некоторого внутреннего списка; спецификация интерфейса не может проверить, действительно ли ваша реализация list.append() делает это правильно, но проверить это свойство в наборе тестов очень просто.
Написание наборов тестов очень полезно, и, возможно, вам стоит проектировать код так, чтобы его было легко тестировать. Один из всё более популярных подходов — разработка через тестирование — предполагает, что части набора тестов пишутся до создания самого кода. Конечно, Python позволяет небрежно подойти к делу и вовсе не писать тесты.
Почему нет goto?
В 1970-х годах люди поняли, что неограниченное использование goto может привести к запутанному коду-«спагетти», который трудно понимать и изменять. В языке высокого уровня goto также не нужен, если есть способы ветвления (в Python — инструкции if и выражения or, and и if-else) и циклов (инструкции while и for, возможно, содержащие continue и break).
Исключения также можно использовать для создания «структурированного goto», работающего даже при переходах между вызовами функций. Многие считают, что исключения удобно имитируют все разумные способы применения конструкций «go» или «goto» в C, Fortran и других языках. Например:
class label(Exception): pass # declare a label
try:
...
if condition: raise label() # goto label
...
except label: # where to goto
pass
...
Так нельзя перейти в середину цикла, но такое использование goto обычно всё равно считается злоупотреблением. Используйте этот приём умеренно.
Почему сырые строки (r-строки) не могут заканчиваться обратной косой чертой?
Точнее, они не могут заканчиваться нечётным числом обратных косых черт: непарная обратная косая черта в конце экранирует закрывающую кавычку, из-за чего строка остаётся незакрытой.
Сырые строки были созданы для упрощения подготовки входных данных для обработчиков (главным образом, механизмов регулярных выражений), которые должны самостоятельно обрабатывать экранирование обратной косой чертой. Такие обработчики и без того считают непарную обратную косую черту в конце ошибкой, поэтому сырые строки не допускают её. Взамен они позволяют передать символ кавычки в строке, экранировав его обратной косой чертой. Эти правила хорошо работают, когда r-строки используются по назначению.
Если вы пытаетесь составить пути Windows, учтите, что все системные вызовы Windows также принимают прямые косые черты:
f = open("/mydir/file.txt") # works fine!
Если вы пытаетесь составить путь для команды DOS, попробуйте, например, один из следующих вариантов:
dir = r"\this\is\my\dos\dir" "\\" dir = r"\this\is\my\dos\dir\ "[:-1] dir = "\\this\\is\\my\\dos\\dir\\"
Почему в Python нет инструкции «with» для присваивания атрибутам?
В Python есть инструкция «with», которая обрамляет выполнение блока и вызывает код при входе в блок и выходе из него. В некоторых языках есть конструкция, выглядящая так:
with obj:
a = 1 # equivalent to obj.a = 1
total = total + 1 # obj.total = obj.total + 1
В Python такая конструкция была бы неоднозначной.
В других языках, таких как Object Pascal, Delphi и C++, используются статические типы, поэтому можно однозначно определить, какому члену выполняется присваивание. В этом и заключается основная идея статической типизации: компилятор всегда знает область видимости каждой переменной во время компиляции.
В Python используются динамические типы. Невозможно заранее узнать, на какой атрибут будет дана ссылка во время выполнения. Атрибуты-члены можно добавлять к объектам и удалять из них на лету. Поэтому при простом чтении кода невозможно определить, к какому атрибуту обращаются: локальному, глобальному или атрибуту-члену?
Рассмотрим, например, следующий неполный фрагмент:
def foo(a):
with a:
print(x)
В этом фрагменте предполагается, что у «a» обязательно есть атрибут-член с именем «x». Однако в Python ничто не сообщает интерпретатору об этом. Что должно произойти, если «a» — скажем, целое число? Если существует глобальная переменная с именем «x», будет ли она использоваться внутри блока with? Как видите, динамическая природа Python значительно усложняет подобные решения.
Однако основное преимущество «with» и подобных языковых конструкций (сокращение объёма кода) в Python легко достигается с помощью присваивания. Вместо:
function(args).mydict[index][index].a = 21 function(args).mydict[index][index].b = 42 function(args).mydict[index][index].c = 63
напишите так:
ref = function(args).mydict[index][index] ref.a = 21 ref.b = 42 ref.c = 63
Это также повышает скорость выполнения, поскольку привязки имён в Python разрешаются во время выполнения, а во втором варианте разрешение требуется выполнить только один раз.
Почему генераторы не поддерживают инструкцию with?
По техническим причинам генератор, используемый непосредственно в качестве менеджера контекста, не будет работать правильно. Если генератор, как это бывает чаще всего, используется в качестве итератора и выполняется до конца, закрывать его не требуется. Если же закрытие необходимо, оберните его в «contextlib.closing(generator)» в инструкции «with».
Зачем двоеточия нужны в инструкциях if/while/def/class?
Двоеточие нужно главным образом для повышения читабельности (это одно из следствий экспериментов с языком ABC). Сравните:
if a == b
print(a)
и
if a == b:
print(a)
Обратите внимание: второй вариант немного легче читать. Заметьте также, что двоеточие вводит пример в этом ответе на часто задаваемый вопрос; это стандартное употребление в английском языке.
Ещё одна, менее важная причина состоит в том, что двоеточие упрощает работу редакторов с подсветкой синтаксиса: они могут искать двоеточия, чтобы определить, когда нужно увеличить отступ, вместо того чтобы выполнять более сложный разбор текста программы.
Почему Python допускает запятые в конце списков и кортежей?
Python позволяет ставить завершающую запятую в конце списков, кортежей и словарей:
[1, 2, 3,]
('a', 'b', 'c',)
d = {
"A": [1, 5],
"B": [6, 7], # last trailing comma is optional but good style
}
Для этого есть несколько причин.
Если литерал списка, кортежа или словаря записан в несколько строк, добавлять новые элементы проще, поскольку не нужно вспоминать, что к предыдущей строке надо добавить запятую. Кроме того, строки можно переставлять местами, не создавая синтаксическую ошибку.
Случайно пропущенная запятая может привести к ошибкам, которые трудно диагностировать. Например:
x = [ "fee", "fie" "foo", "fum" ]
Кажется, что в этом списке четыре элемента, но на самом деле их три: «fee», «fiefoo» и «fum». Если всегда ставить завершающую запятую, этого источника ошибок можно избежать.
Возможность ставить завершающую запятую также может упростить программную генерацию кода.
© 2001–2024 Python Software Foundation
Licensed under the PSF License.
https://docs.python.org/3.11/faq/design.html