Đây sẽ là một câu trả lời dài dòng có thể chỉ phục vụ miễn phí ... nhưng câu hỏi của bạn đã đưa tôi đi xe xuống hố thỏ vì vậy tôi cũng muốn chia sẻ những phát hiện của mình (và nỗi đau).
Cuối cùng bạn có thể thấy câu trả lời này không hữu ích cho vấn đề thực tế của bạn. Trên thực tế, kết luận của tôi là - tôi hoàn toàn không làm điều này. Phải nói rằng, nền tảng cho kết luận này có thể giúp bạn giải trí một chút, vì bạn đang tìm kiếm thêm chi tiết.
Giải quyết một số quan niệm sai lầm
Câu trả lời đầu tiên, trong khi chính xác trong hầu hết các trường hợp, không phải lúc nào cũng đúng. Ví dụ, hãy xem xét lớp này:
class Foo:
def __init__(self):
self.name = 'Foo!'
@property
def inst_prop():
return f'Retrieving {self.name}'
self.inst_prop = inst_prop
inst_prop
, trong khi là một property
, là một thuộc tính thể hiện:
>>> 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!'
Tất cả phụ thuộc vào nơi bạn property
được xác định ở nơi đầu tiên. Nếu của bạn @property
được định nghĩa trong "phạm vi" lớp (hoặc thực sự, namespace
), nó sẽ trở thành một thuộc tính lớp. Trong ví dụ của tôi, lớp học không nhận ra bất kỳ điều gì inst_prop
cho đến khi được khởi tạo. Tất nhiên, nó không hữu dụng như một tài sản ở đây.
Nhưng trước tiên, hãy giải quyết nhận xét của bạn về độ phân giải thừa kế ...
Vì vậy, làm thế nào chính xác yếu tố thừa kế vào vấn đề này? Bài viết tiếp theo này đi sâu một chút vào chủ đề và Thứ tự giải quyết phương pháp có phần liên quan, mặc dù nó thảo luận chủ yếu về Bề rộng của thừa kế thay vì Độ sâu.
Kết hợp với phát hiện của chúng tôi, đưa ra những thiết lập dưới đây:
@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"
Hãy tưởng tượng những gì xảy ra khi những dòng này được thực thi:
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}')
Kết quả là như vậy:
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>
Nhận thấy như thế nào:
self.world_view
đã có thể được áp dụng, trong khi self.culture
thất bại
culture
không tồn tại trong Child.__dict__
( mappingproxy
lớp, không bị nhầm lẫn với thể hiện __dict__
)
- Mặc dù
culture
tồn tại trong c.__dict__
, nó không được tham chiếu.
Bạn có thể đoán tại sao - world_view
được ghi đè bởi Parent
lớp là một tài sản, vì vậy Child
cũng có thể ghi đè lên nó. Trong khi đó, kể từ khi culture
được thừa hưởng, nó chỉ tồn tại trong phạm vi mappingproxy
củaGrandparent
:
Grandparent.__dict__ is: {
'__module__': '__main__',
'culture': <property object at 0x00694C00>,
'world_view': <property object at 0x00694C00>,
...
}
Trong thực tế nếu bạn cố gắng loại bỏ Parent.culture
:
>>> del Parent.culture
Traceback (most recent call last):
File "<pyshell#67>", line 1, in <module>
del Parent.culture
AttributeError: culture
Bạn sẽ nhận thấy nó thậm chí không tồn tại Parent
. Bởi vì đối tượng đang trực tiếp tham khảo lại Grandparent.culture
.
Vậy, còn về Nghị quyết thì sao?
Vì vậy, chúng tôi quan tâm để quan sát Thứ tự Nghị quyết thực tế, Parent.world_view
thay vào đó , hãy thử xóa :
del Parent.world_view
print(f'c.world_view is: {c.world_view}')
print(f'Child.world_view is: {Child.world_view}')
Tự hỏi kết quả là gì?
c.world_view is: Family property
Child.world_view is: <property object at 0x00694C00>
Nó được hoàn nguyên về Grandparent world_view
property
, mặc dù chúng tôi đã quản lý thành công để gán self.world_view
trước đó! Nhưng điều gì sẽ xảy ra nếu chúng ta thay đổi mạnh mẽ world_view
ở cấp độ lớp, giống như câu trả lời khác? Nếu chúng ta xóa nó thì sao? Điều gì xảy ra nếu chúng ta gán thuộc tính lớp hiện tại là một thuộc tính?
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}')
Kết quả là:
# 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>
Điều này rất thú vị vì c.world_view
được khôi phục lại thuộc tính cá thể của nó, trong khi đó Child.world_view
là thuộc tính chúng ta đã gán. Sau khi loại bỏ thuộc tính cá thể, nó trở lại thuộc tính lớp. Và sau khi gán lại Child.world_view
tài sản, chúng tôi ngay lập tức mất quyền truy cập vào thuộc tính cá thể.
Do đó, chúng ta có thể phỏng đoán thứ tự giải quyết sau :
- Nếu một thuộc tính lớp tồn tại và nó là một
property
, hãy lấy giá trị của nó thông qua getter
hoặc fget
(nhiều hơn về điều này sau). Lớp hiện tại đầu tiên đến lớp Cơ sở cuối cùng.
- Mặt khác, nếu một thuộc tính thể hiện tồn tại, lấy giá trị thuộc tính thể hiện.
- Khác, lấy
property
thuộc tính phi lớp. Lớp hiện tại đầu tiên đến lớp Cơ sở cuối cùng.
Trong trường hợp đó, hãy xóa gốc property
:
del Grandparent.culture
print(f'c.culture is: {c.culture}')
print(f'Child.culture is: {Child.culture}')
Cung cấp cho:
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
bây giờ có riêng của họ culture
dựa trên chèn mạnh mẽ vào c.__dict__
. Child.culture
dĩ nhiên không tồn tại vì nó không bao giờ được định nghĩa trong Parent
hoặc Child
thuộc tính lớp và Grandparent
đã bị xóa.
Đây có phải là nguyên nhân gốc rễ của vấn đề của tôi?
Thật ra, không . Lỗi bạn gặp phải, mà chúng tôi vẫn đang quan sát khi gán self.culture
, là hoàn toàn khác nhau . Nhưng thứ tự thừa kế đặt bối cảnh cho câu trả lời - đó là property
chính nó.
Bên cạnh getter
phương pháp đã đề cập trước đó , property
cũng có một vài thủ thuật gọn gàng lên tay áo của nó. Liên quan nhất trong trường hợp này là setter
, hoặc fset
phương thức, được kích hoạt bởiself.culture = ...
dòng. Vì bạn property
không thực hiện bất kỳ chức năng setter
hoặc fget
chức năng nào, python không biết phải làm gì và ném AttributeError
thay vào đó (nghĩa là can't set attribute
).
Tuy nhiên, nếu bạn thực hiện một setter
phương pháp:
@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...
Khi bắt đầu Child
lớp bạn sẽ nhận được:
Instantiating Child class...
property setter is called!
Thay vì nhận được một AttributeError
, bây giờ bạn thực sự gọisome_prop.setter
phương thức. Điều này cho phép bạn kiểm soát nhiều hơn đối tượng của mình ... với những phát hiện trước đây của chúng tôi, chúng tôi biết rằng chúng tôi cần phải có một thuộc tính lớp được ghi đè trước khi nó đến tài sản. Điều này có thể được thực hiện trong lớp cơ sở như là một kích hoạt. Đây là một ví dụ mới:
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)
Kết quả nào trong:
Fine, have your own culture
I'm a millennial!
TA-DAH! Bây giờ bạn có thể ghi đè lên thuộc tính cá thể của riêng bạn trên một thuộc tính được kế thừa!
Vậy, vấn đề được giải quyết?
... Không hẳn vậy. Vấn đề với phương pháp này là, bây giờ bạn không thể có một setter
phương pháp thích hợp . Có những trường hợp bạn muốn đặt giá trị của bạn property
. Nhưng bây giờ, bất cứ khi nào bạn thiết lập, self.culture = ...
nó sẽ luôn ghi đè bất kỳ chức năng nào bạn đã xác định trong getter
(trong trường hợp này, thực sự chỉ là @property
phần được bao bọc. Bạn có thể thêm vào các biện pháp nhiều sắc thái hơn, nhưng bằng cách này hay cách khác, nó sẽ luôn liên quan nhiều hơn chỉ self.culture = ...
. ví dụ:
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
Đó là waaaaay phức tạp hơn câu trả lời khác, size = None
ở cấp độ lớp.
Thay vào đó, bạn cũng có thể xem xét việc viết mô tả của riêng mình để xử lý __get__
và __set__
hoặc các phương pháp bổ sung. Nhưng vào cuối ngày, khi self.culture
được tham chiếu, ý __get__
chí sẽ luôn được kích hoạt trước và khi self.culture = ...
được tham chiếu,__set__
sẽ luôn được kích hoạt trước. Không có xung quanh nó như tôi đã cố gắng.
Mấu chốt của vấn đề, IMO
Vấn đề tôi thấy ở đây là - bạn không thể có bánh của bạn và cũng ăn nó. property
có nghĩa giống như một mô tả với truy cập thuận tiện từ các phương thức như getattr
hoặcsetattr
. Nếu bạn cũng muốn các phương pháp này đạt được mục đích khác, bạn chỉ cần yêu cầu sự cố. Tôi có lẽ sẽ suy nghĩ lại về cách tiếp cận:
- Tôi có thực sự cần một
property
cái này không?
- Một phương pháp có thể phục vụ tôi khác nhau?
- Nếu tôi cần một
property
, có bất kỳ lý do tôi sẽ cần phải ghi đè lên nó?
- Có phải lớp con thực sự thuộc về cùng một gia đình nếu những
property
điều này không áp dụng?
- Nếu tôi cần ghi đè lên bất kỳ / tất cả
property
, liệu một phương thức riêng có phục vụ tôi tốt hơn là chỉ định lại, vì việc gán lại có thể vô tình làm mất hiệu lực property
của s không?
Đối với điểm 5, cách tiếp cận của tôi sẽ có một overwrite_prop()
phương thức trong lớp cơ sở ghi đè lên thuộc tính lớp hiện tại để ý property
chí không còn được kích hoạt:
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
Như bạn có thể thấy, trong khi vẫn còn một chút giả định, nó ít nhất là rõ ràng hơn một mật mã size = None
. Điều đó nói rằng, cuối cùng, tôi sẽ không ghi đè lên tài sản, và sẽ xem xét lại thiết kế của tôi từ gốc.
Nếu bạn đã đi xa đến thế - cảm ơn bạn đã cùng tôi đi bộ hành trình này. Đó là một bài tập nhỏ vui vẻ.