def...return

Functions are declared using def. Here is a typical Python function:

  1. >>> def f(a, b):
  2. ... return a + b
  3. ...
  4. >>> print f(4, 2)
  5. 6

There is no need (or way) to specify types of the arguments or the return type(s). In this example, a function f is defined that can take two arguments.

Functions are the first code syntax feature described in this chapter to introduce the concept of scope, or namespace. In the above example, the identifiers a and b are undefined outside of the scope of function f:

  1. >>> def f(a):
  2. ... return a + 1
  3. ...
  4. >>> print f(1)
  5. 2
  6. >>> print a
  7. Traceback (most recent call last):
  8. File "<stdin>", line 1, in <module>
  9. NameError: name 'a' is not defined

Identifiers defined outside of function scope are accessible within the function; observe how the identifier a is handled in the following code:

  1. >>> a = 1
  2. >>> def f(b):
  3. ... return a + b
  4. ...
  5. >>> print f(1)
  6. 2
  7. >>> a = 2
  8. >>> print f(1) # new value of a is used
  9. 3
  10. >>> a = 1 # reset a
  11. >>> def g(b):
  12. ... a = 2 # creates a new local a
  13. ... return a + b
  14. ...
  15. >>> print g(2)
  16. 4
  17. >>> print a # global a is unchanged
  18. 1

If a is modified, subsequent function calls will use the new value of the global a because the function definition binds the storage location of the identifier a, not the value of a itself at the time of function declaration; however, if a is assigned-to inside function g, the global a is unaffected because the new local a hides the global value. The external-scope reference can be used in the creation of closures:

  1. >>> def f(x):
  2. ... def g(y):
  3. ... return x * y
  4. ... return g
  5. ...
  6. >>> doubler = f(2) # doubler is a new function
  7. >>> tripler = f(3) # tripler is a new function
  8. >>> quadrupler = f(4) # quadrupler is a new function
  9. >>> print doubler(5)
  10. 10
  11. >>> print tripler(5)
  12. 15
  13. >>> print quadrupler(5)
  14. 20

Function f creates new functions; and note that the scope of the name g is entirely internal to f. Closures are extremely powerful.

Function arguments can have default values, and can return multiple results:

  1. >>> def f(a, b=2):
  2. ... return a + b, a - b
  3. ...
  4. >>> x, y = f(5)
  5. >>> print x
  6. 7
  7. >>> print y
  8. 3

Function arguments can be passed explicitly by name, and this means that the order of arguments specified in the caller can be different than the order of arguments with which the function was defined:

  1. >>> def f(a, b=2):
  2. ... return a + b, a - b
  3. ...
  4. >>> x, y = f(b=5, a=2)
  5. >>> print x
  6. 7
  7. >>> print y
  8. -3

Functions can also take a runtime-variable number of arguments:

  1. >>> def f(*a, **b):
  2. ... return a, b
  3. ...
  4. >>> x, y = f(3, 'hello', c=4, test='world')
  5. >>> print x
  6. (3, 'hello')
  7. >>> print y
  8. {'c':4, 'test':'world'}

Here arguments not passed by name (3, ‘hello’) are stored in the tuple a, and arguments passed by name (c and test) are stored in the dictionary b.

In the opposite case, a list or tuple can be passed to a function that requires individual positional arguments by unpacking them:

  1. >>> def f(a, b):
  2. ... return a + b
  3. ...
  4. >>> c = (1, 2)
  5. >>> print f(*c)
  6. 3

and a dictionary can be unpacked to deliver keyword arguments:

  1. >>> def f(a, b):
  2. ... return a + b
  3. ...
  4. >>> c = {'a':1, 'b':2}
  5. >>> print f(**c)
  6. 3

lambda

lambda provides a way to create a very short unnamed function very easily:

  1. >>> a = lambda b: b + 2
  2. >>> print a(3)
  3. 5

The expression “lambda [a]:[b]“ literally reads as “a function with arguments [a] that returns [b]“. The lambda expression is itself unnamed, but the function acquires a name by being assigned to identifier a. The scoping rules for def apply to lambda equally, and in fact the code above, with respect to a, is identical to the function declaration using def:

  1. >>> def a(b):
  2. ... return b + 2
  3. ...
  4. >>> print a(3)
  5. 5

The only benefit of lambda is brevity; however, brevity can be very convenient in certain situations. Consider a function called map that applies a function to all items in a list, creating a new list:

  1. >>> a = [1, 7, 2, 5, 4, 8]
  2. >>> map(lambda x: x + 2, a)
  3. [3, 9, 4, 7, 6, 10]

This code would have doubled in size had def been used instead of lambda. The main drawback of lambda is that (in the Python implementation) the syntax allows only for a single expression; however, for longer functions, def can be used and the extra cost of providing a function name decreases as the length of the function grows. Just like def, lambda can be used to curry functions: new functions can be created by wrapping existing functions such that the new function carries a different set of arguments:

  1. >>> def f(a, b): return a + b
  2. >>> g = lambda a: f(a, 3)
  3. >>> g(2)
  4. 5

There are many situations where currying is useful, but one of those is directly useful in web2py: caching. Suppose you have an expensive function that checks whether its argument is prime:

  1. >>> def isprime(number):
  2. ... for p in range(2, number):
  3. ... if (number % p) == 0:
  4. ... return False
  5. ... return True

This function is obviously time consuming.

Suppose you have a caching function cache.ram that takes three arguments: a key, a function and a number of seconds.

  1. >>> value = cache.ram('key', f, 60)

The first time it is called, it calls the function f(), stores the output in a dictionary in memory (let’s say “d”), and returns it so that value is:

  1. >>> value = d['key'] = f()

The second time it is called, if the key is in the dictionary and not older than the number of seconds specified (60), it returns the corresponding value without performing the function call.

  1. >>> value = d['key']

How would you cache the output of the function isprime for any input? Here is how:

  1. >>> number = 7
  2. >>> seconds = 60
  3. >>> print cache.ram(str(number), lambda: isprime(number), seconds)
  4. True
  5. >>> print cache.ram(str(number), lambda: isprime(number), seconds)
  6. True

The output is always the same, but the first time cache.ram is called, isprime is called; the second time it is not.

Python functions, created with either def or lambda allow re-factoring existing functions in terms of a different set of arguments. cache.ram and cache.disk are web2py caching functions.