10. Subroutines

Introduction

#-----------------------------
# DO NOT DO THIS...
greeted = 0
def hello():
    global greeted
    greeted += 1
    print "hi there"

#... as using a callable object to save state is cleaner
# class hello
#     def __init__(self):
#         self.greeted = 0
#     def __call__(self):
#         self.greeted += 1
#         print "hi there"
# hello = hello()
#-----------------------------
hello()                 # call subroutine hello with no arguments/parameters
#-----------------------------

Accessing Subroutine Arguments

#-----------------------------
import math
# Provided for demonstration purposes only.  Use math.hypot() instead.
def hypotenuse(side1, side2):
    return math.sqrt(side1**2 + side2**2)

diag = hypotenuse(3, 4)  # diag is 5.0
#-----------------------------
print hypotenuse(3, 4)               # prints 5.0

a = (3, 4)
print hypotenuse(*a)                 # prints 5.0
#-----------------------------
both = men + women
#-----------------------------
nums = [1.4, 3.5, 6.7]
# Provided for demonstration purposes only.  Use:
#     ints = [int(num) for num in nums] 
def int_all(nums):
    retlist = []            # make new list for return
    for n in nums:
        retlist.append(int(n))
    return retlist
ints = int_all(nums)        # nums unchanged
#-----------------------------
nums = [1.4, 3.5, 6.7]

def trunc_em(nums):
    for i,elem in enumerate(nums):
        nums[i] = int(elem)
trunc_em(nums)               # nums now [1,3,6]

#-----------------------------
# By convention, if a method (or function) modifies an object
# in-place, it returns None rather than the modified object.
# None of Python's built-in functions modify in-place; methods
# such as list.sort() are somewhat more common.
mylist = [3,2,1]
mylist = mylist.sort()   # incorrect - returns None
mylist = sorted(mylist)  # correct - returns sorted copy
mylist.sort()            # correct - sorts in-place
#-----------------------------

Making Variables Private to a Function

#-----------------------------
# Using global variables is discouraged - by default variables
# are visible only at and below the scope at which they are declared.
# Global variables modified by a function or method must be declared 
# using the "global" keyword if they are modified
def somefunc():
    variable = something  # variable is invisible outside of somefunc
#-----------------------------
import sys
name, age = sys.args[1:]  # assumes two and only two command line parameters
start = fetch_time()
#-----------------------------
a, b = pair
c = fetch_time()

def check_x(x):
    y = "whatever"
    run_check()
    if condition:
        print "got", x
#-----------------------------
def save_list(*args):
    Global_List.extend(args)
#-----------------------------

Creating Persistent Private Variables

#-----------------------------
## Python allows static nesting of scopes for reading but not writing,
## preferring to use objects.  The closest equivalent to:
#{
#    my $counter;
#    sub next_counter { return ++$counter }
#}
## is:
def next_counter(counter=[0]):  # default lists are created once only.
    counter[0] += 1
    return counter[0]

# As that's a little tricksy (and can't make more than one counter),
# many Pythonistas would prefer either:
def make_counter():
    counter = 0
    while True:
        counter += 1
        yield counter
next_counter = make_counter().next

# Or:
class Counter:
    def __init__(self):
        self.counter = 0
    def __call__(self):
        self.counter += 1
        return self.counter
next_counter = Counter()

#-----------------------------
## A close equivalent of
#BEGIN {
#    my $counter = 42;
#    sub next_counter { return ++$counter }
#    sub prev_counter { return --$counter }
#}
## is to use a list (to save the counter) and closured functions:
def make_counter(start=0):
    counter = [start]
    def next_counter():
        counter[0] += 1
        return counter[0]
    def prev_counter():
        counter[0] -= 1
        return counter[0]
    return next_counter, prev_counter
next_counter, prev_counter = make_counter()

## A clearer way uses a class:
class Counter:
    def __init__(self, start=0):
        self.value = start
    def next(self):
        self.value += 1
        return self.value
    def prev(self):
        self.value -= 1
        return self.value
    def __int__(self):
        return self.value

counter = Counter(42)
next_counter = counter.next
prev_counter = counter.prev
#-----------------------------

Determining Current Function Name

## This sort of code inspection is liable to change as
## Python evolves.  There may be cleaner ways to do this.
## This also may not work for code called from functions
## written in C.
#-----------------------------
import sys
this_function = sys._getframe(0).f_code.co_name
#-----------------------------
i = 0 # how far up the call stack to look
module = sys._getframe(i).f_globals["__name__"]
filename = sys._getframe(i).f_code.co_filename
line = sys._getframe(i).f_lineno
subr = sys._getframe(i).f_code.co_name
has_args = bool(sys._getframe(i+1).f_code.co_argcount)

# 'wantarray' is Perl specific

#-----------------------------
me = whoami()
him = whowasi()

def whoami():
    sys._getframe(1).f_code.co_name
def whowasi():
    sys._getframe(2).f_code.co_name
#-----------------------------

Passing Arrays and Hashes by Reference

#-----------------------------
# Every variable name is a reference to an object, thus nothing special
# needs to be done to pass a list or a dict as a parameter.
list_diff(list1, list2)
#-----------------------------
# Note: if one parameter to zip() is longer it will be truncated
def add_vecpair(x, y):
    return [x1+y1 for x1, y1 in zip(x, y)]

a = [1, 2]
b = [5, 8]
print " ".join([str(n) for n in add_vecpair(a, b)])
#=> 6 10
#-----------------------------
# DO NOT DO THIS:
assert isinstance(x, type([])) and isinstance(y, type([])), \
    "usage: add_vecpair(list1, list2)"
#-----------------------------

Detecting Return Context

#-----------------------------
# perl return context is not something standard in python...
# but still you can achieve something alike if you really need it
# (but you must really need it badly since you should never use this!!)
#
# see http://aspn.activestate.com/ASPN/Cookbook/Python/Recipe/284742 for more
#
# NB: it has been tested under Python 2.3.x and no guarantees can be given
#     that it works under any future Python version.
import inspect,dis

def expecting():
    """Return how many values the caller is expecting"""
    f = inspect.currentframe().f_back.f_back
    bytecode = f.f_code.co_code
    i = f.f_lasti
    instruction = ord(bytecode[i+3])
    if instruction == dis.opmap['UNPACK_SEQUENCE']:
        howmany = ord(bytecode[i+4])
        return howmany
    elif instruction == dis.opmap['POP_TOP']:
        return 0
    return 1

def cleverfunc():
    howmany = expecting()
    if howmany == 0:
        print "return value discarded"
    if howmany == 2:
        return 1,2
    elif howmany == 3:
        return 1,2,3
    return 1

cleverfunc()
x = cleverfunc()
print x
x,y = cleverfunc()
print x,y
x,y,z = cleverfunc()
print x,y,z

Passing by Named Parameter

#-----------------------------
thefunc(increment= "20s", start="+5m", finish="+30m")
thefunc(start= "+5m",finish="+30m")
thefunc(finish= "+30m")
thefunc(start="+5m", increment="15s")
#-----------------------------
def thefunc(increment='10s',
            finish='0',
            start='0'):
    if increment.endswith("m"):
        pass
#-----------------------------

Skipping Selected Return Values

#-----------------------------
a, _, c = func()       # Use _ as a placeholder...
a, ignore, c = func()  # ...or assign to an otherwise unused variable
#-----------------------------

Returning More Than One Array or Hash

#-----------------------------
def somefunc():
    mylist = []
    mydict = {}
    # ...
    return mylist, mydict

mylist, mydict = somefunc()
#-----------------------------
def fn():
    return a, b, c

#-----------------------------
h0, h1, h2 = fn()
tuple_of_dicts = fn()   # eg: tuple_of_dicts[2]["keystring"]
r0, r1, r2 = fn()       # eg: r2["keystring"]

#-----------------------------

Returning Failure

#-----------------------------
# Note: Exceptions are almost always preferred to error values
return
#-----------------------------
def empty_retval():
    return None

def empty_retval():
    return          # identical to return None

def empty_retval():
    pass            # None returned by default (empty func needs pass)
#-----------------------------
a = yourfunc()
if a:
    pass
#-----------------------------
a = sfunc()
if not a:
    raise AssertionError("sfunc failed")

assert sfunc(), "sfunc failed"
#-----------------------------

Prototyping Functions

# Prototypes are inapplicable to Python as Python disallows calling
# functions without using brackets, and user functions are able to
# mimic built-in functions with no special actions required as they
# only flatten lists (and convert dicts to named arguments) if
# explicitly told to do so.  Python functions use named parameters
# rather than shifting arguments:

def myfunc(a, b, c=4):
   print a, b, c

mylist = [1,2]

mydict1 = {"b": 2, "c": 3}
mydict2 = {"b": 2}

myfunc(1,2,3)
#=> 1 2 3

myfunc(1,2)
#=> 1 2 4

myfunc(*mylist)
#=> 1 2 4

myfunc(5, *mylist)
#=> 5, 1, 2

myfunc(5, **mydict1)
#=> 5, 2, 3

myfunc(5, **mydict2)
#=> 5, 2, 4

myfunc(c=3, b=2, a=1)
#=> 1, 2, 3

myfunc(b=2, a=1)
#=> 1, 2, 4

myfunc(mylist, mydict1)
#=> [1, 2] {'c': 3, 'b': 2} 4

# For demonstration purposes only - don't do this
def mypush(mylist, *vals):
   mylist.extend(vals)

mylist = []
mypush(mylist, 1, 2, 3, 4, 5)
print mylist
#=> [1, 2, 3, 4, 5]

Handling Exceptions

#-----------------------------
raise ValueError("some message")  # specific exception class
raise Exception("use me rarely")  # general exception
raise "don't use me"              # string exception (deprecated)
#-----------------------------
# Note that bare excepts are considered bad style.  Normally you should
# trap specific exceptions.  For instance these bare excepts will
# catch KeyboardInterrupt, SystemExit, and MemoryError as well as
# more common errors.  In addition they force you to import sys to
# get the error message.
import warnings, sys
try:
    func()
except:
    warnings.warn("func raised an exception: " + str(sys.exc_info()[1]))
#-----------------------------
try:
    func()
except:
    warnings.warn("func blew up: " + str(sys.exc_info()[1]))
#-----------------------------
class MoonPhaseError(Exception):
    def __init__(self, phase):
        self.phase = phase
class FullMoonError(MoonPhaseError):
    def __init__(self):
        MoonPhaseError.__init__("full moon")

def func():
    raise FullMoonError()

# Ignore only FullMoonError exceptions
try:
    func()
except FullMoonError:
    pass
#-----------------------------
# Ignore only MoonPhaseError for a full moon
try:
    func()
except MoonPhaseError, err:
    if err.phase != "full moon":
        raise
#-----------------------------

Saving Global Values

# There is no direct equivalent to 'local' in Python, and
# it's impossible to write your own.  But then again, even in
# Perl it's considered poor style.

# DON'T DO THIS (You probably shouldn't use global variables anyway):
class Local(object):
    def __init__(self, globalname, val):
        self.globalname = globalname
        self.globalval = globals()[globalname]
        globals()[globalname] = val
        
    def __del__(self):
        globals()[self.globalname] = self.globalval

foo = 4

def blah():
    print foo

def blech():
    temp = Local("foo", 6)
    blah()

blah()
blech()
blah()

#-----------------------------

Redefining a Function

#-----------------------------
grow = expand
grow()                     # calls expand()

#-----------------------------
one.var = two.table   # make one.var the same as two.table
one.big = two.small   # make one.big the same as two.small
#-----------------------------
fred = barney     # alias fred to barney
#-----------------------------
s = red("careful here")
print s
#> <FONT COLOR='red'>careful here</FONT>
#-----------------------------
# Note: the 'text' should be HTML escaped if it can contain
# any of the characters '<', '>' or '&'
def red(text):
    return "<FONT COLOR='red'>" + text + "</FONT>"
#-----------------------------
def color_font(color, text):
    return "<FONT COLOR='%s'>%s</FONT>" % (color, text)

def red(text): return color_font("red", text)
def green(text): return color_font("green", text)
def blue(text): return color_font("blue", text)
def purple(text): return color_font("purple", text)
# etc
#-----------------------------
# This is done in Python by making an object, instead of
# saving state in a local anonymous context.
class ColorFont:
    def __init__(self, color):
        self.color = color
    def __call__(self, text):
        return "<FONT COLOR='%s'>%s</FONT>" % (self.color, text)

colors = "red blue green yellow orange purple violet".split(" ")
for name in colors:
    globals()[name] = ColorFont(name)
#-----------------------------
# If you really don't want to make a new class, you can
# fake it somewhat by passing in default args.
colors = "red blue green yellow orange purple violet".split(" ")
for name in colors:
    def temp(text, color = name):
        return "<FONT COLOR='%s'>%s</FONT>" % (color, text)
    globals()[name] = temp

#-----------------------------

Trapping Undefined Function Calls with AUTOLOAD


# Python has the ability to derive from ModuleType and add
# new __getattr__ and __setattr__ methods.  I don't know the
# expected way to use them to emulate Perl's AUTOLOAD.  Instead,
# here's how something similar would be done in Python.  This
# uses the ColorFont defined above.

#-----------------------------
class AnyColor:
    def __getattr__(self, name):
        return ColorFont(name)

colors = AnyColor()

print colors.chartreuse("stuff")

#-----------------------------
## Skipping this translation because 'local' is too Perl
## specific, and there isn't enough context to figure out
## what this is supposed to do.
#{
#    local *yellow = \&violet;
#    local (*red, *green) = (\&green, \&red);
#    print_stuff();
#}
#-----------------------------

Nesting Subroutines

#-----------------------------
def outer(arg1):
    x = arg1 + 35
    def inner():
        return x * 19
    return x + inner()
#-----------------------------

Program: Sorting Your Mail

#-----------------------------
import mailbox, sys
mbox = mailbox.PortableUnixMailbox(sys.stdin)

def extract_data(msg, idx):
    subject = msg.getheader("Subject", "").strip()
    if subject[:3].lower() == "re:":
        subject = subject[3:].lstrip()
    text = msg.fp.read()
    return subject, idx, msg, text
messages = [extract_data(idx, msg) for idx, msg in enumerate(mbox)]

#-----------------------------
# Sorts by subject then by original position in the list
for subject, pos, msg, text in sorted(messages):
    print "%s\n%s"%(msg, text)

#-----------------------------
# Sorts by subject then date then original position
def subject_date_position(elem):
    return (elem[0], elem[2].getdate("Date"), elem[1])
messages.sort(key=subject_date_position)

# Pre 2.4:
messages = sorted(messages, key=subject_date_position)
#-----------------------------