Часто задаваемые вопросы по дизайну и истории

Содержание

Почему 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 использует double C для хранения. 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 являются (по определению!) теми переменными, которым назначается значение в теле функции (и явно не объявленные global), должен быть какой-то способ сказать интерпретатору, что назначение предназначалось для назначения переменной сущности, а не локальной переменной, и оно должно быть предпочтительно синтаксическим (из соображений эффективности). C++ делает это через декларации, но у Python нет деклараций, и было бы жаль вводить их только для этой цели. Использование явного self.var решает это красиво. Аналогично, для использования сущности переменных, необходимость записи self.var означает, что ссылки на неквалифицированные имена внутри метода не должны искать в каталогах сущность. Другими словами, локальные переменные и переменные сущности находятся в двух различных пространствах имен, и вам необходимо указать Python, какое пространство имен использовать.

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

Начиная с Python 3.8, можно!

Выражения назначения с помощью моржового оператора „: =“ присваивают переменную в выражении:

while chunk := fp.read(200):
   print(chunk)

Дополнительные сведения см. в разделе PEP 572.

Почему Python использует методы для некоторой функциональности (например, list.index()), но функции для других (например, len(list))?

Как сказал Гвидо:

(а) для некоторых операций, префикс нотации просто читается лучше, чем постфикс – префикс (и инфикс!) операции имеют давние традиции в математике, которая любит нотации, где визуальные помогает математику думать о проблеме. Сравним простоту, с которой мы переписываем формулу, такую как x*(a+b) в x*a + x*b, до неуклюжести делать то же самое, используя необработанную объектно-ориентированную нотацию.

б) когда я читаю содержащий len(х) код, мне известно, что он просит вычислить длину чего-то. Это говорит мне о двух вещах: результат — целое число, а аргумент — это какой-то контейнер. Наоборот, когда я читаю x.len(), я уже должен знать, что x — это какой-то контейнер, реализующий интерфейс или наследующий от класса, который имеет стандартный len(). Наблюдаю иногда путаницу, когда не реализующий сопоставление класс, содержит метод get() или keys(), или что-то, что не является файлом, имеет метод write().

оригинал

Почему join() является методом строки, а не методом списка или кортежа?

Строки стали больше похожи на другие стандартные типы начиная с Python 1.6, когда были добавлены предоставляющие ту же функциональность методы, которая всегда была доступна с помощью функций модуля string. Большинство из этих новых методов были широко приняты, но есть пример который, похоже, заставляет некоторых программистов чувствовать себя некомфортно:

", ".join(['1', '2', '4', '8', '16'])

что возвращает результат:

"1, 2, 4, 8, 16"

Есть два общих аргумента против этого использования.

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

Второе возражение обычно звучит так: «Я действительно говорю последовательности, чтобы объединить ее элементы с помощью строковой константы». К сожалению, это не так. Почему-то кажется, гораздо меньше трудностей с split() в качестве метода строки, так как в этом случае легко увидеть что:

"1, 2, 4, 8, 16".split(", ")

оператор примененная к строковому литералу для возвращения подстрок, разделенных заданным разделителем (или, по умолчанию, произвольными пустыми пробелами).

join() — это метод строки, поскольку при его использовании строка-разделитель должна выполнять итерацию по последовательности строки и вставляться между смежными элементами. Данный метод можно использовать с любым аргументом, который подчиняется правилам для объектов последовательности, включая любые новые определяемые самостоятельно классы. Аналогичные методы существуют для байтов и объектов bytearray.

Насколько быстры исключения?

Блок try/except чрезвычайно эффективен, если не вызывается исключения. На самом деле поймать исключение дорого. В версиях Python до 2.0 было обычным использовать эту идиому:

try:
    value = mydict[key]
except KeyError:
    mydict[key] = getvalue(key)
    value = mydict[key]

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

if key in mydict:
    value = mydict[key]
else:
    value = mydict[key] = getvalue(key)

Для этого случая можно также использовать value = dict.setdefault(key, getvalue(key)), но только если getvalue() вызов достаточно дешев, поскольку он вычисляет во всех случаях.

Почему в Python нет оператора switch или case?

Вы можете сделать это достаточно легко с последовательностью if... elif... elif... else. Было несколько предложений относительно синтаксиса оператора switch, но пока нет консенсуса относительно того, следует ли и как проводить тестирование диапазона. Подробные сведения и текущее состоянии см. в разделе PEP 275.

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

def function_1(...):
    ...

functions = {'a': function_1,
             'b': function_2,
             'c': self.method_1, ...}

func = functions[value]
func()

Для вызова методов объектов можно упростить использование встроенного getattr() для извлечения методов с определенным именем:

def visit_a(self, ...):
    ...
...

def dispatch(self, value):
    method_name = 'visit_' + str(value)
    method = getattr(self, method_name)
    method()

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

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

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

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

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

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

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

Можно ли скомпилировать Python в машинный код, C или какой-либо другой язык?

Cython компилирует измененную версию Python с дополнительными аннотациями в расширения C. Nuitka является новым компилятором Python в C++ код, целью которого является поддержка полного языка Python. При компиляции в Java можно учитывать VOC.

Как Python управляет памятью?

Детали управления памятью Python зависят от реализации. Стандартная реализация Python, CPython, использует подсчет ссылок для обнаружения недоступных объектов и другой механизм для сборки мусора ссылочных циклов, периодически выполняя алгоритм обнаружения циклов, который ищет недоступные циклы и удаляет вовлеченные объекты. Модуль gc предоставляет функции для выполнения сбора мусора, получения статистики отладки и настройки параметров сборщика.

Другие реализации (Jython или PyPy), однако, могут опираться на другой механизм сборщика мусора. Это различие может вызвать некоторые проблемы с переносом, если Python код зависит от поведения реализации счетчика ссылок.

В некоторых Python реализациях следующий код (который хорошо подходит в CPython), вероятно, закончатся файловые дескрипторы:

for file in very_long_list_of_files:
    f = open(file)
    c = f.read(1)

Действительно, используя подсчет ссылок и деструктора CPython, каждое новое назначение f закрывает предыдущий файл. Однако при использовании традиционного GC эти файловые объекты будут собираться (и закрываться) только через различные и, возможно, длинные интервалы.

Если требуется написать код, который будет работать с любой Python реализацией, следует явно закрыть файл или использовать инструкцию with; это будет работать независимо от схемы управления памятью:

for file in very_long_list_of_files:
    with open(file) as f:
        c = f.read(1)

Почему CPython не использует более традиционную схему сбора мусора?

Во-первых, это не стандартная функция C и, следовательно, не портабельная. (Да, мы знаем о библиотеке Boehm GC. Он имеет биты ассемблерного кода для большинства общих платформ, не для всех из них, и хотя он в основном прозрачен, он не полностью прозрачен; требуются патчи, чтобы заставить Python работать с ним.)

Традиционный GC также становится проблемой, когда Python встраивается в другие приложения. Хотя в автономном Python можно заменить стандартные malloc() и free() версиями, предоставляемыми библиотекой GC, приложения встраивающие Python могут захотеть иметь свои замены malloc() и free() и может не захотеть Python’овские. Сейчас CPython работает со всем, что правильно реализует malloc() и free().

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

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

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

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

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

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

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

Как реализуются списки в CPython?

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

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

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

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

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

Словари работают, вычисляя хеш-код для каждого ключа, сохраненного в словаре, используя встроенную функцию hash(). Хеш-код сильно варьируется в зависимости от ключа и начального числа каждого процесса; например, «Python» может иметь значение -539294296, в то время как строка «python», которая отличается одним битом, может иметь значение 1142331976. Затем хеш-код используется для вычисления местоположения во внутреннем массиве, где будет храниться значение. Предполагая, что вы сохраняете ключи, которые имеют разные хеш-значения, это означает, что словарям требуется постоянное время — O(1), в нотации Большого-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):
    ...  # делать что угодно с 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?

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

class label(Exception): pass  # объявить ярлык

try:
    ...
    if condition: raise label()  # goto ярлык
    ...
except label:  # куда goto
    pass
...

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

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

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

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

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

f = open("/mydir/file.txt")  # работает отлично!

Если вы пытаетесь создать путь для команды 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               # эквивалентно 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, а вторая версия должна выполнять разрешение только один раз.

Почему требуется двоеточия для инструкций 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],  # последняя запятая не обязательна, но в хорошем стиле
}

Есть несколько причин допускающих это.

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

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

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

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

Разрешение запятой может также облегчить программную генерацию кода.