Spec-Zone.ru › Python 3.14

Часто задаваемые вопросы о проектировании и истории

  • Почему в 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. Со временем большинству людей начинает нравиться эта особенность.

Поскольку здесь нет скобок begin/end, не может возникнуть разногласий между группировкой, распознаваемой парсером, и той, которую видит читатель. Иногда программистам на C встречается такой фрагмент кода:

if (x <= y)
        x++;
        y--;
z++;

Если условие истинно, выполняется только инструкция x++, но отступ заставляет многих думать иначе. Даже опытные программисты на C иногда долго смотрят на этот фрагмент, пытаясь понять, почему y уменьшается даже для x > y.

Поскольку скобок begin/end нет, в Python гораздо реже возникают разногласия по поводу стиля написания кода. В C есть множество способов расставлять фигурные скобки. Привыкнув читать и писать код в определённом стиле, обычно испытываешь некоторую неловкость, читая (или будучи вынужденным писать) в другом стиле.

Во многих стилях программирования скобки begin/end размещаются на отдельных строках. Это значительно удлиняет программы и расходует ценное пространство на экране, затрудняя получение общего представления о программе. В идеале функция должна помещаться на одном экране (скажем, занимать 20–30 строк). 20 строк Python могут выполнять гораздо больше работы, чем 20 строк C. Дело не только в отсутствии скобок begin/end: свою роль играют и отсутствие объявлений, и высокоуровневые типы данных, — но синтаксис, основанный на отступах, безусловно, помогает.

Почему при простых арифметических операциях получаются странные результаты?

См. следующий вопрос.

Почему вычисления с плавающей точкой настолько неточны?

Пользователи часто удивляются таким результатам:

>>> 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. Дополнительную информацию об инструкциях см. в спецификации и руководстве по match. Более старый вариант — последовательность 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: К сожалению, интерпретатор выделяет как минимум один фрейм стека C для каждого фрейма стека Python. Кроме того, расширения могут вызывать 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 активно очищает память при завершении работы и пытается уничтожить каждый объект.

Если вы хотите принудительно удалить определённые объекты при освобождении памяти, используйте модуль atexit, чтобы запустить функцию, которая выполнит такое удаление.

Почему для кортежей и списков предусмотрены разные типы данных?

Хотя списки и кортежи во многом похожи, обычно они используются принципиально по-разному. Кортежи можно считать аналогом Pascal records или C structs: это небольшие наборы связанных данных, которые могут иметь разные типы и обрабатываются как единое целое. Например, декартовы координаты удобно представлять кортежем из двух или трёх чисел.

Списки, напротив, больше похожи на массивы в других языках. Обычно они содержат переменное число объектов одного типа, которые обрабатываются по одному. Например, os.listdir('.') возвращает список строк с именами файлов в текущем каталоге. Функции, работающие с этим результатом, обычно продолжат работать корректно, даже если добавить в каталог ещё один или два файла.

Кортежи неизменяемы: после создания кортежа нельзя заменить ни один из его элементов новым значением. Списки изменяемы, то есть их элементы можно менять. Ключами словаря могут быть только неизменяемые элементы, поэтому в качестве ключей можно использовать кортежи, но не списки.

Как списки реализованы в CPython?

Списки CPython — это массивы переменной длины, а не связанные списки в стиле Lisp. Реализация использует непрерывный массив ссылок на другие объекты и хранит указатель на этот массив и его длину в заголовочной структуре списка.

Благодаря этому индексирование списка a[i] выполняется за время, не зависящее от размера списка или значения индекса.

При добавлении или вставке элементов массив ссылок изменяет размер. Для повышения эффективности многократного добавления элементов применяется небольшая хитрость: когда массив приходится увеличивать, выделяется дополнительное пространство, чтобы в следующие несколько раз не нужно было менять его размер.

Как словари реализованы в CPython?

Словари CPython реализованы как хеш-таблицы с изменяемым размером. По сравнению с B-деревьями это обеспечивает лучшую производительность поиска (безусловно, самой распространённой операции) в большинстве случаев, а сама реализация проще.

Для каждого ключа в словаре вычисляется хеш-код с помощью встроенной функции hash(). Хеш-код сильно различается в зависимости от ключа и начального значения, уникального для каждого процесса; например, 'Python' может иметь хеш -539294296, а 'python' — строка, отличающаяся всего одним битом — может иметь хеш 1142331976. Затем хеш-код используется для вычисления позиции во внутреннем массиве, где будет храниться значение. Если все сохранённые ключи имеют разные хеш-значения, поиск ключа в словаре выполняется за постоянное время — O(1) в нотации Big-O.

Почему ключи словаря должны быть неизменяемыми?

Реализация словарей на основе хеш-таблиц использует значение хеша, вычисленное по ключу, чтобы найти этот ключ. Если бы ключ был изменяемым объектом, его значение могло бы измениться, а вместе с ним — и хеш. Но тот, кто изменяет объект-ключ, не может знать, что он используется в качестве ключа словаря, поэтому запись нельзя переместить в другое место словаря. Тогда при попытке найти тот же объект в словаре он не обнаружится, поскольку его хеш-значение изменилось. Не удастся найти и старое значение, поскольку значение объекта, найденного в соответствующей ячейке хеш-таблицы, будет уже другим.

Если вам нужен словарь с индексами в виде списка, сначала преобразуйте список в кортеж: функция 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 Python Software Foundation
Licensed under the PSF License.
https://docs.python.org/3.14/faq/design.html

Spec-Zone.ru

Настройки Оффлайн Что нового Помощь О нас
Spec-Zone .ru
спецификации, руководства, описания, API