What is going on in this SimPy Monitoring example?
05 Apr 2017What is going on in this SimPy Monitoring example?
This post is long and rambling (not unlike a midsummer Sunday’s walk around the Surrey Hills) so you may get frutsrated and angry at the author while reading this. The author wishes to apologize to her readers in advance.
SimPy is a simulation library that you can use to easily create event driven simulations in Python. It’s great to play around with if you like simulating queues, traffic systems and monitoring the usage of shared resources ( for example, the number of seats currently occupied on a train or the number of cashiers free at a checkout queue ). In the simplest cases, you can have SimPy print out the usage statistics for your shared SimPy resources to stdout so that you can monitor it, but for longer and more complex situations, it is better to have an automated simulation data collection tool.
Luckily, the SimPy documentation comes in handy and the authors have included a helpful example, which will allow you to patch a SimPy Resource
object and monitor it during the simulation. I have reproduced it, with a few comments removed for the sake of brevity, below. Decorators and functools are a bit of a deep sea for me, so in this blogpost I am going to clarify what is going on in the patch_resource
function and why it can be used to monitor resource usage in SimPy simulations.
Patching resources in SimPy
from functools import partial, wraps
import simpy
def patch_resource(resource, pre=None, post=None):
def get_wrapper(func):
@wraps(func)
def wrapper(*args, **kwargs):
if pre:
pre(resource)
ret = func(*args, **kwargs)
if post:
print 'Calling post function'
post(resource)
return ret
return wrapper
for name in ['put', 'get', 'request', 'release']:
if hasattr(resource, name):
setattr(resource, name, get_wrapper(getattr(resource, name)))
Whoah, whoah there - functools, decorators, some strange @wraps(func) thing! - let’s take it one step at a time!
Functools
Functools is a Python library for higher-order functions - functions that take other functions as arguments, manipulate functions and return new functions. In the patch_resource
method, we use two items from the functools module: wraps
and partial
. Let’s take a look at wraps
.
According to the official documentation wraps
is a “convenience function for invoking update_wrapper
“, another method in the functools
module, as a decorator (as the SimPy documentation authors have done in the example above). After reading the documentation for update_wrapper
, I’m not really sure what it does - at this point it might be better to poke the function instead of trying to decipher what the documentation means. The update_wrapper
function takes in a wrapper
function and a wrapped
function as arguments, so I am going to setup a simple simplefunc
that prints out ‘hello, world’ and a wrapper
func, which pretends to do something useful before and after calling simplefunc
.
from functools import update_wrapper
def simplefunc():
print 'hello, world'
def wrapper(func, *args, **kwargs):
print 'Doing some stuff before calling func'
value = func(*args, **kwargs)
print 'Doing some stuff after calling func'
return value
obj = update_wrapper(wrapper, simplefunc)
According to the documentation, the wrapper
function should now look like the wrapped
function. If I understood it correctly, I should now be able to call wrapper
like simplefuncs
.
obj()
results in
>>> obj()
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: wrapper() takes at least 1 argument (0 given)
so I have clearly missed something. A quick googling lands me on this SO answer and reveals that update_wrapper
has nothing to do with the function signatures. What it does, instead, is carries over important dunder attributes from the original function to the new wrapped function. For example, if we rework the simplefunc
and wrapper
example above to the following:
def simplefunc():
'''print hello world'''
print 'hello world'
def decoratorfunc(f):
def wrapper(*args, **kwargs):
print 'f.__name__', f.__name__
value = f(*args, **kwargs)
return value
return wrapper
newFunc = decoratorfunc(simplefunc)
newFunc()
The output is something like this
>>> newFunc()
f.__name__ simplefunc
f.__doc__ print hello world
hello world
However, if we try to print out the name and doc properties of newFunc
, which is essentially our simplefunc
, just wrapped with some helpful methods, we get the following
>>> newFunc.__name__
'wrapper'
>>> newFunc.__doc__
>>>
The docstring and name of the original simplefunc
have been lost. This is where update_wrapper
or its decorator equivalent can come in handy. Let’s rewrite the example above to user the @wraps
syntax:
from functools import wraps
def decoratorfunc(f):
@wraps(f)
def wrapper(*args, **kwargs):
print 'f.__name__', f.__name__
value= f(*args, **kwargs)
return value
return wrapper
newFunc = decoratorfunc(simplefunc)
newFunc()
print newFunc.__name__
print newFunc.__doc__
We get the following output
>>> newFunc()
f.__name__ simplefunc
hello world
>>> newFunc.__name__
'simplefunc'
>>> newFunc.__doc__
'print hello world'
>>>
As the example shows, update_wrapper
or its decorator equivalent @wraps
preserves the original function’s attributes in the decorated function.