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

Env Setup – Part 1.0 – Software Needed For Maya/Python Development

First off, we need to have an appropriate environment. In order for us to be set up for success, we need to well… set ourselves up for success. I spent years using the “wrong” IDE. I fought with it, I yelled at it, i even re installed it a few times thinking it would fix my problems. For the longest time i was convinced that the problem was me, not the software. While there are many right answers to the question of the “Best IDE for Python” questions, there are definitely some wrong ones. So don’t be like me, pick a better one.

Here is the list of software we need. Some of it costs money… well it all costs money, but some of it can be used in trial, education, or limited versions. I will attempt to have an accessible environment during this project, so we can all speak the same language.

  1. Maya. Well sure, this is a given, but I had to put it in here. The software is not exactly cheap, but there are some things you can do. If you are a student enrolled in a school, you probably have access to an education version (that one will do nicely). You can also try out the software for 30 days (we probably wont be finished by then, but 30 days should be enough to figure out if you want to continue).
  2. Python 2.7. Unfortunately, Autodesk has not moved on from Python 2.x to 3.x. This means that we need to do our work in an older version of Python. Since Autodesk is pretty much a monopoly when it comes to rigging, then we must make do. Make sure you download 2.7.x, and not 3.x.
  3. PyCharm. This is my go-to IDE for Python. It integrates nicely with Maya and with Github.
  4. Sublime Text. Why have 2 IDEs? Well, there are some nice things about having a light weight text editor that is fully featured. I use it quite a bit. If you are on a  tight budget, you can skip this one.
  5. Github account. I will share all the code written for this project in Github, so it will be helpful to have an account so you can follow along. To setup Git, follow these instructions: Set Up Git.

OK, now go install all the software and come back, I’ll wait.

// Isoparm

Next: Connect Pycharm and Github

Env Setup – Part 1.2 – Connect PyCharm To Maya

In order for our dev environment to be complete, we need to have PyCharm debug code in Maya. This is something that you ‘could’ live without. I recommend you have this done because it will make your life much easier.

I like to use MayaCharm. It’s a PyCharm plugin that has a Python interpreter for PyCharm that emulates Maya. Also, it can connect directly with Maya to use PyCharm as a debugger. It’s all kinds of wonderful.

If you are interested on the Github page, or want to see the docs, here they are MayaCharm documentation.

For the quick and easy install, it’s best if you do it from inside PyCharm.

Go to File / Settings …

pyCharm_maya_connect_01

On the Search Bar, type “MayaCharm“. Only one plugin will come up. Install it and restart PyCharm.

You are not done yet. You still have to set up your Python interpreter for your current PyCharm project. So go back to your Settings Window ( File / Settings … ). Click on the “Project Interpreter” drop down, and select “Show All…

pyCharm_maya_connect_02

A new window appears. Here you need to point PyCharm to mayapy.exe (which is the Maya Python interpreter).

pyCharm_maya_connect_03
Click the “+” button
pyCharm_maya_connect_04
Select the System Interpreter on the left, and click on the “…” button.
pyCharm_maya_connect_05
Navigate to mayapy.exe in your Maya install folder

Click “OK” and you should now see the mayapy.exe interpreter added to your Project Interpreters Window:

pyCharm_maya_connect_06

Depending on your version of PyCharm, you might need to install the Python Packaging Tools. If it asks for it, do so.

At this point you need to restart PyCharm again. Even if you restarted it for the Python Packaging Tools, you need to restart anyway. If you don’t, the Active Maya SDK section of the Settings Window will be empty and you wont be able to continue. So

pyCharm_maya_connect_07

You now have to tell Maya to listen for incoming commands from PyCharm. So navigate to your userSetup.py file. This file is normally located here: “C:\Users\…user…\OneDrive\Documents\maya\2018\scripts

Open it and add these lines:

import maya.cmds as cmds

if not cmds.commandPort(':4434', query=True):
    cmds.commandPort(name=':4434')

Notice how the port name in the code is 4434, and the port in the MayaCharm Settings Window is also 4434. These number HAVE to match in order for the connection to work.

Alternatively, you could run these lines directly from the Script Editor in Maya if you wanted. This will work as well, but you will have to do it every time you open Maya. On the one hand, you don’t worry about Maya keeping the port open when you are not using PyCharm, and on the other, you have to run the command every time you want to debug with PyCharm. Ultimately, the choice is yours.

You are done with the Maya / PyCharm set up!

pyCharm_maya_connect_08

In the “Run” menu at the top of PyCharm, there are now 3 new lines. These will send the commands to Maya.

Try it out.

highlight a print command and watch it print in the Maya Script History.

I hope this worked out for you guys. If not, leave me a note with what problems you have and we will troubleshoot it together.

// Isoparm

Env Setup – Part 1.1 – Connect PyCharm And Github

Lets setup PyCharm / Github.

You need a Github account, so make one.

Make a new Github Repository. The project I will be starting out using is this one: https://github.com/isoparms/dis.shared. I recommend you create a similar one.

github_newRepo
github_newRepo_options

Yay, you have a repository! Now we will clone it to your system.

Open GitBash (it got installed when you installed Git).

this is the clone command:

git clone https://github.com/isoparms/dis.shared C:\code_folder\disorder_project\shared

Once cloned, you can pull (get latest from Github) with this command:

cd C:\code_folder\disorder_project\shared
git pull

Alright, you have your repository set up. Now we move to PyCharm.

Make a File \ New Project

Use the same folder as the one you used for your repository.

Set up Git: File \ Settings

pyCharm_git_connect_01
pyCharm_git_connect_02

Cool, we are almost done. Now make a change, add a file, etc. Then press Ctrl + K. This is how you commit or check in, so memorize it.

pyCharm_git_connect_03

PyCharm will prompt you for your account and password. Make sure you make your settings global.

Now you are done.

Make sure you make all changes repository through PyCharm. Add, remove, edit, and even copy and paste files using PyCharm. That way, Github will know what you are doing at all times, so all you have to do, is commit every now and then.

Enjoy!

//Isoparm

Next: Connect PyCharm To Maya