Mengapa mengatur deskriptor pada kelas menimpa deskriptor?

10

Repro sederhana:

class VocalDescriptor(object):
    def __get__(self, obj, objtype):
        print('__get__, obj={}, objtype={}'.format(obj, objtype))
    def __set__(self, obj, val):
        print('__set__')

class B(object):
    v = VocalDescriptor()

B.v # prints "__get__, obj=None, objtype=<class '__main__.B'>"
B.v = 3 # does not print "__set__", evidently does not trigger descriptor
B.v # does not print anything, we overwrote the descriptor

Pertanyaan ini memiliki duplikat yang efektif , tetapi duplikat itu tidak dijawab, dan saya menggali lebih dalam sumber CPython sebagai latihan pembelajaran. Peringatan: saya pergi ke gulma. Saya benar-benar berharap bisa mendapatkan bantuan dari seorang kapten yang mengetahui perairan itu . Saya mencoba untuk menjadi sejelas mungkin dalam melacak panggilan yang saya lihat, untuk keuntungan saya di masa depan dan manfaat pembaca di masa depan.

Saya telah melihat banyak tinta tumpah karena perilaku yang __getattribute__diterapkan pada deskriptor, misalnya pencarian prioritas. Python potongan di "menyerukan Deskriptor" tepat di bawah For classes, the machinery is in type.__getattribute__()...kira-kira setuju dalam pikiran saya dengan apa yang saya percaya adalah yang sesuai sumber CPython di type_getattro, yang saya melacak dengan melihat "tp_slots" maka di mana tp_getattro dihuni . Dan fakta bahwa B.vawalnya mencetak __get__, obj=None, objtype=<class '__main__.B'>masuk akal bagi saya.

Yang tidak saya mengerti adalah, mengapa tugas itu B.v = 3secara buta menimpa deskriptor, bukannya memicu v.__set__? Saya mencoba melacak panggilan CPython, mulai sekali lagi dari "tp_slots" , kemudian melihat di mana tp_setattro diisi , kemudian melihat type_setattro . type_setattro tampaknya menjadi pembungkus tipis di sekitar _PyObject_GenericSetAttrWithDict . Dan ada inti dari kebingungan saya: _PyObject_GenericSetAttrWithDicttampaknya memiliki logika yang memberikan diutamakan untuk keterangan ini __set__metode !! Dengan mengingat hal ini, saya tidak tahu mengapa B.v = 3menindih secara membabi buta vdaripada memicu v.__set__.

Penafian 1: Saya tidak membangun kembali Python dari sumber dengan printfs, jadi saya tidak sepenuhnya yakin type_setattroapa yang dipanggil selama B.v = 3.

Penafian 2: VocalDescriptortidak dimaksudkan untuk memberikan contoh definisi deskriptor "tipikal" atau "direkomendasikan". Ini adalah no-op verbose untuk memberi tahu saya ketika metode dipanggil.

Michael Carilli
sumber
1
Bagi saya ini mencetak 3 pada baris terakhir ... Kode berfungsi dengan baik
Jab
3
Deskriptor berlaku ketika mengakses atribut dari instance , bukan kelas itu sendiri. Bagi saya, misterinya adalah mengapa __get__bekerja sama sekali, daripada mengapa __set__tidak.
jasonharper
1
@ Jab OP berharap masih menggunakan __get__metode ini. B.v = 3telah secara efektif menimpa atribut dengan int.
mulai
2
@jasonharper Akses atribut menentukan apakah __get__dipanggil, dan implementasi default object.__getattribute__dan type.__getattribute__memanggil __get__ketika menggunakan instance atau kelas. Penugasan via __set__hanya untuk instance.
chepner
@ jasonharper Saya percaya __get__metode deskriptor seharusnya dipicu ketika dipanggil dari kelas itu sendiri. Ini adalah bagaimana @classmethods dan @staticmethods diimplementasikan, sesuai dengan panduan cara . @ Jab Saya bertanya-tanya mengapa B.v = 3bisa menimpa deskriptor kelas. Berdasarkan implementasi CPython, saya berharap B.v = 3juga memicu __set__.
Michael Carilli

Jawaban:

6

Anda benar bahwa B.v = 3hanya menimpa deskriptor dengan integer (sebagaimana mestinya).

Untuk B.v = 3memohon deskriptor, deskriptor seharusnya didefinisikan pada metaclass, yaitu pada type(B).

>>> class BMeta(type): 
...     v = VocalDescriptor() 
... 
>>> class B(metaclass=BMeta): 
...     pass 
... 
>>> B.v = 3 
__set__

Untuk mengaktifkan deskriptor pada B, Anda akan menggunakan contoh: B().v = 3akan melakukannya.

Alasan untuk B.vmemanggil pengambil adalah untuk memungkinkan mengembalikan contoh deskriptor itu sendiri. Biasanya Anda akan melakukan itu, untuk memungkinkan akses pada deskriptor melalui objek kelas:

class VocalDescriptor(object):
    def __get__(self, obj, objtype):
        if obj is None:
            return self
        print('__get__, obj={}, objtype={}'.format(obj, objtype))
    def __set__(self, obj, val):
        print('__set__')

Sekarang B.vakan mengembalikan beberapa contoh seperti <mymodule.VocalDescriptor object at 0xdeadbeef>yang dapat Anda berinteraksi. Secara harfiah objek deskriptor, didefinisikan sebagai atribut kelas, dan kondisinya B.v.__dict__dibagi antara semua contoh B.

Tentu saja tergantung pada kode pengguna untuk menentukan dengan tepat apa yang ingin mereka B.vlakukan, mengembalikan selfhanya pola umum.

wim
sumber
1
Untuk melengkapi jawaban ini, saya ingin menambahkan yang __get__dirancang untuk disebut sebagai atribut instance atau atribut kelas, tetapi __set__dirancang untuk dipanggil hanya sebagai atribut instance. Dan dokumen yang relevan: docs.python.org/3/reference/datamodel.html#object.__get__
sanyash
@wim Magnificent !! Secara paralel saya melihat sekali lagi pada rantai panggilan type_setattro. Saya melihat bahwa panggilan ke _PyObject_GenericSetAttrWithDict memasok jenis (pada titik B, dalam kasus saya).
Michael Carilli
Di dalam _PyObject_GenericSetAttrWithDict, ia menarik Py_TYPE dari B sebagai tp, yang merupakan B's metaclass ( typedalam kasus saya), maka itu adalah metaclass tp yang diperlakukan oleh logika hubung singkat deskriptor . Jadi deskriptor didefinisikan langsung pada B tidak terlihat bahwa logika hubungan arus pendek (karena itu dalam kode asli saya __set__tidak disebut), tapi deskriptor didefinisikan pada metaclass yang adalah dilihat oleh logika hubungan arus pendek.
Michael Carilli
Oleh karena itu, dalam kasus Anda di mana metaclass memiliki deskriptor, __set__metode deskriptor itu disebut.
Michael Carilli
@sanyash merasa bebas untuk mengedit langsung.
wim
3

Kecuali segala override, B.vsama dengan type.__getattribute__(B, "v"), sementara b = B(); b.vsetara dengan object.__getattribute__(b, "v"). Kedua definisi tersebut memanggil __get__metode hasil jika didefinisikan.

Perhatikan, pikirkan, bahwa panggilan untuk __get__berbeda dalam setiap kasus. B.vlewat Nonesebagai argumen pertama, sementara B().vmelewati instance itu sendiri. Dalam kedua kasus Bdilewatkan sebagai argumen kedua.

B.v = 3, di sisi lain, sama dengan type.__setattr__(B, "v", 3), yang tidak meminta __set__.

chepner
sumber