Skip to main content

Python decorator: 8 The @synchronized decorator as context manager

This is the eigth post in my series of blog posts about Python decorators and how I believe they are generally poorly implemented. It follows on from the previous post titled The missing @synchronized decorator, with the very first post in the series being How you implemented your Python decorator is wrong.
In the previous post I described how we could use our new universal decorator pattern to implement a better @synchronized decorator for Python. The intent in doing this was to come up with a better approximation of the equivalent synchronization mechanisms in Java.
Of the two synchronization mechanisms provided by Java, synchronized methods and synchronized statements, we have however so far only implemented an equivalent to synchronized methods.
In this post I will describe how we can take our @synchronized decorator and extend it to also be used as a context manager, thus providing an an equivalent of synchronized statements in Java.

The original @synchronized decorator

The implementation of our @synchronized decorator so far is:
@decorator
def synchronized(wrapped, instance, args, kwargs):
    if instance is None:
        owner = wrapped
    else:
        owner = instance  

    lock = vars(owner).get('_synchronized_lock', None)  

    if lock is None:
        meta_lock = vars(synchronized).setdefault(
                '_synchronized_meta_lock', threading.Lock())  

        with meta_lock:
            lock = vars(owner).get('_synchronized_lock', None)
            if lock is None:
                lock = threading.RLock()
                setattr(owner, '_synchronized_lock', lock)  

    with lock:
        return wrapped(*args, **kwargs)
By determining whether the decorator is being used to wrap a normal function, a method bound to a class instance or a class, and with the decorator changing behaviour as a result, we are able to use the one decorator implementation in a number of scenarios.
@synchronized # lock bound to function1
def function1():
    pass 

@synchronized # lock bound to function2
def function2():
    pass 

@synchronized # lock bound to Class
class Class(object):  

    @synchronized # lock bound to instance of Class
    def function_im(self):
        pass 

    @synchronized # lock bound to Class
    @classmethod
    def function_cm(cls):
        pass

    @synchronized # lock bound to function_sm
    @staticmethod
    def function_sm():
        pass
What we now want to do is modify the decorator to also allow:
class Object(object):  

    @synchronized
    def function_im_1(self):
        pass  

    def function_im_2(self):
        with synchronized(self):
            pass
That is, as well as being able to be used as a decorator, we enable it to be used as a context manager in conjunction with the with statement. By doing this it then provides the ability to only acquire the corresponding lock for a selected number of statements within a function rather than the whole function.
For the case of acquiring the same lock used by instance methods, we would pass the self argument or instance object into synchronized when used as a context manager. It could instead also be passed the class type if needing to synchronize with class methods.

The function wrapper as context manager

Right now with how the decorator is implemented, when we use synchronized as a function call, it will return an instance of our function wrapper class.
>>> synchronized(None)
<__main__.function_wrapper object at 0x107b7ea10>
This function wrapper does not implement the __enter__() and __exit__() functions that are required for an object to be used as a context manager. Since the function wrapper type is our own class, all we need to do though is create a derived version of the class and use that instead. Because though the creation of that function wrapper is bound up within the definition of @decorator, we need to bypass @decorator and use the function wrapper directly.
The first step therefore is to rewrite our @synchronized decorator so it doesn't use @decorator.
def synchronized(wrapped): 
    def _synchronized_lock(owner):
        lock = vars(owner).get('_synchronized_lock', None) 

        if lock is None:
            meta_lock = vars(synchronized).setdefault(
                    '_synchronized_meta_lock', threading.Lock()) 

            with meta_lock:
                lock = vars(owner).get('_synchronized_lock', None)
                if lock is None:
                    lock = threading.RLock()
                    setattr(owner, '_synchronized_lock', lock) 

        return lock 

    def _synchronized_wrapper(wrapped, instance, args, kwargs):
        with _synchronized_lock(instance or wrapped):
            return wrapped(*args, **kwargs) 

    return function_wrapper(wrapped, _synchronized_wrapper)
This works the same as our original implementation but we now have access to the point where the function wrapper was created. With that being the case, we can now create a class which derives from the function wrapper and adds the required methods to satisfy the context manager protocol.
def synchronized(wrapped): 
    def _synchronized_lock(owner):
        lock = vars(owner).get('_synchronized_lock', None) 

        if lock is None:
            meta_lock = vars(synchronized).setdefault(
                    '_synchronized_meta_lock', threading.Lock()) 

            with meta_lock:
                lock = vars(owner).get('_synchronized_lock', None)
                if lock is None:
                    lock = threading.RLock()
                    setattr(owner, '_synchronized_lock', lock) 

        return lock 

    def _synchronized_wrapper(wrapped, instance, args, kwargs):
        with _synchronized_lock(instance or wrapped):
            return wrapped(*args, **kwargs) 

    class _synchronized_function_wrapper(function_wrapper): 

        def __enter__(self):
            self._lock = _synchronized_lock(self.wrapped)
            self._lock.acquire()
            return self._lock 

        def __exit__(self, *args):
            self._lock.release() 

    return _synchronized_function_wrapper(wrapped, _synchronized_wrapper)
We now have two scenarios for what can happen.
In the case of synchronized being used as a decorator still, our new derived function wrapper will wrap the function or method it was applied to. When that function or class method is called, the existing __call__() method of the function wrapper base class will be called and the decorator semantics will apply with the wrapper called to acquire the appropriate lock and call the wrapped function.
In the case where is was instead used for the purposes of a context manager, our new derived function wrapper would actually be wrapping the class instance or class type. Nothing is being called though and instead the with statement will trigger the execution of the __enter__() and __exit__() methods to acquire the appropriate lock around the context of the statements to be executed.
So all nice and neat and not even that complicated compared to previous attempts at doing the same thing which were referenced in the prior post on this topic.

It isn't just about @decorator

Now one of the things that this hopefully shows is that although @decorator can be used to create your own custom decorators, this isn't always the most appropriate way to go. The separate existence of the function wrapper gives a great deal of flexibility in what one can do as far as wrapping objects to modify the behaviour. One can also drop down and use the object proxy directly in certain circumstances.
All together these provide a general purpose set of tools for doing any sort of wrapping or monkey patching and not just for use in decorators. I will now start to shift the focus of this series of blog posts to look more at the more general area of wrapping and monkey patching.
Before I do get into that, in my next post I will first talk about the performance implications implicit in using the function wrapper which has been described when compared to the more traditional way of using function closures to implement decorators. This will include overhead comparisons where the complete object proxy and function wrapper classes are implemented as a Python C extension for added performance.

Comments

Popular posts from this blog

Python decorator: 5 Decorators which accept arguments

This is the fifth post in my series of blog posts about Python decorators and how I believe they are generally poorly implemented. It follows on from the previous post titled  Implementing a universal decorator , with the very first post in the series being  How you implemented your Python decorator is wrong . So far in this series of posts I have explained the short comings of implementing a decorator in the traditional way they are done in Python. I have shown an alternative implementation based on an object proxy and a descriptor which solves these issues, as well as provides the ability to implement what I call a universal decorator. That is, a decorator which understands the context it was used in and can determine whether it was applied to a normal function, an instance method, a class method or a class type. In this post, I am going to take the decorator factory which was described in the previous posts and describe how one can use that to implement decorators whic...

Python decorator: 7 The missing @synchronized decorator

This is the seventh post in my series of blog posts about Python decorators and how I believe they are generally poorly implemented. It follows on from the previous post titled  Maintaining decorator state using a class , with the very first post in the series being  How you implemented your Python decorator is wrong . In the previous post I effectively rounded out the discussion on the implementation of the decorator pattern, or at least the key parts that I care to cover at this point. I may expand on a few other things that can be done at a later time. At this point I want to start looking at ways this decorator pattern can be used to implement better decorators. For this post I want to look at the  @synchronized  decorator. The concept of the  @synchronized  decorator originates from Java and the idea of being able to write such a decorator in Python was a bit of a poster child when decorators were first added to Python. Despite this, there is no...