3 個可以使你的 Python 程式碼更優雅、可讀、直觀和易於維護的工具
魔術方法
魔術方法可以看作是 Python 的管道。它們被稱為“底層”方法,用於某些內建的方法、符號和操作。你可能熟悉的常見魔術方法是__init__()
,當我們想要初始化一個類的新例項時,它會被呼叫。
你可能已經看過其他常見的魔術方法,如__str__
和__repr__
。Python 中有一整套魔術方法,通過實現其中的一些方法,我們可以修改一個物件的行為,甚至使其行為類似於內建資料型別,例如數字、列表或字典。
讓我們建立一個Money
類來示例:
classMoney: 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]) returnMoney(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__
這個魔術方法:
classMoney: # ... previously defined methods ... def __add__(self,other): """ Add 2 Money instances using '+' """ new_amount=self.amount+self.convert(other).amount returnMoney(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__
等等。閱讀模擬數字型別
或魔術方法指南
來獲得更多資訊。
我們學習到__add__
對映到內建運算子+
。其他魔術方法可以對映到像[]
這樣的符號。例如,在字典中通過索引或鍵來獲得一項,其實是使用了__getitem__
方法:
>>>d={'one':1,'two':2} >>>d['two'] 2 >>>d.__getitem__('two') 2
一些魔術方法甚至對映到內建函式,例如__len__()
對映到len()
。
classAlphabet: letters='ABCDEFGHIJKLMNOPQRSTUVWXYZ' def __len__(self): returnlen(self.letters) >>>my_alphabet=Alphabet() >>>len(my_alphabet) 26
自定義迭代器
對於新的和經驗豐富的 Python 開發者來說,自定義迭代器是一個非常強大的但令人迷惑的主題。
許多內建型別,例如列表、集合和字典,已經實現了允許它們在底層迭代的協議。這使我們可以輕鬆地遍歷它們。
>>>forfood in['Pizza','Fries']: print(food+'. Yum!') Pizza.Yum! Fries.Yum!
我們如何迭代我們自己的自定義類?首先,讓我們來澄清一些術語。
-
要成為一個可迭代物件,一個類需要實現
__iter__()
-
__iter__()
方法需要返回一個迭代器 -
要成為一個迭代器,一個類需要實現
__next__()
(或在 Python 2 中是next()
),當沒有更多的項要迭代時,必須丟擲一個StopIteration
異常。
呼!這聽起來很複雜,但是一旦你記住了這些基本概念,你就可以在任何時候進行迭代。
我們什麼時候想使用自定義迭代器?讓我們想象一個場景,我們有一個Server
例項在不同的埠上執行不同的服務,如http
和ssh
。其中一些服務處於active
狀態,而其他服務則處於inactive
狀態。
classServer: services=[ {'active':False,'protocol':'ftp','port':21}, {'active':True,'protocol':'ssh','port':22}, {'active':True,'protocol':'http','port':80}, ]
當我們遍歷Server
例項時,我們只想遍歷那些處於active
的服務。讓我們建立一個IterableServer
類:
classIterableServer: def __init__(self): self.current_pos=0 def __next__(self): pass# TODO: 實現並記得丟擲 StopIteration
首先,我們將當前位置初始化為0
。然後,我們定義一個__next__()
方法來返回下一項。我們還將確保在沒有更多項返回時丟擲StopIteration
。到目前為止都很好!現在,讓我們實現這個__next__()
方法。
classIterableServer: def__init__(self): self.current_pos=0.# 我們初始化當前位置為 0 def__iter__(self):# 我們可以在這裡返回 self,因為實現了 __next__ returnself def__next__(self): whileself.current_pos<len(self.services): service=self.services[self.current_pos] self.current_pos+=1 ifservice['active']: returnservice['protocol'],service['port'] raiseStopIteration next=__next__# 可選的 Python2 相容性
我們對列表中的服務進行遍歷,而當前的位置小於服務的個數,但只有在服務處於活動狀態時才返回。一旦我們遍歷完服務,就會丟擲一個StopIteration
異常。
因為我們實現了__next__()
方法,當它耗盡時,它會丟擲StopIteration
。我們可以從__iter__()
返回self
,因為IterableServer
類遵循iterable
協議。
現在我們可以遍歷一個IterableServer
例項,這將允許我們檢視每個處於活動的服務,如下所示:
>>>forprotocol,port inIterableServer(): print('service %s is running on port %d'%(protocol,port)) service ssh isrunning on port22 service http isrunning on port21
太棒了,但我們可以做得更好!在這樣類似的例項中,我們的迭代器不需要維護大量的狀態,我們可以簡化程式碼並使用generator(生成器) 來代替。
classServer: services=[ {'active':False,'protocol':'ftp','port':21}, {'active':True,'protocol':'ssh','port':22}, {'active':True,'protocol':'http','port':21}, ] def __iter__(self): forservice inself.services: ifservice['active']: yield service['protocol'],service['port']
yield
關鍵字到底是什麼?在定義生成器函式時使用 yield。這有點像return
,雖然return
在返回值後退出函式,但yield
會暫停執行直到下次呼叫它。這允許你的生成器的功能在它恢復之前保持狀態。檢視yield 的文件
以瞭解更多資訊。使用生成器,我們不必通過記住我們的位置來手動維護狀態。生成器只知道兩件事:它現在需要做什麼以及計算下一個專案需要做什麼。一旦我們到達執行點,即yield
不再被呼叫,我們就知道停止迭代。
這是因為一些內建的 Python 魔法。在
Python 關於__iter__()
的文件
中我們可以看到,如果__iter__()
是作為一個生成器實現的,它將自動返回一個迭代器物件,該物件提供__iter__()
和__next__()
方法。閱讀這篇很棒的文章,深入瞭解迭代器,可迭代物件和生成器
。
方法魔法
由於其獨特的方面,Python 提供了一些有趣的方法魔法作為語言的一部分。
其中一個例子是別名功能。因為函式只是物件,所以我們可以將它們賦值給多個變數。例如:
>>>def foo(): return'foo' >>>foo() 'foo' >>>bar=foo >>>bar() 'foo'
我們稍後會看到它的作用。
Python 提供了一個方便的內建函式
稱為getattr()
,它接受object, name, default
引數並在object
上返回屬性name
。這種程式設計方式允許我們訪問例項變數和方法。例如:
>>>classDog: sound='Bark' defspeak(self): print(self.sound+'!',self.sound+'!') >>>fido=Dog() >>>fido.sound 'Bark' >>>getattr(fido,'sound') 'Bark' >>>fido.speak <bound method Dog.speak of<__main__.Dog objectat0x102db8828>> >>>getattr(fido,'speak') <bound method Dog.speak of<__main__.Dog objectat0x102db8828>> >>>fido.speak() Bark!Bark! >>>speak_method=getattr(fido,'speak') >>>speak_method() Bark!Bark!
這是一個很酷的技巧,但是我們如何在實際中使用getattr
呢?讓我們看一個例子,我們編寫一個小型命令列工具來動態處理命令。
classOperations: 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 Thisoperation isnotsupported.
接下來,我們來看看partial
。例如,functool.partial(func, *args, **kwargs)
允許你返回一個新的partial 物件
,它的行為類似func
,引數是args
和kwargs
。如果傳入更多的args
,它們會被附加到args
。如果傳入更多的kwargs
,它們會擴充套件並覆蓋kwargs
。讓我們通過一個簡短的例子來看看:
>>>fromfunctoolsimportpartial >>>basetwo=partial(int,base=2) >>>basetwo <functools.partial objectat0x1085a09f0> >>>basetwo('10010') 18 # 這等同於 >>>int('10010',base=2)
讓我們看看在我喜歡的一個
名為agithub
的庫中的一些示例程式碼中,這個方法魔術是如何結合在一起的,這是一個(名字起得很 low 的) REST API 客戶端,它具有透明的語法,允許你以最小的配置快速構建任何 REST API 原型(不僅僅是 GitHub)。我發現這個專案很有趣,因為它非常強大,但只有大約 400 行 Python 程式碼。你可以在大約 30 行配置程式碼中新增對任何 REST API 的支援。agithub
知道協議所需的一切(REST
、HTTP
、TCP
),但它不考慮上游 API。讓我們深入到它的實現中。
以下是我們如何為 GitHub API 和任何其他相關連線屬性定義端點 URL 的簡化版本。在這裡檢視完整程式碼 。
classGitHub(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)
然後,一旦配置了訪問令牌 ,就可以開始使用GitHub API 。
>>>gh=GitHub('token') >>>status,data=gh.user.repos.get(visibility='public',sort='created') >>># ^ 對映到 GET /user/repos >>>data ...['tweeter','snipey','...']
請注意,你要確保 URL 拼寫正確,因為我們沒有驗證 URL。如果 URL 不存在或出現了其他任何錯誤,將返回 API 丟擲的錯誤。那麼,這一切是如何運作的呢?讓我們找出答案。首先,我們將檢視一個
API
類
的簡化示例:
classAPI: # ... other methods ... def __getattr__(self,key): returnIncompleteRequest(self.client).__getattr__(key) __getitem__=__getattr__
在API
類上的每次呼叫都會呼叫
IncompleteRequest
類
作為指定的key
。
classIncompleteRequest: # ... other methods ... def __getattr__(self,key): ifkey inself.client.http_methods: htmlMethod=getattr(self.client,key) returnpartial(htmlMethod,url=self.url) else: self.url+='/'+str(key) returnself __getitem__=__getattr__ classClient: http_methods=('get')# 還有 post, put, patch 等等。 def get(self,url,headers={},**params): returnself.request('GET',url,None,headers)
如果最後一次呼叫不是 HTTP 方法(如get
、post
等),則返回帶有附加路徑的IncompleteRequest
。否則,它從
Client
類
獲取 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 上閱讀有關裝飾器、上下文管理器、上下文生成器和命名元組的內容。隨著你成為一名更好的 Python 開發人員,我鼓勵你到那裡閱讀一些設計良好的專案的原始碼。Requests 和Flask 是兩個很好的起步的程式碼庫。