Являются ли вложенные блоки try / кроме в Python хорошей практикой программирования?

202

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

dict_container = DictContainer()
dict_container['foo'] = bar
...
print dict_container.foo

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

def __getattribute__(self, item):
    try:
        return object.__getattribute__(item)
    except AttributeError:
        try:
            return self.dict[item]
        except KeyError:
            print "The object doesn't have such attribute"

Я не уверен, являются ли вложенные блоки try / кроме хорошей практики, поэтому другим способом было бы использовать hasattr()и has_key():

def __getattribute__(self, item):
        if hasattr(self, item):
            return object.__getattribute__(item)
        else:
            if self.dict.has_key(item):
                return self.dict[item]
            else:
                raise AttributeError("some customised error")

Или использовать одну из них и одну попытку блока catch, например:

def __getattribute__(self, item):
    if hasattr(self, item):
        return object.__getattribute__(item)
    else:
        try:
            return self.dict[item]
        except KeyError:
            raise AttributeError("some customised error")

Какой вариант является наиболее питонным и элегантным?

Michal
источник
Был бы признателен предоставлению богов питонов if 'foo' in dict_container:. Аминь.
gseattle

Ответы:

182

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

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

def __getattribute__(self, item):
    try:
        return object.__getattribute__(item)
    except AttributeError:
        pass  # fallback to dict
    try:
        return self.dict[item]
    except KeyError:
        raise AttributeError("The object doesn't have such attribute") from None

PS. has_key()долгое время считалось устаревшим в Python 2. Используйте item in self.dictвместо этого.

LQC
источник
2
return object.__getattribute__(item)неверно и приведет TypeErrorк неправильному количеству передаваемых аргументов. Так должно быть return object.__getattribute__(self, item).
Мартино
13
PEP 20: квартира лучше, чем вложенная.
Иоаннис Филиппидис
7
Что from Noneозначает в последней строке?
Никлас
2
@niklas По сути, он подавляет контекст исключения («во время обработки этого исключения возникло другое исключение»). Смотрите здесь
Kade
Тот факт, что документы Python рекомендуют вложение попробовать, является своего рода безумным. Это явно ужасающий стиль. Правильный способ обработки цепочки операций, в которых вещи могут потерпеть неудачу, - использовать монадическую конструкцию, которую Python не поддерживает.
Генри Хенринсон
19

Хотя в Java действительно плохая практика использовать исключения для управления потоком (главным образом потому, что исключения заставляют jvm собирать ресурсы ( подробнее здесь )), в Python у вас есть два важных принципа: Duck Typing и EAFP . В основном это означает, что вам рекомендуется использовать объект так, как вы думаете, он будет работать, и обращаться с ним, когда все не так.

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

Бруно Пентедо
источник
10

Для вашего конкретного примера вам не нужно их вкладывать. Если выражение в tryблоке выполнится успешно, функция вернется, поэтому любой код после всего блока try / Кроме того, будет выполнен только в случае неудачной первой попытки. Так что вы можете просто сделать:

def __getattribute__(self, item):
    try:
        return object.__getattribute__(item)
    except AttributeError:
        pass
    # execution only reaches here when try block raised AttributeError
    try:
        return self.dict[item]
    except KeyError:
        print "The object doesn't have such attribute"

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

Кстати, вы можете подумать о том, действительно ли вы хотите использовать __getattribute__вместо __getattr__здесь. Использование __getattr__упростит вещи, потому что вы будете знать, что нормальный процесс поиска атрибутов уже потерпел неудачу.

BrenBarn
источник
10

Только будьте осторожны - в этом случае сначала finallyдотрагивается, НО пропускается тоже.

def a(z):
    try:
        100/z
    except ZeroDivisionError:
        try:
            print('x')
        finally:
            return 42
    finally:
        return 1


In [1]: a(0)
x
Out[1]: 1
Славомир Ленарт
источник
Вау, это поражает меня ... Не могли бы вы указать мне на какой-то фрагмент документации, объясняющий это поведение?
Михал
1
@Michal: fyi: оба finallyблока выполняются a(0), но finally-returnвозвращается только родитель .
Славомир Ленарт
7

На мой взгляд, это был бы самый Pythonic способ справиться с этим, хотя и потому, что это делает ваш вопрос спорным. Обратите внимание, что это определяет __getattr__()вместо того, __getattribute__()потому что это означает, что он имеет дело только со «специальными» атрибутами, хранящимися во внутреннем словаре.

def __getattr__(self, name):
    """only called when an attribute lookup in the usual places has failed"""
    try:
        return self.my_dict[name]
    except KeyError:
        raise AttributeError("some customized error message")
Мартино
источник
2
Обратите внимание, что повышение исключения в exceptблоке может привести к сбивающим с толку выводам в Python 3. Это потому, что (согласно PEP 3134) Python 3 отслеживает первое исключение (the KeyError) как «контекст» второго исключения (the AttributeError), и если оно достигает верхний уровень, он напечатает трассировку, которая включает оба исключения. Это может быть полезно, когда второе исключение не ожидалось, но если вы намеренно повышаете второе исключение, это нежелательно. Для Python 3.3 в PEP 415 добавлена ​​возможность подавления контекста с помощью raise AttributeError("whatever") from None.
Blckknght
3
@Blckknght: в этом случае было бы неплохо распечатать трассировку, включающую оба исключения. Другими словами, я не думаю, что ваше полное утверждение, что оно всегда нежелательно, является правдой. При использовании здесь, он превращается KeyErrorв AttributeErrorи показывает, что то, что произошло в трассировке, было бы полезно и уместно.
Мартино
В более сложных ситуациях вы можете быть правы, но я думаю, что при преобразовании между типами исключений вы часто знаете, что детали первого исключения не имеют значения для внешнего пользователя. То есть, если __getattr__возникает исключение, ошибка, вероятно, является опечаткой в ​​доступе к атрибуту, а не ошибкой реализации в коде текущего класса. Отображение более раннего исключения в качестве контекста может запутать это. И даже когда вы подавляете контекст с помощью raise Whatever from None, вы все равно можете получить предыдущее исключение при необходимости через ex.__context__.
Blckknght
1
Я хотел принять ваш ответ, однако в вопросе мне было более любопытно, является ли использование вложенного блока try / catch хорошей практикой. С другой стороны, это самое элегантное решение, и я собираюсь использовать его в своем коде. Большое спасибо Мартин.
Михал
Михал: Всегда пожалуйста. Это также быстрее, чем использовать __getattribute__().
Мартино
4

В Python проще просить прощения, чем разрешения. Не беспокойтесь о вложенной обработке исключений.

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

Игнасио Васкес-Абрамс
источник
4

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

import sys

try:
    f = open('myfile.txt')
    s = f.readline()
    i = int(s.strip())
except IOError as e:
    print "I/O error({0}): {1}".format(e.errno, e.strerror)
except ValueError:
    print "Could not convert data to an integer."
except:
    print "Unexpected error:", sys.exc_info()[0]
    raise
Blairg23
источник
2
Этот ответ на самом деле не относится к первоначальному вопросу, но для любого, кто его читает, обратите внимание, что «голое», за исключением того, что в конце, это ужасная идея (обычно), так как он поймает все, включая, например, NameError & KeyboardInterrupt - что обычно не является тем, что вы имели в виду!
проиграл
Учитывая, что код повторно вызывает то же исключение сразу после оператора print, действительно ли это большое дело. В этом случае он может предоставить больше контекста исключения, не скрывая его. Если бы он не повысился, я бы полностью согласился, но я не думаю, что есть риск скрыть исключение, которое вы не собирались делать.
NimbusScale
4

Хороший и простой пример для вложенного try / кроме может быть следующим:

import numpy as np

def divide(x, y):
    try:
        out = x/y
    except:
        try:
            out = np.inf * x / abs(x)
        except:
            out = np.nan
    finally:
        return out

Теперь попробуйте различные комбинации, и вы получите правильный результат:

divide(15, 3)
# 5.0

divide(15, 0)
# inf

divide(-15, 0)
# -inf

divide(0, 0)
# nan

[конечно, у нас есть numpy, поэтому нам не нужно создавать эту функцию]

Ravaging Care
источник
2

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

Например, в моем коде я изначально написал

try:
    return tuple.__getitem__(self, i)(key)
except IndexError:
    raise KeyError(key)

И я получил это сообщение.

>>> During handling of above exception, another exception occurred.

То, что я хотел, было это:

try:
    return tuple.__getitem__(self, i)(key)
except IndexError:
    pass
raise KeyError(key)

Это не влияет на то, как обрабатываются исключения. В любом блоке кода KeyError был бы пойман. Это просто вопрос получения очков стиля.

Стив Желязник
источник
Посмотрите использование принятого ответа на повышение, from Noneхотя, для еще большего количества стилей. :)
Пианозавр
1

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

def f2():
    try:
        a = 4
        raise SyntaxError
    except SyntaxError as se:
        print('log SE')
        raise se from None
    finally:
        try:
            raise ValueError
        except ValueError as ve:
            a = 5
            print('log VE')
            raise ve from None
        finally:
            return 6       
        return a

In [1]: f2()
log SE
log VE
Out[2]: 6
Гуанхуа Шу
источник
Это поведение отличается от примера, данного @ Sławomir Lenart, когда, наконец, вложен внутрь, кроме блока.
Гуанхуа Шу
0

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

owobeid
источник
Из документов: В многопоточной среде подход LBYL (Look Before You Leap) может создать риск возникновения расы между «смотрящим» и «прыгающим». Например, код if key в mapping: return mapping [key] может завершиться ошибкой, если другой поток удаляет ключ из отображения после теста, но перед поиском. Эта проблема может быть решена с помощью блокировок или с помощью подхода
Нуно Андре