Friday, May 28, 2021

Python 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 

<function fib at 10042ed0> 

>>> 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 "<stdin>", 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')] 

Others 

# The lambda keyword in Python provides a 

# shortcut for declaring small and  

# anonymous functions: 

 

>>> add = lambda x, y: x + y 

>>> add(5, 3) 

8 

 

# You could declare the same add()  

# function with the def keyword: 

 

>>> def add(x, y): 

...     return x + y 

>>> add(5, 3) 

8 

 

# So what's the big fuss about? 

# Lambdas are *function expressions*: 

>>> (lambda x, y: x + y)(5, 3) 

8 

 

• Lambda functions are single-expression  

# functions that are not necessarily bound 

# to a name (they can be anonymous). 

 

• Lambda functions can't use regular  

# Python statements and always include an 

# implicit `return` statement. 

 

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': <class 'int'>, '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