解密Python中的描述符

來源:互聯網
上載者:User

解密Python中的描述符

   這篇文章主要介紹瞭解密Python中的描述符(descriptor),本文詳細講解了描述符(descriptor)的作用、訪問描述符、對描述符賦值、刪除描述符等內容,需要的朋友可以參考下

  Python中包含了許多內建的語言特性,它們使得代碼簡潔且易於理解。這些特性包括列表/集合/字典推導式,屬性(property)、以及裝飾器(decorator)。對於大部分特性來說,這些“中級”的語言特性有著完善的文檔,並且易於學習。

  但是這裡有個例外,那就是描述符。至少對於我來說,描述符是Python語言核心中困擾我時間最長的一個特性。這裡有幾點原因如下:

  1.有關描述符的官方文檔相當難懂,而且沒有包含優秀的樣本告訴你為什麼需要編寫描述符(我得為Raymond Hettinger辯護一下,他寫的其他主題的Python文章和視頻對我的協助還是非常大的)

  2.編寫描述符的文法顯得有些怪異

  3.自訂描述符可能是Python中用的最少的特性,因此你很難在開源項目中找到優秀的樣本

  但是一旦你理解了之後,描述符的確還是有它的應用價值的。這篇文章告訴你描述符可以用來做什麼,以及為什麼應該引起你的注意。

  一句話概括:描述符就是可重用的屬性

  在這裡我要告訴你:從根本上講,描述符就是可以重複使用的屬性。也就是說,描述符可以讓你編寫這樣的代碼:

   代碼如下:

  f = Foo()

  b = f.bar

  f.bar = c

  del f.bar

  而在解譯器執行上述代碼時,當發現你試圖訪問屬性(b = f.bar)、對屬性賦值(f.bar = c)或者刪除一個執行個體變數的屬性(del f.bar)時,就會去調用自訂的方法。

  讓我們先來解釋一下為什麼把對函數的調用偽裝成對屬性的訪問是大有好處的。

  property——把函數調用偽裝成對屬性的訪問

  想象一下你正在編寫管理電影資訊的代碼。你最後寫好的Movie類可能看上去是這樣的:

   代碼如下:

  class Movie(object):

  def __init__(self, title, rating, runtime, budget, gross):

  self.title = title

  self.rating = rating

  self.runtime = runtime

  self.budget = budget

  self.gross = gross

  def profit(self):

  return self.gross - self.budget

  你開始在項目的其他地方使用這個類,但是之後你意識到:如果不小心給電影打了負分怎麼辦?你覺得這是錯誤的行為,希望Movie類可以阻止這個錯誤。 你首先想到的辦法是將Movie類修改為這樣:

   代碼如下:

  class Movie(object):

  def __init__(self, title, rating, runtime, budget, gross):

  self.title = title

  self.rating = rating

  self.runtime = runtime

  self.gross = gross

  if budget < 0:

  raise ValueError("Negative value not allowed: %s" % budget)

  self.budget = budget

  def profit(self):

  return self.gross - self.budget

  但這行不通。因為其他部分的代碼都是直接通過Movie.budget來賦值的——這個新修改的類只會在init方法中捕獲錯誤的資料,但對於已經存在的類執行個體就無能為力了。如果有人試著運行m.budget = -100,那麼誰也沒法阻止。作為一個Python程式員同時也是電影迷,你該怎麼辦?

  幸運的是,Python的property解決了這個問題。如果你從未見過property的用法,下面是一個樣本:

   代碼如下:

  class Movie(object):

  def __init__(self, title, rating, runtime, budget, gross):

  self._budget = None

  self.title = title

  self.rating = rating

  self.runtime = runtime

  self.gross = gross

  self.budget = budget

  @property

  def budget(self):

  return self._budget

  @budget.setter

  def budget(self, value):

  if value < 0:

  raise ValueError("Negative value not allowed: %s" % value)

  self._budget = value

  def profit(self):

  return self.gross - self.budget

  m = Movie('Casablanca', 97, 102, 964000, 1300000)

  print m.budget # calls m.budget(), returns result

  try:

  m.budget = -100 # calls budget.setter(-100), and raises ValueError

  except ValueError:

  print "Woops. Not allowed"

  964000

  Woops. Not allowed

  我們用@property裝飾器指定了一個getter方法,用@budget.setter裝飾器指定了一個setter方法。當我們這麼做時,每當有人試著訪問budget屬性,Python就會自動調用相應的getter/setter方法。比方說,當遇到m.budget = value這樣的代碼時就會自動調用budget.setter。

  花點時間來欣賞一下Python這麼做是多麼的優雅:如果沒有property,我們將不得不把所有的執行個體屬性隱藏起來,提供大量顯式的類似get_budget和set_budget方法。像這樣編寫類的話,使用起來就會不斷的去調用這些getter/setter方法,這看起來就像臃腫的Java代碼一樣。更糟的是,如果我們不採用這種編碼風格,直接對執行個體屬性進行訪問。那麼稍後就沒法以清晰的方式增加對非負數的條件檢查——我們不得不重新建立set_budget方法,然後搜尋整個工程中的原始碼,將m.budget = value這樣的代碼替換為m.set_budget(value)。太蛋疼了!!

  因此,property讓我們將自訂的代碼同變數的訪問/設定聯絡在了一起,同時為你的類保持一個簡單的訪問屬性的介面。幹得漂亮!

  property的不足

  對property來說,最大的缺點就是它們不能重複使用。舉個例子,假設你想為rating,runtime和gross這些欄位也添加非負檢查。下面是修改過的新類:

   代碼如下:

  class Movie(object):

  def __init__(self, title, rating, runtime, budget, gross):

  self._rating = None

  self._runtime = None

  self._budget = None

  self._gross = None

  self.title = title

  self.rating = rating

  self.runtime = runtime

  self.gross = gross

  self.budget = budget

  #nice

  @property

  def budget(self):

  return self._budget

  @budget.setter

  def budget(self, value):

  if value < 0:

  raise ValueError("Negative value not allowed: %s" % value)

  self._budget = value

  #ok

  @property

  def rating(self):

  return self._rating

  @rating.setter

  def rating(self, value):

  if value < 0:

  raise ValueError("Negative value not allowed: %s" % value)

  self._rating = value

  #uhh...

  @property

  def runtime(self):

  return self._runtime

  @runtime.setter

  def runtime(self, value):

  if value < 0:

  raise ValueError("Negative value not allowed: %s" % value)

  self._runtime = value

  #is this forever?

  @property

  def gross(self):

  return self._gross

  @gross.setter

  def gross(self, value):

  if value < 0:

  raise ValueError("Negative value not allowed: %s" % value)

  self._gross = value

  def profit(self):

  return self.gross - self.budget

  可以看到代碼增加了不少,但重複的邏輯也出現了不少。雖然property可以讓類從外部看起來介面整潔漂亮,但是卻做不到內部同樣整潔漂亮。

  描述符登場(最終的大殺器)

  這就是描述符所解決的問題。描述符是property的升級版,允許你為重複的property邏輯編寫單獨的類來處理。下面的樣本展示了描述符是如何工作的(現在還不必擔心NonNegative類的實現):

  代碼如下:

  from weakref import WeakKeyDictionary

  class NonNegative(object):

  """A descriptor that forbids negative values"""

  def __init__(self, default):

  self.default = default

  self.data = WeakKeyDictionary()

  def __get__(self, instance, owner):

  # we get here when someone calls x.d, and d is a NonNegative instance

  # instance = x

  # owner = type(x)

  return self.data.get(instance, self.default)

  def __set__(self, instance, value):

  # we get here when someone calls x.d = val, and d is a NonNegative instance

  # instance = x

  # value = val

  if value < 0:

  raise ValueError("Negative value not allowed: %s" % value)

  self.data[instance] = value

  class Movie(object):

  #always put descriptors at the class-level

  rating = NonNegative(0)

  runtime = NonNegative(0)

  budget = NonNegative(0)

  gross = NonNegative(0)

  def __init__(self, title, rating, runtime, budget, gross):

  self.title = title

  self.rating = rating

  self.runtime = runtime

  self.budget = budget

  self.gross = gross

  def profit(self):

  return self.gross - self.budget

  m = Movie('Casablanca', 97, 102, 964000, 1300000)

  print m.budget # calls Movie.budget.__get__(m, Movie)

  m.rating = 100 # calls Movie.budget.__set__(m, 100)

  try:

  m.rating = -1 # calls Movie.budget.__set__(m, -100)

  except ValueError:

  print "Woops, negative value"

  964000

  Woops, negative value

  這裡引入了一些新的文法,我們一條條的來看:

  NonNegative是一個描述符對象,因為它定義了__get__,__set__或__delete__方法。

  Movie類現在看起來非常清晰。我們在類的層面上建立了4個描述符,把它們當做普通的執行個體屬性。顯然,描述符在這裡為我們做非負檢查。

  訪問描述符

  當解譯器遇到print m.buget時,它就會把budget當作一個帶有__get__ 方法的描述符,調用Movie.budget.__get__方法並將方法的傳回值列印出來,而不是直接傳遞m.budget來列印。這和你訪問一個property相似,Python自動調用一個方法,同時返回結果。

  __get__接收2個參數:一個是點號左邊的執行個體對象(在這裡,就是m.budget中的m),另一個是這個執行個體的類型(Movie)。在一些Python文檔中,Movie被稱作描述符的所有者(owner)。如果我們需要訪問Movie.budget,Python將會調用Movie.budget.__get__(None, Movie)。可以看到,第一個參數要麼是所有者的執行個體,要麼是None。這些輸入參數可能看起來很怪,但是這裡它們告訴了你描述符屬於哪個對象的一部分。當我們看到NonNegative類的實現時這一切就合情合理了。

  對描述符賦值

  當解譯器看到m.rating = 100時,Python識別出rating是一個帶有set方法的描述符,於是就調用Movie.rating.__set__(m, 100)。和__get__一樣,__set__的第一個參數是點號左邊的類執行個體(m.rating = 100中的m)。第二個參數是所賦的值(100)。

  刪除描述符

  為了說明的完整,這裡提一下刪除。如果你調用del m.budget,Python就會調用Movie.budget.__delete__(m)。

  NonNegative類是如何工作的?

  帶著前面的困惑,我們終於要揭示NonNegative類是如何工作的了。每個NonNegative的執行個體都維護著一個字典,其中儲存著所有者執行個體和對應資料的映射關係。當我們調用m.budget時,__get__方法會尋找與m相關聯的資料,並返回這個結果(如果這個值不存在,則會返回一個預設值)。__set__採用的方式相同,但是這裡會包含額外的非負檢查。我們使用WeakKeyDictionary來取代普通的字典以防止記憶體泄露——我們可不想僅僅因為它在描述符的字典中就讓一個無用
的執行個體一直存活著。

  使用描述符會有一點彆扭。因為它們作用於類的層次上,每一個類執行個體都共用同一個描述符。這就意味著對不同的執行個體對象而言,描述符不得不手動地管理不同的狀態,同時需要顯式的將類執行個體作為第一個參數準確傳遞給__get__、__set__以及__delete__方法。

  我希望這個例子解釋清楚了描述符可以用來做什麼——它們提供了一種方法將property的邏輯隔離到單獨的類中來處理。如果你發現自己正在不同的property之間重複著相同的邏輯,那麼本文也許會成為一個線索供你思考為何用描述符重構代碼是值得一試的。

  秘訣和陷阱

  把描述符放在類的層次上(class level)

  為了讓描述符能夠正常工作,它們必須定義在類的層次上。如果你不這麼做,那麼Python無法自動為你調用__get__和__set__方法。

   代碼如下:

  class Broken(object):

  y = NonNegative(5)

  def __init__(self):

  self.x = NonNegative(0) # NOT a good descriptor

  b = Broken()

  print "X is %s, Y is %s" % (b.x, b.y)

  X is <__main__.NonNegative object at 0x10432c250>, Y is 5

  可以看到,訪問類層次上的描述符y可以自動調用__get__。但是訪問執行個體層次上的描述符x只會返回描述符本身,真是魔法一般的存在啊。

  確保執行個體的資料只屬於執行個體本身

  你可能會像這樣編寫NonNegative描述符:

   代碼如下:

  class BrokenNonNegative(object):

  def __init__(self, default):

  self.value = default

  def __get__(self, instance, owner):

  return self.value

  def __set__(self, instance, value):

  if value < 0:

  raise ValueError("Negative value not allowed: %s" % value)

  self.value = value

  class Foo(object):

  bar = BrokenNonNegative(5)

  f = Foo()

  try:

  f.bar = -1

  except ValueError:

  print "Caught the invalid assignment"

  Caught the invalid assignment

  這麼做看起來似乎能正常工作。但這裡的問題就在於所有Foo的執行個體都共用相同的bar,這會產生一些令人痛苦的結果:

  代碼如下:

  class Foo(object):

  bar = BrokenNonNegative(5)

  f = Foo()

  g = Foo()

  print "f.bar is %s\ng.bar is %s" % (f.bar, g.bar)

  print "Setting f.bar to 10"

  f.bar = 10

  print "f.bar is %s\ng.bar is %s" % (f.bar, g.bar) #ouch

  f.bar is 5

  g.bar is 5

  Setting f.bar to 10

  f.bar is 10

  g.bar is 10

  這就是為什麼我們要在NonNegative中使用資料字典的原因。__get__和__set__的第一個參數告訴我們需要關心哪一個執行個體。NonNegative使用這個參數作為字典的key,為每一個Foo執行個體單獨儲存一份資料。

    代碼如下:

  class Foo(object):

  bar = NonNegative(5)

  f = Foo()

  g = Foo()

  print "f.bar is %s\ng.bar is %s" % (f.bar, g.bar)

  print "Setting f.bar to 10"

  f.bar = 10

  print "f.bar is %s\ng.bar is %s" % (f.bar, g.bar) #better

  f.bar is 5

  g.bar is 5

  Setting f.bar to 10

  f.bar is 10

  g.bar is 5

  這就是描述符最令人感到彆扭的地方(坦白的說,我不理解為什麼Python不讓你在執行個體的層次上定義描述符,並且總是需要將實際的處理分發給get和set。這麼做行不通一定是有原因的)

  注意不可雜湊的描述符所有者

  NonNegative類使用了一個字典來單獨儲存專屬於執行個體的資料。這個一般來說是沒問題的,除非你用到了不可雜湊(unhashable)的對象:

   代碼如下:

  class MoProblems(list): #you can't use lists as dictionary keys

  x = NonNegative(5)

  m = MoProblems()

  print m.x # womp womp

  ---------------------------------------------------------------------------

  TypeError Traceback (most recent call last)

   in ()

  3

  4 m = MoProblems()

  ----> 5 print m.x # womp womp

   in __get__(self, instance, owner)

  9 # instance = x

  10 # owner = type(x)

  ---> 11 return self.data.get(instance, self.default)

  12

  13 def __set__(self, instance, value):

  TypeError: unhashable type: 'MoProblems'

  因為MoProblems的執行個體(list的子類)是不可雜湊的,因此它們不能為MoProblems.x用做資料字典的key。有一些方法可以規避這個問題,但是都不完美。最好的方法可能就是給你的描述符加標籤了。

   代碼如下:

  class Descriptor(object):

  def __init__(self, label):

  self.label = label

  def __get__(self, instance, owner):

  print '__get__', instance, owner

  return instance.__dict__.get(self.label)

  def __set__(self, instance, value):

  print '__set__'

  instance.__dict__[self.label] = value

  class Foo(list):

  x = Descriptor('x')

  y = Descriptor('y')

  f = Foo()

  f.x = 5

  print f.x

  __set__

  __get__ []

  5

  這種方法依賴於Python的方法解析順序(即,MRO)。我們給Foo中的每個描述符加上一個標籤名,名稱和我們賦值給描述符的變數名相同,比如x = Descriptor(‘x')。之後,描述符將特定於執行個體的資料儲存在f.__dict__['x']中。這個字典條目通常是當我們請求f.x時Python給出的傳回值。然而,由於Foo.x 是一個描述符,Python不能正常的使用f.__dict__[‘x'],但是描述符可以安全的在這裡儲存資料。只是要記住,不要在別的地方也給這個描述符添加標籤。

   代碼如下:

  class Foo(object):

  x = Descriptor('y')

  f = Foo()

  f.x = 5

  print f.x

  f.y = 4 #oh no!

  print f.x

  __set__

  __get__ <__main__.Foo object at 0x10432c810>

  5

  __get__ <__main__.Foo object at 0x10432c810>

  4

  我不喜歡這種方式,因為這樣的代碼很脆弱也有很多微妙之處。但這個方法的確很普遍,可以用在不可雜湊的所有者類上。David Beazley在他的書中用到了這個方法。

  在元類中使用帶標籤的描述符

  由於描述符的標籤名和賦給它的變數名相同,所以有人使用元類來自動處理這個簿記(bookkeeping)任務。

  代碼如下:

  class Descriptor(object):

  def __init__(self):

  #notice we aren't setting the label here

  self.label = None

  def __get__(self, instance, owner):

  print '__get__. Label = %s' % self.label

  return instance.__dict__.get(self.label, None)

  def __set__(self, instance, value):

  print '__set__'

  instance.__dict__[self.label] = value

  class DescriptorOwner(type):

  def __new__(cls, name, bases, attrs):

  # find all descriptors, auto-set their labels

  for n, v in attrs.items():

  if isinstance(v, Descriptor):

  v.label = n

  return super(DescriptorOwner, cls).__new__(cls, name, bases, attrs)

  class Foo(object):

  __metaclass__ = DescriptorOwner

  x = Descriptor()

  f = Foo()

  f.x = 10

  print f.x

  __set__

  __get__. Label = x

  10

  我不會去解釋有關元類的細節——參考文獻中David Beazley已經在他的文章中解釋的很清楚了。 需要指出的是元類自動的為描述符添加標籤,並且和賦給描述符的變數名字相匹配。

  儘管這樣解決了描述符的標籤和變數名不一致的問題,但是卻引入了複雜的元類。雖然我很懷疑,但是你可以自行判斷這麼做是否值得。

  訪問描述符的方法

  描述符僅僅是類,也許你想要為它們增加一些方法。舉個例子,描述符是一個用來回調property的很好的手段。比如我們想要一個類的某個部分的狀態發生變化時就立刻通知我們。下面的大部分代碼是用來做這個的:

   代碼如下:

  class CallbackProperty(object):

  """A property that will alert observers when upon updates"""

  def __init__(self, default=None):

  self.data = WeakKeyDictionary()

  self.default = default

  self.callbacks = WeakKeyDictionary()

  def __get__(self, instance, owner):

  return self.data.get(instance, self.default)

  def __set__(self, instance, value):

  for callback in self.callbacks.get(instance, []):

  # alert callback function of new value

  callback(value)

  self.data[instance] = value

  def add_callback(self, instance, callback):

  """Add a new function to call everytime the descriptor updates"""

  #but how do we get here?!?!

  if instance not in self.callbacks:

  self.callbacks[instance] = []

  self.callbacks[instance].append(callback)

  class BankAccount(object):

  balance = CallbackProperty(0)

  def low_balance_warning(value):

  if value < 100:

  print "You are poor"

  ba = BankAccount()

  # will not work -- try it

  #ba.balance.add_callback(ba, low_balance_warning)

  這是一個很有吸引力的模式——我們可以自訂回呼函數用來響應一個類中的狀態變化,而且完全無需修改這個類的代碼。這樣做可真是替人分憂解難呀。現在,我們所要做的就是調用ba.balance.add_callback(ba, low_balance_warning),以使得每次balance變化時low_balance_warning都會被調用。

  但是我們是如何做到的呢?當我們試圖訪問它們時,描述符總是會調用__get__。就好像add_callback方法是無法觸及的一樣!其實關鍵在於利用了一種特殊的情況,即,當從類的層次訪問時,__get__方法的第一個參數是None。

   代碼如下:

  class CallbackProperty(object):

  """A property that will alert observers when upon updates"""

  def __init__(self, default=None):

  self.data = WeakKeyDictionary()

  self.default = default

  self.callbacks = WeakKeyDictionary()

  def __get__(self, instance, owner):

  if instance is None:

  return self

  return self.data.get(instance, self.default)

  def __set__(self, instance, value):

  for callback in self.callbacks.get(instance, []):

  # alert callback function of new value

  callback(value)

  self.data[instance] = value

  def add_callback(self, instance, callback):

  """Add a new function to call everytime the descriptor within instance updates"""

  if instance not in self.callbacks:

  self.callbacks[instance] = []

  self.callbacks[instance].append(callback)

  class BankAccount(object):

  balance = CallbackProperty(0)

  def low_balance_warning(value):

  if value < 100:

  print "You are now poor"

  ba = BankAccount()

  BankAccount.balance.add_callback(ba, low_balance_warning)

  ba.balance = 5000

  print "Balance is %s" % ba.balance

  ba.balance = 99

  print "Balance is %s" % ba.balance

  Balance is 5000

  You are now poor

  Balance is 99

  結語

  希望你現在對描述符是什麼和它們的適用情境有了一個認識。前進吧騷年!

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.