Decorators & Contexts 101

Decorators and contexts are really useful. They let you wrap functions in pre-defined code. They can be incredibly useful for solving a wide array of issues. Here are some examples:

  • Timing how long a function takes to run.
    • I time functions all the time. Having a single line I can add to my function that will tell me how long it took is a great short cut.
  • Clean up Maya dirty scenes.
    • Sometimes you want to do something to your Maya scene that is destructive. Exporters do this all the time. The code could potentially leave the scene in a bad state, and you dont want to save that. You can add a context that saves the scene, does the job, and then restores the scene so it’s left as it was before you started.
  • Restore Maya settings.
    • Animators like to keep AutoKey on. Some tools don’t work well with that on. A decorator could make sure that the setting is off, run the function, and then turn it back on.
    • Batch process is slow if your viewport is updating for every file load. You can make a decorator that will stop updating the viewport while the batch process is happening.

See? They can be quite convenient. I wrote a file manager once that locked, copied, merged, and unlocked files on a server as users opened, changed, and closed those files. Without decorators, the code would have had a ton of repeated code that would then have to be maintained. I saved myself a monster headache by making decorators for all file operations.

Enough pontificating, let us make a context and a decorator.


time_it()

For this example we will write a context and a decorator for timing code and functions. This is a good example, since the solution is simple, and we can focus on the context and decorator.


1.1 Context

Step 1. write the code that solves the problem.

We want to time a block of code. This will solve that:

# record initial time:
time_start = timeit.default_timer()

# ---------------------------------
# code to be timed:
# ---------------------------------

# record time after code ran:
time_end = timeit.default_timer()

# inform the user:
print('Timed result : {0:.20f} sec'.format(time_end - time_start))

Step 2. Make it into a function:

We want to be able to import this code into other modules and run it. So it needs to be saved in a module as a function.

We need to import contextlib. It’s a pretty standard Python library, you should have no problems importing it.

import contextlib

@contextlib.contextmanager
def time_this():
	"""
	Context for timing blocks of code.
	"""
	
	# record initial time:
	time_start = timeit.default_timer()

	try:
		# Here is where/when the code to be timed will run:
		yield

	finally:
		# record time after code ran:
		time_end = timeit.default_timer()

		# inform the user:
		print('Timed result : {0:.20f} sec'.format(time_end - time_start))

To understand this function, you must understand two concepts: try/except/finally, and yield.

try/finally:

It’s not too complicated. You are asking Python to “try” to run some code. If it fails, it will skip the failed code and continue running the rest of the function. Then, “finally” will always run, regardless of the success of “try”. So in our example, regardless of weather or not the code evaluates correctly, the function will tell you how long it took.

yield:

This one is a bit tricky. I think I’ll do a much more in-depth discussion about yield at a later date. For now, assume that “yield” passes the evaluation stream to the code you want to time. It’s like driving. You yield to a pedestrian, which makes you stop, so he can start. When he is done crossing the street, you start up where you left off (you dont go back home and start the trip again). It’s not exactly this, but for now it will do.

So how do we use this function? It’s not hard. Once you have it written, you can use it all over the place. You can even forget how to time scripts, now.

from time_module import time_this

with time_this:

	# ---------------------------------
	# code to be timed:
	# ---------------------------------

1.2 Decorator

Contexts are great, but you have to write them in every time you want to use them. What if we could tell Python, that every time it calls a specific function, it would time it? This way we would not have to write the with context statement every time.

This is what decorators are for. Granted, timing a function is not a great use of a decorator, but it’s not a bad one either. Lets use it.

Step 1. Write the code that solves the problem.

We did that already on the example above. So, moving along…

Step 2: Make a function out of it.

For the same reasons as above (importing from another module), we need a function. This function, however, will be a bit different.

Instead of having code to run, a decorator evaluates a function every time that function is called. Think of it as a wrapper for the function, except the function is variable. We need to be able to tell the wrapper what function to evaluate. Confusing?

Ok, let me try again.

We are going to tell Python that every time we call function A(), we want it to print how long it took to evaluate. We are also telling Python that we want the same treatment for functions B(), C() and D(), but we only want to write the wrapper once.

So first things first, we need a function that accepts a function as an argument:

def time_it(func):

If you have written wrappers before, you know that you need to capture the return from the main function and return it in the wrapper. If you dont do this, you lose the data you got from the function.

To do this, we define a function inside the wrapper that will capture that result.

def time_it(func):

    # with this function, we capture and return the return value:
    def timed():

        # function to be timed:
        result = func()

        return result

    return timed

As you can see, we are storing the return of the function in a variable called result. Then we return result.

We need time_this() to evaluate func, right? But what if func has arguments? I mean func can be any function at all, with any number and style of arguments. So how do we deal with that?

Simple, *args and *kwargs are your friends here.

def time_it(func):

    # with this function, we capture and return the return value:
    # add args and kwargs to be able to pass any and all args of the func:
    def timed(*args, **kwargs):

        # function to be timed:
        result = func(*args, **kwargs)  # pass the arguments to the function

        return result

    return timed

Great, we now have a decorator that calls the function it passes, and it handles the arguments well. Unfortunately, it does nothing else. So lets add the timing code to it:

def time_it(func):

    # with this function, we capture and return the return value:
    # add args and kwargs to be able to pass any and all args of the func:
    def timed(*args, **kwargs):
        
        # record initial time:
        time_start = timeit.default_timer()

        # function to be timed:
        result = func(*args, **kwargs)  # pass the arguments to the function

        # record time after code ran:
        time_end = timeit.default_timer()

        # inform the user:
        print('Timed func : {}'.format(func.__name__))
        print('Timed result : {0:.20f} sec'.format(time_end - time_start))
        
        return result

    return timed

This function should work now for you now. But how do we use it? I really like how this is used. Once ready, check out how easy it is to use a decorator:

from time_module import time_it

@time_it  # <- Right here. Add this to any function you want timed.
def my_function(args):
	print args

Adding the line @time_it above any function declaration will tell Python you want it timed every time it runs. If you were to call my_function(“hello”) in a for loop, you would get a “hello” for every iteration of the loop.


And that’s mostly it. while there are more things we can discuss, this is a good intro to decorators and contexts. I hope this helps. Please leave me a comment with questions, corrections, suggestions, or ideas on how to make this page better. Thank you!

// Isoparm

Discussion – Python Readability

Readability > purity, and sometimes, even practicality. I have a pretty bad memory. When it comes to code it lasts about 2 months (when I’m lucky). This means that if I write a module in June, by September I can’t tell you how the module works, just what it does.

I love writing code. I hate reading code. I had a mentor tell me once that it takes 100% of your brain to write a module, but that reading that module is much harder. He was right. He was so right, in fact, that reading code for me is almost an exercise in futility. I curse at the person who wrote it. I hate reading code.

I have to take steps to make sure I CAN read code. For years I didn’t care about readability and I paid the price in frustrations. One fine day, after I had written a particularly difficult module (for me, at the time), I tried reading it as if I had encountered it for the first time. I started making changes, simple changes, to make it easier to read.

About 4 months later, I had to fix a bug on it. I opened it and I was able to read it!

Now, I go out of my way to make my code as readable as I can. I assume that when I get back to it, it will be in 10 years, and I’ll be half drunk. That way, when I do get back to it, I can read it and understand it quickly.

I also believe that the code itself needs to be readable, not just well commented. Comments are great and I will swear by them all day long, however, I need the code to be readable or comments will be completely lost on me.

Some examples:


# this is not very readable (what is an "isdir" anyway): 
if os.path.isdir("C:\file_path"): 

# this feels better, i think: 
if pyfile.is_dir("C:\file_path"): 

Not convinced? Ok, next example: getting the influence list from a skinned mesh in Maya.

Putting aside the fact that you have to feed it a skinCluster instead of a mesh, the default Maya command is still confusing. Since the commands can be used in query mode or command mode, I end up having to scan the code for “query=”. I dont like it.

# Maya default: 
influences = cmds.skinCluster(cluster, query=True, influence=True) 

# how do you feel about this one? 
influences = myskin.influences(mesh) 

Still not convinced? Lets try to make a Maya reference.

# honestly, I can't read this. 
cmds.file("C:\file_path", reference=True, ignoreVersion=True, namespace=prefix) 

# That's much better. 
reference.create("C:\file_path", namespace) 

If this last example does not convince you, nothing will.

# get the name of a file: 
name = os.path.basename(os.path.splitext(file_path)[0])  #WTF?! 

# or: 
name = pyfile.name(file_path)

Trust me on this, your future self will be very thankful that you spent the time making sure he could read your chicken scratches.

So, what do you think? Comment away, and we can have a fun discussion.

// Isoparm