×

《PyCon2018》系列二:Elegant Solutions For Everyday Python Problems

96
codehub
2018.08.27 21:06* 字数 2056

前言

平日写Python代码的过程中,我们会碰到各种各样的问题。其实大部分问题归结下来主要也就是那么几类,并且其中不少都是我们会反复遇到的。如何用Python优雅的解决这些问题呢?Nina Zakharenko在PyCon2018上的演讲《Elegant Solutions For Everyday Python Problems》或许能给你一些启发。

什么样的代码才是优雅的?

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!

如何写出优雅的代码

充分利用 Magic Method

Python中magic method的名称都以双下划线(double underscore,简称dunder)开头和结尾,比如_getattr,_getattribute等等。通过实现这些magic method,我们可以赋予对象各种神奇而实用的功能。

充分利用标准库

Python的标准库里自带了很多实用的工具。像functools、itertools这些模块,都提供了很多现成的轮子,足以解决我们很多实际问题了,非常值得初学者们好好去了解、学习。如果大家日常工作中遇到了什么问题想要现成的轮子,不妨先去标准库里看看。

例一:Iterable, Iterator 和 Generator

首先明确几个容易混淆的概念:

  • iterable(可迭代的): 当一个类至少满足下列条件中的一个时,则称其(实例)为iterable:
    • 实现了_iter_()方法,返回一个iterator
    • 实现了_getitem_()方法,能接受从0开始的索引或抛出IndexError
  • iterator(迭代器):如果一个类实现了_next_()方法,则称其(对象)为iterator。当没有item可供返回时该方法抛出StopIteration
  • generator(生成器):generator是特殊的iterator,即每个generator都是iterator,但反过来不一定成立。有两种方式构造generator:
    • 通过generator comprehension返回一个generator
    • 通过调用含有yield的function返回一个generator
版本一

假定我们在一台server上运行了多个service,想要只遍历其中状态为active的那些。为此我们可以实现下面的IterableServer类:

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。

运行结果:

>>> 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。

版本二

如果我们的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那句)恢复执行。

例二:functools

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

例三:agithub

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

如果你想在进行某些操作之前和之后都能做一些额外的工作,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。

注:本文部分代码仅适用于Python3。

参考

Nina Zakharenko. Elegant Solutions For Everyday Python Problems. PyCon 2018.

欢迎关注公众号:CodeHub
Python
Web note ad 1