https://opensource.com/article/18/4/elegant-solutions-everyday-python-problems
作者 | Nina Zakharenko
譯者 | MjSeven ? ? ? ? 共計翻譯:60 篇 貢獻時間:172 天
3 個可以使你的 Python 程式碼更優雅、可讀、直觀和易於維護的工具。
Python 提供了一組獨特的工具和語言特性來使你的程式碼更加優雅、可讀和直觀。為正確的問題選擇合適的工具,你的程式碼將更易於維護。在本文中,我們將研究其中的三個工具:魔術方法、迭代器和生成器,以及方法魔術。
魔術方法
魔術方法可以看作是 Python 的管道。它們被稱為“底層”方法,用於某些內建的方法、符號和操作。你可能熟悉的常見魔術方法是 __init__()
,當我們想要初始化一個類的新實體時,它會被呼叫。
你可能已經看過其他常見的魔術方法,如 __str__
和 __repr__
。Python 中有一整套魔術方法,透過實現其中的一些方法,我們可以修改一個物件的行為,甚至使其行為類似於內建資料型別,例如數字、串列或字典。
讓我們建立一個 Money
類來示例:
class Money:
currency_rates = {
'$': 1,
'€': 0.88,
}
def __init__(self, symbol, amount):
self.symbol = symbol
self.amount = amount
def __repr__(self):
return '%s%.2f' % (self.symbol, self.amount)
def convert(self, other):
""" Convert other amount to our currency """
new_amount = (
other.amount / self.currency_rates[other.symbol]
* self.currency_rates[self.symbol])
return Money(self.symbol, new_amount)
該類定義為給定的貨幣符號和匯率定義了一個貨幣匯率,指定了一個初始化器(也稱為建構式),並實現 __repr__
,因此當我們列印這個類時,我們會看到一個友好的表示,例如 $2.00
,這是一個帶有貨幣符號和金額的 Money('$', 2.00)
實體。最重要的是,它定義了一種方法,允許你使用不同的匯率在不同的貨幣之間進行轉換。
開啟 Python shell,假設我們已經定義了使用兩種不同貨幣的食品的成本,如下所示:
>>> soda_cost = Money('$', 5.25)
>>> soda_cost
$5.25
>>> pizza_cost = Money('€', 7.99)
>>> pizza_cost
€7.99
我們可以使用魔術方法使得這個類的實體之間可以相互互動。假設我們希望能夠將這個類的兩個實體一起加在一起,即使它們是不同的貨幣。為了實現這一點,我們可以在 Money
類上實現 __add__
這個魔術方法:
class Money:
# ... previously defined methods ...
def __add__(self, other):
""" Add 2 Money instances using '+' """
new_amount = self.amount + self.convert(other).amount
return Money(self.symbol, new_amount)
現在我們可以以非常直觀的方式使用這個類:
>>> soda_cost = Money('$', 5.25)
>>> pizza_cost = Money('€', 7.99)
>>> soda_cost + pizza_cost
$14.33
>>> pizza_cost + soda_cost
€12.61
當我們將兩個實體加在一起時,我們得到以第一個定義的貨幣符號所表示的結果。所有的轉換都是在底層無縫完成的。如果我們想的話,我們也可以為減法實現 __sub__
,為乘法實現 __mul__
等等。閱讀模擬數字型別[1]或魔術方法指南[2]來獲得更多資訊。
我們學習到 __add__
對映到內建運運算元 +
。其他魔術方法可以對映到像 []
這樣的符號。例如,在字典中透過索引或鍵來獲得一項,其實是使用了 __getitem__
方法:
>>> d = {'one': 1, 'two': 2}
>>> d['two']
2
>>> d.__getitem__('two')
2
一些魔術方法甚至對映到內建函式,例如 __len__()
對映到 len()
。
class Alphabet:
letters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'
def __len__(self):
return len(self.letters)
>>> my_alphabet = Alphabet()
>>> len(my_alphabet)
26
自定義迭代器
對於新的和經驗豐富的 Python 開發者來說,自定義迭代器是一個非常強大的但令人迷惑的主題。
許多內建型別,例如串列、集合和字典,已經實現了允許它們在底層迭代的協議。這使我們可以輕鬆地遍歷它們。
>>> for food in ['Pizza', 'Fries']:
print(food + '. Yum!')
Pizza. Yum!
Fries. Yum!
我們如何迭代我們自己的自定義類?首先,讓我們來澄清一些術語。
__iter__()
__iter__()
方法需要傳回一個迭代器__next__()
(或在 Python 2[3]中是 next()
),當沒有更多的項要迭代時,必須丟擲一個 StopIteration
異常。呼!這聽起來很複雜,但是一旦你記住了這些基本概念,你就可以在任何時候進行迭代。
我們什麼時候想使用自定義迭代器?讓我們想象一個場景,我們有一個 Server
實體在不同的埠上執行不同的服務,如 http
和 ssh
。其中一些服務處於 active
狀態,而其他服務則處於 inactive
狀態。
class Server:
services = [
{'active': False, 'protocol': 'ftp', 'port': 21},
{'active': True, 'protocol': 'ssh', 'port': 22},
{'active': True, 'protocol': 'http', 'port': 80},
]
當我們遍歷 Server
實體時,我們只想遍歷那些處於 active
的服務。讓我們建立一個 IterableServer
類:
class IterableServer:
def __init__(self):
self.current_pos = 0
def __next__(self):
pass # TODO: 實現並記得丟擲 StopIteration
首先,我們將當前位置初始化為 0
。然後,我們定義一個 __next__()
方法來傳回下一項。我們還將確保在沒有更多項傳回時丟擲 StopIteration
。到目前為止都很好!現在,讓我們實現這個 __next__()
方法。
class IterableServer:
def __init__(self):
self.current_pos = 0. # 我們初始化當前位置為 0
def __iter__(self): # 我們可以在這裡傳回 self,因為實現了 __next__
return self
def __next__(self):
while self.current_pos < len(self.services):
service = self.services[self.current_pos]
self.current_pos += 1
if service['active']:
return service['protocol'], service['port']
raise StopIteration
next = __next__ # 可選的 Python2 相容性
我們對串列中的服務進行遍歷,而當前的位置小於服務的個數,但只有在服務處於活動狀態時才傳回。一旦我們遍歷完服務,就會丟擲一個 StopIteration
異常。
因為我們實現了 __next__()
方法,當它耗盡時,它會丟擲 StopIteration
。我們可以從 __iter__()
傳回 self
,因為 IterableServer
類遵循 iterable
協議。
現在我們可以遍歷一個 IterableServer
實體,這將允許我們檢視每個處於活動的服務,如下所示:
>>> for protocol, port in IterableServer():
print('service %s is running on port %d' % (protocol, port))
service ssh is running on port 22
service http is running on port 21
太棒了,但我們可以做得更好!在這樣類似的實體中,我們的迭代器不需要維護大量的狀態,我們可以簡化程式碼並使用 generator(生成器)[4] 來代替。
class Server:
services = [
{'active': False, 'protocol': 'ftp', 'port': 21},
{'active': True, 'protocol': 'ssh', 'port': 22},
{'active': True, 'protocol': 'http', 'port': 21},
]
def __iter__(self):
for service in self.services:
if service['active']:
yield service['protocol'], service['port']
yield
關鍵字到底是什麼?在定義生成器函式時使用 yield。這有點像 return
,雖然 return
在傳回值後退出函式,但 yield
會暫停執行直到下次呼叫它。這允許你的生成器的功能在它恢復之前保持狀態。檢視 yield 的檔案[5]以瞭解更多資訊。使用生成器,我們不必透過記住我們的位置來手動維護狀態。生成器只知道兩件事:它現在需要做什麼以及計算下一個專案需要做什麼。一旦我們到達執行點,即 yield
不再被呼叫,我們就知道停止迭代。
這是因為一些內建的 Python 魔法。在 Python 關於 __iter__()
的檔案[6]中我們可以看到,如果 __iter__()
是作為一個生成器實現的,它將自動傳回一個迭代器物件,該物件提供 __iter__()
和 __next__()
方法。閱讀這篇很棒的文章,深入瞭解迭代器,可迭代物件和生成器[7]。
方法魔法
由於其獨特的方面,Python 提供了一些有趣的方法魔法作為語言的一部分。
其中一個例子是別名功能。因為函式只是物件,所以我們可以將它們賦值給多個變數。例如:
>>> def foo():
return 'foo'
>>> foo()
'foo'
>>> bar = foo
>>> bar()
'foo'
我們稍後會看到它的作用。
Python 提供了一個方便的內建函式稱為 getattr()
[8],它接受 object, name, default
引數併在 object
上傳回屬性 name
。這種程式設計方式允許我們訪問實體變數和方法。例如:
>>> class Dog:
sound = 'Bark'
def speak(self):
print(self.sound + '!', self.sound + '!')
>>> fido = Dog()
>>> fido.sound
'Bark'
>>> getattr(fido, 'sound')
'Bark'
>>> fido.speak
<bound method Dog.speak of <__main__.Dog object at 0x102db8828>>
>>> getattr(fido, 'speak')
<bound method Dog.speak of <__main__.Dog object at 0x102db8828>>
>>> fido.speak()
Bark! Bark!
>>> speak_method = getattr(fido, 'speak')
>>> speak_method()
Bark! Bark!
這是一個很酷的技巧,但是我們如何在實際中使用 getattr
呢?讓我們看一個例子,我們編寫一個小型命令列工具來動態處理命令。
class Operations:
def say_hi(self, name):
print('Hello,', name)
def say_bye(self, name):
print ('Goodbye,', name)
def default(self, arg):
print ('This operation is not supported.')
if __name__ == '__main__':
operations = Operations()
# 假設我們做了錯誤處理
command, argument = input('> ').split()
func_to_call = getattr(operations, command, operations.default)
func_to_call(argument)
指令碼的輸出是:
$ python getattr.py
> say_hi Nina
Hello, Nina
> blah blah
This operation is not supported.
接下來,我們來看看 partial
。例如,functool.partial(func, *args, **kwargs)
允許你傳回一個新的 partial 物件[9],它的行為類似 func
,引數是 args
和 kwargs
。如果傳入更多的 args
,它們會被附加到 args
。如果傳入更多的 kwargs
,它們會擴充套件並改寫 kwargs
。讓我們透過一個簡短的例子來看看:
>>> from functools import partial
>>> basetwo = partial(int, base=2)
>>> basetwo
<functools.partial object at 0x1085a09f0>
>>> basetwo('10010')
18
# 這等同於
>>> 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]。
class GitHub(API):
def __init__(self, token=None, *args, **kwargs):
props = ConnectionProperties(api_url = kwargs.pop('api_url', 'api.github.com'))
self.setClient(Client(*args, **kwargs))
self.setConnectionProperties(props)
然後,一旦配置了訪問令牌[12],就可以開始使用 GitHub API[13]。
>>> gh = GitHub('token')
>>> status, data = gh.user.repos.get(visibility='public', sort='created')
>>> # ^ 對映到 GET /user/repos
>>> data
... ['tweeter', 'snipey', '...']
請註意,你要確保 URL 拼寫正確,因為我們沒有驗證 URL。如果 URL 不存在或出現了其他任何錯誤,將傳回 API 丟擲的錯誤。那麼,這一切是如何運作的呢?讓我們找出答案。首先,我們將檢視一個 API
類[14]的簡化示例:
class API:
# ... other methods ...
def __getattr__(self, key):
return IncompleteRequest(self.client).__getattr__(key)
__getitem__ = __getattr__
在 API
類上的每次呼叫都會呼叫 IncompleteRequest
類[15]作為指定的 key
。
class IncompleteRequest:
# ... other methods ...
def __getattr__(self, key):
if key in self.client.http_methods:
htmlMethod = getattr(self.client, key)
return partial(htmlMethod, url=self.url)
else:
self.url += '/' + str(key)
return self
__getitem__ = __getattr__
class Client:
http_methods = ('get') # 還有 post, put, patch 等等。
def get(self, url, essay-headers={}, **params):
return self.request('GET', url, None, essay-headers)
如果最後一次呼叫不是 HTTP 方法(如 get
、post
等),則傳回帶有附加路徑的 IncompleteRequest
。否則,它從Client
類[16]獲取 HTTP 方法對應的正確函式,並傳回 partial
。
如果我們給出一個不存在的路徑會發生什麼?
>>> status, data = this.path.doesnt.exist.get()
>>> status
... 404
因為 __getattr__
別名為 __getitem__
:
>>> owner, repo = 'nnja', 'tweeter'
>>> status, data = gh.repos[owner][repo].pulls.get()
>>> # ^ Maps to GET /repos/nnja/tweeter/pulls
>>> data
.... # {....}
這真心是一些方法魔術!
瞭解更多
Python 提供了大量工具,使你的程式碼更優雅,更易於閱讀和理解。挑戰在於找到合適的工具來完成工作,但我希望本文為你的工具箱添加了一些新工具。而且,如果你想更進一步,你可以在我的部落格 nnja.io[17] 上閱讀有關裝飾器、背景關係管理器、背景關係生成器和命名元組的內容。隨著你成為一名更好的 Python 開發人員,我鼓勵你到那裡閱讀一些設計良好的專案的原始碼。Requests[18] 和 Flask[19] 是兩個很好的起步的程式碼庫。
via: https://opensource.com/article/18/4/elegant-solutions-everyday-python-problems
作者:Nina Zakharenko[21] 選題:lujun9972 譯者:MjSeven 校對:wxy
本文由 LCTT 原創編譯,Linux中國 榮譽推出