日常 Python 編程優雅之道之Python方法魔法

Python 提供了一組獨特的工具和語言特性來使你的代碼更加優雅、可讀和直觀。為正確的問題選擇合適的工具,你的代碼將更易於維護。在本文中,我們將研究其中的三個工具:魔術方法、迭代器和生成器,以及方法魔術。

關注頭條號,私信回覆資料會有意外驚喜呦………………最後一張照片有資料呦。

日常 Python 編程優雅之道之Python方法魔法

魔術方法

魔術方法可以看作是 Python 的管道。它們被稱為“底層”方法,用於某些內置的方法、符號和操作。你可能熟悉的常見魔術方法是 __init__(),當我們想要初始化一個類的新實例時,它會被調用。

你可能已經看過其他常見的魔術方法,如 __str__ 和 __repr__。Python 中有一整套魔術方法,通過實現其中的一些方法,我們可以修改一個對象的行為,甚至使其行為類似於內置數據類型,例如數字、列表或字典。

讓我們創建一個 Money 類來示例:

  1. class Money:
  2. currency_rates = {
  3. '$': 1,
  4. '€': 0.88,
  5. }
  6. def __init__(self, symbol, amount):
  7. self.symbol = symbol
  8. self.amount = amount
  9. def __repr__(self):
  10. return '%s%.2f' % (self.symbol, self.amount)
  11. def convert(self, other):
  12. """ Convert other amount to our currency """
  13. new_amount = (
  14. other.amount / self.currency_rates[other.symbol]
  15. * self.currency_rates[self.symbol])
  16. return Money(self.symbol, new_amount)

該類定義為給定的貨幣符號和匯率定義了一個貨幣匯率,指定了一個初始化器(也稱為構造函數),並實現 __repr__,因此當我們打印這個類時,我們會看到一個友好的表示,例如 $2.00 ,這是一個帶有貨幣符號和金額的 Money('$', 2.00) 實例。最重要的是,它定義了一種方法,允許你使用不同的匯率在不同的貨幣之間進行轉換。

打開 Python shell,假設我們已經定義了使用兩種不同貨幣的食品的成本,如下所示:

  1. >>> soda_cost = Money('$', 5.25)
  2. >>> soda_cost
  3. $5.25
  4. >>> pizza_cost = Money('€', 7.99)
  5. >>> pizza_cost
  6. €7.99

我們可以使用魔術方法使得這個類的實例之間可以相互交互。假設我們希望能夠將這個類的兩個實例一起加在一起,即使它們是不同的貨幣。為了實現這一點,我們可以在 Money 類上實現 __add__ 這個魔術方法:

  1. class Money:
  2. # ... previously defined methods ...
  3. def __add__(self, other):
  4. """ Add 2 Money instances using '+' """
  5. new_amount = self.amount + self.convert(other).amount
  6. return Money(self.symbol, new_amount)

現在我們可以以非常直觀的方式使用這個類:

  1. >>> soda_cost = Money('$', 5.25)
  2. >>> pizza_cost = Money('€', 7.99)
  3. >>> soda_cost + pizza_cost
  4. $14.33
  5. >>> pizza_cost + soda_cost
  6. €12.61

當我們將兩個實例加在一起時,我們得到以第一個定義的貨幣符號所表示的結果。所有的轉換都是在底層無縫完成的。如果我們想的話,我們也可以為減法實現 __sub__,為乘法實現 __mul__ 等等。閱讀模擬數字類型[1]或魔術方法指南[2]來獲得更多信息。

我們學習到 __add__ 映射到內置運算符 +。其他魔術方法可以映射到像 [] 這樣的符號。例如,在字典中通過索引或鍵來獲得一項,其實是使用了 __getitem__ 方法:

  1. >>> d = {'one': 1, 'two': 2}
  2. >>> d['two']
  3. 2
  4. >>> d.__getitem__('two')
  5. 2

一些魔術方法甚至映射到內置函數,例如 __len__() 映射到 len()。

  1. class Alphabet:
  2. letters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'
  3. def __len__(self):
  4. return len(self.letters)
  5. >>> my_alphabet = Alphabet()
  6. >>> len(my_alphabet)
  7. 26
日常 Python 編程優雅之道之Python方法魔法


自定義迭代器

對於新的和經驗豐富的 Python 開發者來說,自定義迭代器是一個非常強大的但令人迷惑的主題。

許多內置類型,例如列表、集合和字典,已經實現了允許它們在底層迭代的協議。這使我們可以輕鬆地遍歷它們。

  1. >>> for food in ['Pizza', 'Fries']:
  2. print(food + '. Yum!')
  3. Pizza. Yum!
  4. Fries. Yum!

我們如何迭代我們自己的自定義類?首先,讓我們來澄清一些術語。

◈ 要成為一個可迭代對象,一個類需要實現 __iter__()

◈ __iter__() 方法需要返回一個迭代器

◈ 要成為一個迭代器,一個類需要實現 __next__()(或在 Python 2[3]中是 next()),當沒有更多的項要迭代時,必須拋出一個 StopIteration 異常。

呼!這聽起來很複雜,但是一旦你記住了這些基本概念,你就可以在任何時候進行迭代。

我們什麼時候想使用自定義迭代器?讓我們想象一個場景,我們有一個 Server 實例在不同的端口上運行不同的服務,如 http 和 ssh。其中一些服務處於 active 狀態,而其他服務則處於 inactive 狀態。

  1. class Server:
  2. services = [
  3. {'active': False, 'protocol': 'ftp', 'port': 21},
  4. {'active': True, 'protocol': 'ssh', 'port': 22},
  5. {'active': True, 'protocol': 'http', 'port': 80},
  6. ]

當我們遍歷 Server 實例時,我們只想遍歷那些處於 active 的服務。讓我們創建一個 IterableServer 類:

  1. class IterableServer:
  2. def __init__(self):
  3. self.current_pos = 0
  4. def __next__(self):
  5. pass # TODO: 實現並記得拋出 StopIteration

首先,我們將當前位置初始化為 0。然後,我們定義一個 __next__() 方法來返回下一項。我們還將確保在沒有更多項返回時拋出 StopIteration。到目前為止都很好!現在,讓我們實現這個 __next__() 方法。

  1. class IterableServer:
  2. def __init__(self):
  3. self.current_pos = 0. # 我們初始化當前位置為 0
  4. def __iter__(self): # 我們可以在這裡返回 self,因為實現了 __next__
  5. return self
  6. def __next__(self):
  7. while self.current_pos < len(self.services):
  8. service = self.services[self.current_pos]
  9. self.current_pos += 1
  10. if service['active']:
  11. return service['protocol'], service['port']
  12. raise StopIteration
  13. next = __next__ # 可選的 Python2 兼容性

我們對列表中的服務進行遍歷,而當前的位置小於服務的個數,但只有在服務處於活動狀態時才返回。一旦我們遍歷完服務,就會拋出一個 StopIteration 異常。

因為我們實現了 __next__() 方法,當它耗盡時,它會拋出 StopIteration。我們可以從 __iter__() 返回 self,因為 IterableServer 類遵循 iterable 協議。

日常 Python 編程優雅之道之Python方法魔法

現在我們可以遍歷一個 IterableServer 實例,這將允許我們查看每個處於活動的服務,如下所示:

  1. >>> for protocol, port in IterableServer():
  2. print('service %s is running on port %d' % (protocol, port))
  3. service ssh is running on port 22
  4. service http is running on port 21

太棒了,但我們可以做得更好!在這樣類似的實例中,我們的迭代器不需要維護大量的狀態,我們可以簡化代碼並使用 generator(生成器)[4] 來代替。

  1. class Server:
  2. services = [
  3. {'active': False, 'protocol': 'ftp', 'port': 21},
  4. {'active': True, 'protocol': 'ssh', 'port': 22},
  5. {'active': True, 'protocol': 'http', 'port': 21},
  6. ]
  7. def __iter__(self):
  8. for service in self.services:
  9. if service['active']:
  10. yield service['protocol'], service['port']

yield 關鍵字到底是什麼?在定義生成器函數時使用 yield。這有點像 return,雖然 return 在返回值後退出函數,但 yield 會暫停執行直到下次調用它。這允許你的生成器的功能在它恢復之前保持狀態。查看 yield 的文檔[5]以瞭解更多信息。使用生成器,我們不必通過記住我們的位置來手動維護狀態。生成器只知道兩件事:它現在需要做什麼以及計算下一個項目需要做什麼。一旦我們到達執行點,即 yield 不再被調用,我們就知道停止迭代。

這是因為一些內置的 Python 魔法。在 Python 關於 __iter__() 的文檔[6]中我們可以看到,如果 __iter__() 是作為一個生成器實現的,它將自動返回一個迭代器對象,該對象提供 __iter__() 和 __next__() 方法。閱讀這篇很棒的文章,深入瞭解迭代器,可迭代對象和生成器[7]。

方法魔法

由於其獨特的方面,Python 提供了一些有趣的方法魔法作為語言的一部分。

其中一個例子是別名功能。因為函數只是對象,所以我們可以將它們賦值給多個變量。例如:

  1. >>> def foo():
  2. return 'foo'
  3. >>> foo()
  4. 'foo'
  5. >>> bar = foo
  6. >>> bar()
  7. 'foo'

我們稍後會看到它的作用。

Python 提供了一個方便的內置函數稱為 getattr()[8],它接受 object, name, default 參數並在 object 上返回屬性 name。這種編程方式允許我們訪問實例變量和方法。例如:

  1. >>> class Dog:
  2. sound = 'Bark'
  3. def speak(self):
  4. print(self.sound + '!', self.sound + '!')
  5. >>> fido = Dog()
  6. >>> fido.sound
  7. 'Bark'
  8. >>> getattr(fido, 'sound')
  9. 'Bark'
  10. >>> fido.speak
  11. >
  12. >>> getattr(fido, 'speak')
  13. >
  14. >>> fido.speak()
  15. Bark! Bark!
  16. >>> speak_method = getattr(fido, 'speak')
  17. >>> speak_method()
  18. Bark! Bark!

這是一個很酷的技巧,但是我們如何在實際中使用 getattr 呢?讓我們看一個例子,我們編寫一個小型命令行工具來動態處理命令。

  1. class Operations:
  2. def say_hi(self, name):
  3. print('Hello,', name)
  4. def say_bye(self, name):
  5. print ('Goodbye,', name)
  6. def default(self, arg):
  7. print ('This operation is not supported.')
  8. if __name__ == '__main__':
  9. operations = Operations()
  10. # 假設我們做了錯誤處理
  11. command, argument = input('> ').split()
  12. func_to_call = getattr(operations, command, operations.default)
  13. func_to_call(argument)

腳本的輸出是:

  1. $ python getattr.py
  2. > say_hi Nina
  3. Hello, Nina
  4. > blah blah
  5. This operation is not supported.

接下來,我們來看看 partial。例如,functool.partial(func, *args, **kwargs) 允許你返回一個新的 partial 對象[9],它的行為類似 func,參數是 args 和 kwargs。如果傳入更多的 args,它們會被附加到 args。如果傳入更多的 kwargs,它們會擴展並覆蓋 kwargs。讓我們通過一個簡短的例子來看看:

  1. >>> from functools import partial
  2. >>> basetwo = partial(int, base=2)
  3. >>> basetwo
  4. >>> basetwo('10010')
  5. 18
  6. # 這等同於
  7. >>> int('10010', base=2)

讓我們看看在我喜歡的一個名為 agithub[10] 的庫中的一些示例代碼中,這個方法魔術是如何結合在一起的,這是一個(名字起得很 low 的) REST API 客戶端,它具有透明的語法,允許你以最小的配置快速構建任何 REST API 原型(不僅僅是 GitHub)。我發現這個項目很有趣,因為它非常強大,但只有大約 400 行 Python 代碼。你可以在大約 30 行配置代碼中添加對任何 REST API 的支持。agithub 知道協議所需的一切(REST、HTTP、TCP),但它不考慮上游 API。讓我們深入到它的實現中。

以下是我們如何為 GitHub API 和任何其他相關連接屬性定義端點 URL 的簡化版本。在這裡查看完整代碼[11]。

  1. class GitHub(API):
  2. def __init__(self, token=None, *args, **kwargs):
  3. props = ConnectionProperties(api_url = kwargs.pop('api_url', 'api.github.com'))
  4. self.setClient(Client(*args, **kwargs))
  5. self.setConnectionProperties(props)

然後,一旦配置了訪問令牌[12],就可以開始使用 GitHub API[13]。

  1. >>> gh = GitHub('token')
  2. >>> status, data = gh.user.repos.get(visibility='public', sort='created')
  3. >>> # ^ 映射到 GET /user/repos
  4. >>> data
  5. ... ['tweeter', 'snipey', '...']

請注意,你要確保 URL 拼寫正確,因為我們沒有驗證 URL。如果 URL 不存在或出現了其他任何錯誤,將返回 API 拋出的錯誤。那麼,這一切是如何運作的呢?讓我們找出答案。首先,我們將查看一個 API 類[14]的簡化示例:

  1. class API:
  2. # ... other methods ...
  3. def __getattr__(self, key):
  4. return IncompleteRequest(self.client).__getattr__(key)
  5. __getitem__ = __getattr__

在 API 類上的每次調用都會調用 IncompleteRequest 類[15]作為指定的 key。

  1. class IncompleteRequest:
  2. # ... other methods ...
  3. def __getattr__(self, key):
  4. if key in self.client.http_methods:
  5. htmlMethod = getattr(self.client, key)
  6. return partial(htmlMethod, url=self.url)
  7. else:
  8. self.url += '/' + str(key)
  9. return self
  10. __getitem__ = __getattr__
  11. class Client:
  12. http_methods = ('get') # 還有 post, put, patch 等等。
  13. def get(self, url, headers={}, **params):
  14. return self.request('GET', url, None, headers)

如果最後一次調用不是 HTTP 方法(如 get、post 等),則返回帶有附加路徑的 IncompleteRequest。否則,它從Client 類[16]獲取 HTTP 方法對應的正確函數,並返回 partial。

如果我們給出一個不存在的路徑會發生什麼?

  1. >>> status, data = this.path.doesnt.exist.get()
  2. >>> status
  3. ... 404

因為 __getattr__ 別名為 __getitem__:

  1. >>> owner, repo = 'nnja', 'tweeter'
  2. >>> status, data = gh.repos[owner][repo].pulls.get()
  3. >>> # ^ Maps to GET /repos/nnja/tweeter/pulls
  4. >>> data
  5. .... # {....}

這真心是一些方法魔術!

日常 Python 編程優雅之道之Python方法魔法


分享到:


相關文章: