Fork me on GitHub
Google

Useful Hack: Lazy module attribute

昨天晚上师兄在 qq 上和我诉苦,说我们的代码测试起来太不方便了。问题大概出在这段代码:

# motorclient.py
import motor
from settings import mongo_machines, REPLICASET_NAME
from pymongo import ReadPreference

_motorclient = motor.MotorReplicaSetClient(
    ','.join(mongo_machines),
    replicaSet=REPLICASET_NAME,
    readPreference=ReadPreference.NEAREST)

fbt = _motorclient.fbt
reward = _motorclient.fbt_reward
fbt_log = _motorclient.fbt_log

这是我之前写的对 motor 的简单封装,无关代码已经去掉。目的很简单,他们每次要访问数据库只要先 import motorclient,然后用 motorclient.dbname 操作各个数据库就行了。问题在哪里呢?

发现 motorclient 好蛋疼,只要一 import 就必须连接数据库,本地测试每次都要打 mock

这是他的原话,他想在没有配置副本集的本机进行测试,然而 motorclient 只要一 import 就会初始化一个 MotorReplicaSetClient,于是只能 Mock。于是现在有如下需求:

  1. 希望保证现有接口不变
  2. 不要一 import 就初始化
  3. 能非常容易地变成只连接本地数据库而不是副本集

怎么做呢?

我突然想到,David Beazley 的演讲里好像提到了这个概念(关于他的演讲请参考 PyCon2015 笔记)。在 slide 的 150-152 页,他当时想实现的是,import 某个 package 的时候,不直接把 submodules/subpackage 都给 import 进来(因为很耗时间,相当于把所有文件执行一次),而是按需 import,他把这个技巧叫 "Lazy Module Assembly"。我面临的需求和他类似,也要用 "lazy" 的方式加载,只不过针对的是一个 module 里的变量。

上网搜了搜,参考了 SO 上的某答案,完成了 lazy 版,我把它叫做 lazy module attribute

# motorclient.py
mode = None
def set_mode(m):
    global mode
    mode = m

class Wrapper:

    localhost = '127.0.0.1'
    port = 27017
    dbs = {
        'fbt': None,
        'reward': None,
        'fbt_log': None
    }

    def __init__(self, module):
        self.module = module
        self._motorclient = None

    def __getattr__(self, item):
        if item in self.dbs and self._motorclient is None:
            if mode == 'test':
                self._motorclient = motor.MotorClient(host=self.localhost,
                                                      port=self.port)
            else:
                self._motorclient = motor.MotorReplicaSetClient(
                    ','.join(mongo_machines),
                    replicaSet=REPLICASET_NAME,
                    readPreference=ReadPreference.NEAREST)

            self.dbs['fbt'] = self._motorclient.fbt
            self.dbs['reward'] = self._motorclient.fbt_reward
            self.dbs['fbt_log'] = self._motorclient.fbt_log
            self.module.__dict__.update(self.dbs)

        return getattr(self.module, item)

sys.modules[__name__] = Wrapper(sys.modules[__name__])

它的工作流程是这样:
1. 在 import motorclient,会创建一个 Wrapper 类的实例替换掉这个 module 本身,并且把原来的 module object 赋给 self.module别的什么都不做
2. 然后我们在别的文件中访问 motorclient.fbt,进入 Wrapper 实例的 __getattr__ 方法,item='fbt'。因为是初次访问,self._motorclient is None 的条件满足,这时开始初始化变量。
3. 根据全局变量 mode 的值,我们会创建 MotorClient 或是 MotorReplicaSetClient,然后把那几个数据库变量也赋值,并且更新 self.module.__dict__.update(self.dbs)。这个效果就和我们的老版本初始化完全一样了,相当于直接把变量定义写在文件里。
4. 然后调用 getattr(self.module, item),因为我们已经更新过 self.module__dict__,所以能够正常返回属性值。第一次访问至此结束
5. OK,下一次再访问 motorclient.fbt,因为 self._motorclient 已经有值了,所以我们就不再初始化,直接把活交给 self.module 就好了。


下面的内容比较 internal,看不下去的同学就不要看了。。。

本来说到这里就差不多了,不过对 Python import 机制比较了解的同学可能会看出代码中的一个潜在问题。就是这句话:

sys.modules[__name__] = Wrapper(sys.modules[__name__])

为什么 sys.modules 的 key 一定就是 name 呢?下面将追根溯源,证明这一点。

据 Brett Cannon 在《How Import Works》演讲的 slide 第 27 页对 load_module 函数的描述,sys.modules 所用的 key 是 fullname

如果标准库中有类似 module.__name__ = fullname 这种东西,那么我们可以断定 __name__ 就是 fullname。于是苦逼地翻了半天源码,好在终于找到了,有这么一句话

module.__name__ = spec.name

那么这个 Spec 又是什么呢?它实际上是 Python3.4 里才引入的一个类,官方的描述是 "A specification for a module's import-system-related state"。好,就差最后一步了!我又找到了一句代码

spec = spec_from_loader(fullname, self)

没错,Spec 初始化的第一个参数,传入的是 fullname,而它恰恰被赋给了 spec.name。至此,我们的推理终于完成,现在可以肯定地说,sys.modules 的 key 就是这个 module 的 __name__。(实际上这个证明针对的是 Python3.4,不过这种接口肯定是向前兼容的,对于所有版本都成立)

Q.E.D.

当初写 ezcf 的时候其实就遇到过这个问题,只不过没深究。我的代码中有这么一段:

class BaseLoader(_BaseClass):

    def __init__(self, *args, **kwargs):
        pass

    def load_module(self, fullname):
        if fullname in sys.modules:
            mod = sys.modules[fullname]
        else:
            mod = sys.modules.setdefault(fullname, imp.new_module(fullname))

        mod.__file__ = self.cfg_file
        mod.__name__ = fullname  # notice this !!
        mod.__loader__ = self
        mod.__package__ = '.'.join(fullname.split('.')[:-1])

        return mod

当时看到别人都写 self.__name__ = fullname,于是就跟着这么写了,并不明白其中原理。现在终于弄清了,这是 convention,必须得这么写。

不是后记:
感觉好久都没有写文章了啊。。空余时间基本都去刷题了啊。。。跪求 Offer
以及如果读者中有能内推的请联系我(然而并没有什么读者(╥﹏╥)

Google

更好地 partition array

很多算法或者题目里面都有 partition array 这步。所谓 partition array,指的就是把元素按某种条件分开,一部分放前面,另一部分放后面。

比如快速排序,这个条件就是“元素是否小于 pivot”,小于放前面,大于放后面。

大部分人写快排,其中 partition 的一步都喜欢这么写:

def partition(alist, first, last):
    pivot = alist[first]

    left = first + 1
    right = last

    while left <= right:
        while left <= right and alist[left] <= pivote:
            left += 1

        while left <= right and alist[right] >= pivot:
            right += 1

        if left <= right:
            alist[left], alist[right] = alist[right], alist[left]
            left += 1
            right -= 1

    alist[first], alist[right] = alist[right], alist[first]
    return right

弄两根指针 left, right 分别放在 array 的左端和右端,然后 left 右移,right 左移,如果能交换就交换元素,直到 left > right

上面是一个比较标准的 partition 实现,也有一些变体,不过凡是使用 left/right 指针的都是一类。这类实现当然没有问题,但是不优雅,而且难记。

哪里不优雅?仔细看就会发现,代码里有 4 次 left <= right 的判断!而且这 4 次都是必要的。

为什么难记?因为代码多,所以难记!而且稍不注意,最后一步就会写成 alist[first], alist[left] = alist[left], alist[first]

优雅的写法是什么呢?

def partition(alist, first, last):
    pivot = alist[first]        
    i = first

    for j in range(first + 1, last):
        if alist[j] <= pivot:
            i += 1
            alist[i], alist[j] = alist[j], alist[i]

    alist[first], alist[i] = alist[i], alist[first]
    return i

短了这么多,更重要的是再也没有重复的 left <= right 判断了!这段代码也很好理解,j 指针把 array 扫一遍,把 <= pivot 的放到前面,i 用来记录放的地方。停止的时候,i 的位置是 <= pivot 部分的最后一个元素的 index。当然,不要忘记把 pivot 和这个元素交换。

需要注意的地方有两个,首先是初始化:i = first, j = first + 1。还有,每次是先 i += 1,然后再交换i, j元素。

其实这种区别不仅仅在于快排,所有 partition array 都是一样。比如这题:

给出一个整数数组nums和一个整数k。划分数组(即移动数组nums中的元素),使得:

所有小于k的元素移到左边
所有大于等于k的元素移到右边

不优雅的写法这么写:

int partitionArray(vector<int> &nums, int k) {
    int i = 0, j = nums.size() - 1;
    while (i <= j) {
        while (i <= j && nums[i] < k) i++;
        while (i <= j && nums[j] >= k) j--;
        if (i <= j) {
            int temp = nums[i];
            nums[i] = nums[j];
            nums[j] = temp;
            i++;
            j--;
        }
    }
}

还是 4 次比较╮(╯▽╰)╭

优雅的写法:

def partitionArray(nums):        
    i = -1

    for j in range(len(nums)):
        if nums[j] < k:
            i = i + 1
            nums[i], nums[j] = nums[j], nums[i]

当然拿Py和C++比是不合适的,但关键是少了 4 次比较,代码就好看多了。
这一题不存在真实的 pivot,所以我们对快排做了改进,初始化时 i 不再是 0,而是 -1。快排的时候,因为第一次交换时 i = first + 1,所以初始化 i=first 实际上跳过了 pivot 也就是 array[first]。这里因为没有需要跳过的元素,所以 i=-1,然后 j 还是等于 i + 1 也就是 0。其它的没有区别。

熟悉没有真实 pivot 的写法之后,不论按何种规则划分,都只改一行就可以。
比如按照奇偶划分:

def partitionArray(self, nums):
    n = len(nums)
    i = -1

    for j in range(n):
        if nums[j] % 2 == 1:  # changed this line
            i = i + 1
            nums[i], nums[j] = nums[j], nums[i]

其实到这里本来是写完了,但是那天和同学吃饭的时候聊天,又让我意识到优雅写法不光只是优雅而已,有时候只能用它。同学出这题考我:

单链表排序能不能用快排?

我想了想,没有不能用的理由吧,就说能用。他说,不行,因为是单链表,所以不能用快排。我没反应过来,就问为什么单链表不能用快排?他很不解我居然提出这个问题,说你要移动左右两个指针啊,但是单链表你没法把右边的指针往左移动,所以没法用快排。

于是我瞬间懂了,他只知道快排可以用左右指针来写,却不知道另一种写法。原本我只是觉得第二种写法更优雅,没想到在不知道的情况下,居然会误认为单链表没法用快排。

不只是更优雅,而是更好。

Google

PyCon2015 笔记

原本是知乎上的一个答案,现在挪到博客里来。

1. Type Hints - Guido van Rossum

视频:https://www.youtube.com/watch?v=2wDvzy6Hgxg
主要就是讲 PEP 484 - Type Hints,通过 typing 这个模块,从 3.5 版本开始,Python 也可以做静态检查了!

2. Raymond Hettinger - Beyond PEP 8 -- Best practices for beautiful intelligible code

视频:https://www.youtube.com/watch?v=wf-BqAjZb8M
强烈推荐!Raymond Hettinger 的演讲适合所有层次的程序员看。这个演讲说的是,我们都知道 PEP8 来规划 Python 代码,但是这样是否就够了呢?我们可能忽视了一件更重要的事:Pythonic!这个演讲举了几个例子,怎么 make code more pythonic,比如使用 context manager,使用 @property,使用 Adapter 包装已有代码让 import 更简洁。不过其中最神的我觉得还是通过定义 __len____getitem__ 来构造一个 sequence,然后这个 sequence 自动就是 iterable 的,之前我一直以为必须通过定义 __iter____next__ 方法构造 iterator 才能达到让类支持 for in 的效果。
这个演讲没有 slide,不过代码不多。

3. David Beazley - Modules and Packages: Live and Let Die!

视频:https://www.youtube.com/watch?v=0oTh1CXRaQ0
Slide: http://www.dabeaz.com/modulepackage/ModulePackage.pdf
代码:dabeaz/modulepackage · GitHub
David Beazley 以前的视频我也看过几个,他的演讲最大特点就是 mind-blowing,会在短时间内让你意识到你根本就不会 Python! 如果有人能在不查资料的情况下理解他的演讲的50%内容,那这个人绝对是 Python 高手。这次的视频也是一如既往的烧脑,讲的是和 import 相关的各种知识,因为之前写 ezcf 的缘故这次终于能看懂稍微多一点。。。实在不想介绍了,各位可以自己去看。
之前有人问 Python 里有哪些黑魔法。那个问题我没有答,我只想说 David Beazley 的每一个演讲都充斥着黑魔法。。。

4. Dan Crosta - Good Test, Bad Test

视频:https://www.youtube.com/watch?v=RfR_QRoNZxo
Slide:https://speakerdeck.com/dcrosta/good-test-bad-test
文章:http://late.am/post/2015/04/20/good-test-bad-test.html
关于测试质量的演讲,重点讲述了三个测试的误区:
(1) 过分追求 100% 覆盖率。他认为有些代码反而应该尽量避免去测试,比如依赖于网络上某个服务的代码。
(2) 使用过多 assert。他的建议是一个 test function 里面最好只有一个 assert。
(3) Mock 一定会让测试更好。在测试中用到了数据库,与其直接 mock 一个返回值,不如用简单的代码模拟数据库的工作流程。
第二条我觉得像只用一个 assert 在很多情况下并不方便,但是把 dict 的 assert 拆分成多个键值 assert 还是可以借鉴的。关于数据库的 mock,其实有很多工具可以使用,比如见过一个专门用来为单元测试生成假数据的第三方库,还有 Django 的测试自带测试数据库。


top