深入理解 python 虛擬機器:描述器的王炸應用-property、staticmethod 和 classmehtod
在本篇文章當中主要給大家介紹描述器在 python 語言當中有哪些應用,主要介紹如何使用 python 語言實現 python 內建的 proterty 、staticmethod 和 class method 。
property
當你在編寫Python程式碼時,你可能會遇到一些需要透過方法來訪問或設定的屬性。Python中的 property 裝飾器提供了一種優雅的方式來處理這種情況,允許你將這些方法封裝為屬性,從而使程式碼更加簡潔和易於閱讀。在本文中,我將向你介紹 property 裝飾器的工作原理以及如何在你的程式碼中使用它。
什麼是 property?
Python 中的 property 是一種裝飾器,它允許你定義一個方法,使其看起來像一個屬性。換句話說,property 允許你以屬性的方式訪問或設定類的資料成員,而不必直接呼叫一個方法。
在 Python 中,屬性通常是一個物件的資料成員,它們可以透過直接訪問物件來獲取或設定。然而,有時候你可能需要在獲取或設定屬性時執行某些額外的操作,例如進行型別檢查、範圍檢查或計算屬性等。在這種情況下,使用 property 裝飾器可以讓你以屬性的方式訪問或設定這些屬性,並在訪問或設定時執行額外的操作。
如何使用 property?
讓我們看一個簡單的例子,假設你正在編寫一個表示矩形的類,並且你想要在計算矩形的面積時執行一些額外的操作。你可以使用 property 裝飾器來實現這個功能,如下所示:
class Rectangle:
def __init__(self, width, height):
self._width = width
self._height = height
@property
def width(self):
return self._width
@width.setter
def width(self, value):
if value <= 0:
raise ValueError("Width must be positive")
self._width = value
@property
def height(self):
return self._height
@height.setter
def height(self, value):
if value <= 0:
raise ValueError("Height must be positive")
self._height = value
@property
def area(self):
return self._width * self._height
在這個示例中,我們使用 property 裝飾器定義了三個屬性:width、height和area。每個屬性都有一個 getter 方法和一個 setter 方法,它們分別負責獲取和設定屬性的值。當你使用類的例項訪問這些屬性時,你會發現它們似乎就像是一個普通的屬性,而不是一個方法。
注意,getter 方法沒有引數,而 setter 方法接受一個引數。當你透過類的例項訪問屬性時,你只需要使用點運算子即可訪問這些屬性,就像這樣:
rect = Rectangle(10, 20)
print(rect.width)
print(rect.height)
print(rect.area)
輸出結果:
10
20
200
你也可以像下面這樣設定屬性的值:
rect.width = 5
rect.height = 10
print(rect.width)
print(rect.height)
print(rect.area)
輸出結果如下所示:
5
10
50
在設定 width 或 height 屬性的值時,會執行對應的 setter 方法進行型別檢查和範圍檢查。如果值不符合要求,將會丟擲一個 ValueError 異常。這使得你的程式碼更加健壯和可靠。
除了在屬性的 getter 和 setter 方法中執行額外的操作外,你還可以使用 property 裝飾器計算屬性。計算屬性是指,當你訪問屬性時,它不是從類的例項中獲取資料,而是基於類的其他資料成員進行計算。例如,如果你有一個表示溫度的類,你可以定義一個計算屬性,用於將攝氏度轉換為華氏度,如下所示:
class Temperature:
def __init__(self, celsius):
self._celsius = celsius
@property
def celsius(self):
return self._celsius
@celsius.setter
def celsius(self, value):
self._celsius = value
@property
def fahrenheit(self):
return (self._celsius * 9/5) + 32
在這個示例中,我們定義了一個 Temperature 類,它包含一個 celsius 屬性和一個 fahrenheit 屬性。celsius 屬性是一個普通的屬性,可以直接訪問和設定。而 fahrenheit 屬性是一個計算屬性,它基於 celsius 屬性計算而來。當你訪問 fahrenheit 屬性時,它將自動計算出相應的華氏度並返回。你可以會對上面的程式碼有點疑惑celsius.setter
是什麼,他是那裡來的,事實上在它上面的 @property
執行之後 celsius 已經不再是一個函式了,而是一個 property 的類產生的物件了,因此 celsius.setter
是 property 類中的 setter
屬性了,事實上他是一個類的方法了,而裝飾器 @celsius.setter
就是將 def celsius(self, value)
這個函式作為引數傳遞給方法 celsius.setter
。
我們介紹了 Python 中的 property 裝飾器,它允許你將方法封裝為屬性,並在訪問或設定屬性時執行額外的操作。透過使用 property 裝飾器,你可以編寫更加簡潔、優雅和可讀的程式碼,同時使程式碼更加健壯和可靠。
property 的本質
property 是 python 內建的一個類,注意它是類。在前面的內容當中我們已經詳細討論過了裝飾器的原理,並且從位元組碼的角度進行了分析。因此我們可以很容易理解上面 Temperature
類。我們可以將裝飾器展開:
class Temperature:
def __init__(self, celsius):
self._celsius = celsius
def celsius1(self):
return self._celsius
celsius = property(celsius1)
def celsius2(self, value):
self._celsius = value
celsius = celsius.setter(celsius2)
def fahrenheit(self):
return (self._celsius * 9 / 5) + 32
fahrenheit = property(fahrenheit)
if __name__ == '__main__':
t = Temperature(10)
print(t.celsius)
t.celsius = 100
print(t.celsius)
print(t.fahrenheit)
上面的程式輸出結果如下所示:
10
100
212.0
可以看到上面的程式正確的輸出了結果,符合我們對與 property 的理解和使用。從上面的分析我們可以看到 property 本質就是一個 python 的類,因此我可以完全自己實現一個和內建的 property 類相同功能的類。
在 python 語言層面實現 property 機制
具體的實現程式碼如下所示:
class Property:
"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
self._name = ''
def __set_name__(self, owner, name):
self._name = name
def __get__(self, obj, objtype=None):
if obj is None:
return self
if self.fget is None:
raise AttributeError(f"property '{self._name}' has no getter")
return self.fget(obj)
def __set__(self, obj, value):
if self.fset is None:
raise AttributeError(f"property '{self._name}' has no setter")
self.fset(obj, value)
def __delete__(self, obj):
if self.fdel is None:
raise AttributeError(f"property '{self._name}' has no deleter")
self.fdel(obj)
def getter(self, fget):
prop = type(self)(fget, self.fset, self.fdel, self.__doc__)
prop._name = self._name
return prop
def setter(self, fset):
prop = type(self)(self.fget, fset, self.fdel, self.__doc__)
prop._name = self._name
return prop
def deleter(self, fdel):
prop = type(self)(self.fget, self.fset, fdel, self.__doc__)
prop._name = self._name
return prop
現在對上面我們自己實現的類物件進行使用測試:
class Rectangle:
def __init__(self, width, height):
self._width = width
self._height = height
@Property
def width(self):
return self._width
@width.setter
def width(self, value):
if value <= 0:
raise ValueError("Width must be positive")
self._width = value
@Property
def height(self):
return self._height
@height.setter
def height(self, value):
if value <= 0:
raise ValueError("Height must be positive")
self._height = value
@Property
def area(self):
return self._width * self._height
if __name__ == '__main__':
rect = Rectangle(10, 20)
print(rect.width)
print(rect.height)
print(rect.area)
rect.width = 5
rect.height = 10
print(rect.width)
print(rect.height)
print(rect.area)
上面的程式輸出結果如下所示:
10
20
200
5
10
50
可以看到正確的輸出了結果。
現在我們來好好分析一下我們在上面使用到的自己實現的 Property
類是如何被呼叫的,在前面的內容當中我們已經討論過了,只有類屬性才可能是描述器,我們在使用 @Property
的時候是獲取到對應的函式,更準確的說是獲得物件的 get 函式,然後使用 @Property
的類當中的原來的函式就變成了 Property
物件了,後面就可以使用物件的 setter
方法了。
然後在使用 rect.width
或者 rect.height
方法的時候就活觸發描述器的機制, rect 物件就會被傳入到描述器的 __get__
方法,然後在這個方法當中將傳入的物件再傳給之前得到的 fget
函式,就完美的實現了我們想要的效果。
classmethod 和 staticmethod
在 Python 中,staticmethod 和 classmethod 是兩個常用的裝飾器,它們分別用於定義靜態方法和類方法。
staticmethod
staticmethod 是一個裝飾器,它可以將一個函式定義為靜態方法。靜態方法與類例項無關,可以在不建立類例項的情況下直接呼叫,但它們仍然可以透過類名訪問。
下面是一個簡單的示例:
class MyClass:
@staticmethod
def my_static_method(x, y):
return x + y
print(MyClass.my_static_method(1, 2))
在這個示例中,我們定義了一個 MyClass 類,並使用 @staticmethod 裝飾器將 my_static_method 方法定義為靜態方法。然後我們可以透過 MyClass.my_static_method(1, 2) 直接呼叫該方法,而不需要建立 MyClass 的例項。需要注意的是,靜態方法沒有對類或例項進行任何修改,因此它們通常用於一些獨立的、無狀態的函式,或者在類中定義的一些幫助函式。
那麼 staticmethod 是如何在語法層面實現的呢?這又離不開描述器了,在上面的程式碼當中我們使用 staticmethod
裝飾函式 my_static_method
然後在類 MyClass
當中會有一個類 staticmethod 的物件,且名字為 my_static_method 。我們需要注意到的是上面的過程用一行程式碼表示為 my_static_method = staticmethod(my_static_method)
,傳入的 my_static_method 就是 my_static_method 函式,那麼這就很簡單了,當使用 my_static_method 的屬性時候,我們可以在描述器的函式 __get__
當中直接返回傳入的函式即可。
我們自己實現的 StaticMethod 如下所示:
class StaticMethod:
"Emulate PyStaticMethod_Type() in Objects/funcobject.c"
def __init__(self, f):
self.f = f
f = functools.update_wrapper(self, f)
def __get__(self, obj, objtype=None):
return self.f
def __call__(self, *args, **kwds):
return self.f(*args, **kwds)
我們使用上面自己實現的類:
class MyClass(object):
@StaticMethod
def demo():
return "demo"
if __name__ == '__main__':
a = MyClass()
print(a.demo())
上面的程式會輸出字串 "demo"
。
classmethod
classmethod 是另一個裝飾器,它可以將一個函式定義為類方法。類方法與靜態方法類似,但它們接收的第一個引數是類物件而不是例項物件。類方法通常用於實現與類有關的操作,如工廠方法或建構函式。
下面是一個使用 classmethod 的示例:
class MyClass:
num_instances = 0
def __init__(self):
MyClass.num_instances += 1
@classmethod
def get_num_instances(cls):
return cls.num_instances
obj1 = MyClass()
obj2 = MyClass()
print(MyClass.get_num_instances())
在這個示例中,我們定義了一個 MyClass 類,它包含一個類變數 num_instances 和一個建構函式。然後,我們使用 @classmethod 裝飾器將 get_num_instances 方法定義為類方法,並將 cls 引數用於訪問類變數 num_instances。
在建立 MyClass 的兩個例項後,我們呼叫 MyClass.get_num_instances() 來獲取當前建立的例項數。因為我們使用了類方法,所以可以直接透過類名呼叫該方法。
需要注意的是,類方法可以在類和例項之間共享,因為它們都可以訪問類變數。另外,類方法可以被子類繼承和重寫,因為它們接收的第一個引數是類物件,而不是固定的類名。
在小節中,我們介紹了 Python 中的兩種常用裝飾器,即 staticmethod 和 classmethod。staticmethod 用於定義與類例項無關的靜態方法,而 classmethod 用於定義與類相關的操作,如工廠方法或建構函式。兩種裝飾器都可以透過類名進行訪問,但 classmethod 還可以被子類繼承和重寫,因為它們接收的第一個引數是類物件。
需要注意的是,staticmethod 和 classmethod 都可以被類或例項呼叫,但它們不同的是,classmethod 的第一個引數是類物件,而 staticmethod 沒有這樣的引數。因此,classmethod 可以訪問類變數,而 staticmethod 不能訪問類變數。
下面是一個更具體的比較:
class MyClass:
class_var = 'class_var'
@staticmethod
def static_method():
print('This is a static method')
@classmethod
def class_method(cls):
print('This is a class method')
print(f'The class variable is: {cls.class_var}')
obj = MyClass()
# 靜態方法可以被類或例項呼叫
MyClass.static_method()
obj.static_method()
# 類方法可以被類或例項呼叫,並且可以訪問類變數
MyClass.class_method()
obj.class_method()
在這個示例中,我們定義了一個 MyClass 類,並分別定義了靜態方法和類方法。在呼叫靜態方法時,我們可以使用類名或例項名進行呼叫,因為靜態方法與類或例項無關。而在呼叫類方法時,我們必須使用類名或例項名進行呼叫,並且類方法可以訪問類變數。總的來說,staticmethod 和 classmethod 是 Python 中兩個非常有用的裝飾器,它們可以幫助我們更好地組織和管理程式碼。需要根據實際情況來選擇使用哪種裝飾器,以便實現最佳的程式碼設計和可維護性。
同樣的道理我們可以實現自己的 ClassMethod
class ClassMethod:
"Emulate PyClassMethod_Type() in Objects/funcobject.c"
def __init__(self, f):
self.f = f
functools.update_wrapper(self, f)
def __get__(self, obj, cls=None):
if cls is None:
cls = type(obj)
return MethodType(self.f, cls)
我們對上面的程式碼進行測試:
class Myclass:
@ClassMethod
def demo(cls):
return "demo"
if __name__ == '__main__':
a = Myclass()
print(a.demo())
上面的程式碼可以正確的輸出字串"demo"
。
總結
在本篇文章當中主要給大家介紹了描述器的三個應用,仔細介紹了這三個類的使用方法,並且詳細介紹瞭如何使用 python 實現同樣的效果,這對於我們深入理解 python 物件導向程式設計非常有幫助,我們可以理解很多黑科技的內容,對於整個類的語法有更加深入的理解。
本篇文章是深入理解 python 虛擬機器系列文章之一,文章地址:https://github.com/Chang-LeHung/dive-into-cpython
更多精彩內容合集可訪問專案:https://github.com/Chang-LeHung/CSCore
關注公眾號:一無是處的研究僧,瞭解更多計算機(Java、Python、計算機系統基礎、演算法與資料結構)知識。