Sunday, July 8, 2018

Functions


Introduction
------------

Here is a an example of a function:

>>> def fib(n):    # write Fibonacci series up to n
...     """Print a Fibonacci series up to n."""
...     a, b = 0, 1
...     while a < n:
...         print(a, end=' ')
...         a, b = b, a+b
...     print()
...
>>> # Now call the function we just defined:
>>> fib(2000)
0 1 1 2 3 5 8 13 21 34 55 89 144 233 377 610 987 1597
>>>
>>> fib.__doc__
Print a Fibonacci series up to n.
>>>

The keyword def introduces a function definition. It must be followed by the
function name and the parenthesized list of formal parameters. The statements
that form the body of the function start at the next line, and must be indented.

The first statement of the function body can optionally be a string literal;
this string literal is the function’s documentation string, or docstring. (More
about docstrings can be found in the section Documentation Strings.) There are
tools which use docstrings to automatically produce online or printed
documentation, or to let the user interactively browse through code; it’s good
practice to include docstrings in code that you write, so make a habit of it.

The execution of a function introduces a new symbol table used for the local
variables of the function. More precisely, all variable assignments in a
function store the value in the local symbol table; whereas variable references
first look in the local symbol table, then in the local symbol tables of
enclosing functions, then in the global symbol table, and finally in the table
of built-in names. Thus, global variables cannot be directly assigned a value
within a function (unless named in a global statement), although they
may be referenced.

The actual parameters (arguments) to a function call are introduced in the local
symbol table of the called function when it is called; thus, arguments are
passed using call by value (where the value is always an object reference, not
the value of the object). [1] When a function calls another function, a new
local symbol table is created for that call.

A function definition introduces the function name in the current symbol table.
The value of the function name has a type that is recognized by the interpreter
as a user-defined function. This value can be assigned to another name which can
then also be used as a function. This serves as a general renaming mechanism:

>>> fib
>>> f = fib
>>> f(100)
0 1 1 2 3 5 8 13 21 34 55 89

Coming from other languages, you might object that fib is not a function but a
procedure since it doesn’t return a value. In fact, even functions without a
return statement do return a value, albeit a rather boring one. This value is
called None (it’s a built-in name). Writing the value None is normally
suppressed by the interpreter if it would be the only value written. You can see
it if you really want to using print():

>>> fib(0)
>>> print(fib(0))
None
>>>

It is simple to write a function that returns a list of the numbers of the
Fibonacci series, instead of printing it:

>>> def fib2(n): # return Fibonacci series up to n
...     """Return a list containing the Fibonacci series up to n."""
...     result = []
...     a, b = 0, 1
...     while a < n:
...         result.append(a)    # see below
...         a, b = b, a+b
...     return result # script terminates here, anything under will not be read
...
>>> f100 = fib2(100)    # call it
>>> f100                # write the result
[0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89]

This example, as usual, demonstrates some new Python features:

  - The return statement returns with a value from a function. return without an
    expression argument returns None. Falling off the end of a function also
    returns None.
  - The statement result.append(a) calls a method of the list object result. A
    method is a function that ‘belongs’ to an object and is named
    obj.methodname, where obj is some object (this may be an expression), and
    methodname is the name of a method that is defined by the object’s type.
    Different types define different methods. Methods of different types may
    have the same name without causing ambiguity. (It is possible to define your
    own object types and methods, using classes, see Classes) The method
    append() shown in the example is defined for list objects; it adds a new
    element at the end of the list. In this example it is equivalent to
    result = result + [a], but more efficient.

Default Arguments
-----------------

If you don't specify an argument to pass,
Python can provide a default argument for
you.

# code
def provide_or_ignore(a='no argument?'):
  return(a)

print(provide_or_ignore())
print(provide_or_ignore(a='now we are talking!'))

# output
no argument?
now we are talking!
Here is a more detailed example.

The function on the right can be called
in several ways:

 
- only the mandatory argument:
    ask_ok('Do you really want to quit?')
- mandatory argument + 1 of the optional arguments:
    ask_ok('OK to overwrite the file?', 2)
- all arguments given:
    ask_ok('OK to overwrite the file?', 2, 'only yes or no!')

The `in` keyword tests whether or not a sequence contain a
certain value
def ask_ok(prompt, retries=4, complaint='Yes or no, please!'):
    while True:
        ok = input(prompt)
        if ok in ('y', 'ye', 'yes'):
            return True
        if ok in ('n', 'no', 'nop', 'nope'):
            return False
        retries = retries - 1
        if retries < 0:
            raise IOError('uncooperative user')
        print(complaint)
The default value is evaluated at the point of function
definition.
# code
i = 5
def f(arg=i):
    print(arg)

i = 6
f()

# output
5
Default value is evaluated only once.

When default is a mutable object like list, dictionary or an
instance, the value is shared and accumulated between function
calls.
# default value will accumulate between calls
def f(a, L=[]):
    L.append(a)
    return L

print(f(1))
print(f(2))
print(f(3))

# output
[1]
[1, 2]
[1, 2, 3]

# default value doesn't accumulate
def f(a, L=None):
    if L is None:
        L = []
    L.append(a)
    return L

# output
[1]
[2]
[3]

Keyword Arguments
-----------------

- functions may accept arguments in the form of `kwarg=value`, `*kwarg`, or
  `**kwarg`
- there are rules on how you can pass these to functions

This function accepts 1 required argument (voltage) and 3 optional arguments.

def parrot(voltage, state='a stiff', action='voom', type='Norwegian Blue'):
    print("-- This parrot wouldn't", action, end=' ')
    print("if you put", voltage, "volts through it.")
    print("-- Lovely plumage, the", type)
    print("-- It's", state, "!")

It can be called in any of the following ways:

parrot(1000)                                          # 1 positional argument
parrot(voltage=1000)                                  # 1 keyword argument
parrot(voltage=1000000, action='VOOOOOM')             # 2 keyword arguments
parrot(action='VOOOOOM', voltage=1000000)             # 2 keyword arguments
parrot('a million', 'bereft of life', 'jump')         # 3 positional arguments
parrot('a thousand', state='pushing up the daisies')  # 1 positional, 1 keyword

It cannot be called using the ff:

parrot()                     # required argument missing
parrot(voltage=5.0, 'dead')  # non-keyword argument after a keyword argument
parrot(110, voltage=220)     # duplicate value for the same argument
parrot(actor='John Cleese')  # unknown keyword argument

No argument may receive more than 1 value. Example:

>>> def function(a):
...     pass
...
>>> function(0, a=0)
Traceback (most recent call last):
  File "", line 1, in ?
TypeError: function() got multiple values for keyword argument 'a'

Using `*kwarg` allows the function to accept multiple arguments defined by the
user while `**kwarg` allows it to receive a dictionary (also defined by the
user).

# function
def cheeseshop(kind, *arguments, **keywords):
    print("-- Do you have any", kind, "?")
    print("-- I'm sorry, we're all out of", kind)
    for arg in arguments:
        print(arg)
    print("-" * 40)
    keys = sorted(keywords.keys()) # this sort the user-defined dictionary
    for kw in keys:
        print(kw, ":", keywords[kw])

# calling the function
cheeseshop("Limburger", "It's very runny, sir.",
           "It's really very, VERY runny, sir.",
           shopkeeper="Michael Palin",
           client="John Cleese",
           sketch="Cheese Shop Sketch")

# output
-- Do you have any Limburger ?
-- I'm sorry, we're all out of Limburger
It's very runny, sir.
It's really very, VERY runny, sir.
----------------------------------------
client : John Cleese
shopkeeper : Michael Palin
sketch : Cheese Shop Sketch

Arbitrary Argument Lists
------------------------

- least frequently used method of passing arguments to a function
- this method also uses `*kwarg` -> variable number of arguments
- arguments will be wrapped inside a tuple

Zero or more normal arguments may occur before
the variable number of arguments.
def write_multiple_items(file, separator, *args):
    file.write(separator.join(args))
Any parameters appear after `*kwarg` must be
keyword arguments only.
>>> def concat(*args, sep="/"):
...    return sep.join(args)
...
>>> concat("earth", "mars", "venus")
'earth/mars/venus'
>>> concat("earth", "mars", "venus", sep=".")
'earth.mars.venus'

Unpacking Argument Lists
------------------------

- argument lists may be generated by unpacking a list or a dictionary

Unpacking a list
>>> list(range(3, 6))  # normal call with separate arguments
[3, 4, 5]
>>> args = [3, 6]
>>> list(range(*args))  # call with arguments unpacked from a list
[3, 4, 5]
Unpacking a dictionary
>>> def parrot(voltage, state='a stiff', action='voom'):
...     print("-- This parrot wouldn't", action, end=' ')
...     print("if you put", voltage, "volts through it.", end=' ')
...     print("E's", state, "!")
...
>>> d = {"voltage": "four million", "state": "bleedin' demised", "action": "VOOM"}
>>> parrot(**d)
-- This parrot wouldn't VOOM if you put four million volts through it. E's bleedin' demised !

Lambda Expressions
------------------

- special type of functions that return a function
- doesn't include a `return` statement -- always contains an expression that is
  always returned
- see example uses below

Getting the sum of 2 numbers
>>> sum = lambda x, y : x + y
>>> sum(3,4)
7
>>>
Lambda functions can also reference
variables from the containing scope
>>> def make_incrementor(n):
...     return lambda x: x + n
...
>>> f = make_incrementor(42)
>>> f(0)
42
>>> f(1)
43
Passes a small function as an
argument
>>> pairs = [(1, 'one'), (2, 'two'), (3, 'three'), (4, 'four')]
>>> pairs.sort(key=lambda pair: pair[1])
>>> pairs
[(4, 'four'), (1, 'one'), (3, 'three'), (2, 'two')]

Documentation Strings aka docstrings
------------------------------------

- docstrings are string literals that describe your code
- see below the format, examples, and rules in writing them

A single line docstring
>>>
>>> def test():
...   """just a sample"""  # first line must be short and concise summary
...                        # of your code's purpose
>>> test.__doc__
'just a sample'
>>>
If more than 1 line is required, you
must put a blank line after the 1st
line to separate the summary from the
rest of the description.

The 1st non-blank line aftr the 1st
line determines the amount of
indentation for the entire docstring.
>>> def my_function():
...     """Do nothing, but document it.
...
...     No, really, it doesn't do anything.
...     You may use this function without harm
...     """
...     pass
...
>>> print(my_function.__doc__)
Do nothing, but document it.

    No, really, it doesn't do anything.
Some examples of common docstrings
>>> print(len.__doc__)
Return the number of items in a container.
>>>
>>>
>>> print(map.__doc__)
map(func, *iterables) --> map object

Make an iterator that computes the function using arguments from
each of the iterables.  Stops when the shortest iterable is exhausted.
>>>
>>> print(filter.__doc__)
filter(function or None, iterable) --> filter object

Return an iterator yielding those items of iterable for which function(item)
is true. If function is None, return the items that are true.
>>>

Function Annotations
--------------------

- ways of describing function arguments
- has no effect on the code execution
- not commonly used
- annotations are stored in `__annotations__` attribute of the function

In this examle, a has no annotation, b is
annotated with 'a string' and c is
annotated with int.

The return value is annotated with type
float. Notice the "->" syntax.
>>> def foo(a, b: 'a string', c: int) -> float:
...   print(a + b + c)
Using the example above, we can see here
that the annotations have no impact on the
execution of function.
>>> foo('Hello', ', ', 'World!')
Hello, World!
>>>
>>> foo(1, 2, 3)
6
>>>
A complex example with "eggs" being
annotated and at the same time having a
default value of "spam".
>>> def f(ham: 42, eggs: int = 'spam') -> "Nothing to see here":
...     print("Annotations:", f.__annotations__)
...     print("Arguments:", ham, eggs)
...
>>> f('wonderful')
Annotations: {'eggs': , 'return': 'Nothing to see here', 'ham': 42}
Arguments: wonderful spam

Future Statements
-----------------

- Used in Python 2.X to support Python 3.X syntax

Allows Python 2.X to use `print('hello')`
together with `print "hello"`
from __future__ import print_function


No comments:

Post a Comment