Часто задаваемые вопросы о проектировании и истории
- Почему в 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 никакие действия не изменят строку “восемь” на какую-либо другую.
Почему в определениях и вызовах методов необходимо явно использовать ‘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 GC. Она содержит фрагменты ассемблерного кода для большинства распространённых платформ, но не для всех. И хотя она в основном прозрачна, полной прозрачности нет: для работы Python с ней требуются патчи.)
Традиционный сборщик мусора также становится проблемой, когда Python встраивается в другие приложения. В автономном Python можно без проблем заменить стандартные malloc() и free() версиями из библиотеки сборщика мусора, но приложение, в которое встраивается Python, может захотеть использовать собственную замену для malloc() и free() и не захотеть использовать замену Python. Сейчас CPython работает с любыми реализациями, которые корректно реализуют malloc() и free().
Почему при завершении работы CPython освобождается не вся память?
Объекты, на которые ссылаются глобальные пространства имён модулей Python, не всегда освобождаются при завершении работы Python. Это может произойти при наличии циклических ссылок. Кроме того, некоторые участки памяти выделяются библиотекой C и не могут быть освобождены (например, на них пожалуется такой инструмент, как Purify). Тем не менее Python активно очищает память при завершении работы и старается уничтожить каждый объект.
Чтобы заставить Python удалить определённые объекты при освобождении памяти, используйте модуль atexit, чтобы запустить функцию, которая выполнит это удаление.
Почему для кортежей и списков предусмотрены отдельные типы данных?
Хотя списки и кортежи во многом похожи, обычно они используются принципиально по-разному. Кортежи можно считать аналогом записей Pascal records или структур C structs: это небольшие коллекции связанных данных, которые могут иметь разные типы и обрабатываются как единое целое. Например, декартовы координаты удобно представлять кортежем из двух или трёх чисел.
Списки, с другой стороны, больше похожи на массивы в других языках. Обычно они содержат переменное число объектов одного типа, которые обрабатываются по одному. Например, 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 позволяет работать небрежно и вообще не писать тесты.
Почему в 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 привязки имен разрешаются во время выполнения, а во втором варианте разрешение требуется выполнить лишь один раз.
Похожие предложения, призванные еще больше сократить объем кода, например использование «начальной точки», были отклонены в пользу явного кода (см. https://mail.python.org/pipermail/python-ideas/2016-May/040070.html).
Почему генераторы не поддерживают оператор 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–2025 Python Software Foundation
Licensed under the PSF License.
https://docs.python.org/3.13/faq/design.html