Вопросы и ответы по проектированию и истории
- Почему 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, какое пространство имён использовать.
Почему нельзя использовать присваивание в выражении?
Начиная с 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, как в C switch-case-default, возможно, но сложнее и менее необходимо.
Можно ли эмулировать потоки в интерпретаторе вместо того, чтобы полагаться на специфическую для ОС реализацию потоков?
Ответ 1: К сожалению, интерпретатор выталкивает по крайней мере одну C-стековую рамку для каждой Python-стековой рамки. Кроме того, расширения могут вызывать Python в практически произвольные моменты времени. Поэтому для полной реализации потоков требуется поддержка потоков для C.
Ответ 2: К счастью, существует Stackless Python, который имеет полностью переработанный цикл интерпретатора, который избегает C-стека.
Почему лямбда-выражения не могут содержать операторы?
Лямбда-выражения в Python не могут содержать операторов, поскольку синтаксический фреймворк Python не может обрабатывать операторы, вложенные в выражения. Однако в Python это не является серьезной проблемой. В отличие от лямбда-форм в других языках, где они добавляют функциональность, Python-лямбды – это всего лишь сокращенная запись, если вам слишком лень определить функцию.
Функции уже являются объектами первого класса в Python и могут быть объявлены в локальной области. Следовательно, единственное преимущество использования лямбды вместо локально определенной функции заключается в том, что вам не нужно придумывать имя для функции – но это просто локальная переменная, которой присваивается объект функции (который является абсолютно таким же типом объекта, который возвращает лямбда-выражение)!
Можно ли скомпилировать Python в машинный код, C или какой-либо другой язык?
Cython компилирует модифицированную версию Python с необязательными аннотациями в C-расширения. Nuitka – это развивающийся компилятор Python в код C++, стремящийся поддерживать весь язык Python.
Как Python управляет памятью?
Детали управления памятью в Python зависят от реализации. Стандартная реализация Python, CPython, использует подсчет ссылок для обнаружения недоступных объектов и другой механизм для сбора циклов ссылок, периодически выполняя алгоритм обнаружения циклов, который ищет недоступные циклы и удаляет вовлеченные объекты. Модуль gc предоставляет функции для выполнения сбора мусора, получения статистических данных отладки и настройки параметров коллектора.
Другие реализации (например, Jython или PyPy), однако, могут полагаться на другой механизм, такой как полнофункциональный сборщик мусора. Это различие может вызывать некоторые тонкие проблемы переноса, если ваш код Python зависит от поведения реализации подсчета ссылок.
В некоторых реализациях Python следующий код (который работает нормально в CPython) вероятно, исчерпает дескрипторы файлов:
for file in very_long_list_of_files:
f = open(file)
c = f.read(1)
Действительно, используя подсчет ссылок и схему деструкторов CPython, каждое новое присваивание f закрывает предыдущий файл. Однако с традиционным сборщиком мусора эти объекты файлов будут собираться (и закрываться) с различными, и, возможно, длительными интервалами.
Если вы хотите написать код, который будет работать с любой реализацией Python, вам следует явно закрыть файл или использовать оператор with; это будет работать независимо от схемы управления памятью:
for file in very_long_list_of_files:
with open(file) as f:
c = f.read(1)
Почему CPython не использует более традиционную схему сбора мусора?
Во-первых, это не стандартная функция C и, следовательно, она не переносима. (Да, мы знаем о библиотеке Boehm GC. Она содержит части ассемблерного кода для большинства распространенных платформ, но не для всех, и хотя она в основном прозрачна, она не полностью прозрачна; для работы Python с ней требуются исправления.)
Традиционный GC также становится проблемой, когда Python встраивается в другие приложения. В автономном Python допустимо заменить стандартные malloc() и free() версиями, предоставленными библиотекой GC, но приложение, встраивающее Python, может захотеть иметь свои замены для malloc() и free(), и может этого не захотеть. В настоящее время 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.13/faq/design.html