Pertimbangkan kode berikut:
class Foo: def __init__(self): self.bar = 'hello!' foo = Foo() print(foo.bar)
Hari ini kita akan menganalisis jawaban atas pertanyaan: "Apa yang sebenarnya terjadi ketika kita menulis foo.bar ?"
Anda mungkin sudah tahu bahwa sebagian besar objek memiliki kamus __dict__ internal yang berisi semua atributnya. Dan yang paling menyenangkan adalah betapa mudahnya mempelajari detail tingkat rendah seperti itu di Python:
>>> foo = Foo() >>> foo.__dict__ {'bar': 'hello!'}
Mari kita mulai dengan mencoba merumuskan hipotesis (tidak lengkap) ini:
foo.bar setara dengan foo .__ dict __ ['bar'] .
Sementara itu terdengar seperti kebenaran:
>>> foo = Foo() >>> foo.__dict__['bar'] 'hello!'
Sekarang anggaplah Anda sudah menyadari bahwa atribut dinamis dapat dideklarasikan di kelas:
>>> class Foo: ... def __init__(self): ... self.bar = 'hello!' ... ... def __getattr__(self, item): ... return 'goodbye!' ... ... foo = Foo() >>> foo.bar 'hello!' >>> foo.baz 'goodbye!' >>> foo.__dict__ {'bar': 'hello!'}
Hmm ... baiklah. Dapat dilihat bahwa __getattr__ dapat meniru akses ke atribut "palsu", tetapi tidak akan berfungsi jika sudah ada variabel yang dideklarasikan (seperti foo.bar yang mengembalikan 'halo!' Dan bukan 'selamat tinggal!' ). Segalanya tampak sedikit lebih rumit daripada yang tampak di awal.
Dan memang: ada metode ajaib yang dipanggil setiap kali kita mencoba untuk mendapatkan atribut, tetapi, seperti contoh di atas menunjukkan, ini bukan __getattr__ . Metode yang dipanggil disebut __getattribute__ , dan kami akan mencoba memahami cara kerjanya dengan mengamati berbagai situasi.
Sejauh ini, kami memodifikasi hipotesis kami sebagai berikut:
foo.bar setara dengan foo .__ getattribute __ ('bar') , yang kira-kira berfungsi seperti ini:
def __getattribute__(self, item): if item in self.__dict__: return self.__dict__[item] return self.__getattr__(item)
Kami akan mengujinya dengan menerapkan metode ini (dengan nama yang berbeda) dan menyebutnya secara langsung:
>>> class Foo: ... def __init__(self): ... self.bar = 'hello!' ... ... def __getattr__(self, item): ... return 'goodbye!' ... ... def my_getattribute(self, item): ... if item in self.__dict__: ... return self.__dict__[item] ... return self.__getattr__(item) >>> foo = Foo() >>> foo.bar 'hello!' >>> foo.baz 'goodbye!' >>> foo.my_getattribute('bar') 'hello!' >>> foo.my_getattribute('baz') 'goodbye!'
Terlihat benar kan?
Nah, yang tersisa adalah memverifikasi bahwa penugasan variabel didukung, setelah itu Anda bisa pulang ... -
>>> foo.baz = 1337 >>> foo.baz 1337 >>> foo.my_getattribute('baz') = 'h4x0r' SyntaxError: can't assign to function call
Neraka
my_getattribute mengembalikan objek. Kami dapat mengubahnya jika bisa diubah, tetapi kami tidak dapat menggantinya dengan yang lain menggunakan operator penugasan. Apa yang harus dilakukan Lagi pula, jika foo.baz sama dengan memanggil fungsi, bagaimana kita dapat menetapkan nilai baru ke atribut pada prinsipnya?
Ketika kita melihat ekspresi seperti foo.bar = 1 , ada lebih dari itu hanya memanggil fungsi untuk mendapatkan nilai foo.bar . Menetapkan nilai ke atribut tampaknya secara fundamental berbeda dari mendapatkan nilai atribut. Dan kebenarannya: kita bisa menerapkan __setattr__ untuk melihat ini:
>>> class Foo: ... def __init__(self): ... self.__dict__['my_dunder_dict'] = {} ... self.bar = 'hello!' ... ... def __setattr__(self, item, value): ... self.my_dunder_dict[item] = value ... ... def __getattr__(self, item): ... return self.my_dunder_dict[item] >>> foo = Foo() >>> foo.bar 'hello!' >>> foo.bar = 'goodbye!' >>> foo.bar 'goodbye!' >>> foo.baz Traceback (most recent call last): File "<pyshell#75>", line 1, in <module> foo.baz File "<pyshell#70>", line 10, in __getattr__ return self.my_dunder_dict[item] KeyError: 'baz' >>> foo.baz = 1337 >>> foo.baz 1337 >>> foo.__dict__ {'my_dunder_dict': {'bar': 'goodbye!', 'baz': 1337}}
Beberapa hal yang perlu diperhatikan terkait kode ini:
- __setattr__ tidak memiliki padanan dengan __getattribute__ (mis., metode ajaib __setattribute__ tidak ada).
- __setattr__ disebut di dalam __init__ , itulah sebabnya kami terpaksa melakukan self .__ dict __ ['my_dunder_dict'] = {} alih-alih self.my_dunder_dict = {} . Kalau tidak, kita akan mengalami rekursi tak terbatas.
Tapi kami juga punya properti (dan teman-temannya). Dekorator yang memungkinkan metode bertindak sebagai atribut.
Mari kita coba memahami bagaimana ini terjadi.
>>> class Foo(object): ... def __getattribute__(self, item): ... print('__getattribute__ was called') ... return super().__getattribute__(item) ... ... def __getattr__(self, item): ... print('__getattr__ was called') ... return super().__getattr__(item) ... ... @property ... def bar(self): ... print('bar property was called') ... return 100 >>> f = Foo() >>> f.bar __getattribute__ was called bar property was called
Hanya untuk bersenang-senang, apa yang kita miliki di f .__ dict__ ?
>>> f.__dict__ __getattribute__ was called {}
Tidak ada kunci bar di __dict__ , tetapi __getattr__ tidak dipanggil karena alasan tertentu. Wat?
bilah adalah metode yang juga menganggap diri sebagai parameter, hanya metode ini di kelas, dan bukan di instance kelas. Dan ini mudah dilihat:
>>> Foo.__dict__ mappingproxy({'__dict__': <attribute '__dict__' of 'Foo' objects>, '__doc__': None, '__getattr__': <function Foo.__getattr__ at 0x038308A0>, '__getattribute__': <function Foo.__getattribute__ at 0x038308E8>, '__module__': '__main__', '__weakref__': <attribute '__weakref__' of 'Foo' objects>, 'bar': <property object at 0x0381EC30>})
Bilah kunci memang ada dalam kamus atribut kelas. Untuk memahami cara kerja __getattribute__ , kita perlu menjawab pertanyaan: Siapa __getattribute__ dipanggil sebelumnya - kelas atau instance?
>>> f.__dict__['bar'] = 'will we see this printed?' __getattribute__ was called >>> f.bar __getattribute__ was called bar property was called 100
Dapat dilihat bahwa hal pertama verifikasi ada di kelas __dict__ , mis. itu memiliki prioritas di atas instance.
Tunggu sebentar, kapan kita memanggil metode bilah ? Maksudku, pseudo-code kami untuk __getattribute__ tidak pernah memanggil objek. Apa yang sedang terjadi
Memenuhi protokol deskriptor :
descr .__ get __ (self, obj, type = None) -> value
descr .__ set __ (diri, keberatan, nilai) -> Tidak ada
descr .__ hapus __ (diri, keberatan) -> Tidak ada
Intinya ada di sini. Menerapkan salah satu dari tiga metode ini sehingga objek menjadi deskriptor dan dapat mengubah perilaku default ketika diperlakukan sebagai atribut.
Jika suatu objek mendeklarasikan __get __ () dan __set __ () , maka itu disebut deskriptor data. Deskriptor yang mengimplementasikan hanya __get __ () disebut deskriptor non-data.
Kedua jenis deskriptor berbeda dalam bagaimana elemen-elemen dari kamus atribut objek ditimpa. Jika kamus berisi kunci dengan nama yang sama dengan deskriptor data, maka deskriptor data diutamakan (mis., __Set __ () dipanggil). Jika kamus berisi kunci dengan nama yang sama dengan deskriptor tanpa data, maka kamus memiliki prioritas (mis., Elemen kamus ditimpa).
Untuk membuat deskriptor data hanya-baca, nyatakan __get __ () dan __set __ () , di mana __set __ () melempar AttributeError saat dipanggil. Menerapkan __set __set ini () cukup untuk membuat deskriptor data.
Singkatnya, jika Anda mendeklarasikan salah satu dari metode ini __get__ , __set__ atau __delete__ , Anda mengimplementasikan dukungan untuk protokol deskriptor. Dan inilah yang dilakukan oleh dekorator properti : deklarasi ini hanya dapat dibaca yang akan dipanggil dalam __getattribute__ .
Perubahan terakhir dalam implementasi kami:
foo.bar setara dengan foo .__ getattribute __ ('bar') , yang kira-kira berfungsi seperti ini:
def __getattribute__(self, item): if item in self.__class__.__dict__: v = self.__class__.__dict__[item] elif item in self.__dict__: v = self.__dict__[item] else: v = self.__getattr__(item) if hasattr(v, '__get__'): v = v.__get__(self, type(self)) return v
Mari kita coba tunjukkan dalam praktik:
class Foo: class_attr = "I'm a class attribute!" def __init__(self): self.dict_attr = "I'm in a dict!" @property def property_attr(self): return "I'm a read-only property!" def __getattr__(self, item): return "I'm dynamically returned!" def my_getattribute(self, item): if item in self.__class__.__dict__: print('Retrieving from self.__class__.__dict__') v = self.__class__.__dict__[item] elif item in self.__dict__: print('Retrieving from self.__dict__') v = self.__dict__[item] else: print('Retrieving from self.__getattr__') v = self.__getattr__(item) if hasattr(v, '__get__'): print("Invoking descriptor's __get__") v = v.__get__(self, type(self)) return v
>>> foo = Foo() ... ... print(foo.class_attr) ... print(foo.dict_attr) ... print(foo.property_attr) ... print(foo.dynamic_attr) ... ... print() ... ... print(foo.my_getattribute('class_attr')) ... print(foo.my_getattribute('dict_attr')) ... print(foo.my_getattribute('property_attr')) ... print(foo.my_getattribute('dynamic_attr')) I'm a class attribute! I'm in a dict! I'm a read-only property! I'm dynamically returned! Retrieving from self.__class__.__dict__ I'm a class attribute! Retrieving from self.__dict__ I'm in a dict! Retrieving from self.__class__.__dict__ Invoking descriptor's __get__ I'm a read-only property! Retrieving from self.__getattr__ I'm dynamically returned!
Kami baru saja menggaruk permukaan implementasi atribut dengan Python sedikit. Meskipun upaya terakhir kami untuk meniru foo.bar secara umum benar, perlu diingat bahwa mungkin selalu ada detail kecil yang diterapkan secara berbeda.
Saya berharap bahwa selain mengetahui cara kerja atribut, saya juga berhasil menyampaikan keindahan bahasa yang mendorong Anda untuk bereksperimen. Bayar sebagian dari hutang pengetahuan hari ini.