0

UPDATE: As noted by Mr. Fooz, the functional version of the wrapper has a bug, so I reverted to the original class implementation. I've put the code up on GitHub:

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

There are two commits, one working (using the "import" workaround) the second one broken.

The source of the problem seems to be the pickle#dumps function, which just spits out an identifier when called on an function. By the time I call Process, that identifier points to the decorated version of the function, rather than the original one.


ORIGINAL MESSAGE:

I was trying to write a function decorator to wrap a long task in a Process that would be killed if a timeout expires. I came up with this (working but not elegant) version:

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)

As you can see, I commented out the decorator and assigned the modified version of calculatePrimes to calculatePrime. If I tried to reassign it to the same variable, I'd get a "Can't pickle : attribute lookup builtins.function failed" error when trying to call the decorated version.

Anybody has any idea of what is happening under the hood? Is the original function being turned into something different when I assign the decorated version to the identifier referencing it?

UPDATE: To reproduce the error, I just change the main part to

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

which yields:

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. I also wrote a class version of safeExecution, which has exactly the same behaviour.

Dek Dekku
  • 1,441
  • 11
  • 28
  • Can you show us code that causes the pickle error? There's nothing I recognize as pickle-related anywhere in the code you've posted, and I can't make anything go wrong either by replacing the assignment to calculatePrime with a reassignment to calculatePrimes, or removing it and uncommenting the decorator. – Mark Amery Feb 20 '13 at 21:53
  • What are you trying to pickle -- where's the code and a stacktrace? – martineau Feb 20 '13 at 22:09
  • Perhaps it's Windows related. – Dek Dekku Feb 20 '13 at 22:11
  • @martineau: It's the multiprocessing module. – Martijn Pieters Feb 20 '13 at 22:25
  • Oh, AFAIK `pickle` is used internally by the `Process` class on the function passed as a target. – Dek Dekku Feb 20 '13 at 22:26
  • Threads in Python can't be killed. I know there are workarounds, but I thought I could try another way. But right now I'm more interested in why this doesn't work. I should add that this is my first attempt at threading and/or multiprocessing in Python, so I'm just trying to replicate what I remember from Java and C. – Dek Dekku Feb 20 '13 at 22:30

2 Answers2

3

Move the function to a module that's imported by your script.

Functions are only picklable in python if they're defined at the top level of a module. Ones defined in scripts are not picklable by default. Module-based functions are pickled as two strings: the name of the module, and the name of the function. They're unpickled by dynamically importing the module then looking up the function object by name (hence the restriction on top-level-only functions).

It's possible to extend the pickle handlers to support semi-generic function and lambda pickling, but doing so can be tricky. In particular, it can be difficult to reconstruct the full namespace tree if you want to properly handle things like decorators and nested functions. If you want to do this, it's best to use Python 2.7 or later or Python 3.3 or later (earlier versions have a bug in the dispatcher of cPickle and pickle that's unpleasant to work around).

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

Python: pickling nested functions

http://bugs.python.org/issue7689

EDIT:

At least in Python 2.6, the pickling works fine for me if the script only contains the if __name__ block, the script imports calculatePrimes and settimeout from a module, and if the inner start function's name is monkey-patched:

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

    start.__name__ = function.__name__ # ADD THIS LINE

    return start

There's a second problem that's related to Python's variable scoping rules. The assignment to the thread variable inside start creates a shadow variable whose scope is limited to one evaluation of the start function. It does not assign to the thread variable found in the enclosing scope. You can't use the global keyword to override the scope because you want and intermediate scope and Python only has full support for manipulating the local-most and global-most scopes, not any intermediate ones. You can overcome this problem by placing the thread object in a container that's housed in the intermediate scope. Here's how:

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
Community
  • 1
  • 1
Mr Fooz
  • 109,094
  • 6
  • 73
  • 101
  • But my function gets pickled correctly when I execute the code I posted. When I modify it to use the decorator, it breaks. Trying anyway. – Dek Dekku Feb 20 '13 at 22:37
  • Well, if I move the function in a module, it works. But then, if I move the decorator in the module as well I'm back to starting point. – Dek Dekku Feb 20 '13 at 22:49
  • It's the decorator; by wrapping the function you made it impossible for the pickle module to recreate it. Don't mix decorators and pickling. – Martijn Pieters Feb 20 '13 at 22:52
  • I wrapped the function in the first version of the code too, and it worked, as long as I assigned the decorated version to a different variable. It's when I lose the original reference that pickling raises an error. – Dek Dekku Feb 20 '13 at 23:11
  • @Mr. Fooz: Thanks, I'm aware of the second issue in your edit, that's why my first version was a class, and thread was an instance variable. I should just revert to that one. The monkey patch seems irrelevant, your first workaround already worked, but again, it's almost equivalent to the version I posted and anyway, it's a workaround, it doesn't actually explain the pickle error. – Dek Dekku Feb 21 '13 at 09:53
0

Not sure really why you get that problem, but to answer your title question: Why does the decorator not work?

When you pass arguments to a decorator, you need to structure the code slightly different. Essentially you have to implement the decorator as a class with an __init__ and an __call__.

In the init, you collect the arguments that you send to the decorator, and in the call, you'll get the function you decorate:

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)

This should get you going on the decorator front at least.

JCash
  • 312
  • 1
  • 3
  • 10
  • Decorators can either be implemented as classes or as functions, as done in the original post. – Mr Fooz Feb 20 '13 at 22:30
  • Yes, but when you pass arguments to the decorator, you need to implement it as a class (unless I'm entirely mistaken?) – JCash Feb 20 '13 at 22:33
  • 1
    My title is kinda deceptive. My decorator works as expected. It's the decorated function which doesn't work. It doesn't even if I decorate it manually by calling `f = decorate(f)`. But it works if I do `f2 = decorate(f)`, which boggles me and makes me think that something happens in Python internals when I try to reassign `f`. – Dek Dekku Feb 20 '13 at 22:35
  • @JCash: You are mistaken. It is possible to write decorators that take arguments as functions. – recursive Feb 20 '13 at 22:40
  • @recursive: Ah, of course, now I see. Don't really know how I overlooked that. I learned something new today. – JCash Feb 20 '13 at 23:18