นี่จะเป็นคำตอบที่ยืดยาวที่อาจจะให้บริการฟรีเท่านั้น ... แต่คำถามของคุณพาฉันไปที่หลุมกระต่ายดังนั้นฉันจึงต้องการแบ่งปันสิ่งที่ฉันค้นพบ (และความเจ็บปวด) เช่นกัน
ในที่สุดคุณอาจพบคำตอบนี้ไม่เป็นประโยชน์ต่อปัญหาที่แท้จริงของคุณ ที่จริงแล้วข้อสรุปของฉันคือ - ฉันจะไม่ทำสิ่งนี้เลย ต้องบอกว่าเบื้องหลังของข้อสรุปนี้อาจทำให้คุณเพลิดเพลินเล็กน้อยเนื่องจากคุณกำลังมองหารายละเอียดเพิ่มเติม
การแก้ไขความเข้าใจผิดบางประการ
คำตอบแรกในขณะที่ถูกต้องในกรณีส่วนใหญ่ไม่ได้เป็นทุกกรณี ตัวอย่างเช่นพิจารณาคลาสนี้:
class Foo:
def __init__(self):
self.name = 'Foo!'
@property
def inst_prop():
return f'Retrieving {self.name}'
self.inst_prop = inst_prop
inst_prop
ในขณะที่เป็นproperty
, มันเป็นคุณสมบัติตัวอย่าง:
>>> Foo.inst_prop
Traceback (most recent call last):
File "<pyshell#60>", line 1, in <module>
Foo.inst_prop
AttributeError: type object 'Foo' has no attribute 'inst_prop'
>>> Foo().inst_prop
<property object at 0x032B93F0>
>>> Foo().inst_prop.fget()
'Retrieving Foo!'
ทุกอย่างขึ้นอยู่กับที่คุณproperty
กำหนดไว้ตั้งแต่แรก หากคุณ@property
กำหนดไว้ใน "ขอบเขต" ของคลาส (หรือจริง ๆnamespace
) มันจะกลายเป็นแอตทริบิวต์ของคลาส ในตัวอย่างของฉันชั้นเรียนเองไม่ได้ตระหนักถึงสิ่งใดinst_prop
จนกระทั่งอินสแตนซ์ แน่นอนว่ามันไม่ได้มีประโยชน์มากเหมือนเป็นสถานที่ให้บริการเลย
แต่ก่อนอื่นเรามาพูดถึงความคิดเห็นของคุณเกี่ยวกับการแก้ปัญหาการสืบทอด ...
แล้วการถ่ายทอดทางพันธุกรรมมีผลอย่างไรต่อปัญหานี้? บทความต่อไปนี้ดำน้ำเล็กน้อยในหัวข้อและลำดับการแก้ไขเมธอดค่อนข้างเกี่ยวข้องแม้ว่าจะกล่าวถึงความกว้างของมรดกแทนที่จะเป็นความลึก
เมื่อรวมกับการค้นหาของเราการตั้งค่าด้านล่างนี้:
@property
def some_prop(self):
return "Family property"
class Grandparent:
culture = some_prop
world_view = some_prop
class Parent(Grandparent):
world_view = "Parent's new world_view"
class Child(Parent):
def __init__(self):
try:
self.world_view = "Child's new world_view"
self.culture = "Child's new culture"
except AttributeError as exc:
print(exc)
self.__dict__['culture'] = "Child's desired new culture"
ลองนึกภาพว่าจะเกิดอะไรขึ้นเมื่อบรรทัดเหล่านี้ทำงาน:
print("Instantiating Child class...")
c = Child()
print(f'c.__dict__ is: {c.__dict__}')
print(f'Child.__dict__ is: {Child.__dict__}')
print(f'c.world_view is: {c.world_view}')
print(f'Child.world_view is: {Child.world_view}')
print(f'c.culture is: {c.culture}')
print(f'Child.culture is: {Child.culture}')
ผลที่ได้คือ:
Instantiating Child class...
can't set attribute
c.__dict__ is: {'world_view': "Child's new world_view", 'culture': "Child's desired new culture"}
Child.__dict__ is: {'__module__': '__main__', '__init__': <function Child.__init__ at 0x0068ECD8>, '__doc__': None}
c.world_view is: Child's new world_view
Child.world_view is: Parent's new world_view
c.culture is: Family property
Child.culture is: <property object at 0x00694C00>
สังเกตว่า:
self.world_view
สามารถนำไปใช้ในขณะที่self.culture
ล้มเหลว
culture
ไม่มีอยู่ในChild.__dict__
( mappingproxy
ของชั้นเรียนเพื่อไม่ให้สับสนกับอินสแตนซ์__dict__
)
- แม้ว่าจะ
culture
มีอยู่ในc.__dict__
ก็ไม่ได้อ้างอิง
คุณอาจจะสามารถเดาได้ว่าทำไม - world_view
ถูกเขียนทับโดยParent
ชั้นเรียนว่าไม่ใช่สถานที่ให้บริการดังนั้นจึงChild
สามารถเขียนทับได้เช่นกัน ขณะเดียวกันนับตั้งแต่culture
มีการสืบทอดมันมีอยู่เพียงภายในmappingproxy
ของGrandparent
:
Grandparent.__dict__ is: {
'__module__': '__main__',
'culture': <property object at 0x00694C00>,
'world_view': <property object at 0x00694C00>,
...
}
หากคุณพยายามลบParent.culture
:
>>> del Parent.culture
Traceback (most recent call last):
File "<pyshell#67>", line 1, in <module>
del Parent.culture
AttributeError: culture
Parent
คุณจะสังเกตเห็นว่ามันไม่ได้มีอยู่สำหรับ Grandparent.culture
เนื่องจากวัตถุโดยตรงหมายกลับไป
แล้วคำสั่งแก้ปัญหาคืออะไร
ดังนั้นเราจึงสนใจที่จะสังเกตคำสั่งการแก้ปัญหาที่แท้จริงลองเอาออกParent.world_view
แทน:
del Parent.world_view
print(f'c.world_view is: {c.world_view}')
print(f'Child.world_view is: {Child.world_view}')
สงสัยว่าผลลัพธ์คืออะไร?
c.world_view is: Family property
Child.world_view is: <property object at 0x00694C00>
มันเปลี่ยนกลับไปเป็นของปู่ย่าตายายworld_view
property
แม้ว่าเราจะสามารถกำหนดได้สำเร็จself.world_view
ก่อนหน้านี้! แต่ถ้าเราเปลี่ยนworld_view
ระดับชั้นเรียนอย่างแรงเหมือนคำตอบอื่น ถ้าเราลบมันล่ะ? จะเกิดอะไรขึ้นถ้าเรากำหนดแอตทริบิวต์ class ปัจจุบันให้เป็นคุณสมบัติ
Child.world_view = "Child's independent world_view"
print(f'c.world_view is: {c.world_view}')
print(f'Child.world_view is: {Child.world_view}')
del c.world_view
print(f'c.world_view is: {c.world_view}')
print(f'Child.world_view is: {Child.world_view}')
Child.world_view = property(lambda self: "Child's own property")
print(f'c.world_view is: {c.world_view}')
print(f'Child.world_view is: {Child.world_view}')
ผลลัพธ์คือ:
# Creating Child's own world view
c.world_view is: Child's new world_view
Child.world_view is: Child's independent world_view
# Deleting Child instance's world view
c.world_view is: Child's independent world_view
Child.world_view is: Child's independent world_view
# Changing Child's world view to the property
c.world_view is: Child's own property
Child.world_view is: <property object at 0x020071B0>
สิ่งนี้น่าสนใจเพราะc.world_view
ได้รับการกู้คืนสู่แอตทริบิวต์ของอินสแตนซ์ในขณะChild.world_view
ที่เราได้รับมอบหมาย หลังจากลบแอ็ตทริบิวต์อินสแตนซ์แล้วมันจะเปลี่ยนเป็นแอตทริบิวต์คลาส และหลังจากที่กำหนดChild.world_view
ให้กับคุณสมบัติอีกครั้งเราจะสูญเสียการเข้าถึงคุณลักษณะอินสแตนซ์ทันที
ดังนั้นเราสามารถคาดการณ์คำสั่งแก้ปัญหาต่อไปนี้ :
- หากมีคลาสของแอตทริบิวต์และเป็น a
property
ให้เรียกคืนค่าผ่านทางgetter
หรือfget
(เพิ่มเติมในภายหลัง) คลาสปัจจุบันก่อนถึงคลาส Base ล่าสุด
- มิฉะนั้นหากมีแอตทริบิวต์ของอินสแตนซ์อยู่ให้ดึงค่าของคุณสมบัติของอินสแตนซ์
- มิฉะนั้นให้เรียกคืน
property
แอ็ตทริบิวต์ที่ไม่ใช่คลาส คลาสปัจจุบันก่อนถึงคลาส Base ล่าสุด
ในกรณีนี้เราจะลบรูทproperty
:
del Grandparent.culture
print(f'c.culture is: {c.culture}')
print(f'Child.culture is: {Child.culture}')
ซึ่งจะช่วยให้:
c.culture is: Child's desired new culture
Traceback (most recent call last):
File "<pyshell#74>", line 1, in <module>
print(f'Child.culture is: {Child.culture}')
AttributeError: type object 'Child' has no attribute 'culture'
Ta-Dah! Child
ตอนนี้มีของตัวเองอยู่บนพื้นฐานของการแทรกเข้าไปในพลัง culture
ไม่มีอยู่แน่นอนเนื่องจากไม่เคยกำหนดไว้ในหรือแอตทริบิวต์ class และถูกลบออกc.__dict__
Child.culture
Parent
Child
Grandparent
นี่เป็นสาเหตุของปัญหาของฉันหรือไม่
ที่จริงแล้วไม่มี ข้อผิดพลาดที่คุณได้รับซึ่งเราก็ยังคงสังเกตเมื่อกำหนดself.culture
เป็นที่แตกต่างกันโดยสิ้นเชิง แต่ลำดับการสืบทอดกำหนดฉากหลังให้กับคำตอบ - ซึ่งก็คือproperty
ตัวมันเอง
นอกเหนือจากgetter
วิธีการที่กล่าวถึงก่อนหน้านี้property
ยังมีเคล็ดลับเรียบร้อยขึ้นแขนเสื้อของมัน สิ่งที่เกี่ยวข้องมากที่สุดในกรณีนี้คือsetter
, หรือfset
วิธีการ, ซึ่งถูกทริกเกอร์โดยself.culture = ...
บรรทัด เนื่องจากคุณproperty
ไม่ได้ใช้งานsetter
หรือfget
ฟังก์ชั่นใด ๆไพ ธ อนจึงไม่ทราบว่าจะต้องทำอะไรและจะAttributeError
แทนที่ (เช่นcan't set attribute
)
อย่างไรก็ตามหากคุณใช้setter
วิธีการ:
@property
def some_prop(self):
return "Family property"
@some_prop.setter
def some_prop(self, val):
print(f"property setter is called!")
# do something else...
เมื่อสร้างChild
คลาสคุณจะได้รับ:
Instantiating Child class...
property setter is called!
แทนที่จะได้รับAttributeError
คุณจะได้รับการเรียกsome_prop.setter
วิธีการ ซึ่งช่วยให้คุณควบคุมวัตถุของคุณได้มากขึ้น ... จากการค้นพบครั้งก่อนของเราเรารู้ว่าเราต้องมีการเขียนทับแอตทริบิวต์ class ก่อนที่จะถึงคุณสมบัติ สิ่งนี้สามารถนำมาใช้ภายในคลาสฐานเป็นทริกเกอร์ นี่คือตัวอย่างใหม่:
class Grandparent:
@property
def culture(self):
return "Family property"
# add a setter method
@culture.setter
def culture(self, val):
print('Fine, have your own culture')
# overwrite the child class attribute
type(self).culture = None
self.culture = val
class Parent(Grandparent):
pass
class Child(Parent):
def __init__(self):
self.culture = "I'm a millennial!"
c = Child()
print(c.culture)
ซึ่งผลลัพธ์ใน:
Fine, have your own culture
I'm a millennial!
TA-DAH! ตอนนี้คุณสามารถเขียนทับแอตทริบิวต์ของอินสแตนซ์ของคุณเองผ่านคุณสมบัติที่สืบทอดมา!
ดังนั้นการแก้ไขปัญหา?
... ไม่จริง ปัญหาของวิธีนี้คือตอนนี้คุณไม่มีsetter
วิธีการที่เหมาะสม property
มีหลายกรณีที่คุณไม่ต้องการค่าชุดบนเป็นของคุณ แต่ตอนนี้เมื่อใดก็ตามที่คุณตั้งค่าself.culture = ...
ก็จะมักจะเขียนทับสิ่งที่ทำงานคุณกำหนดไว้ในgetter
(ซึ่งในกรณีนี้มันเป็นเพียง@property
ส่วนหนึ่งห่อ. คุณสามารถเพิ่มมาตรการที่เหมาะสมยิ่งขึ้นแต่หรืออีกวิธีหนึ่งก็มักจะเกี่ยวข้องกับการมากกว่าเพียงแค่self.culture = ...
เช่น:
class Grandparent:
# ...
@culture.setter
def culture(self, val):
if isinstance(val, tuple):
if val[1]:
print('Fine, have your own culture')
type(self).culture = None
self.culture = val[0]
else:
raise AttributeError("Oh no you don't")
# ...
class Child(Parent):
def __init__(self):
try:
# Usual setter
self.culture = "I'm a Gen X!"
except AttributeError:
# Trigger the overwrite condition
self.culture = "I'm a Boomer!", True
มันwaaaaayซับซ้อนกว่าคำตอบอื่น ๆsize = None
ในระดับชั้นเรียน
คุณสามารถลองเขียนdescriptorของคุณเองแทนเพื่อจัดการกับ__get__
และ__set__
หรือวิธีการเพิ่มเติม แต่ในตอนท้ายของวันเมื่อself.culture
มีการอ้างอิง__get__
จะถูกเรียกก่อนเสมอและเมื่อself.culture = ...
มีการอ้างอิง__set__
จะถูกเรียกใช้ก่อนเสมอ มันไม่มีทางที่จะไปได้ไกลเท่าที่ฉันได้ลอง
ประเด็นสำคัญของ IMO
ปัญหาที่ฉันเห็นที่นี่คือ - คุณไม่สามารถมีเค้กของคุณและกินมันเกินไป property
มีความหมายเหมือนคำอธิบายถึงที่มีการเข้าถึงสะดวกเช่นวิธีการหรือgetattr
setattr
หากคุณต้องการวิธีการเหล่านี้เพื่อให้บรรลุวัตถุประสงค์ที่แตกต่างกันคุณแค่ขอปัญหา ฉันอาจจะคิดทบทวนวิธีการ:
- ฉันต้องการสิ่ง
property
นี้หรือไม่?
- วิธีการสามารถให้บริการฉันที่แตกต่างกันหรือไม่?
- หากฉันต้องการ a
property
มีเหตุผลใดที่ฉันจะต้องเขียนทับมัน?
- คลาสย่อยนั้นเป็นสมาชิกของครอบครัวเดียวกัน
property
หรือไม่หากไม่ได้ใช้
- หากฉันต้องการเขียนทับใด ๆ / ทั้งหมด
property
วิธีการแยกจะให้บริการฉันดีกว่าเพียงแค่กำหนดใหม่เนื่องจากการกำหนดใหม่อาจทำให้โมฆะเป็นโมฆะproperty
ได้หรือไม่
สำหรับจุดที่ 5 แนวทางของฉันจะมีoverwrite_prop()
วิธีการในคลาสฐานที่เขียนทับแอตทริบิวต์คลาสปัจจุบันเพื่อที่property
จะไม่ถูกทริกเกอร์อีกต่อไป:
class Grandparent:
# ...
def overwrite_props(self):
# reassign class attributes
type(self).size = None
type(self).len = None
# other properties, if necessary
# ...
# Usage
class Child(Parent):
def __init__(self):
self.overwrite_props()
self.size = 5
self.len = 10
ที่คุณสามารถดูในขณะที่ยังคงบิต contrived size = None
มันเป็นอย่างน้อยอย่างชัดเจนกว่าที่เป็นความลับ ที่กล่าวว่าในที่สุดฉันจะไม่เขียนทับทรัพย์สินเลยและจะพิจารณาการออกแบบของฉันใหม่จากราก
ถ้าคุณทำมาไกลขนาดนี้ - ขอบคุณสำหรับการเดินทางครั้งนี้กับฉัน มันเป็นการออกกำลังกายสนุก ๆ