ทำความเข้าใจกับ __get__ และ __set__ และ Python descriptors


310

ฉันกำลังพยายามเข้าใจว่า Python descriptors คืออะไรและมีประโยชน์อย่างไร ฉันเข้าใจว่าพวกเขาทำงานอย่างไร แต่นี่คือข้อสงสัยของฉัน พิจารณารหัสต่อไปนี้:

class Celsius(object):
    def __init__(self, value=0.0):
        self.value = float(value)
    def __get__(self, instance, owner):
        return self.value
    def __set__(self, instance, value):
        self.value = float(value)


class Temperature(object):
    celsius = Celsius()
  1. เหตุใดฉันจึงต้องใช้คลาส descriptor

  2. อะไรinstanceและownerที่นี่ (ใน__get__) วัตถุประสงค์ของพารามิเตอร์เหล่านี้คืออะไร?

  3. ฉันจะโทร / ใช้ตัวอย่างนี้ได้อย่างไร

คำตอบ:


147

ตัวบ่งชี้คือวิธีการpropertyใช้ชนิดของ Python บ่งเพียงการดำเนินการ__get__, __set__ฯลฯ และจะมีการเพิ่มแล้วไปเรียนอีกในความหมายของมัน (ตามที่คุณได้ข้างต้นด้วยระดับอุณหภูมิ) ตัวอย่างเช่น:

temp=Temperature()
temp.celsius #calls celsius.__get__

การเข้าถึงคุณสมบัติที่คุณกำหนดให้ descriptor ไป ( celsiusในตัวอย่างด้านบน) เรียกเมธอด descriptor ที่เหมาะสม

instanceใน__get__เป็นตัวอย่างของชั้นเรียน (ดังนั้นข้างบน__get__จะได้รับtempในขณะที่ownerเป็นชั้นเรียนที่มีคำอธิบาย (ดังนั้นมันจะเป็นTemperature)

คุณต้องใช้คลาส descriptor เพื่อสรุปเหตุผลที่เพิ่มความสามารถ ด้วยวิธีนี้หากใช้ descriptor เพื่อแคชการดำเนินการที่มีราคาแพง (ตัวอย่าง) มันสามารถเก็บค่าไว้ในตัวมันเองและไม่ใช่คลาส

บทความเกี่ยวกับการอธิบายสามารถพบได้ที่นี่

แก้ไข: ในฐานะที่เป็น jchl ชี้ให้เห็นในความคิดเห็นที่ถ้าคุณเพียงแค่ลองTemperature.celsius, จะinstanceNone


6
ความแตกต่างระหว่างselfและinstanceคืออะไร
เล็มม่าปริซึม

2
'อินสแตนซ์' สามารถเป็นอินสแตนซ์ของคลาสใด ๆ ตนเองจะเป็นอินสแตนซ์ของคลาสเดียวกัน
TheBeginner

3
@LemmaPrism selfเป็นอินสแตนซ์ของ descriptor instanceเป็นอินสแตนซ์ของคลาส (ถ้า instantiate) descriptor อยู่ใน ( instance.__class__ is owner)
Tcll

Temperature.celsiusให้ค่าตามรหัส0.0 celsius = Celsius()descriptor Celsius ถูกเรียกใช้ดังนั้นอินสแตนซ์ของมันจึงมีค่าเริ่มต้นที่0.0กำหนดให้กับแอตทริบิวต์ระดับอุณหภูมิคือเซลเซียส
Angel Salazar

109

เหตุใดฉันจึงต้องใช้คลาส descriptor

มันช่วยให้คุณควบคุมการทำงานของคุณลักษณะต่างๆได้เป็นพิเศษ หากคุณเคยชินกับ getters และ setters ใน Java ตัวอย่างเช่นนั่นเป็นวิธีการของ Python ข้อดีอย่างหนึ่งคือมันดูเหมือนกับผู้ใช้เช่นเดียวกับคุณลักษณะ (ไม่มีการเปลี่ยนแปลงในไวยากรณ์) ดังนั้นคุณสามารถเริ่มต้นด้วยคุณสมบัติปกติแล้วเมื่อคุณต้องการทำอะไรแฟนซีเปลี่ยนเป็น descriptor

คุณลักษณะเป็นเพียงค่าที่ไม่แน่นอน ตัวให้คำอธิบายช่วยให้คุณสามารถรันโค้ดโดยพลการเมื่ออ่านหรือตั้งค่า (หรือลบ) ค่า ดังนั้นคุณสามารถจินตนาการได้ว่าใช้มันเพื่อจับคู่แอตทริบิวต์กับเขตข้อมูลในฐานข้อมูลตัวอย่างเช่น ORM

การใช้งานอื่นอาจปฏิเสธที่จะยอมรับคุณค่าใหม่โดยการโยนข้อยกเว้นเข้า__set__- ทำให้แอตทริบิวต์ "อ่านได้อย่างมีประสิทธิภาพเท่านั้น"

อะไรinstanceและownerที่นี่ (ใน__get__) วัตถุประสงค์ของพารามิเตอร์เหล่านี้คืออะไร?

นี่มันค่อนข้างบอบบาง (และเหตุผลที่ฉันเขียนคำตอบใหม่ที่นี่ - ฉันพบคำถามนี้ในขณะที่สงสัยในสิ่งเดียวกันและไม่พบคำตอบที่มีอยู่นั้นยอดเยี่ยม)

descriptor ถูกกำหนดบนคลาส แต่โดยทั่วไปถูกเรียกจากอินสแตนซ์ เมื่อมันเรียกจากตัวอย่างทั้งสองinstanceและownerมีการตั้งค่า (และคุณสามารถทำงานออกownerจากinstanceเพื่อให้ดูเหมือนว่าไม่มีจุดหมายครับ) แต่เมื่อถูกเรียกจากชั้นเรียนownerจะมีการตั้งค่าเท่านั้น- ซึ่งเป็นเหตุผลว่าทำไมถึงมี

นี่เป็นสิ่งจำเป็นเท่านั้น__get__เพราะเป็นสิ่งเดียวที่สามารถเรียกใช้ในชั้นเรียนได้ ถ้าคุณตั้งค่าคลาสคุณตั้ง descriptor เอง ในทำนองเดียวกันสำหรับการลบ ซึ่งเป็นสาเหตุที่ownerไม่จำเป็นต้องมี

ฉันจะโทร / ใช้ตัวอย่างนี้ได้อย่างไร

นี่คือเคล็ดลับสุดเจ๋งที่ใช้คลาสที่คล้ายกัน:

class Celsius:

    def __get__(self, instance, owner):
        return 5 * (instance.fahrenheit - 32) / 9

    def __set__(self, instance, value):
        instance.fahrenheit = 32 + 9 * value / 5


class Temperature:

    celsius = Celsius()

    def __init__(self, initial_f):
        self.fahrenheit = initial_f


t = Temperature(212)
print(t.celsius)
t.celsius = 0
print(t.fahrenheit)

(ฉันใช้ Python 3 สำหรับ python 2 คุณต้องแน่ใจว่าแผนกเหล่านั้นเป็น/ 5.0และ/ 9.0) ที่ให้:

100.0
32.0

ตอนนี้มีวิธีอื่น ๆ วิธีที่ดีกว่าในการบรรลุเอฟเฟ็กต์เหมือนกันในไพ ธ อน (เช่นถ้าเซลเซียสเป็นคุณสมบัติซึ่งเป็นกลไกพื้นฐานเดียวกัน แต่วางแหล่งที่มาทั้งหมดในคลาสอุณหภูมิ) แต่นั่นแสดงให้เห็นว่า


2
การแปลงผิด: ควรเป็น C = 5 (F − 32) / 9, F = 32 + 9C / 5
musiphil

1
ตรวจสอบให้แน่ใจว่าคุณมีวัตถุหนึ่งของอุณหภูมิ ทำตามสิ่งที่ทำให้ยุ่งเหยิง t1 = อุณหภูมิ (190) พิมพ์ t1.celsius t1.celsius = 100 พิมพ์ t1.fahrenheit ตอนนี้เมื่อคุณตรวจสอบ t.celcius และ t.fahrenheit พวกเขาก็จะได้รับการแก้ไขเช่นกัน t.celcius คือ 115 และ t.fahrenheit คือ 32 ซึ่งผิดอย่างชัดเจน @Eric
Ishan Bhatt

1
@IshanBhatt: ฉันคิดว่านั่นเป็นเพราะข้อผิดพลาดที่ชี้ให้เห็นโดย musiphil ข้างต้น นอกจากนี้ไม่ใช่นี่ไม่ใช่คำตอบของฉัน
Eric

69

ฉันพยายามเข้าใจว่า Python descriptors คืออะไรและมีประโยชน์อย่างไร

Descriptors เป็นคุณสมบัติคลาส (เช่นคุณสมบัติหรือเมธอด) ด้วยวิธีพิเศษต่อไปนี้:

  • __get__ (เมธอด descriptor ที่ไม่ใช่ข้อมูลตัวอย่างเช่นบนเมธอด / ฟังก์ชัน)
  • __set__ (เมธอดตัวอธิบายข้อมูลตัวอย่างเช่นบนอินสแตนซ์ของคุณสมบัติ)
  • __delete__ (วิธีอธิบายข้อมูล)

อ็อบเจ็กต์ descriptor เหล่านี้สามารถใช้เป็นแอ็ตทริบิวต์บนนิยามคลาสอ็อบเจ็กต์อื่น (นั่นคือพวกเขาอาศัยอยู่ใน__dict__วัตถุคลาส)

วัตถุ Descriptor สามารถใช้ในการจัดการผลลัพธ์ของการค้นหาแบบประ (เช่นfoo.descriptor) ในทางโปรแกรมในนิพจน์ปกติการกำหนดและแม้แต่การลบ

ฟังก์ชั่น / วิธีการวิธีการที่ถูกผูกไว้property, classmethodและstaticmethodใช้ทุกวิธีการพิเศษเหล่านี้เพื่อควบคุมวิธีการที่พวกเขาจะเข้าถึงได้ผ่านการค้นหาประ

ตัวบ่งชี้ข้อมูลเช่นpropertyสามารถอนุญาตให้ประเมินคุณลักษณะที่ขี้เกียจตามสถานะที่เรียบง่ายของวัตถุทำให้อินสแตนซ์ใช้หน่วยความจำน้อยกว่าถ้าคุณคำนวณค่าแอตทริบิวต์ที่เป็นไปได้แต่ละอย่างล่วงหน้า

บ่งข้อมูลอื่นmember_descriptorที่สร้างขึ้นโดย__slots__ให้เงินฝากออมทรัพย์หน่วยความจำโดยให้ชั้นเรียนเพื่อเก็บข้อมูลในที่ไม่แน่นอน tuple เหมือน datastructure แทนของความยืดหยุ่นมากขึ้น __dict__แต่พื้นที่นาน

อธิบายที่ไม่ใช่ข้อมูลเช่นปกติชั้นเรียนและวิธีการคงได้รับข้อโต้แย้งแรกของพวกเขาโดยปริยาย (มักจะตั้งชื่อclsและselfตามลำดับ) __get__จากวิธีบ่งไม่ใช่ข้อมูลของพวกเขา

ผู้ใช้ Python ส่วนใหญ่จำเป็นต้องเรียนรู้การใช้งานที่เรียบง่ายและไม่จำเป็นต้องเรียนรู้หรือเข้าใจการใช้ descriptor เพิ่มเติม

ในเชิงลึก: คำอธิบายคืออะไร

descriptor เป็นวัตถุที่มีวิธีการอย่างใดอย่างหนึ่งต่อไปนี้ ( __get__, __set__หรือ__delete__) ตั้งใจที่จะใช้ผ่านการค้นหาประราวกับว่ามันเป็นคุณลักษณะทั่วไปของอินสแตนซ์ สำหรับเจ้าของวัตถุobj_instanceกับdescriptorวัตถุ:

  • obj_instance.descriptorจะเรียก
    descriptor.__get__(self, obj_instance, owner_class)กลับvalue
    นี้เป็นวิธีการวิธีการทั้งหมดและgetในการทำงานของสถานที่ให้บริการ

  • obj_instance.descriptor = valueจะเรียก
    descriptor.__set__(self, obj_instance, value)กลับมาNone
    นี้เป็นวิธีการsetterในการทำงานของสถานที่ให้บริการ

  • del obj_instance.descriptorจะเรียก
    descriptor.__delete__(self, obj_instance)กลับมาNone
    นี้เป็นวิธีการdeleterในการทำงานของสถานที่ให้บริการ

obj_instanceเป็นอินสแตนซ์ที่มีคลาสประกอบด้วยอินสแตนซ์ของวัตถุ descriptor selfเป็นตัวอย่างของdescriptor (อาจเป็นเพียงหนึ่งสำหรับคลาสของobj_instance)

ในการกำหนดสิ่งนี้ด้วยรหัสวัตถุจะเป็นตัวบ่งชี้ถ้าชุดของคุณสมบัตินั้นตัดกับแอตทริบิวต์ที่ต้องการ:

def has_descriptor_attrs(obj):
    return set(['__get__', '__set__', '__delete__']).intersection(dir(obj))

def is_descriptor(obj):
    """obj can be instance of descriptor or the descriptor class"""
    return bool(has_descriptor_attrs(obj))

ข้อมูลอธิบายมีและ__set__ / หรือ ไม่อธิบายข้อมูลมีค่ามิได้__delete__
__set____delete__

def has_data_descriptor_attrs(obj):
    return set(['__set__', '__delete__']) & set(dir(obj))

def is_data_descriptor(obj):
    return bool(has_data_descriptor_attrs(obj))

Builtin Descriptor Object ตัวอย่าง:

  • classmethod
  • staticmethod
  • property
  • ฟังก์ชั่นทั่วไป

ตัวอธิบายที่ไม่ใช่ข้อมูล

เราสามารถเห็นได้classmethodและstaticmethodไม่ใช่ผู้ให้ข้อมูล:

>>> is_descriptor(classmethod), is_data_descriptor(classmethod)
(True, False)
>>> is_descriptor(staticmethod), is_data_descriptor(staticmethod)
(True, False)

ทั้งสองมี__get__วิธีการเท่านั้น:

>>> has_descriptor_attrs(classmethod), has_descriptor_attrs(staticmethod)
(set(['__get__']), set(['__get__']))

โปรดทราบว่าฟังก์ชั่นทั้งหมดยังไม่ใช่ตัวอธิบายข้อมูล:

>>> def foo(): pass
... 
>>> is_descriptor(foo), is_data_descriptor(foo)
(True, False)

Data Descriptor property

อย่างไรก็ตามpropertyเป็น Data-Descriptor:

>>> is_data_descriptor(property)
True
>>> has_descriptor_attrs(property)
set(['__set__', '__get__', '__delete__'])

คำสั่งค้นหาแบบประ

สิ่งเหล่านี้มีความแตกต่างที่สำคัญเนื่องจากจะมีผลกับลำดับการค้นหาสำหรับการค้นหาจุด

obj_instance.attribute
  1. ก่อนอื่นข้างต้นจะมีลักษณะเพื่อดูว่าแอตทริบิวต์เป็น Data-Descriptor ในคลาสของอินสแตนซ์หรือไม่
  2. หากไม่เป็นเช่นนั้นจะดูว่าแอตทริบิวต์นั้นเป็นobj_instanceของหรือ__dict__ไม่
  3. ในที่สุดก็กลับไปที่ Non-Data-Descriptor

ผลที่ตามมาของการสั่งซื้อการค้นหานี้คือการที่ไม่เป็นไปตามข้อมูลอธิบายเช่นฟังก์ชั่น / วิธีการสามารถแทนที่โดยอินสแตนซ์

สรุปและขั้นตอนต่อไป

เราได้เรียนรู้ว่าการอธิบายเป็นวัตถุใด ๆ__get__, หรือ__set__ __delete__อ็อบเจ็กต์ descriptor เหล่านี้สามารถใช้เป็นแอ็ตทริบิวต์บนนิยามคลาสอ็อบเจ็กต์อื่น ตอนนี้เราจะดูว่ามีการใช้งานอย่างไรโดยใช้รหัสของคุณเป็นตัวอย่าง


การวิเคราะห์รหัสจากคำถาม

นี่คือรหัสของคุณตามด้วยคำถามและคำตอบของคุณสำหรับแต่ละข้อ:

class Celsius(object):
    def __init__(self, value=0.0):
        self.value = float(value)
    def __get__(self, instance, owner):
        return self.value
    def __set__(self, instance, value):
        self.value = float(value)

class Temperature(object):
    celsius = Celsius()
  1. เหตุใดฉันจึงต้องใช้คลาส descriptor

ตัวให้คำอธิบายของคุณทำให้แน่ใจได้ว่าคุณมีทุ่นสำหรับแอตทริบิวต์ของคลาสนี้Temperatureและคุณไม่สามารถใช้delเพื่อลบแอตทริบิวต์:

>>> t1 = Temperature()
>>> del t1.celsius
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AttributeError: __delete__

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

class Temperature(object):
    celsius = 0.0

สิ่งนี้ทำให้คุณมีพฤติกรรมเช่นเดียวกับตัวอย่างของคุณ (ดูการตอบคำถาม 3 ด้านล่าง) แต่ใช้ Pythons builtin ( property) และถือว่าเป็นสำนวนมากขึ้น:

class Temperature(object):
    _celsius = 0.0
    @property
    def celsius(self):
        return type(self)._celsius
    @celsius.setter
    def celsius(self, value):
        type(self)._celsius = float(value)
  1. อินสแตนซ์และเจ้าของที่นี่คืออะไร ( ได้รับ ) วัตถุประสงค์ของพารามิเตอร์เหล่านี้คืออะไร?

instanceเป็นอินสแตนซ์ของเจ้าของที่เรียก descriptor เจ้าของคือคลาสที่วัตถุ descriptor ถูกใช้เพื่อจัดการการเข้าถึงจุดข้อมูล ดูคำอธิบายของวิธีการพิเศษที่กำหนด descriptor ถัดจากย่อหน้าแรกของคำตอบนี้สำหรับชื่อตัวแปรอธิบายเพิ่มเติม

  1. ฉันจะโทร / ใช้ตัวอย่างนี้ได้อย่างไร

นี่คือการสาธิต:

>>> t1 = Temperature()
>>> t1.celsius
0.0
>>> t1.celsius = 1
>>> 
>>> t1.celsius
1.0
>>> t2 = Temperature()
>>> t2.celsius
1.0

คุณไม่สามารถลบแอตทริบิวต์:

>>> del t2.celsius
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AttributeError: __delete__

และคุณไม่สามารถกำหนดตัวแปรที่ไม่สามารถแปลงเป็นทศนิยมได้:

>>> t1.celsius = '0x02'
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 7, in __set__
ValueError: invalid literal for float(): 0x02

มิฉะนั้นสิ่งที่คุณมีที่นี่คือสถานะโกลบอลสำหรับทุกอินสแตนซ์ที่จัดการโดยกำหนดให้กับอินสแตนซ์

วิธีที่คาดหวังว่าโปรแกรมเมอร์ Python ที่มีประสบการณ์มากที่สุดจะบรรลุผลนี้คือการใช้propertyมัณฑนากรซึ่งใช้ descriptor เดียวกันภายใต้ประทุน แต่นำพฤติกรรมเข้าสู่การใช้งานคลาสเจ้าของ

class Temperature(object):
    _celsius = 0.0
    @property
    def celsius(self):
        return type(self)._celsius
    @celsius.setter
    def celsius(self, value):
        type(self)._celsius = float(value)

ซึ่งมีพฤติกรรมที่คาดหวังเหมือนกันแน่นอนของชิ้นส่วนของรหัสต้นฉบับ:

>>> t1 = Temperature()
>>> t2 = Temperature()
>>> t1.celsius
0.0
>>> t1.celsius = 1.0
>>> t2.celsius
1.0
>>> del t1.celsius
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AttributeError: can't delete attribute
>>> t1.celsius = '0x02'
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 8, in celsius
ValueError: invalid literal for float(): 0x02

ข้อสรุป

เราได้กล่าวถึงคุณลักษณะที่กำหนดตัวอธิบายความแตกต่างระหว่าง data- และ non-data-descriptors วัตถุบิวด์อินที่ใช้พวกเขาและคำถามเฉพาะเกี่ยวกับการใช้งาน

ดังนั้นอีกครั้งคุณจะใช้ตัวอย่างของคำถามได้อย่างไร ฉันหวังว่าคุณจะไม่ ฉันหวังว่าคุณจะเริ่มต้นด้วยคำแนะนำแรกของฉัน (คุณลักษณะระดับง่าย) และไปที่ข้อเสนอแนะที่สอง (มัณฑนากรคุณสมบัติ) ถ้าคุณรู้สึกว่ามันจำเป็น


1
ดีมากฉันเรียนรู้มากที่สุดจากคำตอบนี้ (เรียนรู้จากคนอื่นด้วย) คำถามเกี่ยวกับคำสั่งนี้ "วิธีที่คาดหวังว่าโปรแกรมเมอร์ Python ที่มีประสบการณ์มากที่สุดจะบรรลุผลนี้ ... " คลาส Temeperature ที่คุณกำหนดก่อนและหลังคำสั่งเหมือนกัน ฉันคิดถึงสิ่งที่คุณได้รับที่นี่หรือไม่?
Yolo Voe

1
@YoloVoe ไม่ถูกต้องฉันเพิ่ม verbiage เชิงพา ธ บางส่วนเพื่อเน้นว่ามันเป็นการทำซ้ำของข้างต้น
Aaron Hall

1
นี่คือคำตอบที่น่าอัศจรรย์ ฉันจะต้องอ่านมันอีกสองสามครั้ง แต่ฉันรู้สึกว่าฉันเข้าใจ Python แค่กระแทกรอยหยักสองสามครั้ง
Lucas Young

20

ก่อนที่จะเข้าไปดูรายละเอียดของตัวอธิบายอาจจำเป็นต้องทราบว่าการค้นหาคุณลักษณะใน Python ทำงานอย่างไร นี่ถือว่าคลาสไม่มีคลาส metaclass และใช้การเริ่มต้นใช้งาน__getattribute__(ทั้งคู่สามารถใช้เพื่อ "ปรับแต่ง" พฤติกรรม)

ภาพประกอบที่ดีที่สุดของการค้นหาแอตทริบิวต์ (ในหลาม 3.x หรือสำหรับการเรียนแบบใหม่ในหลาม 2.x) ในกรณีนี้คือจากmetaclasses หลามเข้าใจ (codelog Ionel ของ) รูปภาพใช้:แทน "การค้นหาแอตทริบิวต์ที่ไม่สามารถปรับแต่งได้"

นี้แสดงให้เห็นการค้นหาของแอตทริบิวต์foobarบนinstanceของClass:

ป้อนคำอธิบายรูปภาพที่นี่

สองเงื่อนไขมีความสำคัญที่นี่:

  • ถ้าชั้นของinstanceมีรายการสำหรับชื่อแอตทริบิวต์และมันมีและ__get____set__
  • หากinstanceมีไม่มีรายการสำหรับชื่อแอตทริบิวต์ __get__แต่ชั้นมีหนึ่งและมี

นั่นคือสิ่งที่ผู้อธิบายเข้ามา:

  • อธิบายข้อมูลซึ่งมีทั้งและ__get____set__
  • อธิบายไม่แสวงหาข้อมูล__get__ที่มีเพียง

ในทั้งสองกรณีค่าที่ส่งคืนจะถูก__get__เรียกด้วยอินสแตนซ์เป็นอาร์กิวเมนต์แรกและคลาสเป็นอาร์กิวเมนต์ที่สอง

การค้นหามีความซับซ้อนยิ่งขึ้นสำหรับการค้นหาแอตทริบิวต์ class (ดูตัวอย่างการค้นหาแอตทริบิวต์ Class (ในบล็อกที่กล่าวถึงด้านบน) )

ย้ายไปที่คำถามเฉพาะของคุณ:

เหตุใดฉันจึงต้องใช้คลาส descriptor

ในกรณีส่วนใหญ่คุณไม่จำเป็นต้องเขียนคลาส descriptor! อย่างไรก็ตามคุณอาจเป็นผู้ใช้ทั่วไป ตัวอย่างฟังก์ชั่น ฟังก์ชั่นเป็นตัวอธิบายนั่นเป็นวิธีที่ฟังก์ชั่นสามารถใช้เป็นวิธีการที่มีการselfส่งผ่านโดยนัยเป็นอาร์กิวเมนต์แรก

def test_function(self):
    return self

class TestClass(object):
    def test_method(self):
        ...

หากคุณค้นหาtest_methodอินสแตนซ์คุณจะได้รับ "วิธีการเชื่อมโยง" กลับมา:

>>> instance = TestClass()
>>> instance.test_method
<bound method TestClass.test_method of <__main__.TestClass object at ...>>

ในทำนองเดียวกันคุณสามารถผูกฟังก์ชั่นโดยการเรียกใช้__get__วิธีการด้วยตนเอง (ไม่แนะนำจริงๆเพียงเพื่อวัตถุประสงค์ในการอธิบาย):

>>> test_function.__get__(instance, TestClass)
<bound method test_function of <__main__.TestClass object at ...>>

คุณสามารถเรียกวิธีนี้ว่า "วิธีผูกมัดตนเอง":

>>> test_function.__get__(instance, TestClass)()
<__main__.TestClass at ...>

โปรดทราบว่าฉันไม่ได้ให้ข้อโต้แย้งใด ๆ และฟังก์ชั่นก็คืนค่าอินสแตนซ์ที่ฉันได้ผูกไว้!

ฟังก์ชั่นเป็นคำอธิบายที่ไม่ใช่ข้อมูล !

ตัวอย่างในตัวของ data-descriptor ในpropertyตัว ละเลยgetter, setterและบ่งคือ (จากอธิบาย HowTo คู่มือ "Properties" ):deleterproperty

class Property(object):
    def __init__(self, fget=None, fset=None, fdel=None, doc=None):
        self.fget = fget
        self.fset = fset
        self.fdel = fdel
        if doc is None and fget is not None:
            doc = fget.__doc__
        self.__doc__ = doc

    def __get__(self, obj, objtype=None):
        if obj is None:
            return self
        if self.fget is None:
            raise AttributeError("unreadable attribute")
        return self.fget(obj)

    def __set__(self, obj, value):
        if self.fset is None:
            raise AttributeError("can't set attribute")
        self.fset(obj, value)

    def __delete__(self, obj):
        if self.fdel is None:
            raise AttributeError("can't delete attribute")
        self.fdel(obj)

เนื่องจากเป็นข้อมูลที่บ่งมันเรียกเมื่อใดก็ตามที่คุณมองขึ้น "ชื่อ" ของpropertyและมันก็มอบหมายให้ฟังก์ชั่นการตกแต่งด้วย@property, @name.setterและ@name.deleter(ถ้ามี)

มีการอธิบายอื่น ๆ ในห้องสมุดมาตรฐานเช่น,staticmethodclassmethod

จุดของตัวอธิบายนั้นง่าย (แม้ว่าคุณจะไม่ค่อยต้องการ): รหัสทั่วไปที่เป็นนามธรรมสำหรับการเข้าถึงคุณลักษณะ propertyเป็นสิ่งที่เป็นนามธรรมสำหรับการเข้าถึงตัวแปรอินสแตนซ์functionให้สิ่งที่เป็นนามธรรมสำหรับวิธีการstaticmethodให้สิ่งที่เป็นนามธรรมสำหรับวิธีการที่ไม่จำเป็นต้องมีการเข้าถึงอินสแตนซ์และclassmethodให้สิ่งที่เป็นนามธรรมสำหรับวิธีการที่ต้องการเข้าถึงคลาสมากกว่าการเข้าถึงอินสแตนซ์

อีกตัวอย่างหนึ่งก็จะเป็นสถานที่ให้บริการระดับ

ตัวอย่างความสนุกหนึ่งตัวอย่าง (การใช้__set_name__จาก Python 3.6) อาจเป็นคุณสมบัติที่อนุญาตเฉพาะบางประเภทเท่านั้น:

class TypedProperty(object):
    __slots__ = ('_name', '_type')
    def __init__(self, typ):
        self._type = typ

    def __get__(self, instance, klass=None):
        if instance is None:
            return self
        return instance.__dict__[self._name]

    def __set__(self, instance, value):
        if not isinstance(value, self._type):
            raise TypeError(f"Expected class {self._type}, got {type(value)}")
        instance.__dict__[self._name] = value

    def __delete__(self, instance):
        del instance.__dict__[self._name]

    def __set_name__(self, klass, name):
        self._name = name

จากนั้นคุณสามารถใช้ descriptor ในคลาสได้:

class Test(object):
    int_prop = TypedProperty(int)

และเล่นกับมันเล็กน้อย:

>>> t = Test()
>>> t.int_prop = 10
>>> t.int_prop
10

>>> t.int_prop = 20.0
TypeError: Expected class <class 'int'>, got <class 'float'>

หรือ "ทรัพย์สินขี้เกียจ":

class LazyProperty(object):
    __slots__ = ('_fget', '_name')
    def __init__(self, fget):
        self._fget = fget

    def __get__(self, instance, klass=None):
        if instance is None:
            return self
        try:
            return instance.__dict__[self._name]
        except KeyError:
            value = self._fget(instance)
            instance.__dict__[self._name] = value
            return value

    def __set_name__(self, klass, name):
        self._name = name

class Test(object):
    @LazyProperty
    def lazy(self):
        print('calculating')
        return 10

>>> t = Test()
>>> t.lazy
calculating
10
>>> t.lazy
10

เหล่านี้เป็นกรณีที่การย้ายตรรกะไปที่ descriptor ทั่วไปอาจทำให้รู้สึก แต่ก็สามารถแก้ปัญหาได้ (แต่อาจจะด้วยการทำซ้ำรหัสบางส่วน) ด้วยวิธีการอื่น

อะไรinstanceและownerที่นี่ (ใน__get__) วัตถุประสงค์ของพารามิเตอร์เหล่านี้คืออะไร?

ขึ้นอยู่กับว่าคุณค้นหาแอตทริบิวต์อย่างไร หากคุณค้นหาคุณลักษณะในอินสแตนซ์แล้ว:

  • อาร์กิวเมนต์ที่สองคืออินสแตนซ์ที่คุณค้นหาแอ็ตทริบิวต์
  • อาร์กิวเมนต์ที่สามคือคลาสของอินสแตนซ์

ในกรณีที่คุณค้นหาคุณลักษณะในคลาส (สมมติว่า descriptor ถูกกำหนดบนคลาส):

  • อาร์กิวเมนต์ที่สองคือ None
  • อาร์กิวเมนต์ที่สามคือคลาสที่คุณค้นหาแอ็ตทริบิวต์

ดังนั้นโดยทั่วไปแล้วข้อโต้แย้งที่สามเป็นสิ่งจำเป็นหากคุณต้องการปรับแต่งพฤติกรรมเมื่อคุณทำการค้นหาระดับชั้นเรียน (เพราะinstanceเป็นNone)

ฉันจะโทร / ใช้ตัวอย่างนี้ได้อย่างไร

ตัวอย่างของคุณนั้นเป็นคุณสมบัติที่อนุญาตเฉพาะค่าที่สามารถแปลงเป็นfloatและที่ใช้ร่วมกันระหว่างอินสแตนซ์ทั้งหมดของคลาส (และในคลาส - แม้ว่าหนึ่งสามารถใช้การเข้าถึง "อ่าน" เท่านั้นในชั้นมิฉะนั้นคุณจะแทนที่อินสแตนซ์อธิบาย ):

>>> t1 = Temperature()
>>> t2 = Temperature()

>>> t1.celsius = 20   # setting it on one instance
>>> t2.celsius        # looking it up on another instance
20.0

>>> Temperature.celsius  # looking it up on the class
20.0

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


ไม่แน่ใจว่าภาพพื้นหลังโปร่งใสของกราฟิกที่ได้รับผลกระทบในโหมดมืดควรได้รับการรายงานว่าเป็นจุดบกพร่องของ stackoverflow หรือไม่
Tshirtman

@ เสื้อยืดฉันคิดว่านี่เป็นปัญหาของภาพตัวเอง มันไม่โปร่งใสอย่างสมบูรณ์ ... ฉันเอาโพสต์ของบล็อกมาและไม่รู้วิธีสร้างใหม่ด้วยพื้นหลังโปร่งใสที่เหมาะสม มันแย่มากที่มันดูแปลก ๆ ด้วยพื้นหลังสีดำ :(
MSeifert

9

เหตุใดฉันจึงต้องใช้คลาส descriptor

แรงบันดาลใจจากFluent Pythonโดย Buciano Ramalho

การถ่ายภาพคุณมีชั้นเรียนเช่นนี้

class LineItem:
     price = 10.9
     weight = 2.1
     def __init__(self, name, price, weight):
          self.name = name
          self.price = price
          self.weight = weight

item = LineItem("apple", 2.9, 2.1)
item.price = -0.9  # it's price is negative, you need to refund to your customer even you delivered the apple :(
item.weight = -0.8 # negative weight, it doesn't make sense

เราควรตรวจสอบน้ำหนักและราคาเพื่อหลีกเลี่ยงการกำหนดตัวเลขติดลบให้พวกเขาเราสามารถเขียนรหัสได้น้อยลงหากเราใช้ descriptor เป็นพร็อกซี่เช่นนี้

class Quantity(object):
    __index = 0

    def __init__(self):
        self.__index = self.__class__.__index
        self._storage_name = "quantity#{}".format(self.__index)
        self.__class__.__index += 1

    def __set__(self, instance, value):
        if value > 0:
            setattr(instance, self._storage_name, value)
        else:
           raise ValueError('value should >0')

   def __get__(self, instance, owner):
        return getattr(instance, self._storage_name)

จากนั้นกำหนด class LineItem ดังนี้:

class LineItem(object):
     weight = Quantity()
     price = Quantity()

     def __init__(self, name, weight, price):
         self.name = name
         self.weight = weight
         self.price = price

และเราสามารถขยายคลาสปริมาณเพื่อทำการตรวจสอบทั่วไปเพิ่มเติม


1
กรณีการใช้งานที่น่าสนใจเพราะมันแสดงวิธีการใช้ descriptor เพื่อโต้ตอบกับผู้ใช้หลายอินสแตนซ์ ตอนแรกฉันไม่เข้าใจประเด็นสำคัญ: แอตทริบิวต์ที่มี descriptor จะต้องสร้างขึ้นในคลาสเนมสเปซ (เช่นweight = Quantity()แต่ค่าจะต้องตั้งค่าในอินสแตนซ์ของเนมสเปซที่ใช้self(เช่นself.weight = 4) เท่านั้นและคำอธิบายจะถูกยกเลิก Nice!
นาที

ฉันไม่สามารถเข้าใจสิ่งหนึ่ง คุณกำลังกำหนดweight = Quantity()เป็นตัวแปรคลาสและมัน__get__และ__set__กำลังทำงานกับตัวแปรอินสแตนซ์ อย่างไร?
เทคโนโลยี

0

ฉันลอง (ด้วยการเปลี่ยนแปลงเล็กน้อยตามที่แนะนำ) รหัสจากคำตอบของ Andrew Cooke (ฉันใช้ python 2.7)

รหัส:

#!/usr/bin/env python
class Celsius:
    def __get__(self, instance, owner): return 9 * (instance.fahrenheit + 32) / 5.0
    def __set__(self, instance, value): instance.fahrenheit = 32 + 5 * value / 9.0

class Temperature:
    def __init__(self, initial_f): self.fahrenheit = initial_f
    celsius = Celsius()

if __name__ == "__main__":

    t = Temperature(212)
    print(t.celsius)
    t.celsius = 0
    print(t.fahrenheit)

ผลลัพธ์:

C:\Users\gkuhn\Desktop>python test2.py
<__main__.Celsius instance at 0x02E95A80>
212

ด้วย Python ก่อนหน้า 3 ตรวจสอบให้แน่ใจว่าคุณคลาสย่อยจากวัตถุซึ่งจะทำให้ descriptor ทำงานได้อย่างถูกต้องเนื่องจากget magic ไม่ทำงานสำหรับคลาสสไตล์เก่า


1
Descriptors ทำงานกับคลาสสไตล์ใหม่เท่านั้น สำหรับ python 2.x นี่หมายถึงคลาสของคุณมาจาก "object" ซึ่งเป็นค่าเริ่มต้นใน Python 3
Ivo van der Wijk

0

คุณจะเห็นhttps://docs.python.org/3/howto/descriptor.html#properties

class Property(object):
    "Emulate PyProperty_Type() in Objects/descrobject.c"

    def __init__(self, fget=None, fset=None, fdel=None, doc=None):
        self.fget = fget
        self.fset = fset
        self.fdel = fdel
        if doc is None and fget is not None:
            doc = fget.__doc__
        self.__doc__ = doc

    def __get__(self, obj, objtype=None):
        if obj is None:
            return self
        if self.fget is None:
            raise AttributeError("unreadable attribute")
        return self.fget(obj)

    def __set__(self, obj, value):
        if self.fset is None:
            raise AttributeError("can't set attribute")
        self.fset(obj, value)

    def __delete__(self, obj):
        if self.fdel is None:
            raise AttributeError("can't delete attribute")
        self.fdel(obj)

    def getter(self, fget):
        return type(self)(fget, self.fset, self.fdel, self.__doc__)

    def setter(self, fset):
        return type(self)(self.fget, fset, self.fdel, self.__doc__)

    def deleter(self, fdel):
        return type(self)(self.fget, self.fset, fdel, self.__doc__)

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