среда, 29 ноября 2006 г.

“Бородавки” в Python (Python Warts)

Эта статья - перевод известной статьи Andrew KuchlingPython Warts.
Слово "wart" дословно переводится с английского как "бородавка", и именно в этом контексте используется в английском программистском сленге. Словарь Lingvo также содержит аналогичный перевод этого слова в том же контексте, уже для русского программистского сленга. В своей практике я такого термина в русском языке не встречал, но поверю словарю и не стану изобретать более "приятного" перевода :)

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

Введение


Определение слова "wart" в Jargon File (крупнейшем словаре программистского сленга) звучит:
Небольшая, малопонятная особенность, которая выделяется на фоне общего понятного дизайна. Нечто заметное благодаря своему уродству, проявляющемуся в некоторых обстоятельствах, особенно в качестве специального случая, исключения из стандартных правил поведения. ...


Несмотря на то, что я считаю, что Python спроектирован в очень элегантной манере, что позволяет ему успешно балансировать между минимализмом Lisp и вычурными изысками Perl, его дизайн, конечно, не идеален. Существуют разнообразные особенности дизайна, которые я считаю уродливыми, или по крайней мере в некотором роде неоптимальными. Эта статья расскажет о наиболее значительных, с моей точки зрения, проблемах Python, собранных не без помощи сообщества участников comp.lang.python.

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

Так что давайте изучим некоторые изъяны дизайна Python, перечисленные в произвольном порядке. Некоторые из них сейчас представляют лишь исторический интерес, так как были исправлены в современных версиях Python.


Область видимости и функциональное программирование


(Исправлено в Python 2.1/2.2.)

В Python есть 3 встроенные функции, map(), filter() и reduce(), которые предоставляют некоторую помощь для программирования в функциональном стиле. В Python до версии 2.2 использование этих функций часто требовало небольшого хака при определении аргументов функций по умолчанию.

Возьмем для примера одну из этих функций: filter(func, L) итерирует по списку L, возвращая новый список, содержащий все элементы списка L, для которых func(element) вернула истинное значение. Рассмотрим ситуацию когда у нас есть список чисел L, и мы хотим выделить все четные (кратные 2). Можно написатть это так:

[python]
L2 = filter(lambda N: (N % 2) == 0, L)
[/python]

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

L2 затем присваивается значение списка всех четных элементов списка L:

[python]
>>> L = [0, 5, 4, 32.5, 17, -6]
>>> L2 = filter(lambda N: (N % 2) == 0, L)
>>> print L2
[0, 4, -6]
>>>
[/python]

А что если мы хотим расширить эту функцию так, чтобы мы могли возвращать числа, кратные значению переданного параметра m? В версиях Python вполть до 2.1 из-за правил области видимости переменных этот очевидный код не работал, будучи использованным внутри функции:

[python]
def f(L):
m = 3
L2 = filter(lambda N: (N % m) == 0, L)
return L2
f([0, 2, 3, 17, -15])
[/python]

Если вызвать это под Python 2.1, вы получите NameError для переменной m во второй строке f(). Внутри анонимной функции, определенной в выражении lambda, правила области видимости Python требовали, чтобы переменная искалась сначала в локальной области видимости функции, а затем в области видимости модуля; локальная область видимости f() вообще не рассматривалась при поиске. Решением было определить m как аргумент по умолчанию:

[python]
def f(L):
m = 3
L2 = filter(lambda N, m=m: (N % m) == 0, L)
return L2
f([0, 2, 3, 17, -15])
[/python]

Если функция, используемая для фильтрования, требовала несколько переменных из локальной области видимости f(), все они должны были быть переданы ей как аргументы по умолчанию. Это объясняет присутствие в коде, использующем map() или filter(), таких конструкций как self=self, v=v и т.п. Исправление этой ситуации потребовало изменения правил области видимости в Python.

В Python 2.1 былa введена статическая область видимости (static scoping) для решения этой проблемы. В 2.1 эта возможность должна была быть явно задействована на уровне модуля при помощи директивы from __future__ import nested_scopes в начале модуля, а по умолчанию новое поведение стало доступным только в Python 2.2. Это было сделано для того, чтобы не нарушить работу существующего кода, поведение которого могло измениться в результате ввода новых правил; Python 2.1 печатал предупреждения в тех программах, которые могли производить разные результаты в Python 2.2, так что у всех было достаточно времени для того, чтобы внести исправления. (PEP 236 содержит описание того как изменения, нарушающие совместимость, постепенно интегрируются в Python.)

Первым результатом ввода статической области видимости стало то, что присваивание аргументов по умолчанию (m=m) стало не нужно в приведенном выше примере; первая версия функции теперь работает как и положено. По новым правилам, когда имени переменной не присвоено значение внутри функции (оператором присваивания или объявлениями def, class, или import), переменная будет искаться в локальном пространстве имен блока, который включает данную функцию. Более подробное описание правил и подробности реализации можно почитать в PEP 227: "Nested Scopes".


Правила области видимости


(Исправлено в Python 2.1/2.2.)

Это — другая демонстрация той же проблемы. Рекурсивные функции, такие как в примере ниже, работали только если они были расположены в пространстве имен модуля:

[python]
def fact(n):
if n == 1: return 1
else: return n*fact(n-1)
[/python]

Однако, если эта функция была объявлена внутри другой функции, это приводило к следующему результату:

[python]
def g():
def fact(n):
if n == 1: return 1
else: return n*fact(n-1)
print fact(5)
[/python]

Когда вызвана вложенная версия fact(), у нее не было доступа к своему собственному имени, потому как имя fact не было определено ни в локальной области видимости fact, ни в области видимости модуля.


[amk@mira amk]$ python2.1 /tmp/t.py
Traceback (innermost last):
File "/tmp/t.py", line 8, in ?
g()
File "/tmp/t.py", line 6, in g
print fact(5)
File "/tmp/t.py", line 5, in fact
else: return n*fact(n-1)
NameError: fact


Эта проблема ушла после введения статической области видимости в Python 2.1, о чем написано выше.


Дихотомия типов и классов


(Исправлено в Python 2.2.)

Глубоко в потрохах реализации Python на C, типы и классы имеют тонкие отличия. Тип — это структура C, которая содержит несколько таблиц указателей на функции C. К примеру, одна таблица указывает на функции, реализующие числовые операции, такие как + и *.
Типы, которые не реализуют данную функцию, просто устанавливают соответствующий указатель в NULL; в противном случае, это указатель на функцию C, которая реализует соответствующую операцию. С другой стороны, класс — это объект Python; методы со специальными именами, такими как __getitem__ и __add__ используются для того, чтобы добавить семантики в такие операции как доступ по ключу или поведение как слагаемого операции сложения.

Проблема в том, что типы и классы сходны в одних аспектах, но различны в других. И те и другие содержат атрибуты и методы, и даже можно переопределить операторы для них, но вы не могли определить подкласс типа, потому что типы и классы были реализованы по-разному. Это значило, что вы не могли в Python унаследовать от встроенных типов, таких как список (list), словарь (dict), объект файла, для того, чтобы добавить им новый метод или переопределить поведение. Конечно, было возможно эмулировать встроенный тип путем определения всех требуемых специальных методов. Например, класс UserList из стандартной библиотеки эмулирует списки путем реализации методов __len__, __getitem__, __setitem__, __setslice__ и т.д. Но это утомительно, а также ваш класс-обертка может стать устаревшей если в будущих версиях Python будут добавлены новые методы или атрибуты. Если какой-либо код делал явную проверку типа переменной, например так

[python]
def f(list_arg):
if not isinstance(list_arg, list):
raise TypeError, "list_arg should be a list"
[/python]

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

В Jython нет этой дихотомии потому что в нем можно унаследовать от любого класса Java. ExtensionClass Джима Фултона (Jim Fulton) мог облегчить эту проблему для CPython, но это нестандартный компонент Python, требует компиляции расширения на C, и неспособен эмулировать класс полностью, так что это это закончилось бы тем, что возникли бы новые различные проблемы, которые проявляли бы себя менее часто, зато были бы более трудоемки в отладке и их было бы более сложно обойти при встрече. К примеру, при использовании ExtensionClass невозможно определить класс, похожий на встроенный list и переопределить методы для сравнения экземпляров этого класса и обычными списками Python.

Эта проблема может быть решена, но оборотная сторона в том, что обобщение типов и классов возможно потребует потери в производительности. Код Python на C может использовать быстрые вызовы если известно, что нужный объект Python будет определенного типа C. Например, классы, экземпляры классов и модули, все они реализованы как пространства имен, используя словарный тип. Если есть возможность унаследовать от словаря, то также должна быть возможность использовать подкласс словаря для реализации класса, экземпляра класса или модуля (это может быть полезно для всякого рода умных трюков, связанных с изменением семантики используемого класса словаря — к примеру, словарь, доступный только на чтение, мог бы предотвратить модификацию экземпляров классов или модулей.) Но это означает, что получение значения из словаря по ключу не может быть выполнено при помощи кода для специального случая, который подразумевает реализацию словаря на С по умолчанию, а вместо этого должен быть выполнен более общий код, независимый от типа. Я бы хотел, чтобы эта проблема была решена, потому как это сделало бы возможным реализовывать многие вещи более просто и понятно, но какой потерей в скорости я готов за это заплатить? Я на самом деле не знаю...

В Python 2.2 была введена возможность наследовать от стандартных типов, таких как списки и словари, и различия между классами и типами были значительно сокращены. Эти изменения сложно описать, требуется введение в новые правила семантики наследования и взаимодействий, коротко называемые как "классы нового стиля" (new-style classes).

Обратная совместимость при этом имеет высокий приоритет, поэтому нельзя было просто отделаться от старых правил (т.н. "классических классов", "classic classes"). Вместо этого оба набора правил будут сосуществовать. Если вы определяете подкласс стандартного типа или нового в Python 2.2 типа object, ваш подкласс следует правилам классов нового стиля; иначе он следует правилам классических классов. В какой-нибудь будущей версии, скорее всего в Python 3.0, старые правила могут быть упразднены; как постепенно это изменение будет вводиться, пока не ясно.

Лучше всего для изучения классов нового стиля поможет эссе Гвидо Ван Россума. Более детальный разбор новых правил содержится в трех PEP, описывающих их: PEP 252: "Making Types Look More Like Classes", PEP 253: "Subtyping Built-in Types", и PEP 254: "Making Classes Look More Like Types" (вы должны быть настоящим волшебником в Python для того чтобы найти много интересного в этих PEP; в них действительно про дремучее колдовство.)


Конструкция do отсутствует



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

[python]
# читать строки до тех пор пока не встретится пустая строка
while 1:
line = sys.stdin.readline()
if line == "\n":
break
[/python]

Конструкция while 1: ... if (condition): break часто встречается в коде на Python. Код выглядел бы лучше, если можно было бы написать так:

[python]
# читать строки до тех пор пока не встретится пустая строка
do:
line = sys.stdin.readline()
while line != "\n"
[/python]

Добавление этой новой управляющей конструкции в Python было бы довольно последовательно, но при этом любой существующий код, который использует "do" в качестве переменной, перестал бы работать с введением нового ключевого слова do. PEP 315: "Enhanced While Loop" содержит подробное описание данного предложения, но до сего момента никаких движений по этому поводу не произошло.

Введение в Python 2.2 итераторов сделало возможность описать многие из таких циклов с использованием конструкции for вместо while — способ, который может быть для вас более приемлемым:

[python]
for line in sys.stdin:
if line == '\n':
break
[/python]


Оптимизация локальных переменных



Этот дефект достает людей достаточно часто. Посмотрите на функцию ниже. Что вы думаете произойдет когда вы ее вызовете?

[python]
i=1
def f():
print "i=",i
i = i + 1
f()
[/python]

Наверно, вы думаете, что она выведет "i=1" и увеличит значение i до 2. На самом деле вы получите вот это:


i=
Traceback (innermost last):
File "<stdin>", line 1, in ?
File "<stdin>", line 2, in f
NameError: i


Что происходит?
Транслятор исходного кода Python пытается оптимизировать доступ к локальным переменным. Если переменной в функции когда-либо присваивается значение, он решает, что переменная — локальная. Без присваивания i = i + 1, Python предположил бы что i — глобальная переменная, и сгенерировал бы код для доступа к переменной как к глобальной, на уровне модуля. Когда есть присваивание, транслятор генерирует другой код, который подразумевает, что переменная i — локальная переменная; локальным переменным соответствуют последовательные номера в массиве, поэтому доступ к ним происходит гораздо быстрее. Выражение print, таким образом, скомпилировано так, чтобы искать локальную i, которая пока не существует, и слетает с исключением NameError. В Python 1.6 и позже вызывается другое исключение, UnboundLocalError, в надежде на то, что это сделает проблему более понятной.

Чтобы обойти эту проблему, нужно декларировать i как глобальную (global) в вашей функции, например, так:

[python]
i=1
def f():
global i
print "i=",i
i = i + 1
[/python]

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

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


"Сырые" строки (r-strings)



Это — строковые константы, в которых не производится обработка экранированных \-последовательностей, и были добавлены главным образом для того, чтобы сделать более читаемыми шаблоны регулярных выражений. Строковые константы в Python используют символ \ для специальных последовательностей, как делается практически в любом основанном на C языке, но это конфликтует с частым использованием регулярных выражений. Двойное экранирование часто приводило в замешательство; для того, чтобы написать шаблон, соответствующий команде '\break' из TeX, регулярное выражение должно быть \\break, следовательно строковая константа в Python будет "\\\\break". r-strings убрали один уровень экранирования, так что можно писать r"\\break", что делает шаблон регулярного выражения более читаемым. Это прекрасно работает, но все же в некотором роде это — хак.

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

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

[python]
pattern = re.compile((break|par), re.M)
[/python]

Однако, заставить это работать было бы непростым делом; откуда парсеру знать, где закончился шаблон регулярного выражения? r-strings по крайней мере делают нам милость быть простыми и понятными. Вкратце, хотя я и не считаю идею r-strings очень красивой, они действительно решают проблему, благодаря которой они появились, и у меня пока нет лучшего решения1.


Вызов методов базового класса



Синтаксис для вызова методов базового класса ужасен. Представим себе класс C с методом f(). Если вы делаете подкласс C, переопределяете f(), вам нужно вызвать оригинальный метод, то синтаксис вызова будет такой: C.f(self,x). Если f() принимает именованный аргумент, который должен быть передан оригинальному методу, в Python до версии 2.0 дела были еще хуже, потому что для этого нужно было использовать стандартную функцию apply(): apply(C.f, (self,), kwdict) (в Python 2.0 добавился синтаксис f(arg1, arg2, **kwdict).)

Python не вызывает автоматически конструкторы базовых классов, так что в подклассах нужно это делать явно:

[python]
class Base:
def __init__(self,x,y,**kw):
...

class Derived(base.Base):
def __init__(self,x,y,z,**kw):
base.Base.__init__(self,x,y,**kw)
...
[/python]

Хадсон (Hudson) назвал это "одной из уродливейших вещей, которые я когда-либо встречал в Python, для которых я не могу придумать более красивое решение".

Классы нового стиля в Python 2.2 добавили стандартную функцию super(), которая позволяет вызывать методы базового класса способом получше. В 2.2 можно писать так:

[python]
class Derived(base.Base):
def __init__(self,x,y,z,**kw):
super(Derived, self).__init__(self,x,y,**kw)
...
[/python]

Использование super() будет также более правильным, если класс Derived унаследован от нескольких базовых классов (в случае множественного наследования) и некоторые или все из них определяют свои методы __init__.


Числа целые и с плавающей запятой



Результат выражений с целыми числами время от времени может вас удивить; наиболее известный пример — это то, что 7 / 2 возвращает 3 вместо 3.5. Это приехало в Python в наследство от C; операции с целыми числами возвращают в результате целые числа, а не числа с плавающей запятой. Рэнди Поуч (Randy Pausch), который использовал модифицированную версию Python в системе Alice для преподавания программирования непрограммистам, характеризует это как постоянный источник путаницы. Программисты, знакомые с семантикой C, однако, наверняка привычны к такому поведению.

Эта проблема в процессе исправления, но изменения, связанные с такой фундаментальной операцией, должны продвигаться очень осторожно. Процесс был начат в Python 2.2 с вводом нового оператора // и названного floor-division operator (оператор деления с округлением вниз), который всегда обрезает дробную часть. Существующие программы, которые зависят от такого поведения, могут просто использовать // вместо /.

В Python 2.2 семантика оператора / не изменилась. Но если добавить директиву from __future__ import division, это приведет к тому, что / будет производить настоящее деление (true division); 1/2 будет 0.5, а не 0. Интерпретатор Python поддерживает опции командной строки, которые приведут к выдаче предупреждений в процессе исполнения когда операция деления применена к двум целым числам; эти опции могут быть использованы для поиска кода, подверженного изменениям, и его исправления. Значение оператора / не изменится до выхода Python 3.0.


Перехват нескольких исключений



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

[python]
try:
... whatever ...
except NameError, OverflowError: # Ошибка здесь
... something else ...
[/python]

Если вызвано исключение NameError, выражение связывает объект исключения с именем 'OverflowError'. Позже, если вы захотите перехватить OverflowError в этом же пространстве имен, это не сработает, потому что имя 'OverflowError' уже не будет связано с корректным объектом исключения. Правильный синтаксис такой:

[python]
except (NameError, OverflowError):
[/python]

Тим Петерс (Tim Peters) замечает, что "Никто не говорит, что это правильно, но это также не является обычной ошибкой (перехват N исключений необычен с вероятностью 1 < N < бесконечность). Так что, хотя это и грех, о нем едва ли стоит упоминать."2.

Также стоит заметить, что за try может следовать только except или finally, а не оба, как в Java. Это потому, что сделать так, чтобы генератор байткода работал правильно в обоих случаях, действительно сложно, так что Гвидо плюнул и ввел это ограничение3.


Явное объявление self в методах



Постоянно говорится о том, что требование писать self. при доступе к атрибутам объекта в методах класса терпимо, но неудобно, и неявное this, как в C++ и Java было бы более подходящим. Скорее всего это лишь вопрос предпочтений; мой код на Java легко узнаваем по постоянному использованию явного объявления this.attribute везде где только можно. Многие из стандартов написания кода для Java или C++ требуют, чтобы атрибуты объектов обязательно содержали специальный префикс (например, m_) для того, чтобы отличать их от локальных переменных; скорее всего те, кто не любит self, обречены изобретать ему замену.

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

[python]
def method (s, arg1, arg2=None):
s.attr1 = arg1
if arg2 is not None:
s.other_meth(arg2)
[/python]

Хотя s вместо self не следует стандартам написания кода в Python, немногие почувствуют трудности, если примут такую технику.


Двойные подчеркивания в именах приватных переменных



Использование двойного подчеркивания в начале имени переменной, как простой способ сделать ее чем-то подобной приватной переменной, дает сигнал транслятору Python исказить ее имя в процессе компиляции в байткод. Например, имя атрибута класса C, названного __value, будет транслировано в _C__value. Это, конечно, предотвратит конфликт имен между приватными переменными класса и его подкласса. Однако, это — грязный хак; делать приватность зависимой от посторонней сущности, такой как имя атрибута — неуклюже. Радует, что это уродство ограничено единственным и редко используемым случаем применения; немногие программисты на Python хотя бы раз задумывались об использовании этой особенности для организации приватного доступа к переменным.


Метод .join() строкового объекта



В Python 2.0 были введены методы для объектов строк и Unicode. Например, 'abcdef'.replace('bcd','ZZZ') вернет строку 'aZZZef'. Строки остались неизменяемыми объектами, так что метод всегда возвращает новую строку вместо изменения значения существующего строкового объекта. Большинство функций модуля string теперь также доступны как методы объекта строки, и использование этих методов вместо функций модуля string крайне рекомендовано.

Для большинства методов не существует разногласий относительно уместности их в качестве метода объекта строки. Действительно, то, что s.upper() возвращает версию исходной строки в верхнем регистре, достаточно последовательно и лишено противоречий. Главное исключение из этого — функция string.join(seq, sep), которая принимает последовательность seq, содержащую строки, и склеивает их, вставляя строку sep между склеиваемыми элементами. Аналогичный метод строкового объекта — sep.join(seq), который многим кажется написанным задом наперед. Действительно, довольно странно думать о разделителе как о главном персонаже в этой ситуации; люди считают, что скорее главное здесь — последовательность, и ожидают, что это должно выглядеть скорее как seq.join(sep), где join() — метод объекта последовательности. На это мнение есть возражение, что будет более складно, если использовать этот метод следующим образом:

[python]
space = ' '
newstring = space.join(sequence)
[/python]

Однако очень многие считают этот аргумент неубедительным, полагая, что пример не слишком жизненный, т.к. доступ к объекту строки осуществляется посредством переменной, вместо того, чтобы использовать непосредственно строковую константу. Гвидо Ван Россум высказался против добавления join() в объекты последовательностей, т.к. это бы означало, что каждая последовательность должна была бы реализовывать этот метод; в Python есть три типа последовательностей (строки, кортежи и списки), а также куча классов, которые ведут себя как последовательности.

Было бы не очень обидно (если вы находите метод join() неудобным, вы просто можете его не использовать) если бы не то, что модуль string будет полностью упразднен в Python 3.0, а это означает, что синтаксис string.join() будет недоступен. По этому поводу было сломано немало копий в comp.lang.python и списке рассылки python-dev. В качестве альтернативы могло бы стать добавление стандартной функции join(), которая могла бы использоваться для объединения любой последовательности строк, но на это есть возражение, что объединение последовательностей не является слишком частой операцией для того, чтобы заслужить новую стандартную функцию.


print >>



Другим нововведением в Python 2.0 стала возможность перенаправлять вывод конструкции print в указанный объект с файловым интерфейсом, используя синтаксис, заимствованный из командной оболочки Unix. Синтаксис этой конструкции в Python такой:

[python]
import sys
print >>sys.stderr, "warning: ..."
[/python]

Многим не нравится идея добавления пунктуации в Python для этого конкретного случая; совсем не сложно получить строку и использовать метод файлового объекта write(). С другой стороны, эта функциональность чрезвычайно полезна для записи событий в лог ошибок CGI-скрипта или что-либо подобное, и никто пока что не пришел с лучшим предложением. Python пытается вводить по возможности меньше новых синтаксических изобретений, и многим синтаксис >> знаком по использованию командной оболочки и языков, которые заимствовали ее синтаксис, таких как Perl, так что использование такой конструкции кажется наилучшим выбором. Само собой, вполне можно писать код так, будто этой функциональности нет вообще.

Самый странный из случаев использования этой особенности — print >>None. В списке рассылки python-dev по этому поводу какое-то время спорили; разумным было бы выдавать ошибку, просто игнорировать вывод (так что print >>None было бы эквивалентно записи в /dev/null), или использовать какой-либо файловый объект по умолчанию. Гвидо Ван Россум избрал самый странный и наиболее колдовской вариант поведения; print >>None направляет вывод в стандартный поток вывода — sys.stdout.


В заключение



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


Благодарности



Выражаю благодарность людям, которые комментировали и оставляли предложения по данной статье: Scott Daniels, Bruce Eckel, John Farrell, Michael Hudson, Tim Peters, Reuben Sumner, Jarno Virtanen.


Дополнительные ссылки



Hans Nowak написал статью "10 Python pitfalls" (перевод), описывающую особенности языка, которые бывают ловушками, в которые часто попадаются программисты.

Две похожих на эту статьи про Perl написали Aaron Weiss и Tom Christiansen (очень давно, уже недоступна).


Примечания переводчика


1. Существует еще одна "бородавка", связанная с r-strings, не описанная в этой статье. Символ "\" в r-string не обрабатывается, за исключением случая, когда этот символ стоит в конце строки. В этом случае возникает исключение SyntaxError. Скорее всего, это связано с особенностями трансляции кода в Python, на стадии синтаксического анализа.

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

3. Гвидо все-таки решил эту проблему, и начиная с Python 2.5 можно использовать полную версию конструкции try ... except ... finally

8 комментариев:

  1. Комментарий больше к автору, чем к переводчику. Работа переводчика однозначно 5 баллов.

    Для себя бы я из перечисленного выделил бы локальную область видимости, да неразбериху с делением. Другие либо не считаю "бородавками" (явный вызов конструктора базового класса, явное определение self, двойное подчеркивание в имени метода/атрибута), либо не использую (print с &gt;&gt;), либо их устранили разработчики в текущих версиях (я использую 2.4).

    Между тем, статья очень полезная. Пиши еще!

    ОтветитьУдалить
  2. Статья хорошая (что-то много в последнее время мне попадается статей о недостатках Питона :( ). Перевод понравился, читается легко и непринуждённо :)

    Одна замеченная замечательная опечатка:
    "Нечто заметное блягодаря"

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

    ОтветитьУдалить
  3. Как, это все недостатки? :)
    Кроме "do:" ничего серьезного не нашел, но do жалко. Надеюсь в py3k этот PEP сделают. Остальное фигня :).

    Если это все глюки языка что есть тоды я с него никогда не слезу :)

    ОтветитьУдалить
  4. Такая конструкция,
    do:

    while :

    мне кажется более уродливой бородавкой, чем отсутствие do вообще. Думаю что будет использовать не часто (при использовании do), тогда код должен выглядеть так:
    do:

    while :
    pass
    Мне не нравится. Хотя и использование if раздражает.
    Во всем остальном, в чем то солидарен с автором, в чем то нет. Спасибо за перевод.

    ОтветитьУдалить
  5. Лучше бы вместо do сделали присваивание в if. Тогда можно было бы писать
    while line = sys.stdin.readline():
    pass

    Хотя появляется опасность ошибки написания "=" вместо "==".

    ОтветитьУдалить
  6. [...] “Бородавки” в Python (Python Warts) Auto Date Куда пропала первая строка масива [...]

    ОтветитьУдалить

Постоянные читатели