平日寫Python代碼的過程當中,咱們會碰到各類各樣的問題。其實大部分問題歸結下來主要也就是那麼幾類,而且其中很多都是咱們會反覆遇到的。如何用Python優雅的解決這些問題呢?Nina Zakharenko在PyCon2018上的演講《Elegant Solutions For Everyday Python Problems》或許能給你一些啓發。html
Python裏面有個小彩蛋,是一首名爲《Python之禪》的小詩,算是總結了什麼樣的代碼纔是優雅的:python
>>> import this The Zen of Python, by Tim Peters Beautiful is better than ugly. Explicit is better than implicit. Simple is better than complex. Complex is better than complicated. Flat is better than nested. Sparse is better than dense. Readability counts. Special cases aren't special enough to break the rules. Although practicality beats purity. Errors should never pass silently. Unless explicitly silenced. In the face of ambiguity, refuse the temptation to guess. There should be one-- and preferably only one --obvious way to do it. Although that way may not be obvious at first unless you're Dutch. Now is better than never. Although never is often better than *right* now. If the implementation is hard to explain, it's a bad idea. If the implementation is easy to explain, it may be a good idea. Namespaces are one honking great idea -- let's do more of those!
Python中magic method的名稱都以雙下劃線(double underscore,簡稱dunder)開頭和結尾,好比__getattr__,__getattribute__等等。經過實現這些magic method,咱們能夠賦予對象各類神奇而實用的功能。git
Python的標準庫裏自帶了不少實用的工具。像functools、itertools這些模塊,都提供了不少現成的輪子,足以解決咱們不少實際問題了,很是值得初學者們好好去了解、學習。若是你們平常工做中遇到了什麼問題想要現成的輪子,不妨先去標準庫裏看看。github
首先明確幾個容易混淆的概念:bash
假定咱們在一臺server上運行了多個service,想要只遍歷其中狀態爲active的那些。爲此咱們能夠實現下面的IterableServer類:app
class IterableServer: services = [ {'active': False, 'protocol': 'ftp', 'port': 21}, {'active': True, 'protocol': 'ssh', 'port': 22}, {'active': True, 'protocol': 'http', 'port': 80}, ] def __init__(self): self.current_pos = 0 def __iter__(self): 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
首先,IterableServer類提供了__iter__()方法,所以它是iterable的。其次,它還提供了__next__()方法,所以它也是iterator。實際上,iterator一般都會實現一個僅返回本身的iter()方法,但這不是必需的。當咱們用for對IterableServer的實例進行遍歷時,解釋器會對其調用iter()方法,進而調用其內部的__iter__()方法,獲得其返回的iterator(這裏就是實例本身);接着,解釋器對返回的iterator重複調用next()方法,進而調用其內部的__next__()方法。當全部的services都遍歷完後,拋出StopIteration。less
運行結果:ssh
>>> from iterable_server import IterableServer >>> for protocol, port in IterableServer(): ... print('service %s on port %d' % (protocol, port)) ... service ssh on port 22 service http on port 80
須要注意的是,上面代碼中IterableServer類同時充當了iterator和iterable兩種角色,雖然這樣能夠工做,但在實際中並不推薦這種作法。最主要的一點緣由,就是全部iterator共享了狀態,致使iterable實例無法反覆使用。能夠看到,當遍歷完成後,再次調用__iter__()返回的iterator仍是以前那個,其current_pos已經超出界限了,調用__next__()會直接拋出StopItertion。ide
若是咱們的iterable不須要維護太多的狀態信息,那麼generator可能會是更好的選擇。函數
class IterableServer: services = [ {'active': False, 'protocol': 'ftp', 'port': 21}, {'active': True, 'protocol': 'ssh', 'port': 22}, {'active': True, 'protocol': 'http', 'port': 80}, ] def __iter__(self): for service in self.services: if service['active']: yield service['protocol'], service['port']
運行結果:
>>> for protocol, port in IterableServer(): ... print('service %s on port %d' % (protocol, port)) ... service ssh on port 22 service http on port 80
上面代碼中,咱們去掉了__next__()和current_pos,並將__iter__()改爲了帶yield的函數。遍歷的時候,解釋器對實例調用iter()方法時,返回的是一個generator,接着再對generator重複調用next()。每次調用next(),都會執行__iter__()方法內的代碼,返回一個tuple,而後掛起,直到下一次調用next()時再從掛起的地方(yield那句)恢復執行。
Python內置的functools模塊中提供了不少實用的工具,好比partial、lru_cache、total_ordering、wraps等等。
假定咱們想將二進制字符串轉換成十進制整數,可使用內置的int(),只需加上可選參數base=2就好了。但是每次轉換的時候都要填這個參數,顯得很麻煩,這個時候就可使用partial將base參數固定:
>>> import functools >>> int('10010', base=2) 18 >>> from functools import partial >>> basetwo = partial(int, base=2) >>> basetwo functools.partial(<class 'int'>, base=2) >>> basetwo('10010') 18
partial實際上至關於作了下面這些事情:
def partial(f, *args, **kwargs): def newfunc(*fargs, **fkwargs): newkwargs = kwargs.copy() newkwargs.update(fkwargs) return f(*(args + fargs), **newkwargs) newfunc.func = newfunc newfunc.args = args newfunc.kwargs = kwargs return newfunc
agihub是一個開源的Github REST API客戶端,用法很是簡單直觀。好比發送GET請求到/issues?filter=subscribed:
>>> from agithub.GitHub import GitHub >>> g = GitHub('user', 'pass') >>> status, data = g.issues.get(filter='subscribed') >>> data [ list, of, issues ]
上面的g.issues.get(filter='subscribed'),對應的語法就是<API-object>.<URL-path>.<request-method>(<GET-parameters>)。整個過程當中構建URL和發送請求都是動態的,很是方便。那麼這是如何實現的呢?真相在源碼中:
class API(object): ... def __getattr__(self, key): return IncompleteRequest(self.client).__getattr__(key) __getitem__ = __getattr__ ... class IncompleteRequest(object): ... def __getattr__(self, key): if key in self.client.http_methods: htmlMethod = getattr(self.client, key) wrapper = partial(htmlMethod, url=self.url) return update_wrapper(wrapper, htmlMethod) else: self.url += '/' + str(key) return self __getitem__ = __getattr__ ...
最核心的兩個類就是API(Github的父類)和IncompleteRequest。API中定義了__getattr__()方法,當咱們訪問g.issues時,因爲在g上找不到issues這個屬性,便會調用__getattr__(),返回一個IncompleteRequest的實例。接着當咱們在IncompleteRequest實例上調用get()方法(g.issues.get())時,代碼作了一次判斷:若是是get、post這類http方法的名稱時,返回一個wrapper用於直接發送請求;不然,將self.url屬性更新,返回實例本身。這樣就實現了動態構建URL和發送請求。
若是你想在進行某些操做以前和以後都能作一些額外的工做,Context Manager(上下文管理器)是很是合適的工具。一個典型的場景就是文件的讀寫,咱們首先須要打開文件,而後才能進行讀寫操做,最後還須要把它關掉。
演講中Nina給了Feature Flags的例子,利用Context Manager來管理Feature Flags,能夠應用在A/B Testing、Rolling Releases等場景:
class FeatureFlags: SHOW_BETA = 'Show Beta version of Home Page' flags = { SHOW_BETA: True } @classmethod def is_on(cls, name): return cls.flags[name] @classmethod def toggle(cls, name, value): cls.flags[name] = value feature_flags = FeatureFlags() class feature_flag: """ Implementing a Context Manager using Magic Methods """ def __init__(self, name, on=True): self.name = name self.on = on self.old_value = feature_flags.is_on(name) def __enter__(self): feature_flags.toggle(self.name, self.on) def __exit__(self, exc_type, exc_value, traceback): feature_flags.toggle(self.name, self.old_value)
上面代碼實現了一個簡單的Feature Flags管理器。FeatureFlags類提供了Feature Flags以及控制它們的方法。feature_flag類則是一個Context Manager,由於它實現了__enter__()和__exit__()這兩個特殊方法。當用在with...[as...]語句中時,前者會在進入with代碼塊以前執行,若是with後面有as關鍵字,會將返回的值賦給as後面的變量;後者會在with代碼塊退出時調用,並會傳入exc_type, exc_value, traceback三個與異常相關的參數。只要__enter__()成功執行,就必定會執行__exit__()。所以咱們能夠將setup、cleanup相關的代碼分別放在__enter__()和__exit__()裏。下面這段代碼實現的功能就是在__enter__()中將SHOW_BETA設成False,再在__exit__()中將其恢復,從而保證在with的這段上下文裏SHOW_BETA這個feature是關掉的:
>>> print(feature_flags.is_on(FeatureFlags.SHOW_BETA)) True >>> with feature_flag(FeatureFlags.SHOW_BETA, False): ... print(feature_flags.is_on(FeatureFlags.SHOW_BETA)) ... False >>> print(feature_flags.is_on(FeatureFlags.SHOW_BETA)) True
更簡單的作法,是利用標準庫中的contextmanager裝飾器:
from contextlib import contextmanager @contextmanager def feature_flag(name, on=True): old_value = feature_flags.is_on(name) feature_flags.toggle(name, on) yield feature_flags.toggle(name, old_value)
這裏feature_flag再也不是一個類,而是一個帶yield的函數,而且用了contextmananger裝飾。當用於with語句時,在進入with代碼塊以前會執行這個函數,並在yield那裏掛起返回,接着就會執行with代碼塊中的內容,當退出代碼塊時,feature_flag會從掛起的地方恢復執行,這樣就實現了跟以前相同的效果。能夠簡單理解成,yield以前的內容至關於__enter__(),以後的內容至關於__exit__()。
要想成爲一名優秀的Python開發者,熟悉各類magic methods和標準庫是必不可少的。熟練掌握了這二者,會讓你的代碼更加Pythonic。
Nina Zakharenko. Elegant Solutions For Everyday Python Problems. PyCon 2018.