Spec-Zone.ru › Python 3.11

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

  • Почему Python использует отступы для группировки операторов?
  • Почему я получаю странные результаты при простых арифметических операциях?
  • Почему вычисления с плавающей запятой так неточны?
  • Почему строки Python неизменяемы?
  • Почему необходимо явно использовать «self» в определениях и вызовах методов?
  • Почему я не могу использовать присваивание в выражении?
  • Почему Python использует методы для некоторых функций (например, list.index()) и функции для других (например, len(list))?
  • Почему join() — метод строки, а не списка или кортежа?
  • Насколько быстры исключения?
  • Почему в Python нет операторов switch или case?
  • Нельзя ли эмулировать потоки в интерпретаторе вместо того, чтобы полагаться на реализацию потоков, специфичную для ОС?
  • Почему выражения lambda не могут содержать операторы?
  • Можно ли скомпилировать Python в машинный код, C или другой язык?
  • Как Python управляет памятью?
  • Почему CPython не использует более традиционную схему сбора мусора?
  • Почему не вся память освобождается при завершении работы CPython?
  • Почему существуют отдельные типы данных кортеж и список?
  • Как списки реализованы в CPython?
  • Как словари реализованы в CPython?
  • Почему ключи словаря должны быть неизменяемыми?
  • Почему list.sort() не возвращает отсортированный список?
  • Как задать и применить спецификацию интерфейса в Python?
  • Почему в 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 никакие действия не изменят строку «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. Для литеральных значений или констант в пространстве имен вы также можете использовать оператор match ... case.

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

Как 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.

Почему не вся память освобождается при выходе из 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 так же, как и спецификации интерфейсов. На самом деле, это может быть даже лучше, потому что спецификация интерфейса не может проверить определённые свойства программы. Например, метод list.append() ожидает добавить новые элементы в конец внутреннего списка; спецификация интерфейса не может проверить, что ваша реализация list.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(generator)» в инструкции «with».

Почему для инструкций if/while/def/class необходимы двоеточия?

Двоеточие необходимо прежде всего для повышения читабельности (один из результатов экспериментального языка ABC). Рассмотрите это:

if a == b
    print(a)

против

if a == b:
    print(a)

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

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

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

Python позволяет добавлять запятую в конце списков, кортежей и словарей:

[1, 2, 3,]
('a', 'b', 'c',)
d = {
    "A": [1, 5],
    "B": [6, 7],  # last trailing comma is optional but good style
}

Есть несколько причин для этого.

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

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

x = [
  "fee",
  "fie"
  "foo",
  "fum"
]

Этот список, кажется, имеет четыре элемента, но на самом деле содержит три: «fee», «fiefoo» и «fum». Всегда добавляя запятую, вы избегаете этого источника ошибок.

Разрешение запятой в конце также может упростить генерацию кода на основе программы.

© 2001–2023 Python Software Foundation
Licensed under the PSF License.
https://docs.python.org/3.11/faq/design.html

Spec-Zone.ru

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