Почему Exception прокси __str__ на аргументы? - PullRequest
0 голосов
/ 26 октября 2018

Почему при печати экземпляра исключения печатается значение exc.args вместо непосредственного представления exc?Документы называют это удобством , но на самом деле это неудобство на практике.

Невозможно определить разницу между * аргументами и кортежем:

>>> print(Exception(123, 456))
(123, 456)
>>> print(Exception((123, 456)))
(123, 456)

Невозможно надежно различить тип:

>>> print(Exception('123'))
123
>>> print(Exception(123))
123

И прекрасное "невидимое" исключение:

>>> print(Exception())

>>> 

, которое вы унаследуете, если не попросите:

>>> class MyError(Exception):
...     """an error in MyLibrary"""
...     
>>> print(MyError())

>>> 

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

Чтообоснование такой странной реализации Exception.__str__?Предположительно, если пользователь хочет напечатать exc.args, тогда он должен просто напечатать exc.args?

1 Ответ

0 голосов
/ 26 октября 2018

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

Текущая реализация восходит к PEP 0352, который обеспечивает обоснование:

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

Конечно, сам Python во многих случаях нарушает этот принцип полезных для человека сообщений - например, строка не найдена KeyError - это ключ, который не был найден, что приводит к сообщениям отладки, таким как

An error occurred: 42

Причина, по которой str(e) по сути str(e.args) или str(e.args[0]), изначально была обратно совместима с Python 1.0. В Python 1.0 синтаксис для вызова исключения, такого как ValueError, был бы:

>>> raise ValueError, 'x must be positive'
Traceback (innermost last):
  File "<stdin>", line 1
ValueError: x must be positive

Python сохранил обратную совместимость с 1.0 до 2.7, так что вы можете запускать большинство программ на Python 1.0 без изменений в Python 2.7 (как никогда раньше):

>>> raise ValueError, 'x must be positive'
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
ValueError: x must be positive

Аналогично, в Python 1.0 вы бы поймали ValueError с

>>> try:
...     raise ValueError, 'foo'
... except ValueError, e:
...     print 'Got ValueError', e

, который работал без изменений в Python 2.7.

Но механизм того, как это работает внутренне, изменился: в Python 1.0.1 ValueError была строкой со значением ... 'ValueError'

>>> ValueError, type(ValueError)
('ValueError', <type 'string'>)

Классов исключений не было вообще, и вы могли raise иметь только один аргумент или кортеж со строкой в ​​качестве дискриминатора:

>>> class MyCustomException: 
...     pass
...   
>>> raise MyCustomException, 'my custom exception'
Traceback (innermost last):
  File "<stdin>", line 1
TypeError: exceptions must be strings

Можно также привести кортеж в качестве аргумента:

>>> raise ValueError, ('invalid value for x', 42)
Traceback (innermost last):
  File "<stdin>", line 1
ValueError: ('invalid value for x', 42)

И если вы поймаете это «исключение» в Python 1.0 , то в e вы получите:

>>> try:
...     raise ValueError, ('invalid value for x', 42)
... except ValueError, e:
...     print e, type(e)
... 
('invalid value for x', 42) 42 <type 'tuple'>

A кортеж !

Давайте попробуем код в Python 2.7 :

>>> try:
...     raise ValueError, ('invalid value for x', 42)
... except ValueError, e:
...     print e, e[1], type(e)
... 
('invalid value for x', 42) 42 <type 'exceptions.ValueError'>

Вывод выглядит идентично, за исключением типа значения; который был tuple до и теперь исключением ... Не только делегат Exception __str__ члену args, но также поддерживает индексацию, как это делает кортеж - и распаковку, итерацию и так далее:

Python 2.7

>>> a, b, c = ValueError(1, 2, 3)
>>> print a, b, c
1 2 3

Все эти хаки с целью обеспечения обратной совместимости.

Поведение Python 2.7 происходит от класса BaseException, который был представлен в PEP 0352 ; PEP 0352 изначально был реализован в Python 2.5.


В Python 3 старый синтаксис был удален - вы не можете вызывать исключения с помощью raise discriminator, (arg, um, ents); и except может использовать только синтаксис Exception as e.

В PEP 0352 обсуждается вопрос о снижении поддержки нескольких аргументов до BaseException:

Было решено, что было бы лучше отказаться от атрибута message в Python 2.6 (и удалить его в Python 2.7 и Python 3.0) и рассмотреть более долгосрочную стратегию перехода в Python 3.0 для устранения поддержки нескольких аргументов в BaseException в предпочтении принятия только одного аргумента. Таким образом, введение сообщения и первоначальное осуждение args были отменены.

По-видимому, это устаревание args было забыто, так как оно все еще существует в Python 3.7 и является единственным способом доступа к аргументам, данным для многих встроенных исключений. Точно так же __str__ больше не нужно делегировать аргументам, и он может фактически псевдоним BaseException.__repr__, который дает более хорошее, однозначное представление:

>>> BaseException.__str__(ValueError('foo', 'bar', 'baz'))
"('foo', 'bar', 'baz')"
>>> BaseException.__repr__(ValueError('foo', 'bar', 'baz'))
"ValueError('foo', 'bar', 'baz')"

но никто не учел это.


P.S. repr исключения полезно - в следующий раз попробуйте распечатать исключение в формате !r:

print(f'Oops, I got a {e!r}')

, что приводит к

ZeroDivisionError('division by zero',)

выводится.

...