Spec-Zone.ru › Python 3.9

Вопросы и ответы по проектированию и истории

  • Почему Python использует отступы для группировки операторов?
  • Почему я получаю странные результаты при простых арифметических операциях?
  • Почему вычисления с плавающей запятой так неточны?
  • Почему строки Python неизменяемы?
  • Почему необходимо явно использовать «self» в определениях и вызовах методов?
  • Почему я не могу использовать присваивание в выражении?
  • Почему Python использует методы для некоторых функций (например, list.index()), а для других – функции (например, len(list))?
  • Почему join() является методом строки, а не списка или кортежа?
  • Насколько быстры исключения?
  • Почему в Python нет операторов switch или case?
  • Нельзя ли эмулировать потоки в интерпретаторе вместо использования платформо-зависимой реализации потоков ОС?
  • Почему выражения lambda не могут содержать операторов?
  • Можно ли скомпилировать Python в машинный код, C или другой язык?
  • Как Python управляет памятью?
  • Почему CPython не использует более традиционную схему сбора мусора?
  • Почему не вся память освобождается при выходе из CPython?
  • Почему существуют отдельные типы данных кортеж и список?
  • Как реализованы списки в CPython?
  • Как реализованы словари в CPython?
  • Почему ключи словаря должны быть неизменяемыми?
  • Почему list.sort() не возвращает отсортированный список?
  • Как задать и принудительно соблюдать спецификацию интерфейса в Python?
  • Почему нет оператора goto?
  • Почему необработанные строки (r-строки) не могут оканчиваться обратным слешем?
  • Почему в Python нет инструкции «with» для присваивания атрибутов?
  • Почему генераторы не поддерживают инструкцию with?
  • Почему для операторов if/while/def/class требуется двоеточие?
  • Почему Python допускает запятые в конце списков и кортежей?

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

Гвидо ван Россум считает, что использование отступов для группировки чрезвычайно элегантно и значительно способствует ясности типичных программ на Python. Большинство людей со временем начинают любить эту особенность.

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

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

Только оператор x++ выполняется, если условие истинно, но отступы заставляют многих поверить в обратное. Даже опытные программисты C иногда долго смотрят на него, пытаясь понять, почему y декрементируется даже для x > y.

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

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

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

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

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

Пользователи часто удивляются результатам, подобным этому:

>>> 1.2 - 1.0
0.19999999999999996

и считают, что это ошибка Python. Это не так. Это мало связано с Python и гораздо больше с тем, как платформа обрабатывает числа с плавающей запятой.

Тип float в CPython использует C-тип double для хранения. Значение объекта float хранится в двоичной форме с плавающей запятой с фиксированной точностью (обычно 53 бита), и Python использует C-операции, которые, в свою очередь, полагаются на аппаратную реализацию в процессоре для выполнения операций с плавающей запятой. Это означает, что, что касается операций с плавающей запятой, Python ведет себя так же, как многие популярные языки, включая C и Java.

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

>>> x = 1.2

сохраненное значение для x представляет собой (очень хорошее) приближение к десятичному значению 1.2, но не точно равно ему. На типичном компьютере фактическое сохраненное значение равно:

1.0011001100110011001100110011001100110011001100110011 (binary)

что точно равно:

1.1999999999999999555910790149937383830547332763671875 (decimal)

Типичная точность в 53 бита предоставляет числам с плавающей запятой в Python точность 15–16 десятичных знаков.

Для более подробного объяснения см. главу арифметика с плавающей запятой в руководстве Python.

Почему строки Python неизменяемы?

Есть несколько преимуществ.

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

Другое преимущество заключается в том, что строки в Python рассматриваются как «элементарные» как числа. Никакие действия не изменят значение 8 ни на что другое, и в Python никакие действия не изменят строку «восемь» ни на что другое.

Почему необходимо явно использовать «self» в определениях и вызовах методов?

Эта идея была позаимствована из Modula-3. Оказывается, она очень полезна по разным причинам.

Во-первых, становится более очевидно, что вы используете метод или атрибут экземпляра, а не локальную переменную. Чтение self.x или self.meth() четко показывает, что используется переменная или метод экземпляра, даже если вы не знаете определение класса наизусть. В C++ это можно понять по отсутствию объявления локальной переменной (предполагая, что глобальные переменные редки или легко узнаваемы), но в Python нет объявлений локальных переменных, поэтому вам пришлось бы найти определение класса, чтобы быть уверенным. Некоторые руководства по стилю кодирования для C++ и Java требуют, чтобы атрибуты экземпляров начинались с m_ префикса, поэтому эта ясность полезна и в этих языках.

Во-вторых, это означает, что не требуется специальный синтаксис, если вы хотите явно обратиться к методу или вызвать его из конкретного класса. В C++, если вы хотите использовать метод из базового класса, который переопределяется в производном классе, вам нужно использовать оператор :: — в Python вы можете написать baseclass.methodname(self, <argument list>). Это особенно полезно для методов __init__(), а в целом в случаях, когда метод производного класса хочет расширить метод базового класса с тем же именем и, таким образом, должен каким-то образом вызвать метод базового класса.

Наконец, для переменных экземпляров это решает синтаксическую проблему с присваиванием: поскольку локальные переменные в Python (по определению!) — это те переменные, значения которых присваиваются в теле функции (и которые не объявлены явно глобальными), должен быть способ сообщить интерпретатору, что присваивание предназначено для присваивания переменной экземпляра, а не локальной переменной, и желательно, чтобы это было синтаксическим (по соображениям эффективности). В C++ это делается с помощью деклараций, но в Python нет деклараций, и было бы жаль вводить их только для этой цели. Использование явного self.var элегантно решает эту проблему. Аналогично, для использования переменных экземпляров, необходимость писать self.var означает, что ссылки на имена без квалификаторов внутри метода не должны искать каталоги экземпляра. Другими словами, локальные переменные и переменные экземпляров существуют в двух разных именованных пространствах, и вам нужно сказать Python, какое пространство использовать.

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 в вызываемые функции. Например:

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_ в этом примере. Без такого префикса, если значения поступают из ненадежного источника, злоумышленник сможет вызвать любой метод вашего объекта.

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

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

Почему генераторы не поддерживают инструкцию with?

По техническим причинам генератор, используемый непосредственно как менеджер контекста, не будет работать правильно. Когда, как чаще всего бывает, генератор используется как итератор, выполняемый до конца, закрытие не требуется. Если же требуется, оберните его как «contextlib.closing(генератор)» в инструкции «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–2022 Python Software Foundation
Licensed under the PSF License.
https://docs.python.org/3.9/faq/design.html

Spec-Zone.ru

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