Python makes programming, especially graphics programming, FUN!!

Below is a set of 'topics' to explore sequentially - together, they will give you an introductory view of Python.

Python philosophy

Quote: Python is a freely available, very high-level, interpreted language developed by Guido van Rossum. It combines a clear syntax with powerful (but optional) object-oriented semantics. Python is widely available and highly portable.

Summary: Python is *simple*, yet *powerful*. Elimination of visual clutter, adhering to consistency and useful shorthand for common tasks provides this refreshing combination. For comparison, BASIC is simple but not powerful; C++ is powerful and is in no way 'simple'!

Also, Python comes with so many modules (libraries, ie. separate, bundled code) for doing everything under the sun - 'regular expressions' to UI creation to webservers to graphics to games to data acquisition - all ready to use right out of the box, in identical ways! Hence the expression "Python comes with batteries included".

Python shells

Python is interpreted, so the best way to learn it is to interactively type commands into a Python shell. Choices for the Python shell:

Calculations

Type the following (and everything else!) into Maya's 'Python' tab, select what you typed (just like with MEL), and hit Enter at the right of the keyboard (again, just like with MEL). Maya will run the Python code inside its embedded interpreter, and provide you feedback above the Script Editor window (once again, just like with MEL).

2+5

0.2

8*4.5

a = 2
b = 3
area = a*b
area
peri= 2*(a+b)
peri

Delete names:

del(a)
del(b)
del(peri)
del(area)
area # this name is not defined anymore

7/3 # integer division!

7/-3

7.0/3

7.0/-3

7.0/3

7.0//3 # floored int div, ie integer quotient

float(7)

int (7.0)

float(7)/3

int(float(7))/3 # big noop with int() and float()

int(float(7))/3.0

int(float(7))/int(3.0) # bigger noop


# modulus (excess, or remainder):
45.67%3.2
# remove the excess, divide again, to get exact quotient
(45.67 - (45.67%3.2)) / 3.2
# int quotient, float reminder both in one step:
divmod(7.666,1.0)


pow(2,4) # or 2**4
2**400
13*457 # !!
import math # for math calculations, we need the math "module"
math.sin(1.5)
from math import * # so we don't need to prefix 'math.'
sin(1.57)

dir()
dir(__builtins__)
dir(math)

'Python' ' is ' 'fun' # strings placed next to each other are auto. concatenated

raw_input("Enter a number: ") # raw_input always returns a string

int(raw_input("Enter a number: ")) # int() casts a valid 'intable' string to int

Dynamic, yet strong typing

No need to declare variables' types during creation - Python infers types. Variables stay bound to their types until they're reassigned to new types.

a = 45 # implicitly, a is declared an int
type(a)

b = 12.0
type(b)

a/b # floating point /

s = a/b # s is now a float
s[0] # not a string
type(s)

s = "Python is fun" # now it is
s[0] # Each char is a string


c = 3+4j # complex # type is built in
type(c)

c.real

c.imag

d = 1+2j

c*d # complex number mult :)

c.conjugate()

c = c.conjugate()
c
# again:
c = c.conjugate()
c

c*c.conjugate() # if c=a+ib, this is (a**2+b**2)

pow((c*c.conjugate()).real,.5) # magnitude

abs(c) # simpler way!

c = complex(3,4) # constructor

complex(3,0)

complex(3) # imag. is 0

(0.5+0.8j)**0.5

((0.5+0.8j)**0.5)**2 # :)

More on numeric types: http://python.about.com/od/pythonstandardlibrary/a/numeric_types.htm

Comments
# this is a one liner comment
'''This
comments
spans several
lines'''

No pointers/refs

Every name in Python is a ref. to an object, there are no explicit pointers/refs for the user to manipulate.

Printing
print "Complex c's value is",c # expressions, separated by commas

or use:

format % values

'values' needs to be a "list". A single element of such a list does not need to be in parantheses, but multiple items DO (see below).

a=10
b=45
print "a and b are %d and %d" % (a,b) # two items to print, specified using ( )

c = 3+4j
print "complex number c is %s" % repr(c) # just one item to print, the ( ) is optional
print "complex number c is %s" % (repr(c)) # exact same result as prev. line

d = 5 + 6j
print "c and d are %r, %r" % (repr(c),repr(d)) # two items, specified as a list

%d, %f, %r, %s etc. are 'format specifiers'. repr() is a function that returns a string representation of a non-string (eg. complex) type.

Indentation

Part of the syntax!!!!!!!!!!

Each new level of indentation starts a new *block*; these can be nested.

a = 25
 print a # error!
print a # works

Data types, structures

Built-in types:

path
import sys
print "PYTHONPATH is ",sys.path # list of dirs.

help
help()

help>modules

help>keywords

help>topics

help(5)

help(1+2j)

# HTML help!
# note: 'Tcl' needs to be installed for this to work
import pydoc
pydoc.gui()

Flow control: if, while, for [and also break, continue, pass]

These constructs (if, while, for) serve as building blocks for branching and looping.

if
a,b = 10,2
# usual comparison ops: ==, !=, <, >, <=, >=
a==b
a!=b

# for two tests, use 'and', 'or' to join; 'not' negates a single test
a!=b and b==1
a!=b or b!=2

a = 12
b = 25
if a>b:
  print a
elif a==b:
  print "a and b are both equal to %d" % a
else:
  print b
# elif and else are both independently optional

c = -5
d = 5

if a<b and c<d:
  print a
else:
  print b

elif avoids deep nested blocks like the following. It takes the place of switch() in C/C++/..

if a>b:
  print "a>b"
else:
  if a==b:
    print "a==b"
  else:
    if a==100:
      print "a is 100"

# cleaner
if a>b:
  print "a>b"
elif a==b:
  print "a==b"
elif a==100:
  print "a is 100"

while
import random
random.seed(45)
running = True
while running:
  r = random.random()
  print r
  if r>0.5:
    running = False
else: # this is optional (should have been called 'after'!)
  print "end of while()"


random.seed(450)
# try above loop again..

Fibonacci sequence:

a,b=0,1 # :)
while b<10000:
  a,b=b,a+b
  print b, float(b)/a 
# qn: what is the limit of b/a?

Note that in the above, we did multiple assignments in the same statement. These can even be done to vars of different types!

a,b = 5, "Test"

a,b = b,a # !!!!

a,b,c,d = 1,2,3,4
print a,b,c,d

a,b,c,d = b,c,d,a
print a,b,c,d

for
for i in range(1,10): # iterates NINE times, not 10
  print random.random()

range(6,12) # generates a LIST: [6, 7, 8, 9, 10, 11] 

count=0
for i in range(1,100):
  r = random.random()
  if r<0.5:
    print r
    count = count+1
else: # optional
  print "printed %d values" % count

break, continue, pass

'break' breaks out of an immediate while or for loop, before loop condition becomes false - it is an 'early' exit. When a 'break' occurs, an else: block that is present will *not* get executed.

'continue' skips rest of a loop block, to return control to the top, to start a new iteration.

'pass' is simply a no-op 'filler' statement (it means "do nothing").

a = True
while a==True:
  s = raw_input('Enter something: ')
  if s=="done" or s=="exit" or s=="quit":
    print "all done, 'break'ing"
    break
  if len(s)>5:
    print "input too long - try again"
    continue
  print "You entered ",s
  a = False
else:
  print "End of while() loop"
pass

# infinite loop!
while True:
  pass # NEED this no-op stmt. to create a valid block - eqvt to while(1){} in C/C++

dir(), type()

type(), dir(), len() etc. are examples of built-in functions.

'dir' lists names in the current module or in a named module.

dir()

import math
dir(math)

dir(__builtins__) # "built in" functions, exceptions, attrs

'type()' returns an object's type.

d=56
type(d)

d='Test'
type(d)

type(5)

type({})

type([]}

type(4+6j)

The above material was a brief introduction to Python. Here are more topics that you do need to explore, to round out your knowledge: lists, tuples and dictionaries, modules, classes and functions, "Pythonisms" (eg. list comprehension), built-in modules, file handling, documentation, exceptions, math, recursion, reflection, UI, modules for CG and math..

To use Maya-related commands, use the 'maya.cmds' module.

import maya.cmds # our 'hook' into the world of Maya!!!

a = dir(maya.cmds)
for i in range(len(a)):
  print a[i]

maya.cmds.sphere() # equivalent to MEL's 'sphere()'

Maya - UI creation

Here is an example of UI creation. We use the same window, layout, widgets, showWindow sequence as we did in MEL, but now in Python syntax.

# here is a common 'as' idiom, to save some typing
# (maya.cmds.() can simply be mc.())

# select a bunch of nodes, then run the following

import maya.cmds as mc


def createMyLayout( obj, parent ):
    mc.setParent(parent)
    c = mc.columnLayout()
    text = mc.textField(text=obj)
    button1 = mc.button(label="one")
    button2 = mc.button(label="two")
   
    return c

myLayouts = []
w = mc.window("example", title="Sample UI creation")
c = mc.columnLayout()
for selectedObject in mc.ls(sl=True):
    myLayouts.append(createMyLayout(selectedObject, c))
mc.showWindow(w)

Functions, classes!

As we saw above, a function is defined using 'def', and is called (after defining) just like in MEL.

def sind(x):
  import math
  return math.sin(x*math.pi/180)

In the above, we're recreating MEL's useful sind() function. The 'def' keyword starts off the fn defn; next is the function's name, then comes the arg list (just like in MEL - but HUGE diff - only var names, NO TYPES!). In the body of the fn, one or more 'return' calls return values..

Once a fn has been defined, use it (call it) just like in MEL..

sind(45)
  
for i in range(90):
  print sind(i)

A huge benefit/feature: a fn can return a tuple!!!!!!

The above means that a Python fn can be written to output possibly MULTIPLE items, of possibly MULTIPLE types! This is a huge no-no in other languages, so programmers use sneaky techniques to get around this. In Python, we'll output multiples openly and with blessings :)

# returns a hodgepodge type of 7 items, as a tuple, in a single return call!!
def howCool():
  return (1,0.56,(),[5,"Test",5-3j],[],7.45,[[]])
  
# this alone is worth switching to Python :) 
a,b,c,d,e,f,g = howCool()
a
b
c
d
e
f
g
g[0] 
g[0][0] # not defined, so will generate an error


Python functions can recurse:

def fact(n):
  if n==1:
    print "End of recursion, returning 1"
    return 1
  else:
    print "Recursing: returning",n,"*","fact(",n-1,")"
    return n*fact(n-1)
    
fact(45) #!!!!!

Try fact(105)!! FYI it has 169 digits, which means they can be arranged like so (1+3+5+7+9+11+13+15+17+19+21+23+25=169):

                                        1                        
	                               081                      
	         	              39675                    
	        	             8240290                  
	       	                    900504101                
		                   30580032964              
	    	                  9720646107774            
    		                 902579144176636          
    		                57322653190990515        
  		               3326984536526808240      
 	 	              339776398934872029657    
	 	             99387290781343681609728  
		            0000000000000000000000000

For more on 105!, see http://webdocs.cs.ualberta.ca/~smillie/APE/APE43.html

Classes are used to group together related variables and functions into a useful unit. Once created, a class can be instantiated into an object, whose methods can be get/set and functions invoked. Objects can communicate with each other by calling functions on them. The class/object mechanism is the way to create custom types.

Here is a basic example, to give you a taste. Overall idea - define a class (with a name, acting as a container), place fns inside it, create "objects" (instances, ie rubber stamps, of the class), call fns as usual, but call them as 'object.function_name'.

class A:
  '''Class A, with two methods f1 and f2'''
  def f1(self): # note the 'self' keyword
    print "Running A.f1()"
  def f2(self):
    print "Running A.f2()"


a = A() # a is now a variable (object) of type (class) A

type(A)
type(a)

A
a

dir(A)
dir(a)


A.__doc__
A.__module__


a.f1() # invoke a's f1() method
a.f1(self) # error - no need to (can't) pass in 'self' - that keyword is valid only inside a class def.

a.f2()

'Packages'

The Maya site-packages directory contains paths to all sorts of packages, and contains packages too..

What are packages? They are collections of modules!

import maya # 'maya' is a package, contains maya.cmds module and others..
import os
print os.path.dirname(maya.__file__)

dir(maya) # lists all the modules in the package
dir(maya.cmds) # lists contents of the maya.cmds module

Lists, map(), "list comprehension"

l = [] # an empty list, fillable (unlike a tuple!)
l.append(1)
l.append('test')
l
l.__contains__(7)
l.__contains__('test')

l[1]
len(l)


# print each elemnt in a list
for a in l:
  print a

# roundabout:
for a in range(len(l)):
  print l[a]

l.append([]) # empty list is appended to our list
l

l[2].append('r')
l # can insert elements into a list's list element!
for i in range(10):
  l[2].append('r')
l

l[2] = {} # last list element is replaced with {}
l

del(l[1])
l

m = l + [1,2,3,9,8,7] # + concatenates lists
m
m += m

map() is a function used to iterate a function over a list or several lists. Output is a list of the function's results.

# iterate sind() for each el in range(0,360)
# replaces the for() version shown earlier!
snf = map(sind,range(0,360)) 
snf
# qn: above prints the list in a single line - how would you print
# each item in a separate line?

'List comprehension' is an alternate way to do this kind of function looping (over a range of inputs), esp. for simpler functions. Example:

li = [1,3,5,7]
li
[item*2 for item in li]
li = [item*2 for item in li] # input (li) is a list, so is the output!
li

We can safely overwrite a list variable with its processed version (which is copied to the variable AFTER getting fully formed in memory).

Q. Where is list comprehension useful?

A. provides a useful shortcut for this common pattern:

for item in list
  if condition on item
   process item, accumulate for output

The 'list comprehension' version is

[expr for item in li if condition]

Example: li = [1,2,3,4,5,6,7,8,9,10] li = [x*x for x in li if x%2 == 0] li # prints [4, 16, 36, 64, 100]

In the above, the input is a list, so is the output, which is a PROCESSED VERSION of the input list!

In summary - following are 3 eqvt ways to create this list: [0,.1,.2..0.9]

l = []
for i in range(10):
 l.append(float(i)/10)

def decimalify(x):
  return float(i)/10.0

l = map(decimalify, range(10))

l = [decimalify(i) for i in range(10)] # optionally, *can* have an if 

What's next? PyMEL (and a bit of maya.cmds)

From the doc description for PyMEL:

PyMEL makes python scripting in Maya work the way it should. Maya’s command module is a direct translation of MEL commands into python functions. The result is a very awkward and unpythonic syntax which does not take advantage of python’s strengths – particularly, a flexible, object-oriented design. PyMEL builds on the cmds module by organizing many of its commands into a class hierarchy, and by customizing them to operate in a more succinct and intuitive way.

In other words, maya.cmds is OK, pymel is better!


MAYA -ver 2011.1.0_64


import maya.cmds 
import maya.mel
from pymel.core import *

dir() # all the 'MEL' cmds!


for n in maya.cmds.ls():
  print type(n)
# oh no!!! maya.cmds is simply MEL in Python's clothing :(

for n in ls():
  print type(n)
# THIS is why PyMEL is WAAAAAAAAAAAAAAAAAAY BETTER!!!!!!!!!



values = ['one', 'three', 'two', 'three', 'four']
maya.mel.eval( 'stringArrayRemoveDuplicates( {"'+'","'.join(values)+'"})')

values = ['one', 'three', 'two', 'three', 'four']
mel.stringArrayRemoveDuplicates( values )

mel.eval( '''global proc myScript( string $stringArg, float $floatArray[] ){ float $donuts = `ls -type camera`;}''')
mel.myScript('test',[]) # MelConversionError is thrown

All globals are in a dictionary:
melGlobals['$gMainFileMenu']
melGlobals['$gGridDisplayGridLinesDefault'] = 15

sphere()
// in MEL:
string $sel[] = `ls -sl`;
string $shapes[] = `listRelatives -s $sel[0]`;
string $conn[] = `listConnections -s 1 -d 0 $shapes[0]`;
setAttr ($conn[0] + ".radius") 1;

# Back in Py:
selected()[0].getShape().inputs()[0].radius.set(12)

# intelligent nodes..
camTrans, cam = camera()  # create a new camera
cam.setFocalLength(100)
fov = cam.getHorizontalFieldOfView()
fov
cam.dolly( -3 )
cam.track(left=10)

dir(cam) # all of cam's methods/members


s = polySphere()[0]

dir(s)

if s.visibility.isKeyable() and not s.visibility.isLocked():
    s.visibility.set(False)
    s.visibility.lock()
    print s.visibility.type()

dir(s.getChildren()) # what are we seeing?? 


dir(s.getChildren()[0])

maya.mel.eval("curve -d 3 -p 0 0 0 -p 1 0 0 -p 0 1 0 -p 0 0 1")
mel.eval("curve -d 3 -p 0 0 0 -p 1 0 0 -p 0 1 0 -p 0 0 1")

mel.eval("sphere -r 5")
eval('camera()')

basedir = sceneName().parent

s = polySphere()[0]
for face in s.faces:
    if face.getNormal('world').y > 0.0:
       select( face, add=1)


if 'numbers' not in optionVar:
    optionVar['numbers'] = [1,24,47]
optionVar['numbers'].append(9)
numArray = optionVar.pop('numbers')


camXform, camShape = camera()
sphere = polySphere()[0]
sphere | camXform  # parent the camera to the sphere

camXform.tx >> camXform.ty  # connect operator
camXform.tx // camXform.ty  # disconnect operator

# print help for camShape's methods (but not members)
for i in dir(camShape):
    if (type(eval('camShape.%s' % i))) == type(camShape.tumble):
        help(eval('camShape.%s' % i))


from pymel.core import *

s = sphere()
for i in s:
  print s
  
help(nt.Transform)

PyMEL is nicely O-O..

cam = ls(type='camera')[0]
cam
help(nt.Camera)
cam.getAspectRatio()

Once you learn PyMEL, you can learn writing 'script plugins' (.py plugins, instead of .c++ ones!).

Here is a taste.. Save the foll. as helixCmd.py, and load it via the plugin mgr:


#-
# ==========================================================================
# Copyright (C) 1995 - 2006 Autodesk, Inc. and/or its licensors.  All 
# rights reserved.
#
# The coded instructions, statements, computer programs, and/or related 
# material (collectively the "Data") in these files contain unpublished 
# information proprietary to Autodesk, Inc. ("Autodesk") and/or its 
# licensors, which is protected by U.S. and Canadian federal copyright 
# law and by international treaties.
#
# The Data is provided for use exclusively by You. You have the right 
# to use, modify, and incorporate this Data into other products for 
# purposes authorized by the Autodesk software license agreement, 
# without fee.
#
# The copyright notices in the Software and this entire statement, 
# including the above license grant, this restriction and the 
# following disclaimer, must be included in all copies of the 
# Software, in whole or in part, and all derivative works of 
# the Software, unless such copies or derivative works are solely 
# in the form of machine-executable object code generated by a 
# source language processor.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND. 
# AUTODESK DOES NOT MAKE AND HEREBY DISCLAIMS ANY EXPRESS OR IMPLIED 
# WARRANTIES INCLUDING, BUT NOT LIMITED TO, THE WARRANTIES OF 
# NON-INFRINGEMENT, MERCHANTABILITY OR FITNESS FOR A PARTICULAR 
# PURPOSE, OR ARISING FROM A COURSE OF DEALING, USAGE, OR 
# TRADE PRACTICE. IN NO EVENT WILL AUTODESK AND/OR ITS LICENSORS 
# BE LIABLE FOR ANY LOST REVENUES, DATA, OR PROFITS, OR SPECIAL, 
# DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES, EVEN IF AUTODESK 
# AND/OR ITS LICENSORS HAS BEEN ADVISED OF THE POSSIBILITY 
# OR PROBABILITY OF SUCH DAMAGES.
#
# ==========================================================================
#+

#
#	Creation Date:   2 October 2006
#
#	Description:
#
#		Creates a NURBS curve in the shape of a helix
#
#	Options:
#
#		p=#		The pitch of the helix, default to 0.5
#		r=#		The radius of the helix, default to 4.0
#
#	Example:
#
#		From Python:
#			import maya
#			maya.cmds.spHelix(p=0.3, r=7)
#
#		From Mel:
#			spHelix -p 0.3 -r 7
#

import maya.OpenMaya as OpenMaya
import maya.OpenMayaMPx as OpenMayaMPx
import sys, math

kPluginCmdName="spHelix"

kPitchFlag = "-p"
kPitchLongFlag = "-pitch"
kRadiusFlag = "-r"
kRadiusLongFlag = "-radius"

# command
class scriptedCommand(OpenMayaMPx.MPxCommand):
	def __init__(self):
		OpenMayaMPx.MPxCommand.__init__(self)
	
	def doIt(self, args):
		deg = 3
		ncvs = 20
		spans = ncvs - deg
		nknots = spans+2*deg-1
		radius = 4.0
		pitch = 0.5
		
		# Parse the arguments.
		argData = OpenMaya.MArgDatabase(self.syntax(), args)
		if argData.isFlagSet(kPitchFlag):
			pitch = argData.flagArgumentDouble(kPitchFlag, 0)
		if argData.isFlagSet(kRadiusFlag):
			radius = argData.flagArgumentDouble(kRadiusFlag, 0)

		controlVertices = OpenMaya.MPointArray()
		knotSequences = OpenMaya.MDoubleArray()

		# Set up cvs and knots for the helix
		#
		for i in range(0, ncvs):
			controlVertices.append( OpenMaya.MPoint( radius * math.cos(i),
				pitch * i, radius * math.sin(i) ) )

		for i in range(0, nknots):
			knotSequences.append( i )
		
		# Now create the curve
		#
		curveFn = OpenMaya.MFnNurbsCurve()
		
		nullObj = OpenMaya.MObject()

		try:
			# This plugin normally creates the curve by passing in the
			# cv's.  A function to create curves by passing in the ep's
			# has been added.  Set this to False to get that behaviour.
			#
			if True:
				curveFn.create( controlVertices,
								knotSequences, deg, 
								OpenMaya.MFnNurbsCurve.kOpen, 
								0, 0, 
								nullObj )
			else:
				curveFn.createWithEditPoints(controlVertices,
								3, OpenMaya.MFnNurbsCurve.kOpen,
								False, False, False)
		except:
			sys.stderr.write( "Error creating curve.\n" )
			raise

# Creator
def cmdCreator():
	# Create the command
	return OpenMayaMPx.asMPxPtr( scriptedCommand() )

# Syntax creator
def syntaxCreator():
	syntax = OpenMaya.MSyntax()
	syntax.addFlag(kPitchFlag, kPitchLongFlag, OpenMaya.MSyntax.kDouble)
	syntax.addFlag(kRadiusFlag, kRadiusLongFlag, OpenMaya.MSyntax.kDouble)
	return syntax

# Initialize the script plug-in
def initializePlugin(mobject):
	mplugin = OpenMayaMPx.MFnPlugin(mobject, "Autodesk", "1.0", "Any")
	try:
		mplugin.registerCommand( kPluginCmdName, cmdCreator, syntaxCreator )
	except:
		sys.stderr.write( "Failed to register command: %s\n" % kPluginCmdName )
		raise

# Uninitialize the script plug-in
def uninitializePlugin(mobject):
	mplugin = OpenMayaMPx.MFnPlugin(mobject)
	try:
		mplugin.deregisterCommand( kPluginCmdName )
	except:
		sys.stderr.write( "Failed to unregister command: %s\n" % kPluginCmdName )
		raise

	

Now run the plugin:

import maya
maya.cmds.spHelix(p=0.3, r=7) # voila!!!!!

Finally, Here is a MEL to Python 'transition' guide (very barebones!)..

Here is more on PyMEL; also take a look at this.