Чи є вкладені спробу / крім блоків у python гарною практикою програмування?


201

Я пишу власний контейнер, який повинен надати доступ до словника всередині за викликами атрибутів. Типове використання контейнера було б таким:

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"

Я не впевнений, чи є вкладені спроби / крім блоків хорошою практикою, тому іншим способом було б використання 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")

Або скористатися одним із них та одним блоком спробувати ловити так:

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")

Який варіант є найбільш пітонічним та елегантним?


Буду вдячний, що боги-пітони надають if 'foo' in dict_container:. Амінь.
gseattle

Відповіді:


181

Ваш перший приклад ідеально чудовий. Навіть офіційні документи 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замість цього.


2
return object.__getattribute__(item)невірно і призведе TypeErrorдо того, що передається неправильна кількість аргументів. Натомість це має бути return object.__getattribute__(self, item).
мартіно

13
PEP 20: плоский краще, ніж вкладений.
Іоанніс Філіппідіс

7
Що from Noneозначає останній рядок?
niklas

2
@niklas Це по суті пригнічує контекст винятку ("під час обробки цього винятку стався інший виняток" -повідомлення). Дивіться тут
Каде

Той факт, що документи Python рекомендують вводити спробу гніздування, є безумним. Це, очевидно, жахливий стиль. Правильний спосіб роботи з ланцюжком операцій, коли все може вийти з ладу, як-от використання монадичної конструкції, яку Python не підтримує.
Генрі Генрінсон

19

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

Підсумовуючи, єдиною проблемою буде те, що ваш код стає занадто сильним відступом. Якщо вам це подобається, спробуйте спростити деякі гніздування, як запропоновано lqc


10

Для вашого конкретного прикладу вам насправді не потрібно гніздовати їх. Якщо вираз у 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__спростить речі, оскільки ви будете знати, що нормальний процес пошуку атрибутів вже не вдався.


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

На мою думку, це був би найпітонічніший спосіб вирішити це, хоча і тому, що це робить ваше запитання суперечливим. Зауважте, що це визначає __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
Я хотів прийняти вашу відповідь, проте в питанні мені було цікавіше, чи є використання вкладеного блоку спробу / лову є хорошою практикою. З іншого боку, це найелегантніше рішення, і я збираюся його використовувати у своєму коді. Велике спасибі Мартіну.
Міхал

Міхал: Вас вітають. Це також швидше, ніж використання __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

2
Ця відповідь насправді не стосується оригінального запитання, але для тих, хто її читає, зауважте, що «голою», за винятком кінця, є страшна ідея (як правило), оскільки вона сприйме все, включаючи, наприклад, NameError & KeyboardInterrupt - що зазвичай не те, що ви мали на увазі!
програв

Зважаючи на те, що код повторно викликає той самий виняток відразу після заяви про друк, це справді велика справа. У цьому випадку він може надати більше контексту на виняток, не приховуючи його. Якби це не було повторно, я б повністю погодився, але я не думаю, що існує ризик приховати виняток, якого ви не мали наміру.
NimbusScale

4

Хорошим і простим прикладом для вкладеної спроби / за винятком може бути наступний:

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

[звичайно, у нас нудні, тому нам не потрібно створювати цю функцію]


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

Я не думаю, що це питання бути пітонічним чи елегантним. Це справа запобігати виняткам наскільки це можливо. Винятки мають на увазі обробляти помилки, які можуть виникнути в коді або подіях, над якими ви не маєте контролю. У цьому випадку ви маєте повний контроль під час перевірки того, чи є елемент атрибутом чи словником, тому уникайте вкладених винятків та дотримуйтесь своєї другої спроби.


З Документів: У багатопотоковому середовищі підхід LBYL (Подивись перед тим , як стрибнути ) може ризикувати введенням перегонового стану між "дивлячим" та "стрибковим" Наприклад, код if if key in mapping: return mapping [key] може не вдатися, якщо інший потік видаляє ключ із відображення після тесту, але перед пошуком. Це питання можна вирішити за допомогою блокування або за допомогою підходу EAFP (простіше просити пробачення, ніж дозволу) .
Нуно Андре
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.