将部分函数转换为Python方法
考虑以下这段(有问题的)代码:
import functools
class Foo(object):
def __init__(self):
def f(a,self,b):
print a+b
self.g = functools.partial(f,1)
x=Foo()
x.g(2)
我想做的是把函数 f 部分应用,得到一个新函数 g(self,b)
。我想把这个新函数当作一个方法来用,但现在这样做不行,反而出现了错误:
Traceback (most recent call last):
File "test.py", line 8, in <module>
x.g(2)
TypeError: f() takes exactly 3 arguments (2 given)
不过,执行 x.g(x,2)
是可以的,所以看起来问题在于 g 被当作一个“普通”的函数,而不是这个类的方法。有没有办法让 x.g 像方法那样工作(也就是说,自动传递 self 参数),而不是当作一个普通函数呢?
4 个回答
这只是一个具体的例子,我认为这是解决问题最正确(也就是最符合Python风格的 :))的方法——因为最好的解决方案(在类上的定义!)从来没有被揭示出来——@MikeBoers的解释还是很不错的。
我最近在一个代理API中用过这个模式,已经在生产环境中运行了很多小时,没有出现过任何问题。
from functools import update_wrapper
from functools import partial
from types import MethodType
class Basic(object):
def add(self, **kwds):
print sum(kwds.values())
Basic.add_to_one = MethodType(
update_wrapper(partial(Basic.add, a=1), Basic.add),
None,
Basic,
)
x = Basic()
x.add(a=1, b=9)
x.add_to_one(b=9)
...结果是:
10
10
...这里的关键点是 MethodType(func, inst, cls)
,它可以从另一个可调用对象创建一个未绑定的方法(你甚至可以用这个把实例方法绑定到不相关的类上……当实例化并调用时,原始的实例方法会接收到两个 self
对象!)
注意只使用关键字参数!虽然可能有更好的处理方式,但一般来说,位置参数(args)比较麻烦,因为self
的位置变得不那么可预测。此外,根据我的经验,在最底层的函数中使用 *args
和 **kwds
后续会非常有用。
这里有两个问题。首先,要把一个函数变成方法,它必须存储在类上,而不是实例上。我们来看个例子:
class Foo(object):
def a(*args):
print 'a', args
def b(*args):
print 'b', args
Foo.b = b
x = Foo()
def c(*args):
print 'c', args
x.c = c
在这个例子中,a
是定义在类里的一个函数,b
是后来赋值给类的一个函数,而c
是赋值给实例的一个函数。我们来看看调用它们时会发生什么:
>>> x.a('a will have "self"')
a (<__main__.Foo object at 0x100425ed0>, 'a will have "self"')
>>> x.b('as will b')
b (<__main__.Foo object at 0x100425ed0>, 'as will b')
>>> x.c('c will only recieve this string')
c ('c will only recieve this string',)
如你所见,类里定义的函数和后来赋值给它的函数之间几乎没有区别。我认为只要没有涉及元类,它们实际上是没有区别的,但这又是另一个话题了。
第二个问题是,函数是如何被转换成方法的;函数类型实现了描述符协议。(详细信息请查看文档。) 简单来说,函数类型有一个特殊的__get__
方法,当你在类本身上查找属性时会被调用。这样,你得到的不是函数对象,而是该函数对象的__get__
方法被调用,返回一个绑定的方法对象(这就是提供self
参数的对象)。
这为什么会是个问题呢?因为functools.partial
对象不是描述符!
>>> import functools
>>> def f(*args):
... print 'f', args
...
>>> g = functools.partial(f, 1, 2, 3)
>>> g
<functools.partial object at 0x10042f2b8>
>>> g.__get__
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
AttributeError: 'functools.partial' object has no attribute '__get__'
在这个时候,你有几个选择。你可以明确地给partial提供self
参数:
import functools
class Foo(object):
def __init__(self):
def f(self, a, b):
print a + b
self.g = functools.partial(f, self, 1)
x = Foo()
x.g(2)
...或者你可以把self
和a
的值放在一个闭包里:
class Foo(object):
def __init__(self):
a = 1
def f(b):
print a + b
self.g = f
x = Foo()
x.g(2)
当然,这些解决方案是基于你有一个尚未说明的原因,为什么要在构造函数中把方法赋值给类,因为你完全可以直接在类上定义一个方法来完成你在这里做的事情。
编辑:这里有一个解决方案的想法,假设这些函数是为类而创建的,而不是为实例:
class Foo(object):
pass
def make_binding(name):
def f(self, *args):
print 'Do %s with %s given %r.' % (name, self, args)
return f
for name in 'foo', 'bar', 'baz':
setattr(Foo, name, make_binding(name))
f = Foo()
f.foo(1, 2, 3)
f.bar('some input')
f.baz()
结果是:
Do foo with <__main__.Foo object at 0x10053e3d0> given (1, 2, 3).
Do bar with <__main__.Foo object at 0x10053e3d0> given ('some input',).
Do baz with <__main__.Foo object at 0x10053e3d0> given ().
这个方法是可行的。不过我不太确定这是否是你想要的结果。
class Foo(object):
def __init__(self):
def f(a,self,b):
print a+b
self.g = functools.partial(f,1, self) # <= passing `self` also.
x = Foo()
x.g(2)