Spec-Zone.ru › Python 3.8

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

  • Почему 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» для присваивания атрибутов?
  • Почему для операторов 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 никакая активность не изменит строку “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, какое пространство имен использовать.

END_OF_DOCUMENT_MARKER ```

Почему я не могу использовать присваивание в выражении?

Начиная с Python 3.8, вы можете!

Выражения присваивания с использованием оператора walrus := присваивают переменную в выражении:

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?

Вы можете достаточно легко сделать это с помощью последовательности if... elif... elif... else. Были некоторые предложения по синтаксису оператора switch, но нет единого мнения (ещё) о том, как и нужно ли тестировать диапазоны. См. PEP 275 для получения полной информации и текущего состояния.

В случаях, когда вам нужно выбрать из очень большого количества возможностей, вы можете создать словарь, отображающий значения case в вызываемые функции. Например:

def function_1(...):
    ...

functions = {'a': function_1,
             'b': function_2,
             'c': self.method_1, ...}

func = functions[value]
func()

Для вызова методов объектов вы можете ещё больше упростить, используя встроенную функцию getattr() для получения методов с определённым именем:

def visit_a(self, ...):
    ...
...

def dispatch(self, value):
    method_name = 'visit_' + str(value)
    method = getattr(self, method_name)
    method()

Рекомендуется использовать префикс для имён методов, такой как visit_ в этом примере. Без такого префикса, если значения поступают из ненадежного источника, злоумышленник сможет вызвать любой метод вашего объекта.

Нельзя ли эмулировать потоки в интерпретаторе вместо того, чтобы полагаться на реализацию потоков, специфичную для ОС?

Ответ 1: К сожалению, интерпретатор проталкивает как минимум одну C-кадр стека для каждого Python-кадра стека. Кроме того, расширения могут вызывать Python в практически произвольные моменты. Поэтому полная реализация потоков требует поддержки потоков для C.

Ответ 2: К счастью, есть Stackless Python, который имеет полностью переработанную петлю интерпретатора, избегающую C-стек.

Почему лямбда-выражения не могут содержать операторы?

Лямбда-выражения в Python не могут содержать операторов, потому что синтаксический механизм Python не может обрабатывать операторы, вложенные в выражения. Однако в Python это не серьёзная проблема. В отличие от лямбда-форм в других языках, где они добавляют функциональность, Python-лямбды являются лишь сокращённой записью, если вы слишком ленивы, чтобы определить функцию.

Функции уже являются объектами первого класса в Python и могут быть объявлены в локальном пространстве имён. Таким образом, единственное преимущество использования лямбды вместо локально определённой функции заключается в том, что вам не нужно придумывать имя для функции — но это всего лишь локальная переменная, которой присваивается объектный код функции (который является точно таким же типом объекта, который даёт лямбда-выражение)!

Можно ли скомпилировать Python в машинный код, C или какой-нибудь другой язык?

Cython компилирует изменённую версию Python с необязательными аннотациями в C-расширения. Nuitka — это развивающийся компилятор Python в код C++, который стремится поддерживать весь язык Python. Для компиляции в Java можно рассмотреть VOC.

Как 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 закрывает предыдущий файл. Однако при традиционном GC эти объекты файлов будут собраны (и закрыты) через интервалы, которые могут быть различными и потенциально длительными.

Если вы хотите написать код, который будет работать с любой реализацией Python, вы должны явно закрыть файл или использовать оператор with; это будет работать независимо от схемы управления памятью:

for file in very_long_list_of_files:
    with open(file) as f:
        c = f.read(1)

Почему CPython не использует более традиционную схему сбора мусора?

Во-первых, это не стандартная функция C, и поэтому она не портативна. (Да, мы знаем о библиотеке Boehm GC. Она содержит куски ассемблерного кода для большинства распространённых платформ, а не для всех из них, и хотя она в основном прозрачна, она не полностью прозрачна; для работы Python с ней требуются исправления.)

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

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

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

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

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

Списки и кортежи, хотя и похожи во многих отношениях, обычно используются по-разному. Кортежи можно рассматривать как аналогичные записям Pascal или структурам C; они представляют собой небольшие коллекции связанных данных, которые могут быть разных типов и обрабатываются как группа. Например, декартовы координаты удобно представлять в виде кортежа из двух или трех чисел.

Списки, с другой стороны, больше похожи на массивы в других языках. Они, как правило, содержат переменное количество объектов, все из которых имеют один и тот же тип и обрабатываются по одному. Например, 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 могут запускаться как скрипт для выполнения простого «самотеста». Даже модули, использующие сложные внешние интерфейсы, часто могут быть протестированы изолированно с помощью тривиальных «заглушек» (stub) эмуляции внешнего интерфейса. Модули doctest и unittest или сторонние фреймворки для тестирования могут быть использованы для построения исчерпывающих наборов тестов, которые покрывают каждую строку кода в модуле.

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

Написание наборов тестов очень полезно, и вы можете разработать свой код с учётом лёгкости тестирования. Всё более популярная техника, тест-направленное развитие, предусматривает написание частей набора тестов до написания какого-либо фактического кода. Конечно, Python позволяет быть небрежным и не писать тестовые кейсы вообще.

Почему нет оператора goto?

В 1970-х годах люди поняли, что неограниченный оператор 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, а в последнем варианте требуется выполнить разрешение только один раз.

Почему для инструкций 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–2022 Python Software Foundation
Licensed under the PSF License.
https://docs.python.org/3.8/faq/design.html

Spec-Zone.ru

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