Python中的变量绑定

原文地址:http://alon.horev.net/blog/2013/10/20/argument-binding-in-python/

在最近一次关于pythono中的变量绑定的争论之后,我决定从正反两方面列出一些在不同方法中python的变量绑定情况。我们先从可行的方法开始吧。

def add(x,y):
    return x + y

from functools import partial
add5_partial = partial(add, 5)
add5_partial(10)   # 15  

add5_lambda = lambda x: add(x, 5)
add5_lambda(10)    # 15  

我对partial的抱怨

partial不是function[译注:我觉得这样的术语还是直接用英文比较准确],并且经常得不到一个function应该得到的结果。partial用纯python很容易实现,但我只能猜想,考虑到性能,它是用C来实现的。来看一些例子:

1.Partial在methods里不能工作:

from functools import partial  

class Cell(object):
    def set_state(self, state):
        self._state = state  
    set_alive = partial(set_state, state=True)  
    set_dead = partial(set_state, state=False)

>>>Cell().set_alive()
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: set_state() takes exactly 2 arguments (1 given)  

这是为什么?

你知道self总是在调用实例方法的时候被赋值为该实例吗?这是使用descriptors机制来是实现的。为了实现这个功能,function类型需要实现__get__方法。

以下表示了方法调用是怎么工作的:

class Person(object):
    def __init__(self, name):
        self._name = name
    def speak(self):  
        print 'My name is ', self._name  

>>> p = Person('Neo')  
>>> p.speak()   # 方法调用  
My name is Neo  

>>> # 那么函数(function)和方法(method)的区别是什么呢?  
>>> method = p.speak()  # 这个method封装了实例和函数  
>>> method  
<bound method Person.speak of <__main__.Person object at 0x109d7bb90>>  
>>> method.im_self    # 这是self隐藏的地方  
<__main__.Person object at 0x109d7bb90>>  
>>> method.im_func    # 这是function隐藏的地方  
<function Person.speak at 0x106163950>  
>>> method()          # 与method.im_func(method.im_self)结果相同  
My name is Neo  

>>> # 从function到method传递了些什么?  
>>> Person().speak()    # 触发 __getattribute__('speak')  
>>> # __getattribute__从实例的__dict__中搜索属性  
>>> # 然后__getattribute__从类的__dict__中搜索属性  
>>> # 当它找到之后,它会检查这个值(function)是不是实现了一个__get__方法  
>>> # 如果没有实现__get__,返回这个值  
>>> # 如果实现了__get__,返回__get__所返回的值,不管是什么  
>>> method = Person.speak.__get__(Person('Neo'))  
>>> method  
>>> <bound method ?.speak of <__main__.Person object at 0x7f8527ccbad0>>  

2.partial不能检查:

>>> import inspect, functools  
>>> p = functools.partial(lambda x, y: x + y, 10)  
>>> inspect.getargspect(p)  
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AttributeError: 'module' object has no attribute 'getargspect'  

>>> print p.__doc__    # 没有保持被包裹的function的__doc__  
partial(func, *args, **keywords) - new function with partial application
    of the given arguments and keywords.  

3.partial能够更安全,验证变量的数量和名称:

>>> from functools import partial  
>>> f = partial(lambda: None, 1, 2, 3)   # 为什么在这里不检查信号?!  
>>> f()  
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: <lambda>() takes no arguments (3 given)  

partial的替代品

你可以实现一个你自己的返回一个function的partial:

from functools import wraps  

def partial(func, *a, **k):  
    @wraps(func)
    def new_func(*args, **kwargs):  
        return func(*(a + args), **dict(k, **kwargs))
    return new_func  

备注:不要使用上面的代码,它没有保证键的变量唯一。

你也可以使用lambda:

class Cell(object):  
   def set_state(self, state):
       self._state = state  
   set_alive = lambda self: self.set_state(True)
   set_dead = lambda self: self.set_state(False)  

关于lambdas,我的问题

就像我的朋友@EyalIl说的:

Lambdas获取变量,partial获取值。
后者一般更有用。

这里有一个例子可以说清这个问题:

callbacks = []  
for i in xrange(5):
    callbacks.append(lambda: i)  
>>> print [callback() for callback in callbacks]
[4, 4, 4, 4, 4]  

为什么发生了这个?

因为python支持闭包(一个通常很好的东西):

var = 1
f = lambda: var
print f()  # 1
var = 2  
print f()  # 2
# 但是,但是python是怎么知道的?好吧,function能够hold住外部变量的一个引用  
print f.func_closure() #  (<cell at 0x101bdfb40: int object at 0x7fd3e9c106d8>,)  注:我不知道这作者是怎么得出来的,我的测试未得出这样的结果,而是抛出TypeError: 'NoneType' object is not callable的错误  

# 这些cells是什么?cell是一个指向某个外部范围某个名称的一个指针。它hold住了一个允许改变的反射,甚至是改变不可变的数据类型。  
print f.func_closure()[0].cell_contents  # 2  

[注:上面这段代码我在python2.7.3和python3.3.1下测试都没得到作者所说的结果,如果有懂的望赐教.]

将不是函数(function)参数的变量绑定为函数(function)变量是一个解决方法:

callbacks = []  
for i in xrange(5):
    callbacks.append(lambda x=i:x)
>>> print [callback() for callback in callbacks]  
[0, 1, 2, 3, 4]  

我们能做到更好嘛?

我打算提一个跟JavascriptFunction.bind功能相似的一个机制。

这是我想它所起的作用(这只是一个建议,这些代码不能真正的工作):

def add(x, y):  
    return x + y

from functools import partial
add5_partial = partial(add, 5)     # 需要一次import
add5_lambda = lambda x: add(x, 5)  # 太长了  

add5_bind = add.bind(5)  # 最短的  

import inspect
>>> print inspect.getargspec(add)  
ArgSpec(args=['x', 'y'], varargs=None, keywords=None, defaults=None)  
>>> print inspect.getargspect(add5_bind)  # works with inspect
ArgSpec(args=['y'], varargs=None, keywords=None, defaults=None)  

如果你想深挖bind,请回复/投票。如果有了足够的反馈,那我就有动力去写一个PEP啦.

Go Top
comments powered by Disqus