Bagaimana cara mengimplementasikan __getattribute__ tanpa kesalahan rekursi tak terbatas?

101

Saya ingin mengganti akses ke satu variabel di kelas, tetapi mengembalikan semua variabel secara normal. Bagaimana saya melakukannya __getattribute__?

Saya mencoba yang berikut (yang juga harus menggambarkan apa yang saya coba lakukan) tetapi saya mendapatkan kesalahan rekursi:

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
Greg
sumber

Jawaban:

127

Anda mendapatkan kesalahan rekursi karena upaya Anda untuk mengakses self.__dict__atribut di dalam __getattribute__memanggil __getattribute__kembali Anda . Jika Anda menggunakan object's __getattribute__, ini berfungsi:

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)

Ini berfungsi karena object(dalam contoh ini) adalah kelas dasar. Dengan memanggil versi dasar __getattribute__Anda menghindari neraka rekursif Anda sebelumnya.

Keluaran Ipython dengan kode di 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

Memperbarui:

Ada sesuatu di bagian yang berjudul Akses atribut lainnya untuk kelas gaya baru dalam dokumentasi saat ini, di mana mereka merekomendasikan melakukan hal ini untuk menghindari rekursi tak terbatas.

Agil
sumber
1
Menarik. Jadi apa yang kamu lakukan disana Mengapa objek memiliki variabel saya?
Greg
1
..dan apakah saya ingin selalu menggunakan objek? Bagaimana jika saya mewarisi dari kelas lain?
Greg
1
Ya, setiap kali Anda membuat kelas dan Anda tidak menulis kelas Anda sendiri, Anda menggunakan getattribute yang disediakan oleh objek.
Agil
20
bukankah lebih baik menggunakan super () dan dengan demikian menggunakan metode getattribute pertama yang ditemukan python di kelas dasar Anda? -super(D, self).__getattribute__(name)
gepatino
10
Atau Anda bisa menggunakan super().__getattribute__(name)Python 3.
jeromej
25

Sebenarnya, saya yakin Anda ingin menggunakan __getattr__ metode khusus sebagai gantinya.

Kutipan dari dokumen Python:

__getattr__( self, name)

Dipanggil ketika pencarian atribut tidak menemukan atribut di tempat biasa (misalnya, ini bukan atribut instance atau tidak ditemukan di pohon kelas untuk dirinya sendiri). name adalah nama atribut. Metode ini harus mengembalikan nilai atribut (dihitung) atau memunculkan pengecualian AttributeError.
Perhatikan bahwa jika atribut ditemukan melalui mekanisme normal, __getattr__()tidak dipanggil. (Ini adalah asimetri yang disengaja antara __getattr__()dan __setattr__().) Ini dilakukan untuk alasan efisiensi dan karena jika __setattr__()tidak, tidak akan ada cara untuk mengakses atribut lain dari instance. Perhatikan bahwa setidaknya untuk variabel instance, Anda dapat memalsukan kontrol total dengan tidak memasukkan nilai apa pun dalam kamus atribut instance (tetapi memasukkannya ke objek lain). Lihat__getattribute__() metode di bawah ini untuk cara benar-benar mendapatkan kontrol total dalam kelas gaya baru.

Catatan: agar ini berfungsi, instance tidak boleh memiliki testatribut, jadi barisself.test=20 harus dihapus.

tzot.dll
sumber
2
Sebenarnya, menurut sifat kode OP, menimpa __getattr__untuk testtidak akan berguna, karena akan selalu menemukannya "di tempat biasa".
Casey Kuball
1
tautan terkini ke dokumen Python yang relevan (tampaknya berbeda dari yang direferensikan dalam jawaban): docs.python.org/3/reference/datamodel.html#object.__getattr__
CrepeGoat
17

Referensi bahasa Python:

Untuk menghindari rekursi tak terbatas dalam metode ini, implementasinya harus selalu memanggil metode kelas dasar dengan nama yang sama untuk mengakses atribut apa pun yang dibutuhkannya, misalnya object.__getattribute__(self, name),.

Berarti:

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

Anda memanggil atribut yang disebut __dict__. Karena itu adalah atribut, __getattribute__dipanggil untuk mencari __dict__panggilan __getattribute__mana yang memanggil ... yada yada yada

return  object.__getattribute__(self, name)

Menggunakan kelas dasar __getattribute__membantu menemukan atribut sebenarnya.

ttepasse
sumber
13

Anda yakin ingin menggunakan __getattribute__? Apa yang sebenarnya ingin Anda capai?

Cara termudah untuk melakukan apa yang Anda minta adalah:

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

    test = 0

atau:

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

    @property
    def test(self):
        return 0

Edit: Perhatikan bahwa sebuah instance Dakan memiliki nilai yang berbeda testdalam setiap kasus. Dalam kasus pertamad.test akan menjadi 20, dalam kasus kedua akan menjadi 0. Saya akan menyerahkan kepada Anda untuk mencari tahu mengapa.

Edit2: Greg menunjukkan bahwa contoh 2 akan gagal karena propertinya hanya baca dan __init__metode mencoba menyetelnya ke 20. Contoh yang lebih lengkap untuk itu adalah:

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)

Jelas, sebagai kelas ini hampir seluruhnya tidak berguna, tetapi ini memberi Anda ide untuk melanjutkan.

Singletoned
sumber
Oh, itu tidak bekerja dengan tenang saat Anda menjalankan kelas, bukan? File "Script1.py", baris 5, di init self.test = 20 AttributeError: tidak dapat menyetel atribut
Greg
Benar. Saya akan memperbaikinya sebagai contoh ketiga. Terlihat bagus.
Singletoned
5

Ini adalah versi yang lebih andal:

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)

Ini panggilan __ getAttribute __ metode dari kelas induk, akhirnya jatuh kembali ke objek. __ getattribute __ jika leluhur lain tidak menimpanya .

ElmoVanKielmo
sumber
5

Bagaimana __getattribute__metode ini digunakan?

Ini dipanggil sebelum pencarian titik-titik normal. Jika itu timbul AttributeError, maka kita panggil __getattr__.

Penggunaan metode ini agak jarang. Hanya ada dua definisi di pustaka standar:

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

Praktek terbaik

Cara yang tepat untuk mengontrol akses secara terprogram ke satu atribut adalah dengan property. Kelas Dharus ditulis sebagai berikut (dengan setter dan deleter secara opsional untuk mereplikasi perilaku yang dimaksudkan):

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'''

Dan penggunaan:

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

Properti adalah deskriptor data, jadi ini adalah hal pertama yang dicari dalam algoritme pencarian titik-titik normal.

Opsi untuk __getattribute__

Anda memiliki beberapa opsi jika Anda benar-benar perlu mengimplementasikan pencarian untuk setiap atribut melalui __getattribute__.

  • meningkatkan AttributeError, menyebabkan __getattr__dipanggil (jika diterapkan)
  • mengembalikan sesuatu darinya
    • menggunakan superuntuk memanggil orang tua (mungkinobject implementasi )
    • panggilan __getattr__
    • menerapkan algoritme pencarian titik-titik Anda sendiri

Sebagai contoh:

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

Apa yang awalnya Anda inginkan.

Dan contoh ini menunjukkan bagaimana Anda dapat melakukan apa yang awalnya Anda inginkan:

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)

Dan akan berperilaku seperti ini:

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

Peninjauan kode

Kode Anda dengan komentar. Anda memiliki pencarian titik-titik pada diri sendiri __getattribute__. Inilah mengapa Anda mendapatkan kesalahan rekursi. Anda dapat memeriksa apakah namanya "__dict__"dan digunakan superuntuk mengatasinya, tetapi itu tidak mencakup __slots__. Saya akan serahkan itu sebagai latihan kepada pembaca.

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
Aaron Hall
sumber