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

Содержание

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

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

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

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

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

Поскольку здесь нет скобок начала/конца, Python гораздо меньше подвержен конфликтам в стиле кодирования. В языке Си существует множество различных способов размещения скобок. Если вы привыкли читать и писать код в определенном стиле, то вполне нормально чувствовать себя несколько неловко, когда вы читаете (или должны писать) в другом стиле.

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

Для более полного объяснения, пожалуйста, обратитесь к главе floating point arithmetic в учебнике по 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, вы можете!

Выражения присваивания с использованием оператора моржа := присваивают переменную в выражении:

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]

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

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.

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

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

func = functions[value]
func()

Для вызова методов на объектах можно упростить еще больше, используя встроенную функцию getattr() для получения методов с определенным именем:

class MyVisitor:
    def visit_a(self):
        ...

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

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

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

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

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

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

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

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

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

Cython компилирует модифицированную версию Python с необязательными аннотациями в расширения языка C. Nuitka - это перспективный компилятор Python в код C++, нацеленный на поддержку полного языка Python.

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

if a == b
    print(a)

против

if a == b:
    print(a)

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

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

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

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

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

Есть несколько причин, чтобы разрешить это.

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

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

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

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

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

Вернуться на верх