为什么这个功能在用作装饰器时不起作用?

时间:2013-02-20 21:41:45

标签: python multiprocessing pickle

更新:正如Fooz先生所指出的,包装器的功能版本有一个错误,所以我恢复了原来的类实现。我把代码放在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。如果我试图将它重新分配给同一个变量,我会在尝试调用装饰版本时遇到“无法pickle:属性查找builtins.function failed”错误。

任何人都知道引擎盖下发生了什么?当我将装饰版本分配给引用它的标识符时,原始函数是否会变成不同的东西?

更新:要重现错误,我只需将主要部分更改为

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

P.S。我还写了一个safeExecution的类版本,它具有完全相同的行为。

2 个答案:

答案 0 :(得分:2)

将函数移动到脚本导入的模块。

如果函数在模块的顶层定义,则它们只能在python中被选中。脚本中定义的Ones默认情况下不可选。基于模块的函数被腌制为两个字符串:模块的名称和函数的名称。它们通过动态导入模块然后按名称查找函数对象(因此限制仅限顶级函数)来进行unpickled。

可以扩展pickle处理程序以支持半泛型函数和lambda酸洗,但这样做可能会很棘手。特别是,如果要正确处理装饰器和嵌套函数之类的东西,可能很难重构完整的命名空间树。如果你想这样做,最好使用Python 2.7或更高版本或Python 3.3或更高版本(早期版本在cPicklepickle的调度程序中有一个错误,这是一个令人不愉快的解决方法。)

Is there an easy way to pickle a python function (or otherwise serialize its code)?

Python: pickling nested functions

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的变量范围规则有关。对threadstart变量的赋值创建了一个影子变量,其范围仅限于对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

答案 1 :(得分:0)

不确定为什么你会遇到这个问题,但要回答你的标题问题:为什么装饰师不起作用?

将参数传递给装饰器时,需要将代码结构略有不同。基本上,您必须将装饰器实现为具有__init____call__的类。

在init中,你收集你发送给装饰者的参数,并在通话中,你将得到你装饰的功能:

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)

至少应该让你去装饰前线。