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 использует формат 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?

В общем случае структурированные операторы 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 с fallthrough, как в операторе switch-case-default языка C, возможна, но значительно сложнее и менее необходима.

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

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

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

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

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

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

Можно ли скомпилировать 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’s. Сейчас CPython работает с любым объектом, который правильно реализует malloc() и free().

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

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

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

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

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

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

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

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

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

Это делает индексирование списка a[i] операцией, стоимость которой не зависит от размера списка или значения индекса.

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

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

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

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

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

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

Если вам нужен словарь, индексированный списком, просто сначала преобразуйте список в кортеж; функция tuple(L) создаёт кортеж с теми же элементами, что и список L. Кортежи неизменяемы и поэтому могут использоваться в качестве ключей словарей.

Некоторые неприемлемые решения, которые были предложены:

  • Хешировать списки по их адресу (идентификатору объекта). Это не работает, потому что если вы создадите новый список с тем же значением, он не будет найден; например:

    mydict = {[1, 2]: '12'}
    print(mydict[[1, 2]])
    

    вызовет исключение KeyError, потому что идентификатор [1, 2], используемый во второй строке, отличается от идентификатора в первой строке. Другими словами, ключи словарей должны сравниваться с помощью ==, а не с помощью is.

  • Создавать копию при использовании списка в качестве ключа. Это не работает, потому что список, будучи изменяемым объектом, может содержать ссылку на себя, и тогда код копирования зациклится.
  • Разрешать списки в качестве ключей, но говорить пользователю, что их нельзя изменять. Это позволит создать класс трудно отслеживаемых ошибок в программах, когда вы случайно забудете или измените список. Это также нарушает важную инвариантность словарей: каждое значение в d.keys() может использоваться в качестве ключа словаря.
  • Отмечать списки как неизменяемые после их использования в качестве ключей словаря. Проблема в том, что измениться может не только верхнеуровневый объект; вы можете использовать кортеж, содержащий список, в качестве ключа. Ввод любого объекта в качестве ключа в словарь потребовал бы маркировки всех объектов, доступных из него, как неизменяемых — и опять же, самоссылочные объекты могут вызвать бесконечную рекурсию.

Если вам нужно обойти это ограничение, есть уловка, но используйте её на свой страх и риск: вы можете обернуть изменяемую структуру в экземпляр класса, у которого есть методы __eq__() и __hash__(). Вам нужно убедиться, что хеш-значение для всех таких обернутых объектов, находящихся в словаре (или другой структуре на основе хеша), остаётся неизменным, пока объект находится в словаре (или другой структуре).

class ListWrapper:
    def __init__(self, the_list):
        self.the_list = the_list

    def __eq__(self, other):
        return self.the_list == other.the_list

    def __hash__(self):
        l = self.the_list
        result = 98767 - len(l)*555
        for i, el in enumerate(l):
            try:
                result = result + (hash(el) % 9999999) * 1001 + i
            except Exception:
                result = (result % 7777777) + i * 333
        return result

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

Кроме того, должно всегда выполняться условие, что если o1 == o2 (то есть o1.__eq__(o2) is True), то hash(o1) == hash(o2) (то есть o1.__hash__() == o2.__hash__()), независимо от того, находится ли объект в словаре или нет. Если вы не выполните эти ограничения, словари и другие структуры на основе хешей будут работать неправильно.

В случае ListWrapper, когда обернутый объект находится в словаре, обернутый список не должен меняться, чтобы избежать аномалий. Не делайте этого, если вы не готовы хорошо подумать о требованиях и последствиях некорректного выполнения. Будьте предупреждены.

Почему list.sort() не возвращает отсортированный список?

В ситуациях, где важна производительность, создание копии списка только для его сортировки было бы неэффективно. Поэтому list.sort() сортирует список на месте. Чтобы напомнить вам об этом, он не возвращает отсортированный список. Таким образом, вас не обманут и вы не перезапишете список случайно, когда вам нужна отсортированная копия, но при этом нужна и неотсортированная версия.

Если вы хотите вернуть новый список, используйте встроенную функцию sorted() вместо этого. Эта функция создаёт новый список из предоставленного итерируемого объекта, сортирует его и возвращает. Например, вот как можно перебрать ключи словаря в отсортированном порядке:

for key in sorted(mydict):
    ...  # do whatever with mydict[key]...

Как указать и обеспечить соблюдение спецификации интерфейса в Python?

Спецификация интерфейса для модуля, как предоставляется языками, такими как C++ и Java, описывает прототипы методов и функций модуля. Многие считают, что проверка спецификаций интерфейса во время компиляции помогает при построении больших программ.

Python 2.6 добавляет модуль abc, который позволяет определять абстрактные базовые классы (ABC). Затем вы можете использовать isinstance() и issubclass(), чтобы проверить, реализует ли экземпляр или класс определённый ABC. Модуль collections.abc определяет набор полезных ABC, таких как Iterable, Container и MutableMapping.

Для Python многие преимущества спецификаций интерфейсов можно получить, используя соответствующую дисциплину тестирования для компонентов.

Хороший набор тестов для модуля может одновременно обеспечить регрессионное тестирование и служить спецификацией интерфейса модуля и набором примеров. Многие модули Python можно запускать как скрипт, чтобы предоставить простой «самотест». Даже модули, использующие сложные внешние интерфейсы, часто могут быть протестированы изолированно с помощью тривиальных «заглушек» (stub) для эмуляции внешнего интерфейса. Модули doctest и unittest или сторонние фреймворки для тестирования могут быть использованы для построения исчерпывающих наборов тестов, которые проверяют каждую строку кода в модуле.

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

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

Почему в 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, а второй вариант требует разрешения только один раз.

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

Обратите внимание, как второй вариант немного легче читать. Обратите также внимание, как двоеточие выделяет пример в этом ответе FAQ; это стандартное использование в английском языке.

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

Почему 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