Được lồng thử / ngoại trừ các khối trong python là một thực hành lập trình tốt?


200

Tôi đang viết container của riêng mình, nó cần cấp quyền truy cập vào một từ điển bên trong bằng các lệnh gọi thuộc tính. Việc sử dụng điển hình của container sẽ như thế này:

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

Tôi biết rằng có thể thật ngu ngốc khi viết một cái gì đó như thế này, nhưng đó là chức năng tôi cần cung cấp. Tôi đã suy nghĩ về việc thực hiện điều này theo một cách sau:

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"

Tôi không chắc liệu các khối thử / ngoại trừ lồng nhau có phải là một cách thực hành tốt hay không nên sử dụng một cách khác 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")

Hoặc để sử dụng một trong số chúng và thử một khối bắt như thế này:

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

Lựa chọn nào là pythonic và thanh lịch nhất?


Sẽ đánh giá cao các vị thần trăn cung cấp if 'foo' in dict_container:. Amen.
gseatussy

Câu trả lời:


180

Ví dụ đầu tiên của bạn là hoàn toàn tốt. Ngay cả các tài liệu Python chính thức cũng đề xuất phong cách này được gọi là EAFP .

Cá nhân, tôi thích tránh làm tổ khi không cần thiết:

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

Tái bút has_key()đã bị phản đối trong một thời gian dài trong Python 2. item in self.dictThay vào đó hãy sử dụng .


2
return object.__getattribute__(item)là không chính xác và sẽ tạo ra một TypeErrorvì số lượng đối số sai đang được thông qua. Nó nên thay thế return object.__getattribute__(self, item).
martineau

13
PEP 20: phẳng tốt hơn lồng nhau.
Ioannis Filippidis

7
from Nonenghĩa là gì trong dòng cuối cùng?
niklas

2
@niklas Về cơ bản, nó ngăn chặn bối cảnh ngoại lệ ("trong khi xử lý ngoại lệ này, một thông báo ngoại lệ khác đã xảy ra". Xem tại đây
Kade

Thực tế là các tài liệu Python khuyên dùng thử lồng nhau là một kiểu điên rồ. Đó rõ ràng là phong cách khủng khiếp. Cách xử lý chính xác một chuỗi các hoạt động trong đó mọi thứ có thể thất bại như thế sẽ sử dụng một loại cấu trúc đơn điệu nào đó, mà Python không hỗ trợ.
Henry Henrinson

19

Mặc dù trong Java thực sự là một cách thực hành tồi để sử dụng Ngoại lệ để kiểm soát luồng (chủ yếu là do ngoại lệ buộc jvm phải thu thập tài nguyên ( nhiều hơn ở đây )), trong Python bạn có 2 nguyên tắc quan trọng: Gõ vịtEAFP . Điều này về cơ bản có nghĩa là bạn được khuyến khích thử sử dụng một đối tượng theo cách bạn nghĩ nó sẽ hoạt động và xử lý khi mọi thứ không như vậy.

Tóm lại, vấn đề duy nhất là mã của bạn bị thụt quá nhiều. Nếu bạn cảm thấy thích nó, hãy thử đơn giản hóa một số tổ như lqc đề xuất


10

Ví dụ cụ thể của bạn, bạn thực sự không cần phải lồng chúng. Nếu biểu thức trong trykhối thành công, hàm sẽ trả về, do đó, bất kỳ mã nào sau toàn bộ khối thử / ngoại trừ sẽ chỉ được chạy nếu lần thử đầu tiên thất bại. Vì vậy, bạn chỉ có thể làm:

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"

Làm tổ chúng không tệ, nhưng tôi cảm thấy việc để nó phẳng khiến cấu trúc rõ ràng hơn: bạn đang thử tuần tự một loạt các thứ và trả lại cái đầu tiên hoạt động.

Ngẫu nhiên, bạn có thể muốn suy nghĩ về việc bạn có thực sự muốn sử dụng __getattribute__thay vì __getattr__ở đây. Việc sử dụng __getattr__sẽ đơn giản hóa mọi thứ vì bạn sẽ biết rằng quy trình tra cứu thuộc tính thông thường đã thất bại.


10

Chỉ cần cẩn thận - trong trường hợp này đầu tiên finallyđược chạm vào NHƯNG bỏ qua quá.

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

Wow nó thổi vào tâm trí của tôi ... Bạn có thể chỉ cho tôi một số đoạn tài liệu giải thích hành vi này?
Michal

1
@Michal: fyi: cả hai finallykhối được thực thi a(0), nhưng chỉ có cha mẹ finally-returnđược trả về.
Sławomir Lenart

7

Theo tôi đây sẽ là cách Pythonic nhất để xử lý nó, mặc dù và bởi vì nó làm cho câu hỏi của bạn được đưa ra. Lưu ý rằng điều này xác định __getattr__()thay __getattribute__()vì bởi vì làm như vậy có nghĩa là nó chỉ phải xử lý các thuộc tính "đặc biệt" được giữ trong từ điển nội bộ.

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
Lưu ý rằng việc đưa ra một ngoại lệ trong một exceptkhối có thể tạo ra đầu ra khó hiểu trong Python 3. Đó là vì (theo PEP 3134) Python 3 theo dõi ngoại lệ đầu tiên (là KeyError) là "bối cảnh" của ngoại lệ thứ hai (và AttributeError) cấp cao nhất, nó sẽ in một dấu vết bao gồm cả hai ngoại lệ. Điều này có thể hữu ích khi không có ngoại lệ thứ hai, nhưng nếu bạn cố tình nâng ngoại lệ thứ hai, điều đó là không mong muốn. Đối với Python 3.3, PEP 415 đã thêm khả năng triệt tiêu ngữ cảnh bằng cách sử dụng raise AttributeError("whatever") from None.
Blckknght

3
@Blckknght: In một dấu vết bao gồm cả hai trường hợp ngoại lệ sẽ ổn trong trường hợp này. Nói cách khác, tôi không nghĩ tuyên bố chăn của bạn rằng điều đó luôn không mong muốn là đúng. Trong cách sử dụng ở đây, nó biến KeyErrorthành một AttributeErrorvà cho thấy những gì xảy ra trong một lần truy nguyên sẽ hữu ích và phù hợp.
martineau

Đối với các tình huống phức tạp hơn, bạn có thể đúng, nhưng tôi nghĩ rằng khi bạn chuyển đổi giữa các loại ngoại lệ, bạn thường biết rằng các chi tiết của ngoại lệ đầu tiên không quan trọng đối với người dùng bên ngoài. Đó là, nếu __getattr__đưa ra một ngoại lệ, lỗi có thể là lỗi chính tả trong quyền truy cập thuộc tính, không phải là lỗi thực thi trong mã của lớp hiện tại. Hiển thị ngoại lệ trước đó như bối cảnh có thể làm rối loạn điều đó. Và ngay cả khi bạn triệt tiêu bối cảnh raise Whatever from None, bạn vẫn có thể có được ngoại lệ trước đó nếu cần thông qua ex.__context__.
Blckknght

1
Tôi muốn chấp nhận câu trả lời của bạn tuy nhiên trong câu hỏi tôi tò mò hơn liệu việc sử dụng khối thử / bắt lồng có hay không là một cách thực hành tốt. Mặt khác, đó là giải pháp tao nhã nhất và tôi sẽ sử dụng nó trong mã của mình. Rất cám ơn Martin.
Michal

Michal: Bạn được chào đón. Nó cũng nhanh hơn sử dụng __getattribute__().
martineau

4

Trong Python dễ xin tha thứ hơn là cho phép. Đừng đổ mồ hôi xử lý ngoại lệ lồng nhau.

(Bên cạnh đó, has*hầu như luôn sử dụng các ngoại lệ dưới mọi cách.)


4

Theo tài liệu , tốt hơn là xử lý nhiều trường hợp ngoại lệ thông qua các bộ dữ liệu hoặc như thế này:

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
Câu trả lời này không thực sự giải quyết được câu hỏi ban đầu nhưng đối với bất kỳ ai đọc nó đều lưu ý "trần" ngoại trừ ở cuối là một ý tưởng tồi tệ (thông thường) vì nó sẽ nắm bắt mọi thứ, ví dụ như NameError & KeyboardInterrupt - thường không phải là ý bạn!
mất

Cho rằng mã tăng lại ngoại lệ tương tự ngay sau câu lệnh in, đây thực sự là một vấn đề lớn. Trong trường hợp này, nó có thể cung cấp thêm ngữ cảnh về ngoại lệ mà không che giấu nó. Nếu nó không tăng trở lại thì tôi hoàn toàn đồng ý, nhưng tôi không nghĩ có nguy cơ che giấu một ngoại lệ mà bạn không có ý định.
NimbusScale

4

Một ví dụ tốt và đơn giản cho thử / lồng nhau có thể là như sau:

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

Bây giờ hãy thử các kết hợp khác nhau và bạn sẽ nhận được kết quả chính xác:

divide(15, 3)
# 5.0

divide(15, 0)
# inf

divide(-15, 0)
# -inf

divide(0, 0)
# nan

[tất nhiên chúng tôi có numpy vì vậy chúng tôi không cần phải tạo chức năng này]


2

Một điều tôi muốn tránh là đưa ra một ngoại lệ mới trong khi xử lý một ngoại lệ cũ. Nó làm cho các thông báo lỗi khó đọc.

Ví dụ, trong mã của tôi, ban đầu tôi đã viết

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

Và tôi đã nhận được tin nhắn này.

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

Điều tôi muốn là đây:

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

Nó không ảnh hưởng đến cách xử lý ngoại lệ. Trong một trong hai khối mã, KeyError sẽ bị bắt. Đây chỉ là một vấn đề nhận được điểm phong cách.


Xem câu trả lời được chấp nhận sử dụng tăng from None, mặc dù có nhiều điểm phong cách hơn . :)
Pianosaurus

1

Nếu cuối cùng thử ngoại trừ được lồng vào bên trong khối cuối cùng, kết quả từ "con" cuối cùng vẫn được giữ nguyên. Tôi chưa tìm thấy bản khai chính thức, nhưng đoạn mã sau đây cho thấy hành vi này trong 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

Hành vi này khác với ví dụ được đưa ra bởi @ Sławomir Lenart khi cuối cùng được lồng vào bên trong ngoại trừ khối.
Quảng Hoa Shu

0

Tôi không nghĩ đó là vấn đề của pythonic hay thanh lịch. Đó là vấn đề ngăn chặn ngoại lệ càng nhiều càng tốt. Các ngoại lệ có nghĩa là để xử lý các lỗi có thể xảy ra trong mã hoặc các sự kiện mà bạn không kiểm soát được. Trong trường hợp này, bạn có toàn quyền kiểm tra khi kiểm tra xem một mục là thuộc tính hoặc trong từ điển, vì vậy hãy tránh các ngoại lệ lồng nhau và tuân theo lần thử thứ hai của bạn.


Từ các tài liệu: Trong một môi trường đa luồng, cách tiếp cận LBYL (Nhìn trước khi bạn nhảy) có thể mạo hiểm đưa ra một điều kiện cuộc đua giữa những người tìm kiếm và một người nhảy vọt. Ví dụ: mã, nếu khóa trong ánh xạ: ánh xạ trở lại [khóa] có thể thất bại nếu một luồng khác loại bỏ khóa khỏi ánh xạ sau khi kiểm tra, nhưng trước khi tra cứu. Vấn đề này có thể được giải quyết bằng các khóa hoặc bằng cách sử dụng phương pháp EAFP (Dễ xin tha thứ hơn là cho phép) .
Nuno André
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.