Spec-Zone.ru › Python 3.12

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

  • Почему в 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 есть много разных способов расставлять фигурные скобки. Привыкнув читать и писать код в определённом стиле, обычно испытываешь некоторый дискомфорт, когда читаешь код (или вынужден писать его) в другом стиле.

Во многих стилях программирования открывающие и закрывающие скобки размещают на отдельных строках. Из-за этого программы становятся значительно длиннее, а ценное место на экране расходуется впустую, затрудняя получение общего представления о программе. В идеале функция должна помещаться на одном экране (скажем, занимать 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 никакие действия не изменят строку «восемь» на что-либо другое.

Почему в определениях и вызовах методов необходимо явно указывать «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: К сожалению, интерпретатор помещает в стек 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 GC. Она содержит фрагменты кода на ассемблере для большинства распространённых платформ, но не для всех. И хотя она в основном прозрачна, полной прозрачности нет: чтобы Python работал с ней, нужны исправления.)

Традиционная сборка мусора также становится проблемой, когда Python встраивается в другие приложения. В автономном Python вполне можно заменить стандартные malloc() и free() версиями из библиотеки сборщика мусора, но приложение, в которое встроен Python, может захотеть использовать собственные замены для malloc() и free(), а не замены Python. Сейчас CPython совместим со всем, что правильно реализует malloc() и free().

Почему при завершении работы CPython освобождается не вся память?

Объекты, на которые ссылаются глобальные пространства имён модулей Python, при завершении работы Python освобождаются не всегда. Это может произойти при наличии циклических ссылок. Кроме того, некоторые фрагменты памяти выделяются библиотекой C и не могут быть освобождены (например, на них будет жаловаться такая программа, как Purify). Тем не менее Python активно очищает память при завершении работы и пытается уничтожить каждый объект.

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

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

Списки и кортежи во многом похожи, но обычно используются принципиально по-разному. Кортежи можно считать аналогами записей records в Pascal или structs в 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 позволяет вам небрежно относиться к этому и вовсе не писать тесты.

Почему в 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–2024 Python Software Foundation
Licensed under the PSF License.
https://docs.python.org/3.12/faq/design.html

Spec-Zone.ru

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