Ini akan menjadi jawaban panjang lebar yang mungkin hanya berfungsi sebagai pelengkap ... tapi pertanyaan Anda membawa saya untuk turun ke lubang kelinci jadi saya ingin berbagi temuan saya (dan rasa sakit) juga.
Anda mungkin pada akhirnya menemukan jawaban ini tidak membantu masalah Anda yang sebenarnya. Bahkan, kesimpulan saya adalah - saya tidak akan melakukan ini sama sekali. Karena itu, latar belakang kesimpulan ini mungkin sedikit menghibur Anda, karena Anda mencari detail lebih lanjut.
Mengatasi beberapa kesalahpahaman
Jawaban pertama, meskipun benar dalam banyak kasus, tidak selalu demikian. Sebagai contoh, pertimbangkan kelas ini:
class Foo:
def __init__(self):
self.name = 'Foo!'
@property
def inst_prop():
return f'Retrieving {self.name}'
self.inst_prop = inst_prop
inst_prop
, sementara menjadi property
, adalah atribut instance yang tidak dapat dibatalkan:
>>> 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!'
Itu semua tergantung di mana Anda property
didefinisikan di tempat pertama. Jika Anda @property
didefinisikan dalam "lingkup" kelas (atau benar-benar, namespace
), itu menjadi atribut kelas. Dalam contoh saya, kelas itu sendiri tidak mengetahui adanya inst_prop
sampai instantiated. Tentu saja, ini sama sekali tidak berguna sebagai properti di sini.
Tapi pertama-tama, mari alamat komentar Anda pada resolusi warisan ...
Jadi bagaimana tepatnya faktor pewarisan dalam masalah ini? Artikel berikut ini sedikit menyimpang ke dalam topik, dan Metode Resolusi Urutan agak terkait, meskipun sebagian besar membahas Luasnya warisan daripada Kedalaman.
Dikombinasikan dengan temuan kami, dengan pengaturan di bawah ini:
@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"
Bayangkan apa yang terjadi ketika baris ini dieksekusi:
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}')
Hasilnya adalah sebagai berikut:
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>
Perhatikan caranya:
self.world_view
dapat diterapkan, sementara self.culture
gagal
culture
tidak ada di Child.__dict__
( mappingproxy
kelas, jangan dikelirukan dengan instance __dict__
)
- Meskipun
culture
ada di c.__dict__
, itu tidak dirujuk.
Anda mungkin dapat menebak mengapa - world_view
ditimpa oleh Parent
kelas sebagai non-properti, sehingga Child
dapat menimpanya juga. Sementara itu, sejak culture
diwariskan, hanya ada dalam mappingproxy
dariGrandparent
:
Grandparent.__dict__ is: {
'__module__': '__main__',
'culture': <property object at 0x00694C00>,
'world_view': <property object at 0x00694C00>,
...
}
Bahkan jika Anda mencoba untuk menghapus Parent.culture
:
>>> del Parent.culture
Traceback (most recent call last):
File "<pyshell#67>", line 1, in <module>
del Parent.culture
AttributeError: culture
Anda akan melihat itu bahkan tidak ada Parent
. Karena objek secara langsung merujuk kembali ke Grandparent.culture
.
Jadi, bagaimana dengan Resolution Order?
Jadi kami tertarik untuk mengamati Resolution Order yang sebenarnya, mari kita coba hapus 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}')
Bertanya-tanya apa hasilnya?
c.world_view is: Family property
Child.world_view is: <property object at 0x00694C00>
Itu dikembalikan ke kakek-nenek world_view
property
, meskipun kami telah berhasil menugaskan self.world_view
sebelumnya! Tetapi bagaimana jika kita dengan paksa berubah world_view
di tingkat kelas, seperti jawaban yang lain? Bagaimana jika kita menghapusnya? Bagaimana jika kita menetapkan atribut kelas saat ini menjadi properti?
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}')
Hasilnya adalah:
# 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>
Ini menarik karena c.world_view
dikembalikan ke atribut instansinya, sedangkan atribut Child.world_view
yang kami tetapkan. Setelah menghapus atribut instance, itu akan kembali ke atribut class. Dan setelah menugaskan kembali Child.world_view
ke properti, kami langsung kehilangan akses ke atribut instance.
Oleh karena itu, kami dapat menduga urutan resolusi berikut :
- Jika atribut kelas ada dan itu adalah
property
, ambil nilainya melalui getter
atau fget
(lebih lanjut tentang ini nanti). Kelas saat ini pertama ke kelas Base terakhir.
- Jika tidak, jika ada atribut instance, ambil nilai atribut instance.
- Lain, ambil
property
atribut non- kelas. Kelas saat ini pertama ke kelas Base terakhir.
Dalam hal ini, mari kita hapus root property
:
del Grandparent.culture
print(f'c.culture is: {c.culture}')
print(f'Child.culture is: {Child.culture}')
Pemberian yang mana:
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
sekarang memiliki sendiri culture
berdasarkan penyisipan paksa ke c.__dict__
. Child.culture
tidak ada, tentu saja, karena tidak pernah didefinisikan dalam Parent
atau Child
atribut kelas, dan Grandparent
sudah dihapus.
Apakah ini penyebab utama masalah saya?
Sebenarnya tidak . Kesalahan yang Anda dapatkan, yang masih kami amati saat menetapkan self.culture
, sangat berbeda . Tetapi urutan warisan mengatur latar belakang untuk jawaban - yang merupakan property
dirinya sendiri.
Selain getter
metode yang disebutkan sebelumnya , property
juga memiliki beberapa trik rapi di lengan bajunya. Yang paling relevan dalam kasus ini adalah setter
, atau fset
metode, yang dipicu oleh self.culture = ...
garis. Karena Anda property
tidak mengimplementasikan setter
atau fget
fungsi apa pun , python tidak tahu apa yang harus dilakukan, dan AttributeError
sebaliknya melemparkan (yaitu can't set attribute
).
Namun jika Anda menerapkan setter
metode:
@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...
Saat membuat instance Child
kelas Anda akan mendapatkan:
Instantiating Child class...
property setter is called!
Alih-alih menerima AttributeError
, Anda sekarang benar-benar memanggil some_prop.setter
metode. Yang memberi Anda lebih banyak kontrol atas objek Anda ... dengan temuan kami sebelumnya, kami tahu bahwa kami perlu memiliki atribut kelas yang ditimpa sebelum mencapai properti. Ini bisa diimplementasikan dalam kelas dasar sebagai pemicu. Ini contoh baru:
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)
Yang mengakibatkan:
Fine, have your own culture
I'm a millennial!
TA-DAH! Sekarang Anda dapat menimpa atribut instance Anda sendiri di atas properti yang diwarisi!
Jadi, masalah terpecahkan?
... Tidak juga. Masalah dengan pendekatan ini adalah, sekarang Anda tidak dapat memiliki setter
metode yang tepat . Ada kasus di mana Anda ingin menetapkan nilai pada property
. Tapi sekarang setiap kali Anda mengaturnya self.culture = ...
akan selalu menimpa fungsi apa pun yang Anda tetapkan dalam getter
(yang dalam hal ini, benar-benar hanya bagian @property
terbungkus. Anda dapat menambahkan dalam langkah-langkah yang lebih bernuansa, tetapi dengan satu atau lain cara itu akan selalu melibatkan lebih dari sekadar self.culture = ...
. misalnya:
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
Itu waaaaay lebih rumit dari jawaban yang lain, size = None
di tingkat kelas.
Anda juga dapat mempertimbangkan untuk menulis deskriptor Anda sendiri untuk menangani metode __get__
dan __set__
, atau tambahan. Tetapi pada akhirnya, ketika self.culture
direferensikan, yang __get__
akan selalu dipicu pertama, dan ketika self.culture = ...
direferensikan, __set__
akan selalu dipicu terlebih dahulu. Tidak ada jalan keluar sejauh yang saya coba.
Inti masalahnya, IMO
Masalah yang saya lihat di sini adalah - Anda tidak dapat memiliki kue dan memakannya juga. property
dimaksudkan seperti deskriptor dengan akses mudah dari metode seperti getattr
atau setattr
. Jika Anda juga ingin metode ini mencapai tujuan yang berbeda, Anda hanya meminta masalah. Saya mungkin akan memikirkan kembali pendekatan:
- Apakah saya benar-benar membutuhkan
property
ini?
- Dapatkah metode melayani saya berbeda?
- Jika saya membutuhkan
property
, apakah ada alasan saya perlu menimpanya?
- Apakah subclass benar-benar termasuk dalam keluarga yang sama jika ini
property
tidak berlaku?
- Jika saya perlu menimpa semua / semua
property
, akankah metode terpisah bermanfaat bagi saya lebih baik daripada hanya menugaskan kembali, karena penugasan kembali secara tidak sengaja dapat membatalkan property
s?
Untuk poin 5, pendekatan saya akan memiliki overwrite_prop()
metode di kelas dasar yang menimpa atribut kelas saat ini sehingga property
tidak akan lagi dipicu:
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
Seperti yang Anda lihat, sementara masih sedikit dibuat-buat, itu setidaknya lebih eksplisit daripada yang samar size = None
. Yang mengatakan, pada akhirnya, saya tidak akan menimpa properti sama sekali, dan akan mempertimbangkan kembali desain saya dari root.
Jika Anda telah sampai sejauh ini - terima kasih telah berjalan dalam perjalanan ini bersama saya. Itu adalah latihan kecil yang menyenangkan.