平日写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.