深入理解Python虛擬機(jī)中描述器的實現(xiàn)原理
在本篇文章當(dāng)中主要給大家介紹一個我們在使用類的時候經(jīng)常使用但是卻很少在意的黑科技——描述器,在本篇文章當(dāng)中主要分析描述器的原理,以及介紹使用描述器實現(xiàn)屬性訪問控制和 orm 映射等等功能!在后面的文章當(dāng)中我們將繼續(xù)去分析描述器的實現(xiàn)原理。
描述器的基本用法
描述器是一個實現(xiàn)了 __get__、__set__ 或 __delete__ 中至少一個方法的 Python 類。這些方法分別用于在屬性被訪問、設(shè)置或刪除時調(diào)用。當(dāng)一個描述器被定義為一個類的屬性時,它可以控制該屬性的訪問、修改和刪除。
下面是一個示例,演示了如何定義一個簡單的描述器:
class Descriptor:
def __get__(self, instance, owner):
print(f"Getting {self.__class__.__name__}")
return instance.__dict__.get(self.attrname)
def __set__(self, instance, value):
print(f"Setting {self.__class__.__name__}")
instance.__dict__[self.attrname] = value
def __delete__(self, instance):
print(f"Deleting {self.__class__.__name__}")
del instance.__dict__[self.attrname]
def __set_name__(self, owner, name):
self.attrname = name
在這個例子中,我們定義了一個名為 Descriptor 的描述器類,它有三個方法:__get__、__set__ 和 __delete__。當(dāng)我們在另一個類中使用這個描述器時,這些方法將被調(diào)用,以控制該類的屬性的訪問和修改。
要使用這個描述器,我們可以在另一個類中將其定義為一個類屬性:
class MyClass:
x = Descriptor()現(xiàn)在,我們可以創(chuàng)建一個 MyClass 對象并訪問其屬性:
>>> obj = MyClass()
>>> obj.x = 1
Setting Descriptor
>>> obj.x
Getting Descriptor
1
>>> del obj.x
Deleting Descriptor
>>> obj.x
Getting Descriptor
在這個例子中,我們首先創(chuàng)建了一個 MyClass 對象,并將其 x 屬性設(shè)置為 1。然后,我們再次訪問 x 屬性時,會調(diào)用 __get__ 方法并返回 1。最后,我們刪除了 x 屬性,并再次訪問它時,會調(diào)用 __get__ 方法并返回 None。從上面的輸出結(jié)果可以看到對應(yīng)的方法都被調(diào)用了,這是符合上面對描述器的定義的。如果一個類對象不是描述器,那么在使用對應(yīng)的屬性的時候是不會調(diào)用__get__、__set__ 和 __delete__三個方法的。比如下面的代碼:
class NonDescriptor(object):
pass
class MyClass():
nd = NonDescriptor()
if __name__ == '__main__':
a = MyClass()
print(a.nd)
上面的代碼輸出結(jié)果如下所示:
<__main__.NonDescriptor object at 0x1012cce20>
從上面程序的輸出結(jié)果可以知道,當(dāng)使用一個非描述器的類屬性的時候是不會調(diào)用對應(yīng)的方法的,而是直接得到對應(yīng)的對象。
描述器的實現(xiàn)原理
描述器的實現(xiàn)原理可以用以下三個步驟來概括:
- 當(dāng)一個類的屬性被訪問時,Python 解釋器會檢查該屬性是否是一個描述器。如果是,它會調(diào)用描述器的
__get__方法,并將該類的實例作為第一個參數(shù),該實例所屬的類作為第二個參數(shù),并將屬性名稱作為第三個參數(shù)傳遞給__get__方法。 - 當(dāng)一個類的屬性被設(shè)置時,Python 解釋器會檢查該屬性是否是一個描述器。如果是,它會調(diào)用描述器的
__set__方法,并將該類的實例作為第一個參數(shù),設(shè)置的值作為第二個參數(shù),并將屬性名稱作為第三個參數(shù)傳遞給__set__方法。 - 當(dāng)一個類的屬性被刪除時,Python 解釋器會檢查該屬性是否是一個描述器。如果是,它會調(diào)用描述器的
__delete__方法,并將該類的實例作為第一個參數(shù)和屬性名稱作為第二個參數(shù)傳遞給__delete__方法。
在描述器的實現(xiàn)中,通常還會使用 __set_name__ 方法來在描述器被綁定到類屬性時設(shè)置屬性名稱。這使得描述器可以在被多個屬性使用時,正確地識別每個屬性的名稱。
現(xiàn)在來仔細(xì)了解一下上面的幾個函數(shù)的參數(shù),我們以下面的代碼為例子進(jìn)行說明:
class Descriptor(object):
def __set_name__(self, obj_type, attr_name):
print(f"__set_name__ : {obj_type } {attr_name = }")
return "__set_name__"
def __get__(self, obj, obj_type):
print(f"__get__ : {obj = } { obj_type = }")
return "__get__"
def __set__(self, instance, value):
print(f"__set__ : {instance = } {value = }")
return "__set__"
def __delete__(self, obj):
print(f"__delete__ : {obj = }")
return "__delete__"
class MyClass(object):
des = Descriptor()
if __name__ == '__main__':
a = MyClass()
_ = MyClass.des
_ = a.des
a.des = "hello"
del a.des上面的代碼輸入結(jié)果如下所示:
__set_name__ : <class '__main__.MyClass'> attr_name = 'des'
__get__ : obj = None obj_type = <class '__main__.MyClass'>
__get__ : obj = <__main__.MyClass object at 0x1054abeb0> obj_type = <class '__main__.MyClass'>
__set__ : instance = <__main__.MyClass object at 0x1054abeb0> value = 'hello'
__delete__ : obj = <__main__.MyClass object at 0x1054abeb0>
__set_name__這個函數(shù)一共有兩個參數(shù)傳入的參數(shù)第一個參數(shù)是使用描述器的類,第二個參數(shù)是使用這個描述器的類當(dāng)中使用的屬性名字,在上面的例子當(dāng)中就是 "des" 。__get__,這個函數(shù)主要有兩個參數(shù),一個是使用屬性的對象,另外一個是對象的類型,如果是直接使用類名使用屬性的話,obj 就是 None,比如上面的 MyClass.des 。__set__,這個函數(shù)主要有兩個參數(shù)一個是對象,另外一個是需要設(shè)置的值。__delete__,這函數(shù)有一個參數(shù),就是傳入的對象,比如 del a.des 傳入的就是對象 a 。
描述器的應(yīng)用場景
描述器在 Python 中有很多應(yīng)用場景。以下是其中的一些示例:
實現(xiàn)屬性訪問控制
通過使用描述器,可以實現(xiàn)對類屬性的訪問控制,例如只讀屬性、只寫屬性、只讀/只寫屬性等。通過在 __get__ 和 __set__ 方法中添加相應(yīng)的訪問控制邏輯,可以限制對類屬性的訪問和修改。
class ReadOnly:
def __init__(self, value):
self._value = value
def __get__(self, instance, owner):
return self._value
def __set__(self, instance, value):
raise AttributeError("Read only attribute")
class MyClass:
read_only_prop = ReadOnly(42)
writeable_prop = None
my_obj = MyClass()
print(my_obj.read_only_prop) # 42
my_obj.writeable_prop = "hello"
print(my_obj.writeable_prop) # hello
my_obj.read_only_prop = 100 # raises AttributeError在上面的例子中,ReadOnly 描述器只實現(xiàn)了 __get__ 方法,而 __set__ 方法則拋出了 AttributeError 異常,從而實現(xiàn)了只讀屬性的訪問控制。
實現(xiàn)數(shù)據(jù)驗證和轉(zhuǎn)換
描述器還可以用于實現(xiàn)數(shù)據(jù)驗證和轉(zhuǎn)換邏輯。通過在 __set__ 方法中添加數(shù)據(jù)驗證和轉(zhuǎn)換邏輯,可以確保設(shè)置的值符合某些特定的要求。例如,可以使用描述器來確保設(shè)置的值是整數(shù)、在某個范圍內(nèi)、符合某個正則表達(dá)式等。
class Bounded:
def __init__(self, low, high):
self._low = low
self._high = high
def __get__(self, instance, owner):
return self._value
def __set__(self, instance, value):
if not self._low <= value <= self._high:
raise ValueError(f"Value must be between {self._low} and {self._high}")
self._value = value
class MyClass:
bounded_prop = Bounded(0, 100)
my_obj = MyClass()
my_obj.bounded_prop = 50
print(my_obj.bounded_prop) # 50
my_obj.bounded_prop = 200 # raises ValueError在上面的例子中,Bounded 描述器在 __set__ 方法中進(jìn)行了數(shù)值范圍的檢查,如果值不在指定范圍內(nèi),則拋出了 ValueError 異常。
實現(xiàn)延遲加載和緩存
描述器還可以用于實現(xiàn)延遲加載和緩存邏輯。通過在 __get__ 方法中添加邏輯,可以實現(xiàn)屬性的延遲加載,即當(dāng)屬性第一次被訪問時才進(jìn)行加載。此外,還可以使用描述器來實現(xiàn)緩存邏輯,以避免重復(fù)計算。
class LazyLoad:
def __init__(self, func):
self._func = func
def __get__(self, instance, owner):
if instance is None:
return self
value = self._func(instance)
setattr(instance, self._func.__name__, value)
return value
class MyClass:
def __init__(self):
self._expensive_data = None
@LazyLoad
def expensive_data(self):
print("Calculating expensive data...")
self._expensive_data = [i ** 2 for i in range(10)]
return self._expensive_data
my_obj = MyClass()
print(my_obj.expensive_data) # Calculating expensive data...
print(my_obj.expensive_data)上面的程序的輸出結(jié)果如下所示:
Calculating expensive data...
[0, 1, 4, 9, 16, 25, 36, 49, 64, 81]
[0, 1, 4, 9, 16, 25, 36, 49, 64, 81]
從上面的結(jié)果可以看到,只有在第一次使用屬性的時候才調(diào)用函數(shù),后續(xù)再次調(diào)用函數(shù)將不會再調(diào)用函數(shù)而是直接返回緩存的結(jié)果。
實現(xiàn) ORM 映射
ORM 的主要作用是把數(shù)據(jù)庫中的關(guān)系數(shù)據(jù)轉(zhuǎn)化為面向?qū)ο蟮臄?shù)據(jù),讓開發(fā)者可以通過編寫面向?qū)ο蟮拇a來操作數(shù)據(jù)庫。ORM 技術(shù)可以把面向?qū)ο蟮木幊陶Z言和關(guān)系數(shù)據(jù)庫之間的映射關(guān)系抽象出來,開發(fā)者可以不用寫 SQL 語句,而是直接使用面向?qū)ο蟮恼Z法進(jìn)行數(shù)據(jù)庫操作。
我們現(xiàn)在需要實現(xiàn)一個功能,user.name 直接從數(shù)據(jù)庫的 user 表當(dāng)中查詢 name 等于 user.name 的數(shù)據(jù),user.name = "xxx" 根據(jù) user 的主鍵 id 進(jìn)行更新數(shù)據(jù)。這個功能我們就可以使用描述器實現(xiàn),因為只需要了解如何使用描述器的,因此在下面的代碼當(dāng)中并沒有連接數(shù)據(jù)庫:
conn = dict()
class Field:
def __set_name__(self, owner, name):
self.fetch = f'SELECT {name} FROM {owner.table} WHERE {owner.key}=?;'
print(f"{self.fetch = }")
self.store = f'UPDATE {owner.table} SET {name}=? WHERE {owner.key}=?;'
print(f"{self.store = }")
def __get__(self, obj, objtype=None):
return conn.execute(self.fetch, [obj.key]).fetchone()[0]
def __set__(self, obj, value):
conn.execute(self.store, [value, obj.key])
conn.commit()
class User:
table = 'User' # Table name
key = 'id' # Primary key
name = Field()
age = Field()
def __init__(self, key):
self.key = key
if __name__ == '__main__':
u = User("Bob")上面的程序輸出結(jié)果如下所示:
self.fetch = 'SELECT name FROM User WHERE id=?;'
self.store = 'UPDATE User SET name=? WHERE id=?;'
self.fetch = 'SELECT age FROM User WHERE id=?;'
self.store = 'UPDATE User SET age=? WHERE id=?;
從上面的輸出結(jié)果我們可以看到針對 name 和 age 兩個字段的查詢和更新語句確實生成了,當(dāng)我們調(diào)用 u.name = xxx 或者 u.age = xxx 的時候就執(zhí)行 __set__ 函數(shù),就會連接數(shù)據(jù)庫進(jìn)行相應(yīng)的操作了。
總結(jié)
在本篇文章當(dāng)中主要給大家介紹了什么是描述器以及我們能夠使用描述器來實現(xiàn)什么樣的功能,事實上 python 是一個比較隨意的語言,因此我們可以利用很多有意思的語法做出黑多黑科技。python 語言本身也利用描述器實現(xiàn)了很多有意思的功能,比如 property、staticmethod 等等,這些內(nèi)容我們在后面的文章當(dāng)中再進(jìn)行分析。
到此這篇關(guān)于深入理解Python虛擬機(jī)中描述器的實現(xiàn)原理的文章就介紹到這了,更多相關(guān)Python虛擬機(jī)描述器內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
python3發(fā)送郵件需要經(jīng)過代理服務(wù)器的示例代碼
今天小編就為大家分享一篇python3發(fā)送郵件需要經(jīng)過代理服務(wù)器的示例代碼,具有很好的參考價值,希望對大家有所幫助。一起跟隨小編過來看看吧2019-07-07
用Python從0開始實現(xiàn)一個中文拼音輸入法的思路詳解
中文輸入法是一個歷史悠久的問題,但也實在是個繁瑣的活,不知道這是不是網(wǎng)上很少有人分享中文拼音輸入法的原因,接下來通過本文給大家分享使用Python從0開始實現(xiàn)一個中文拼音輸入法,需要的朋友可以參考下2019-07-07
python實現(xiàn)動態(tài)數(shù)組的示例代碼
這篇文章主要介紹了python實現(xiàn)動態(tài)數(shù)組的示例代碼,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2019-07-07

