Làm cách nào để triển khai __getattribute__ mà không gặp lỗi đệ quy vô hạn?


100

Tôi muốn ghi đè quyền truy cập vào một biến trong một lớp, nhưng trả về tất cả các biến khác một cách bình thường. Làm cách nào để thực hiện điều này với __getattribute__?

Tôi đã thử cách sau (điều này cũng sẽ minh họa những gì tôi đang cố gắng thực hiện) nhưng tôi gặp lỗi đệ quy:

class D(object):
    def __init__(self):
        self.test=20
        self.test2=21
    def __getattribute__(self,name):
        if name=='test':
            return 0.
        else:
            return self.__dict__[name]

>>> print D().test
0.0
>>> print D().test2
...
RuntimeError: maximum recursion depth exceeded in cmp

Câu trả lời:


126

Bạn gặp lỗi đệ quy vì cố gắng truy cập vào self.__dict__thuộc tính bên trong lại __getattribute__gọi ra thuộc tính của bạn __getattribute__. Nếu bạn sử dụng object's __getattribute__thay thế, nó hoạt động:

class D(object):
    def __init__(self):
        self.test=20
        self.test2=21
    def __getattribute__(self,name):
        if name=='test':
            return 0.
        else:
            return object.__getattribute__(self, name)

Điều này hoạt động vì object(trong ví dụ này) là lớp cơ sở. Bằng cách gọi phiên bản cơ sở của __getattribute__bạn tránh được địa ngục đệ quy mà bạn đã ở trước đó.

Ipython đầu ra với mã trong foo.py:

In [1]: from foo import *

In [2]: d = D()

In [3]: d.test
Out[3]: 0.0

In [4]: d.test2
Out[4]: 21

Cập nhật:

Có một cái gì đó trong phần có tiêu đề Quyền truy cập thuộc tính khác cho các lớp kiểu mới trong tài liệu hiện tại, nơi họ khuyên bạn nên thực hiện chính xác điều này để tránh đệ quy vô hạn.


1
Hấp dẫn. Vậy em đang làm gì ở đây thế? Tại sao đối tượng sẽ có các biến của tôi?
Greg

1
..và tôi có muốn luôn sử dụng đối tượng không? Nếu tôi kế thừa từ các lớp khác thì sao?
Greg

1
Có, mỗi khi bạn tạo một lớp và bạn không viết lớp của riêng mình, bạn sử dụng getattribute được cung cấp bởi đối tượng.
Egil 16/12/08

19
không phải là tốt hơn khi sử dụng super () và do đó sử dụng phương thức getattribute đầu tiên python tìm thấy trong các lớp cơ sở của bạn? -super(D, self).__getattribute__(name)
gepatino

10
Hoặc bạn chỉ có thể sử dụng super().__getattribute__(name)trong Python 3.
jeromej 12/11/13

25

Trên thực tế, tôi tin rằng bạn muốn sử dụng __getattr__phương pháp đặc biệt thay thế.

Trích dẫn từ tài liệu Python:

__getattr__( self, name)

Được gọi khi một tra cứu thuộc tính không tìm thấy thuộc tính ở những vị trí thông thường (tức là nó không phải là thuộc tính thể hiện cũng như không được tìm thấy trong cây lớp đối với chính nó). name là tên thuộc tính. Phương thức này sẽ trả về giá trị thuộc tính (tính toán) hoặc tăng một ngoại lệ AttributeError.
Lưu ý rằng nếu thuộc tính được tìm thấy thông qua cơ chế bình thường, __getattr__()sẽ không được gọi. (Đây là sự bất đối xứng có chủ ý giữa __getattr__()__setattr__().) Điều này được thực hiện vì lý do hiệu quả và vì nếu không __setattr__()sẽ không có cách nào để truy cập các thuộc tính khác của cá thể. Lưu ý rằng ít nhất đối với các biến đối tượng, bạn có thể giả mạo kiểm soát toàn bộ bằng cách không chèn bất kỳ giá trị nào vào từ điển thuộc tính đối tượng (mà thay vào đó chèn chúng vào một đối tượng khác). Xem__getattribute__() dưới đây để biết cách thực sự có được toàn quyền kiểm soát trong các lớp kiểu mới.

Lưu ý: để điều này hoạt động, cá thể không đượctestthuộc tính, do đó, dòng self.test=20phải được xóa.


2
Trên thực tế, theo bản chất của mã OP, ghi đè __getattr__cho testsẽ là vô ích, vì nó sẽ luôn tìm thấy nó "ở những nơi thông thường".
Casey Kuball

1
liên kết hiện tại để tài liệu Python có liên quan (có vẻ là khác so với tham chiếu trong câu trả lời): docs.python.org/3/reference/datamodel.html#object.__getattr__
CrepeGoat

17

Tham chiếu ngôn ngữ Python:

Để tránh đệ quy vô hạn trong phương thức này, việc triển khai nó phải luôn luôn gọi phương thức lớp cơ sở có cùng tên để truy cập bất kỳ thuộc tính nào mà nó cần, chẳng hạn object.__getattribute__(self, name).

Ý nghĩa:

def __getattribute__(self,name):
    ...
        return self.__dict__[name]

Bạn đang gọi một thuộc tính được gọi là __dict__. Bởi vì nó là một thuộc tính, __getattribute__được gọi khi tìm kiếm __dict__cuộc gọi __getattribute__nào gọi ... yada yada yada

return  object.__getattribute__(self, name)

Sử dụng các lớp cơ sở __getattribute__giúp tìm ra thuộc tính thực.


13

Bạn có chắc chắn muốn sử dụng __getattribute__? Bạn thực sự đang cố gắng đạt được điều gì?

Cách dễ nhất để làm những gì bạn yêu cầu là:

class D(object):
    def __init__(self):
        self.test = 20
        self.test2 = 21

    test = 0

hoặc là:

class D(object):
    def __init__(self):
        self.test = 20
        self.test2 = 21

    @property
    def test(self):
        return 0

Chỉnh sửa: Lưu ý rằng một phiên bản của Dsẽ có các giá trị khác nhau testtrong mỗi trường hợp. Trong trường hợp đầu tiên d.testsẽ là 20, trong trường hợp thứ hai sẽ là 0. Tôi sẽ giao nó cho bạn để tìm ra lý do.

Edit2: Greg đã chỉ ra rằng ví dụ 2 sẽ không thành công vì thuộc tính chỉ được đọc và __init__phương thức đã cố gắng đặt nó thành 20. Một ví dụ đầy đủ hơn cho điều đó sẽ là:

class D(object):
    def __init__(self):
        self.test = 20
        self.test2 = 21

    _test = 0

    def get_test(self):
        return self._test

    def set_test(self, value):
        self._test = value

    test = property(get_test, set_test)

Rõ ràng, với tư cách là một lớp học, điều này gần như hoàn toàn vô dụng, nhưng nó cho bạn ý tưởng để tiếp tục.


Ồ, điều đó không hoạt động êm ái khi bạn điều hành lớp học, phải không? File "Script1.py", dòng 5, trong init self.test = 20 AttributeError: không thể thiết lập thuộc tính
Greg

Thật. Tôi sẽ sửa điều đó làm ví dụ thứ ba. Được phát hiện tốt.
Singletoned

5

Đây là một phiên bản đáng tin cậy hơn:

class D(object):
    def __init__(self):
        self.test = 20
        self.test2 = 21
    def __getattribute__(self, name):
        if name == 'test':
            return 0.
        else:
            return super(D, self).__getattribute__(name)

Nó kêu gọi __ getAttribute __ phương pháp từ lớp cha mẹ, cuối cùng té ngửa ra đối tượng. __ getattribute __ method nếu các tổ tiên khác không ghi đè nó.


5

Làm thế nào là __getattribute__phương pháp được sử dụng?

Nó được gọi trước khi tra cứu dạng chấm thông thường. Nếu nó tăng lên AttributeError, sau đó chúng tôi gọi __getattr__.

Sử dụng phương pháp này là khá hiếm. Chỉ có hai định nghĩa trong thư viện chuẩn:

$ grep -Erl  "def __getattribute__\(self" cpython/Lib | grep -v "/test/"
cpython/Lib/_threading_local.py
cpython/Lib/importlib/util.py

Thực hành tốt nhất

Cách thích hợp để kiểm soát lập trình quyền truy cập vào một thuộc tính duy nhất là với property. Lớp Dphải được viết như sau (với bộ cài đặt và bộ xóa tùy chọn để sao chép hành vi dự kiến ​​rõ ràng):

class D(object):
    def __init__(self):
        self.test2=21

    @property
    def test(self):
        return 0.

    @test.setter
    def test(self, value):
        '''dummy function to avoid AttributeError on setting property'''

    @test.deleter
    def test(self):
        '''dummy function to avoid AttributeError on deleting property'''

Và cách sử dụng:

>>> o = D()
>>> o.test
0.0
>>> o.test = 'foo'
>>> o.test
0.0
>>> del o.test
>>> o.test
0.0

Thuộc tính là một bộ mô tả dữ liệu, do đó nó là thứ đầu tiên được tìm kiếm trong thuật toán tra cứu dạng chấm thông thường.

Tùy chọn cho __getattribute__

Bạn có một số tùy chọn nếu bạn thực sự cần triển khai tra cứu cho mọi thuộc tính thông qua __getattribute__.

  • tăng AttributeError, gây ra __getattr__được gọi (nếu được thực hiện)
  • trả lại một cái gì đó từ nó bằng cách
    • sử dụng superđể gọi triển khai cha (có thể objectlà)
    • kêu gọi __getattr__
    • triển khai thuật toán tra cứu dạng chấm của riêng bạn bằng cách nào đó

Ví dụ:

class NoisyAttributes(object):
    def __init__(self):
        self.test=20
        self.test2=21
    def __getattribute__(self, name):
        print('getting: ' + name)
        try:
            return super(NoisyAttributes, self).__getattribute__(name)
        except AttributeError:
            print('oh no, AttributeError caught and reraising')
            raise
    def __getattr__(self, name):
        """Called if __getattribute__ raises AttributeError"""
        return 'close but no ' + name    


>>> n = NoisyAttributes()
>>> nfoo = n.foo
getting: foo
oh no, AttributeError caught and reraising
>>> nfoo
'close but no foo'
>>> n.test
getting: test
20

Những gì bạn muốn ban đầu.

Và ví dụ này cho thấy cách bạn có thể làm những gì bạn muốn ban đầu:

class D(object):
    def __init__(self):
        self.test=20
        self.test2=21
    def __getattribute__(self,name):
        if name=='test':
            return 0.
        else:
            return super(D, self).__getattribute__(name)

Và sẽ hành xử như thế này:

>>> o = D()
>>> o.test = 'foo'
>>> o.test
0.0
>>> del o.test
>>> o.test
0.0
>>> del o.test

Traceback (most recent call last):
  File "<pyshell#216>", line 1, in <module>
    del o.test
AttributeError: test

Đánh giá mã

Mã của bạn với nhận xét. Bạn có một chấm điểm tra cứu về bản thân __getattribute__. Đây là lý do tại sao bạn gặp lỗi đệ quy. Bạn có thể kiểm tra xem tên có phải là "__dict__"và sử dụng superđể giải quyết hay không, nhưng điều đó không bao gồm __slots__. Tôi sẽ để nó như một bài tập cho người đọc.

class D(object):
    def __init__(self):
        self.test=20
        self.test2=21
    def __getattribute__(self,name):
        if name=='test':
            return 0.
        else:      #   v--- Dotted lookup on self in __getattribute__
            return self.__dict__[name]

>>> print D().test
0.0
>>> print D().test2
...
RuntimeError: maximum recursion depth exceeded in cmp
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.