调用保存在类属性中的函数:内置函数与普通函数的不同行为

Ric*_*sen 6 python python-2.7 python-internals

使用以下脚本:

import time

class Foo(object):
    func = time.gmtime
    def go(self):
        return self.func(0.0)

print time.strftime('%Y', Foo().go())
Run Code Online (Sandbox Code Playgroud)

我得到以下输出:

1970
Run Code Online (Sandbox Code Playgroud)

但是,如果我稍作修改并换行time.gmtime:

import time

def tmp(stamp):
    return time.gmtime(stamp)

class Foo(object):
    func = tmp
    def go(self):
        return self.func(0.0)

print time.strftime('%Y', Foo().go())
Run Code Online (Sandbox Code Playgroud)

然后我收到以下错误:

Traceback (most recent call last):
  File "./test.py", line 13, in <module>
    print time.strftime('%Y', Foo().go())
  File "./test.py", line 11, in go
    return self.func(0.0)
TypeError: tmp() takes exactly 1 argument (2 given)
Run Code Online (Sandbox Code Playgroud)

显然它试图调用Foo.func就好像它是一个实例方法并self作为第一个参数传递.

两个问题:

  • 这两个time.gmtimetmp是采取单一的说法,那么为什么两个脚本行为不同的功能呢?
  • 如何在类/实例属性中安全地保存普通函数并稍后从实例方法调用它?

the*_*eye 6

  1. 这两个time.gmtimetmp是采取单一的说法,那么为什么两个脚本行为不同的功能呢?

让我们了解 Python 在你做的时候做了什么

self.func
Run Code Online (Sandbox Code Playgroud)

文档中

当引用的实例属性不是数据属性时,将搜索其类。如果名称表示作为函数对象的有效类属性,则通过打包(指向)实例对象和刚刚在抽象对象中一起找到的函数对象来创建方法对象:这就是方法对象。当使用参数列表调用方法对象时,会根据实例对象和参数列表构造一个新的参数列表,并使用这个新的参数列表调用函数对象。

所以,如果func是一个有效的功能对象self,然后绑定方法的对象将被创建,它预计的第一个参数是在其上该功能所调用的对象。

现在让我们比较一下,

print type(time.gmtime)

class Foo(object):
    func = time.gmtime
    def go(self):
        print type(self.func)
        return self.func(0.0)
Run Code Online (Sandbox Code Playgroud)

当用go调用时Foo().go(),它会打印

<type 'builtin_function_or_method'>
<type 'builtin_function_or_method'>
Run Code Online (Sandbox Code Playgroud)

这是正确的。由于time.gmtime是内置函数(与函数对象不同),这里没有创建绑定方法对象。现在让我们试试你的第二个例子,

def tmp(stamp):
    return time.gmtime(stamp)

print type(tmp), tmp

class Foo(object):
    func = tmp
    def go(self):
        print type(self.func), self.func
        return self.func(0.0)
Run Code Online (Sandbox Code Playgroud)

会打印

<type 'function'> <function tmp at 0x7f34d9983578>
<type 'instancemethod'> <bound method Foo.tmp of <__main__.Foo object at ...>>
Run Code Online (Sandbox Code Playgroud)

由于tmp是一个函数对象,根据上面显示的文档,创建了一个绑定方法对象,它期望一个 Object ofFoo作为第一个参数。因此,tmp实际上绑定Fooinstancemethod. 当您调用golike 时Foo().go(),它会tmp像这样在内部调用

Foo.func(self, 0.0)
Run Code Online (Sandbox Code Playgroud)

这是有效的

tmp(self, 0.0)
Run Code Online (Sandbox Code Playgroud)

这就是您收到以下错误的原因

self.func
Run Code Online (Sandbox Code Playgroud)
  1. 如何安全地将普通函数保存在类/实例属性中并稍后从实例方法中调用它?

解决方案1:

再次引用Python 文档

同样重要的是要注意,作为类实例属性的用户定义函数不会转换为绑定方法;这只发生在函数是类的属性时。

这意味着,当您将用户定义的函数分配给类变量时,将发生绑定方法构造。但是,如果你将它分配给一个实例,那么它就不会发生。

所以,你可以利用它来发挥你的优势,就像这样

import time

def tmp(stamp):
    return time.gmtime(stamp)

class Foo(object):
    def __init__(self):
        self.func = tmp       # Instance variable, not a class variable

    def go(self):
        return self.func(0.0)

print time.strftime('%Y', Foo().go())
Run Code Online (Sandbox Code Playgroud)

在这里,self.func将转换为tmp(0.0),因为没有绑定方法构造发生。

解决方案2:

使用这样的staticmethod功能

class Foo(object):
    func = staticmethod(tmp)

    def go(self):
        # `return Foo.func(0.0)` This will also work
        return self.func(0.0)
Run Code Online (Sandbox Code Playgroud)

现在,self.func仍然会参考tmp. 它类似于像这样定义你的类

class Foo(object):

    @staticmethod
    def func(stamp):
        return time.gmtime(stamp)

    def go(self):
        # `return Foo.func(0.0)` This will also work
        return self.func(0.0)
Run Code Online (Sandbox Code Playgroud)