平常 Python 編程優雅之道


平常 Python 編程優雅之道


編譯自: https://opensource.com/article/18/4/elegant-solutions-everyday-python-problemshtml

做者: Nina Zakharenkopython

譯者: MjSevengit

3 個可使你的 Python 代碼更優雅、可讀、直觀和易於維護的工具。github

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

魔術方法

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

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

讓咱們建立一個 Money 類來示例:api

class Money:ssh

currency_rates = {ide

'$': 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__ 等等。閱讀 模擬數字類型 或 魔術方法指南 來得到更多信息。

咱們學習到 __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 中是 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(生成器) 來代替。

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 的文檔 以瞭解更多信息。使用生成器,咱們沒必要經過記住咱們的位置來手動維護狀態。生成器只知道兩件事:它如今須要作什麼以及計算下一個項目須要作什麼。一旦咱們到達執行點,即 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。這種編程方式容許咱們訪問實例變量和方法。例如:

>>> 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 對象 ,它的行爲相似 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 的庫中的一些示例代碼中,這個方法魔術是如何結合在一塊兒的,這是一個(名字起得很 low 的) REST API 客戶端,它具備透明的語法,容許你以最小的配置快速構建任何 REST API 原型(不只僅是 GitHub)。我發現這個項目頗有趣,由於它很是強大,但只有大約 400 行 Python 代碼。你能夠在大約 30 行配置代碼中添加對任何 REST API 的支持。agithub 知道協議所需的一切(REST、HTTP、TCP),但它不考慮上游 API。讓咱們深刻到它的實現中。

如下是咱們如何爲 GitHub API 和任何其餘相關鏈接屬性定義端點 URL 的簡化版本。在這裏查看 完整代碼 。

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)

而後,一旦配置了 訪問令牌 ,就能夠開始使用 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 類 的簡化示例:

class API:

# ... other methods ...

def __getattr__(self, key):

return IncompleteRequest(self.client).__getattr__(key)

__getitem__ = __getattr__

在 API 類上的每次調用都會調用 IncompleteRequest 類 做爲指定的 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, headers={}, **params):

return self.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

.... # {....}

這真心是一些方法魔術!

相關文章
相關標籤/搜索