Spec-Zone.ru › Python 3.10

ЧАВО по проектированию и истории

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

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

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

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

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

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

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

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

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

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

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

Пользователей часто удивляют такие результаты:

>>> 1.2 - 1.0
0.19999999999999996

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

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

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

>>> x = 1.2

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

1.0011001100110011001100110011001100110011001100110011 (binary)

что равно:

1.1999999999999999555910790149937383830547332763671875 (decimal)

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Некоторые неприемлемые решения, которые предлагались:

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Как указать и применить спецификацию интерфейса в Python?

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

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

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

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

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

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

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

В 1970-х годах люди поняли, что неконтролируемый оператор goto может привести к запутанному «спагетти-коду», который трудно понять и переработать. В языке высокого уровня он также не нужен, поскольку есть способы ветвления (в Python, с помощью операторов if и or, and, и if-else выражений) и циклов (с помощью операторов while и for, которые могут содержать continue и break).

Также можно использовать исключения для предоставления «структурированного goto», который работает даже через вызовы функций. Многие считают, что исключения удобно эмулируют все разумные применения конструкций «go» или «goto» языков C, Fortran и других языков. Например:

class label(Exception): pass  # declare a label

try:
    ...
    if condition: raise label()  # goto label
    ...
except label:  # where to goto
    pass
...

Это не позволяет перепрыгивать в середину цикла, но это обычно считается злоупотреблением оператором goto. Используйте экономно.

Почему строковые литералы (r-строки) не могут заканчиваться обратной косой чертой?

Точнее, они не могут заканчиваться нечетным количеством обратных косых черт: непарная обратная косая черта в конце экранирует символ закрывающей кавычки, оставляя неполную строку.

Необработанные строки были разработаны для облегчения создания входных данных для процессоров (в основном движков регулярных выражений), которые хотят обрабатывать экранирование обратной косой чертой самостоятельно. Такие процессоры рассматривают несоответствующую обратную косую черту в конце как ошибку, поэтому необработанные строки этого не допускают. Взамен они позволяют передавать символ кавычки строки, экранируя его обратной косой чертой. Эти правила хорошо работают, когда r-строки используются по назначению.

Если вы пытаетесь создать имена путей Windows, обратите внимание, что все системные вызовы Windows также принимают косые черты:

f = open("/mydir/file.txt")  # works fine!

Если вы пытаетесь создать имя пути для команды DOS, попробуйте, например, одно из

dir = r"\this\is\my\dos\dir" "\\"
dir = r"\this\is\my\dos\dir\ "[:-1]
dir = "\\this\\is\\my\\dos\\dir\\"

Почему в Python нет оператора «with» для присваивания атрибутов?

Python имеет оператор «with», который оборачивает выполнение блока кода, вызывая код при входе и выходе из блока. Некоторые языки имеют конструкцию, которая выглядит так:

with obj:
    a = 1               # equivalent to obj.a = 1
    total = total + 1   # obj.total = obj.total + 1

В Python такая конструкция была бы неоднозначной.

Другие языки, такие как Object Pascal, Delphi и C++, используют статические типы, поэтому можно однозначно узнать, к какому члену происходит присваивание. Это основная цель статической типизации — компилятор всегда знает область действия каждой переменной на этапе компиляции.

Python использует динамические типы. Невозможно заранее узнать, к какому атрибуту будет обращаться во время выполнения. Атрибуты членов могут добавляться или удаляться из объектов в режиме реального времени. Это делает невозможным определить с первого взгляда, к какому атрибуту происходит обращение: локальному, глобальному или атрибуту члена?

Например, рассмотрим следующий неполный фрагмент:

def foo(a):
    with a:
        print(x)

Фрагмент предполагает, что «a» должен иметь член-атрибут под названием «x». Однако в Python нет ничего, что сообщит об этом интерпретатору. Что произойдет, если «a» — это, скажем, целое число? Если существует глобальная переменная с именем «x», будет ли она использована внутри блока with? Как вы видите, динамическая природа Python делает такие решения намного сложнее.

Однако основное преимущество оператора «with» и аналогичных языковых функций (сокращение объема кода) можно легко получить в Python с помощью присваивания. Вместо:

function(args).mydict[index][index].a = 21
function(args).mydict[index][index].b = 42
function(args).mydict[index][index].c = 63

напишите это:

ref = function(args).mydict[index][index]
ref.a = 21
ref.b = 42
ref.c = 63

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

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

По техническим причинам генератор, используемый непосредственно как менеджер контекста, не будет работать правильно. Когда генератор используется как итератор, доведенный до завершения, как это обычно бывает, закрытие не требуется. Когда это необходимо, оберните его как «contextlib.closing(генератор)» в операторе «with».

Почему для операторов if/while/def/class требуется двоеточие?

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

if a == b
    print(a)

против

if a == b:
    print(a)

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

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

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

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

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

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

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

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

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

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

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

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

Spec-Zone.ru

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