ทำไมการตั้งค่า descriptor บนคลาสจะเขียนทับ descriptor?


10

เล่นง่าย:

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

คำถามนี้มีการทำสำเนาที่มีประสิทธิภาพแต่การทำซ้ำไม่ได้รับคำตอบและฉันขุดลงในแหล่งที่มาของ CPython เป็นแบบฝึกหัดการเรียนรู้อีกเล็กน้อย คำเตือน: ฉันเข้าไปในวัชพืช ฉันจริงๆหวังว่าฉันจะได้รับความช่วยเหลือจากกัปตันที่รู้น้ำเหล่านั้น ฉันพยายามอย่างชัดเจนที่สุดเท่าที่จะเป็นไปได้ในการติดตามการโทรที่ฉันกำลังดูเพื่อผลประโยชน์ในอนาคตของฉันเองและประโยชน์ของผู้อ่านในอนาคต

ฉันเห็นหมึกจำนวนมากหกใส่ลงบนลักษณะการ__getattribute__ใช้งานของคำอธิบายเช่นการค้นหามาก่อน งูหลาม snippet ใน"อัญเชิญอธิบาย"เพียงด้านล่างFor classes, the machinery is in type.__getattribute__()...ประมาณตกลงในใจของฉันกับสิ่งที่ผมเชื่อว่าเป็นที่สอดคล้องแหล่ง CPythonในtype_getattroซึ่งผมติดตามลงโดยดูที่"tp_slots"แล้วที่ tp_getattro เป็นประชากร และความจริงที่ว่าการB.vพิมพ์ครั้งแรกนั้น__get__, obj=None, objtype=<class '__main__.B'>สมเหตุสมผลสำหรับฉัน

สิ่งที่ฉันไม่เข้าใจคือทำไมมอบหมายB.v = 3สุ่มสี่สุ่มห้าเขียนทับบ่งมากกว่าวิกฤติv.__set__? ผมพยายามที่จะติดตามการโทร CPython เริ่มต้นอีกครั้งจาก"tp_slots"แล้วมองหาที่ที่มีประชากร tp_setattroแล้วมองไปที่type_setattro type_setattro ดูเหมือนจะเป็นเสื้อคลุมบาง ๆ รอบ_PyObject_GenericSetAttrWithDict และมีปมของความสับสนของฉัน: _PyObject_GenericSetAttrWithDictดูเหมือนจะมีตรรกะที่ให้ความสำคัญกับคำอธิบายถึงของ__set__วิธี !! กับในใจฉันไม่สามารถคิดออกว่าทำไมB.v = 3สุ่มสี่สุ่มห้าเขียนทับมากกว่าวิกฤติvv.__set__

ข้อสงวนสิทธิ์ 1: ฉันไม่ได้สร้าง Python ใหม่จากแหล่งที่มาด้วย printfs ดังนั้นฉันไม่แน่ใจว่าtype_setattroสิ่งที่ถูกเรียกระหว่างB.v = 3นั้น

ข้อจำกัดความรับผิดชอบ 2: VocalDescriptorไม่ได้มีไว้เพื่อเป็นตัวอย่างคำจำกัดความตัวอธิบาย "ปกติ" หรือ "แนะนำ" มันไม่มีทางเลือกที่จะบอกฉันเมื่อมีการเรียกวิธีการ


1
สำหรับฉันนี่พิมพ์ 3 ที่บรรทัดสุดท้าย ... รหัสใช้งานได้ดี
Jab

3
Descriptors ใช้เมื่อเข้าถึงแอ็ตทริบิวต์จากอินสแตนซ์ไม่ใช่คลาสเอง สำหรับฉันแล้วความลึกลับคือเหตุผลที่ทำไมจึง__get__ทำงานได้มากกว่าที่จะ__set__ทำไม่ได้
jasonharper

1
@Jab OP คาดว่าจะยังคงเรียกใช้__get__เมธอด ได้เขียนทับได้อย่างมีประสิทธิภาพแอตทริบิวต์ที่มีB.v = 3 int
r.ook

2
@jasonharper การเข้าถึงแอตทริบิวต์ระบุว่า__get__มีการเรียกและการใช้งานเริ่มต้นของobject.__getattribute__และtype.__getattribute__เรียกใช้__get__เมื่อใช้อินสแตนซ์หรือคลาส การกำหนดผ่าน__set__เป็นเพียงตัวอย่างเท่านั้น
chepner

@ jasonharper ฉันเชื่อว่า__get__เมธอดdescriptors ควรถูกเรียกใช้เมื่อเรียกใช้จากคลาส นี่คือวิธีที่ @classmethods และ @staticmethods จะดำเนินการตามวิธีการแนะนำ @ Jab ฉันสงสัยว่าทำไมB.v = 3สามารถเขียนทับตัวบอกคลาสได้ จากการใช้งาน CPython ฉันคาดว่าB.v = 3จะเปิดใช้งาน__set__เช่นกัน
Michael Carilli

คำตอบ:


6

คุณถูกต้องที่B.v = 3เพียงแค่เขียนทับ descriptor ด้วยจำนวนเต็ม (เท่าที่ควร)

สำหรับ B.v = 3การเรียกบ่งที่บ่งควรได้รับการกำหนดไว้ใน metaclass type(B)คือบน

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

ในการเรียกใช้ descriptor บน Bคุณจะต้องใช้อินสแตนซ์: B().v = 3จะทำ

เหตุผลสำหรับ B.vเรียกใช้ getter คืออนุญาตให้ส่งคืนอินสแตนซ์ descriptor เอง โดยปกติแล้วคุณจะทำเช่นนั้นเพื่อให้สามารถเข้าถึง descriptor ผ่าน object class:

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

ตอนนี้B.vจะกลับตัวอย่างเช่น<mymodule.VocalDescriptor object at 0xdeadbeef>ที่คุณสามารถโต้ตอบได้ เป็นอักษรวัตถุบ่งกำหนดแอตทริบิวต์ชั้นเรียนและรัฐร่วมกันระหว่างทุกกรณีB.v.__dict__B

แน่นอนว่ามันขึ้นอยู่กับรหัสของผู้ใช้ในการกำหนดสิ่งที่พวกเขาต้องการอย่างแท้จริงB.vการกลับมาselfเป็นเพียงรูปแบบทั่วไป


1
เพื่อให้คำตอบนี้สมบูรณ์ฉันจะเพิ่มที่__get__ได้รับการออกแบบให้เรียกว่าเป็นคุณลักษณะของอินสแตนซ์หรือแอตทริบิวต์คลาส แต่__set__ถูกออกแบบมาให้เรียกว่าเป็นคุณสมบัติของอินสแตนซ์เท่านั้น และเอกสารที่เกี่ยวข้อง: docs.python.org/3/reference/datamodel.html#object.__get__
sanyash

@wim Magnificent !! ในแบบคู่ขนานฉันดูอีกครั้งที่เครือข่ายการโทร type_setattro ฉันเห็นว่าการเรียกไปยัง_PyObject_GenericSetAttrWithDictระบุประเภท (ที่จุด B ในกรณีของฉัน)
Michael Carilli

ภายใน_PyObject_GenericSetAttrWithDictก็ดึง Py_TYPE ของบีเป็นtpซึ่งเป็น metaclass บี ( typeในกรณีของฉัน) แล้วมันเป็นmetaclass tpที่ได้รับการรักษาโดยให้คำอธิบายตรรกะลัดวงจรสั้น ดังนั้น descriptor ที่นิยามโดยตรงB จะไม่เห็นโดยตรรกะการลัดวงจรนั้น (ดังนั้นในรหัสดั้งเดิมของฉัน__set__ไม่ได้ถูกเรียก) แต่ descriptor ที่กำหนดบน metaclass นั้นถูกมองเห็นโดยตรรกะการลัดวงจร
Michael Carilli

ดังนั้นในกรณีของคุณที่ metaclass มี descriptor __set__เมธอดของ descriptor นั้นจะถูกเรียก
Michael Carilli

@sanyash อย่าลังเลที่จะแก้ไขโดยตรง
Wim

3

แบริ่งแทนที่ใด ๆB.vเทียบเท่ากับtype.__getattribute__(B, "v")ในขณะที่เทียบเท่ากับb = B(); b.v object.__getattribute__(b, "v")นิยามทั้งสองเรียกใช้__get__เมธอดของผลลัพธ์หากกำหนดไว้

โปรดทราบว่าการโทรนั้น__get__แตกต่างกันในแต่ละกรณี B.vผ่านNoneเป็นอาร์กิวเมนต์แรกในขณะที่B().vผ่านอินสแตนซ์ของตัวเอง ในทั้งสองกรณีBจะถูกส่งเป็นอาร์กิวเมนต์ที่สอง

B.v = 3บนมืออื่น ๆ ที่เทียบเท่ากับการtype.__setattr__(B, "v", 3)ซึ่งจะไม่__set__วิงวอน

โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.