Python
  11.1 Functions:   
Introduction


11.1.0 Python as a Functional Language: Here is a quote from Guido van Rossum, Python's creator and BDFL:

"I have never considered Python to be heavily influenced by functional languages, no matter what people say or think. I was much more familiar with imperative languages such as C and Algol 68 and although I had made functions first-class objects, I didn't view Python as a functional programming language. However, earlier on, it was clear that users wanted to do much more with lists and functions.

It is also worth noting that even though I didn't envision Python as a functional language, the introduction of closures has been useful in the development of many other advanced programming features. For example, certain aspects of new-style classes, decorators, and other modern features rely upon this capability.

Lastly, even though a number of functional programming features have been introduced over the years, Python still lacks certain features found in 'real' functional programming languages. For instance, Python does not perform certain kinds of optimizations (e.g., tail recursion). In general, because of Python's extremely dynamic nature, it is impossible to do the kind of compile-time optimization known from functional languages like Haskell or ML. And that's fine."


11.1.1 Functions as first-class objects:

Functions are first-class objects: they can be passed as arguments, placed in data structures, and returned.

In fact: All objects in Python are said to be "first-class": All objects that can be named by an identifier have equal status and can be treated as data. (Beazley)

The example below shows functions as the values in a dictionary.

Evaluate Reverse Polish Notation
# rpn.py: evaluate RPN
import sys
from operator import *

ops = {'+':add, '-':sub, '*':mul, '/':div,
       '^':pow, '%':mod}

def error(n):
    sys.stdout.write("Err: " + str(n) + ", ")

def eval_rpn(s): # s is input rpn
    st = [] # evaluate stack
    for c in s:
        if c.isdigit(): # operand, single dig
            st.append(float(c))
        elif len(st) >= 2:
            if not c in ops.keys():
                error(1)
            else: # operator
                # do: next-to-top ops top
                arg = st.pop()
                st.append(ops[c](st.pop(),arg))
                # ops['+'] is function "add"
        else:
            error(2)
    if len(st) != 1:
        error(3)
    else:
        return st[0]
def rpn(s):
    sys.stdout.write("rpn:"+s+" \t")
    sys.stdout.write("res:" +
      str(eval_rpn(s)) + "\n")

rpn("23+")    # res: 5.0
rpn("234*+")  # res: 14.0
rpn("34*5+")  # res: 17.0
rpn("23+4*")  # res: 20.0
rpn("324+*51+/2-")  # res: 1.0
rpn("53+21+2^^")    # res: 134217728.0
rpn("2345^*6*+7+")  # res: 18441.0
rpn("32^41*2*-12/^3-21*/") # res: -1.0
rpn("23-41+5*^6/24-7*-8-") # res:5.833
rpn("23/")    # res: 0.666666666
rpn("83%")    # res: 2.0
rpn("99+5%")  # res: 3.0
# erroneous inputs
rpn("")       # Err: 3
rpn("23+*")   # Err: 2
rpn("2+3")    # Err: 2, 3
rpn("23+4")   # Err: 3
rpn("+23")    # Err: 2, 3
rpn("+*")     # Err: 2, 3
rpn("23")     # Err: 3
rpn("%@")     # Err: 2, 3
rpn("2 3 +")  # Err: 1, 2

Items of interest or for study:

  • Inside a class, the operators: +, *, etc. are also represented by: __add__, __mul__, etc.
    See for example: Complex class.

  • The operator module provides these special identifiers that are defined in classes as above, and also provides names without underscores, as in the example above.


11.1.2 Parameters: This part shows formal Parameters (those in the function definition) with default values, and actual parameters (those in the function call) that are named. For this page you should look at the early parts of: Functions, which is a part of: Building Skills in Python.

Parameter demo + output
# shorten.py
import sys

def shorten(text, length=25, ind="..."):
    if len(text) > length:
        text = text[:length - len(ind)] + ind
    return text

shorten("The Silkie")                    # returns: "The Silkie"
shorten(length=7, text="The Silkie")     # returns: "The ..."
shorten("The Silkie", ind="&", length=7) # returns: "The Si&"
shorten("The Silkie", 7, "*")            # returns: "The Si*"

Items of interest or for study:

  • The example above (from Summerfield's book) takes an input string (the text) and if necessary shortens its length to the value of the second parameter minus the length of the third parameter. If the text is shortened, the text of the third parameter is added at the end.

  • This example shows:

    • default values for formal parameters in a function's definition (the "=25" and the "='...' "), and
    • named actual parameters in a function's call ("length=7", "text='The Silkie' ", "ind='&' ").

  • Of the four calls above:

    • The first shows both defaults used, with only the first positional parameter.
    • The fourth shows all three positional parameters used, with no named parameters and ignoring the defaults.
    • The second shows two named parameters used (out of order), and one default value used.
    • The third shows the first positional parameters used, and the other two as named parameters (out of order).

  • Because the first formal parameter is just an identifier, the actual parameters must have either a simple first parameter, or a parameter of the form: text = something.


11.1.3. *args and **kwargs: Python has special features for passing arbitrarily many normal arguments (*args) and a dictionary of keyword-value arguments (*kwargs).

Program, showing *args and **kwargs
# test_args.py: test of *arg and **kwargs
import sys

def func(first_arg, *args, **kwargs):
    # first_arg is positional-only param
    print first_arg

    # args is a tuple of positional args,
    # because the param name has * first
    if args: # If args is not empty.
        print args

    # kwargs = dictionary of keyword args,
    # because the param name has ** first
    if kwargs: # If kwargs is not empty.
        print kwargs
>>> from test_args import *
>>> n = 4
>>> func("item")
item
>>> func("item", 1, 2, '3')
item
(1, 2, '3')
>>> func("item", 1, 2, '3', k1=4, k2="foo")
item
(1, 2, '3')
{'k2': 'foo', 'k1': 4}
>>> func("item", k1=-4.5E-13, k2=2*n+5)
item
{'k2': 13, 'k1': -4.5e-13}
>>> func(k2=True, first_arg="item")
item
{'k2': True}

Items of interest or for study:

  • Here the * and the ** are not operators, but part of the syntax. The names args and kwargs can be any identifiers. (Kind of flaky C-like notation.)

  • These two items more-or-less encompass all possibilities for a sequence of Python formal parameters. For decorators and elsewhere this allows Python to handle arbitrary parameters in functional programming, but this looks pretty complicated. See Decorators for use of this notation.


11.1.4. Using *args and **kwargs: See Advanced Parameter Handling For Functions. First use *args to handle arbitrarily many parameters (note that you don't have to use the identifier "args"). Then use a dictionary for the params:

Python Parameters
n Arbitrary Number of Params Dictionary of Params
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
# arb_num_params.py:
def myMax(*args):
    max = args[0]
    for a in args[1:]:
        if a > max: max= a
    return max
print myMax(2, 3, 7, 1)  # prints 7
data = (2, 3, 7, 1)
print myMax(*data)       # prints 7
print myMax(*(2,3,7,1))  # prints 7
print myMax(5)           # prints 5
print myMax(None)        # prints None

def myAve(*args):
    tot = 0
    for a in args:
        tot += a
    # float needed for Python 2.x
    return tot / float(len(args))
print myAve(2, 3, 4, 5)  # prints 3.5
print myAve(*data)       # prints 3.25
print myAve(5)           # prints 5.0
# dict_of_params.py:
import sys
def avg3(a, b, c):
    return (a+b+c)/3.0

# tuple of parameters
data = (5, 6, 7)
print avg3(*data)       # prints 6.0
print avg3(a=5,b=6,c=7) # prints 6.0

# dictionary for parameters
d = {'a':5, 'b':6, 'c':7}
print avg3(**d)         # prints 6.0

def myAve(**dict):
    tot = 0
    for a in dict:
        tot += dict[a]
    return tot / float(len(dict)) 

print myAve(**d)        # prints 6.0
d2 = {'w':2, 'x':3, 'y':7, 'z':1}
print myAve(**d2)       # prints 3.25
print myAve(a=5,b=6,c=7)# prints 6.0

Items of interest or for study:

  • The program on the left above shows how *args (or *anything) can stand in for an arbitrary list or tuple of parameters.

    • Python has a built-in function max( ) that works fine in place of myMax( ) above.
    • The three lines 14-16 can be accomplished fine with just sum(args). In the next section we'll see a more general way to do this. So myAve could be just:
         def myAve(*args):
              return sum(args) / float(len(args))
    • You can use a sequence of actual parameters (line 7), or use a tuple as in line 8, use a list in place of this tuple. You can even put a * in front of an actual tuple (line 10).

  • The program on the right shows how **kwargs (or **anything) can stand in for an arbitrary dictionary of parameters. This dictionary gives named parameters.

    • Lines 3-4 (on the right) give almost the worst possible way to define an "average" function, since it will only work for three ordinary parameters, and will only work for dictionaries that use the variables a, b, and c. Surprisingly, this terrible definition of myAve( ) does work with a general tuple of length 3, and even works with a dictionary as long as the keys use only the three letters.
    • The boxed-in definition above is a reasonable, functional definition of myAve( ). However, this definition doesn't work for a dictionary.
    • The definition of myAve( ) on the right works for an arbitrary dictionary. It is presumably possible to write a functional version of this.


11.1.5. The lambda operator: This allows one to define an anonymous function in the form of an expression. There are significant restrictions on these expressions. lambdas are important in functional programming languages, but in Python they seem to be only a convenient shortcut that avoids using an extra function name.

The lambda statement has the form (from Beazley):

    lambda  args  :  expression
where args is a comma-separated list of arguments, and expression is an expression involving those arguments. (Can't have multiple statements, or non-expression statements.) Here is an example:
    a = lambda x, y : x+y
    r = a(2, 3)  # r gets 5

lambda used for case-insensitive sort:

Case-Insensitive Sort
# test_sort.py: mixed upper- lowercase

n = ["nale","name","Neal","Neil","NEAL",
     "nile","Nile","nada","Nick","nick"]
     
n2 = list(n) # make copy
# could also write:  n2 = n[:]
n2.sort() # sort n2 in place
print n2
n2 = list(n) # copy again
# sort case-insensitive
n2.sort(key=lambda n: n.lower())
print n2

# sort without lambda
n2 = list(n) # copy third time
def mytolower(n):
    return n.lower()
n2.sort(key=mytolower)
print n2
Output
['NEAL', 'Neal', 'Neil', 'Nick', 'Nile', 'nada', 'nale', 'name', 'nick', 'nile']
['nada', 'nale', 'name', 'Neal', 'NEAL', 'Neil', 'Nick', 'nick', 'nile', 'Nile']
['nada', 'nale', 'name', 'Neal', 'NEAL', 'Neil', 'Nick', 'nick', 'nile', 'Nile']

The sort library function has an optional function to apply to each sort key. In the first case above it's an anonymous lambda function, while in the second case it's explicitly defined.

Other uses of lambda: The items given below are "almost always better expressed using a generator expression or a list comprehension" (Beazley). Still, these features are prominent in true functional programming languages.

Case-Insensitive Sort
v = [1, 3, 5, 7]
map(lambda x:x+x, v)            # [2, 6, 10, 14]
map(lambda x:x*x, v)            # [1, 9, 25, 49]
reduce(lambda x,y: (x + y), v)  # 16
reduce(lambda x,y: (x * y), v)  # 105
reduce(operator.mul, v)         # 105
reduce(operator.mul, v, 2)      # 210
# Python's BDFL recommends this instead:
res = 2
for x in v:
    res = operator.mul(res, x)  # 210

(Revision date: 2015-08-10. Please use ISO 8601, the International Standard.)