为什么这个函数作为装饰器时不工作?

0 投票
2 回答
1426 浏览
提问于 2025-04-17 16:33

更新:正如Mr. Fooz所提到的,包装器的功能版本有个bug,所以我又回到了原来的类实现。我把代码放到了GitHub上:

https://github.com/nofatclips/timeout/commits/master

这里有两个提交,一个是可用的(使用了“导入”的解决方法),另一个是坏掉的。

问题的根源似乎在于pickle#dumps这个函数,当它被用在一个函数上时,只会返回一个标识符。等我调用Process的时候,这个标识符指向的是被装饰过的函数,而不是原来的那个。


原始信息

我试着写一个函数装饰器,把一个长时间运行的任务包裹在一个进程中,如果超时了就会被杀掉。我写出了这个(可用但不优雅)的版本:

from multiprocessing import Process
from threading import Timer
from functools import partial
from sys import stdout

def safeExecution(function, timeout):

    thread = None

    def _break():
        #stdout.flush()
        #print (thread)
        thread.terminate()

    def start(*kw):
        timer = Timer(timeout, _break)
        timer.start()
        thread = Process(target=function, args=kw)
        ret = thread.start() # TODO: capture return value
        thread.join()
        timer.cancel()
        return ret

    return start

def settimeout(timeout):
    return partial(safeExecution, timeout=timeout)

#@settimeout(1)
def calculatePrimes(maxPrimes):
    primes = []

    for i in range(2, maxPrimes):

        prime = True
        for prime in primes:
            if (i % prime == 0):
                prime = False
                break

        if (prime):
            primes.append(i)
            print ("Found prime: %s" % i)

if __name__ == '__main__':
    print (calculatePrimes)
    a = settimeout(1)
    calculatePrime = a(calculatePrimes)
    calculatePrime(24000)

如你所见,我把装饰器注释掉了,并把修改后的calculatePrimes赋值给calculatePrime。如果我试图把它重新赋值给同一个变量,调用被装饰的版本时就会出现“无法序列化:属性查找builtins.function失败”的错误。

有没有人知道这背后发生了什么?当我把被装饰的版本赋值给引用它的标识符时,原始函数是不是变成了其他东西?

更新:为了重现这个错误,我只需把主要部分改成

if __name__ == '__main__':
    print (calculatePrimes)
    a = settimeout(1)
    calculatePrimes = a(calculatePrimes)
    calculatePrimes(24000)
    #sleep(2)

这样就会得到:

Traceback (most recent call last):
  File "c:\Users\mm\Desktop\ING.SW\python\thread2.py", line 49, in <module>
    calculatePrimes(24000)
  File "c:\Users\mm\Desktop\ING.SW\python\thread2.py", line 19, in start
    ret = thread.start()
  File "C:\Python33\lib\multiprocessing\process.py", line 111, in start
    self._popen = Popen(self)
  File "C:\Python33\lib\multiprocessing\forking.py", line 241, in __init__
    dump(process_obj, to_child, HIGHEST_PROTOCOL)
  File "C:\Python33\lib\multiprocessing\forking.py", line 160, in dump
    ForkingPickler(file, protocol).dump(obj)
_pickle.PicklingError: Can't pickle <class 'function'>: attribute lookup builtin
s.function failed
Traceback (most recent call last):
  File "<string>", line 1, in <module>
  File "C:\Python33\lib\multiprocessing\forking.py", line 344, in main
    self = load(from_parent)
EOFError

附言:我还写了一个类版本的safeExecution,它的行为完全一样。

2 个回答

0

我不太确定你为什么会遇到这个问题,但我可以回答你标题中的问题:为什么装饰器不工作?

当你给装饰器传递参数时,你需要稍微调整一下代码的结构。简单来说,你需要把装饰器实现成一个类,并且要有一个 __init__ 方法和一个 __call__ 方法。

__init__ 方法中,你会收集传给装饰器的参数,而在 __call__ 方法中,你会得到你要装饰的函数:

class settimeout(object):
    def __init__(self, timeout):
        self.timeout = timeout

    def __call__(self, func):
        def wrapped_func(n):
            func(n, self.timeout)
        return wrapped_func

@settimeout(1)
def func(n, timeout):
    print "Func is called with", n, 'and', timeout

func(24000)

这样至少可以让你在使用装饰器时有所进展。

3

把这个函数移动到一个你的脚本可以导入的模块里。

在Python中,只有在模块的顶层定义的函数才能被“序列化”(也就是可以保存和恢复)。如果函数是在脚本里定义的,默认情况下是不能被序列化的。模块里的函数会被序列化成两个字符串:一个是模块的名字,另一个是函数的名字。反序列化时,Python会动态导入这个模块,然后通过名字找到对应的函数对象(所以只能在顶层定义函数)。

虽然可以扩展序列化处理器来支持一些通用的函数和匿名函数的序列化,但这样做可能会比较复杂。特别是,如果你想正确处理装饰器和嵌套函数,重建完整的命名空间树会很困难。如果你想这样做,最好使用Python 2.7或更高版本,或者Python 3.3或更高版本(早期版本在pickle的调度器上有个bug,处理起来很麻烦)。

有没有简单的方法可以序列化一个Python函数(或者以其他方式保存它的代码)?

Python:序列化嵌套函数

http://bugs.python.org/issue7689

编辑

至少在Python 2.6中,如果脚本只包含if __name__块,并且脚本从一个模块中导入calculatePrimessettimeout,而且内部的start函数的名字被“猴子补丁”了,那么序列化对我来说是可以正常工作的:

def safeExecution(function, timeout):
    ...    
    def start(*kw):
        ...

    start.__name__ = function.__name__ # ADD THIS LINE

    return start

还有一个与Python的变量作用域规则相关的第二个问题。在start函数内部对thread变量的赋值会创建一个作用域仅限于这次start函数调用的“影子变量”。它并不会赋值给外部作用域中的thread变量。你不能使用global关键字来覆盖作用域,因为你需要一个中间作用域,而Python只完全支持操作最内层和最外层的作用域,而不支持任何中间的作用域。你可以通过把线程对象放在一个位于中间作用域的容器中来解决这个问题。方法如下:

def safeExecution(function, timeout):
    thread_holder = []  # MAKE IT A CONTAINER

    def _break():
        #stdout.flush()
        #print (thread)
        thread_holder[0].terminate() # REACH INTO THE CONTAINER

    def start(*kw):
        ...
        thread = Process(target=function, args=kw)
        thread_holder.append(thread) # MUTATE THE CONTAINER
        ...

    start.__name__ = function.__name__ # MAKES THE PICKLING WORK

    return start

撰写回答