1. 程式人生 > 程式設計 >Python 從attribute到property詳解

Python 從attribute到property詳解

字面意思上的區別

Attribute與property,都可翻譯成屬性. 雖然無論是在中文中還是英文中 它們的意思都幾乎一樣,但仍有些許差別. Google了好幾下,找到了一個看起來比較靠譜的解釋:

According to Webster,a property is a characteristic that belongs to a thing's essential nature and may be used to describe a type or species.

An attribute is a modifier word that serves to limit,identify,particularize,describe,or supplement the meaning of the word it modifies.

簡單來說,property是類的本質屬性,可用於定義和描述一個類別或物種; attribute則是用於詳細說明它所描述的物體,是物體的具體屬性.

例如: 人都有嘴巴. 有的人嘴巴很大,嘴巴是人的property之一,而大嘴巴只能說是部分人的attribute.

從這個意義上講,property是attribute的子集.

Python裡的attribute與property

回到Python.

Attribute與property在Java中不作區分,但在Python中有所不同. 下面是Fluent Python(Chapter 19)給出的(非正式)定義:

Python 從attribute到property詳解

接下來分別解釋.

attribute

所有的資料屬性(data attribute)與方法(method)都是attribute. 根據attribute的所有者,可分為class attribute與instance attribute. class或instance的所有attribute都儲存在各自的__dict__屬性中.

例如:

# Python3
class Foo():
 name = 'Foo class attribute'
 def fn(self):
  pass
print('class attribute:',Foo.__dict__)
print()
foo = Foo()
foo.name = 'foo instance attribute'
print('instance attribute:',foo.__dict__)

輸出:

class attribute: {'fn': <function Foo.fn at 0x7fd135ec8ea0>,...,'name': 'Foo class attribute'}

instance attribute: {'name': 'foo instance attribute'}

property

property是出於安全考慮用setter/getter方法替代data attribute,例如,只讀屬性與屬性值合法性驗證.

只讀屬性

例如:

class Foo():
 def __init__(self,name):
  self.name = name

foo = Foo('I do not want to be changed')
print('foo.name = ',foo.name)
foo.name = 'Unluckily,I can be changed'
print('foo.name = ',foo.name)

輸出:

foo.name = I do not want to be changed
foo.name = Unluckily,I can be changed

在上面的程式碼中,假如我們只想將foo的name屬性暴露給外部讀取,但並不想它被修改,我們該怎麼辦? 之前在Python 定義只讀屬性中列出了兩種解決方案. 第一種方案:”通過私有屬性”,其實就是用property替代attribute.

將上面的foo.name改寫成property:

class Foo():
 def __init__(self,name):
  self.__name = name

 @property
 def name(self):
  return self.__name

foo = Foo('I do not want to be changed')
print('foo.name = ',foo.name)
foo.name = 'Luckily,I really can not be changed'

輸出:

foo.name = I do not want to be changed

---------------------------------------------------------------------------
AttributeError       Traceback (most recent call last)
<ipython-input-69-101c96ba497e> in <module>()
  9 foo = Foo('I do not want to be changed')
  10 print('foo.name = ',foo.name)
---> 11 foo.name = 'Luckily,I really can not be changed'

AttributeError: can't set attribute

有兩點需要注意:

foo.name確實已經不能通過foo.name = ...來修改了,即,foo.name已經是隻讀屬性.

將foo.name從attribute變成property之後,它的訪問方式並沒有改變. 也就是說,對外介面沒有改變. 這個優點可以讓我們從容的寫程式碼,不用在一開始就糾結於是使用property還是attribute,因為可以都使用attribute,如果有需要,以後可以在不影響外部程式碼的前提下隨時修改. 而在Java裡要做到這一點很難(如果可以做到的話).

屬性值合法性驗證

在上面的例子中,foo.name只有getter方法,是隻讀的,但其實property也是可修改的,只需要為它新增一個setter方法就行了. 那麼問題就來了,如果property也是可讀可改,那為何要費事將attribute改寫成property呢?

想象一個簡單的購物相關的業務場景. 一個Item代表使用者購買的一樣東西,主要有類別,價格和數量屬性:

class Item():
 def __init__(self,category,count,price):
  self.cat = category
  self.count = count
  self.price = price

正常的呼叫是類似於這樣的,價格與數量都是正數:

item = Item('Bread',1,10)

可是,若價格或數量設定為負數也不會報錯:

item.price = -10
item.count = -1
invalid_item1 = Item('Bread',-1,10)
invalid_item2 = Item('Bread',-10)

從語法上看,這些語句都是合法的,但從業務上看,它們都是不合法的. 那麼,怎樣才能防止這種非法賦值呢? 一種解決方案是按照Java風格,實現一個Java式的setter方法,通過item.set_price(price)設定price屬性,然後在set_price方法裡寫驗證程式碼. 這樣是可行的,但不夠Pythonic. Python的風格是讀與寫都通過屬性名進行:

print(item.price)
item.price = -10

這樣做的好處之前提到過: 將attribute改寫成property時不會改變對外介面. 那麼,如何在執行item.price = -10時檢驗-10的合法性呢? 最直白的方法是在__setattr__方法裡設定攔截,但很麻煩,特別是當需要驗證的屬性很多時.(不信的話可以參照Python 定義只讀屬性的方案二試試).

Python提供的最佳方案是通過property的setter方法:

class Item():
 def __init__(self,price):
  self.__cat = category # attribute
  self.count = count # property
  self.price = price # property

 @property
 def cat(self):
  return self.__cat

 @property
 def count(self):
  return self.__dict__['count']
 @count.setter
 def count(self,value):
  if value < 0:
   raise ValueError('count can not be minus: %r'%(value))
  self.__dict__['count'] = value

 @property
 def price(self):
  return self.__dict__['price']

 @price.setter
 def price(self,value):
  if value < 0:
   raise ValueError('price can not be minus: %r'%(value))
  self.__dict__['price'] = value

之前合法的語句現在仍然可以正常執行:

item = Item('Bread',10)
item.price = 20
item.count = 2
print(item.price)

但下面的語句執行時便會報錯了:

item = Item('Bread',-10)
# or
item.price = -10

會報出同一個錯誤:

---------------------------------------------------------------------------
ValueError        Traceback (most recent call last)
<ipython-input-93-4fcbd1284b2d> in <module>()
----> 1 item.price = -10

<ipython-input-91-7546240b5469> in price(self,value)
  27  def price(self,value):
  28   if value < 0:
---> 29    raise ValueError('price can not be minus: %r'%(value))
  30   self.__dict__['price'] = value

ValueError: price can not be minus: -10

定義property的其他方式

@property中的property雖可被當作修飾器來使用,但它其實是一個class(具體API請參考文件),所以上面的程式碼還可以改寫為:

class Item():
 def __init__(self,price):
  self.__cat = category # attribute
  self.count = count # property
  self.price = price # property

 def get_cat(self):
  return self.__cat

 def get_count(self):
  return self.__dict__['count']

 def set_count(self,value):
  if value < 0:
   raise ValueError('count can not be minus: %r'%(value))
  self.__dict__['count'] = value

 def get_price(self):
  return self.__dict__['price']

 def set_price(self,value):
  if value < 0:
   raise ValueError('price can not be minus: %r'%(value))
  self.__dict__['price'] = value
 bill = property(get_bill)
 cat = property(get_cat)
 count = property(get_count,set_count)
 price = property(get_price,set_price) 

功能上達到要求了,可程式碼本身看起來很冗長,比Java中的getter/setter風格還要長. 這時可以通過property factory來簡化程式碼:

先定義可共用的property factory函式:

def readonly_prop(storage_name):
 def getter(instance):
  return instance.__dict__[storage_name]
 return property(getter)
def positive_mutable_prop(storage_name):
 def getter(instance):
  return instance.__dict__[storage_name]
 def setter(instance,value):
  if value < 0:
   raise ValueError('%s can not be minus: %r'%(storage_name,value))
  instance.__dict__[storage_name] = value
 return property(getter,setter)

然後,之前的示例程式碼可以簡化為:

class Item():
 def __init__(self,price):
  self.__cat = category # attribute
  self.count = count # property
  self.price = price # property

 cat = readonly_prop('__cat')
 count = positive_mutable_prop('count')
 price = positive_mutable_prop('price')

這樣一來,在保證程式碼簡潔的前提下實現了訪問控制和合法性驗證.

property不會被instance attribute覆蓋

之前在Python物件的屬性訪問過程一文中展示了attribute的解析過程,從中知道class attribute可以被instance attribute覆蓋:

class Foo():
 name = 'Foo'

foo = Foo()
foo.name = 'foo'
codes = ['Foo.name','foo.name']
for code in codes:
 print(code,'=',eval(code))

輸出為:

Foo.name = Foo
foo.name = foo

但在property身上不會發生這種事情:

class Foo():
 @property
 def name(self):
  return 'Foo'

foo = Foo()
foo.__dict__['name'] = 'foo'# 已經不能通過foo.name賦值了
codes = ['Foo.name',eval(code))

輸出:

Foo.name = <property object at 0x7fd135e7ecc8>
foo.name = Foo

至少可以看出兩點:

1. 通過class Foo訪問Foo.name得到的是property物件,而非property值.

2. 訪問 foo.name時返回的是Foo.name的property值. 究其原因,是因為在屬性解析過程中,property的優先順序是最高的.

總結

1.Python的attribute與property不同:

attribute: data attribute + method

property: replace attribute with access control methods like getter/setter,for security reasons.

2.可以通過多種方式定義property:

@property

property(getter,setter)

property factory

3.property在屬性解析時的優先順序最高,不會被instance attribute覆蓋.

以上這篇Python 從attribute到property詳解就是小編分享給大家的全部內容了,希望能給大家一個參考,也希望大家多多支援我們。