August 12, 2016

Python 的世界里没有代码块

以前经常写 ruby 和 js, 对于以下的代码非常习惯:

item = {id: 1, name: 'Donald Clinton'}
attrs = [:id, :name]
attr_getters = attrs.map do |attr|
  lambda do
    "The #{attr} is #{item[attr]}"
  end
end
> attr_getters[0][]
"The id is 1"
> attr_getters[1][]
"The name is Donald Clinton"
var item = {id: 1, name: 'Donald Clinton'};
var attrs = ['id', 'name'];
var attr_getters = attrs.map(function (attr) {
  return function() { 
    var value_with_name = 'The ' + attr + ' is ' + item[attr];
    return value_with_name; 
  };
});
> attr_getters[0]()
'The id is 1'
> attr_getters[1]()
'The name is Donald Clinton'

最近的工作开始要写 python, 于是我想当然的也这么写了....

item = {'id': 1, 'name': 'Donald Clinton'}
attrs = ['id', 'name']

attr_getters = []
for attr in attrs:
    getter = lambda : 'The {0} is {1}'.format(attr, item[attr])
    attr_getters.append(getter)

可是并不 work!

>>> attr_getters[0]()
'The name is Donald Clinton'
>>> attr_getters[1]()
'The name is Donald Clinton'

仔细观察了一下代码... 这 for ... in ...: 虽然看起来很清真, 可是他并不是代码块. 我这傻乎乎的 lambda 的闭包始终绑定着 for 循环所在的环境, 于是当循环完成后, attr 就定格在了 'name' 上.

于是一个蠢呼呼的 fix 是在 lambda 外面再包一个

item = {'id': 1, 'name': 'Donald Clinton'}
attrs = ['id', 'name']

attr_getters = []
for attr in attrs:
    getter = (lambda a: lambda : 'The {0} is {1}'.format(a, item[a]))(attr)
    attr_getters.append(getter)

鉴于这很蠢...于是代码只能再改一下

item = {'id': 1, 'name': 'Donald Clinton'}
attrs = ['id', 'name']

def build_getter(attr):
    def getter():
        return 'The {0} is {1}'.format(attr, item[attr])
    return getter

attr_getters = map(build_getter, attrs)

lambda, 闭包等等, 这些东西在 python 世界里并不是一等公民, 随手定义函数的编程习惯在 python 中基本是完全不适用的. 单行 lambda 更是残废, 基本上想做点事情都必须预先定义一个函数才行, 没法定义匿名 block.

PS: 在 python 的世界里非常注重 callable 的. 小到一个函数, 大到一个类, 甚至一个实例, 只要定义了 __call__ 方法, 他就是 callable 的.

class A(object)
  def __init__(self, ...):
    ...

map(A, some_list_of_init_params) # => list of A