More Control Flow Tools

4.1. if Statements

  • There can be zero or more elif parts, and the else part is optional.

  • An ifelifelif … sequence is a substitute for the switch or case statements found in other languages.

4.2. for Statements

Python’s for statement iterates over the items of any sequence (a list or a string), in the order that they appear in the sequence.

  • the behavior of modifying a collection while looping it can be tricky, so it is suggested that

    • Loop the copy of the collection

      1
      2
      3
      4
      # Strategy:  Iterate over a copy
      for user, status in users.copy().items():
      if status == 'inactive':
      del users[user]
    • create a new collection with the original one.

      1
      2
      3
      4
      5
      # Strategy:  Create a new collection
      active_users = {}
      for user, status in users.items():
      if status == 'active':
      active_users[user] = status

      4.3. The range() Function

  • To iterate over the indices of a sequence, you can combine range() and len() as follows:

    1
    2
    for i in range(len(collection)):
    item = collection[i]
  • When looping through a sequence, the position index and corresponding value can be retrieved at the same time using the enumerate() function.

    1
    2
    3
    4
    5
    6
    >>> for i , v in enumerate(ls):
    ... print(i,v)
    ...
    0 tic
    1 tac
    2 toe
  • See more at Looping Techniques.

  • the return value of range() is an object which returns the successive items of the desired sequence when you iterate over it, but it doesn’t really make the list, thus saving space. So it is iterable

  • We say such an object is iterable, that is, suitable as a target for functions and constructs that expect something from which they can obtain successive items until the supply is exhausted.

4.4. break and continue Statements, and else Clauses on Loops

  • The for statement can have an else clause, which will be executed if the loop do NOT terminated with a break statement

    1
    2
    3
    4
    5
    6
    for i in range(2, 50):
    for x in range(2,i):
    if i % x == 0:
    break
    else:
    print(i , "is a prime number")
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    2 is a prime number
    3 is a prime number
    5 is a prime number
    7 is a prime number
    11 is a prime number
    13 is a prime number
    17 is a prime number
    19 is a prime number
    23 is a prime number
    29 is a prime number
    31 is a prime number
    37 is a prime number
    41 is a prime number
    43 is a prime number
    47 is a prime number
  • a try statement’s else clause runs when no exception occurs, and a loop’s else clause runs when no break occurs. For more on the try statement and exceptions, see Handling Exceptions.

4.5. pass Statements

The pass statement does nothing. It can be used when a statement is required syntactically but the program requires no action

it is commonly used for:

  • creating minimal classes
  • be used is as a place-holder for a function or conditional body when you are working on new code, allowing you to keep thinking at a more abstract level.

4.6. Defining Functions

  • The first statement of the function body can optionally be a string literal; this string literal is the function’s documentation string, or Documentation Strings. It’s good practice to include docstrings in code that you write, so make a habit of it.

    1
    2
    3
    def DoNothing():
    """I am a lazy function"""
    pass
  • The execution of a function introduces a new symbol table used for the local variables of the function. The sequence of refering is

    1. local symbol table(created by the function)
    2. the local symbol tables of enclosing functions(can be referred by a nonlocal statement)
    3. the global symbol table(can be referred by a global statement)
    4. the table of built-in names
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    >>> ls = [1] # create a variable in the global symbol table
    >>> def f():
    ... ls = [2,3] # will create a name in the local symbol table created by the function when called
    ... return
    ...
    >>> ls
    [1]
    >>> f()
    >>> ls # f() does not change the value of ls
    [1]
    >>> def ff():
    ... print(ls) # will use the name in the global variable table directly
    ...
    >>> ff()
    [1]
  • 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.

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    >>> def exchange(a,b):
    ... """Intended to exchange the value of parameters a and b\
    but failed"""
    ... a , b = b ,a
    ... return a,b
    ...
    >>> exchange(3,4)
    (4, 3)
    >>> x = 999
    >>> y = 666
    >>> exchange(x,y)
    (666, 999)
    >>> x
    999
    >>> y
    666
  • A function definition associates the function name with the function object in the current symbol table. The interpreter recognizes the object pointed to by that name as a user-defined function. Other names can also point to that same function object and can also be used to access the function: (works like C function pointer from my prespective)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    >>> exchange
    <function exchange at 0x109a5cc10>
    >>> newname = exchange
    >>> id(exchange)
    4456827920
    >>> id(newname)
    4456827920
    >>> newname(x,y)
    (666, 999)
  • every function has a return value, functions without a return statement have the return value of None (the interpreter often suppress it, so you need printing it out to make it visible)(Falling off the end of a function also returns None.)(None can be similar to void in C?)

    1
    2
    3
    4
    5
    6
    >>> def Nothing():
    ... pass
    ...
    >>> Nothing()
    >>> print(Nothing())
    None

    4.7. More on Defining Functions

There are three form of defining arguments, which can be combined.

4.7.1. Default Argument Values

  • non-default argument shouldn’t follow default argument

  • The default values are evaluated at the point of function definition in the defining scope, so that

    1
    2
    3
    4
    5
    6
    7
    >>> i = 5
    >>> def f(arg = 5):
    ... print(arg)
    ...
    >>> i = 6
    >>> f()
    5
  • The default value is evaluated only once. So when the argument is mutable, such as list or dictionary, it will be shared between function calls

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    def g(a , L = []): # shared version
    L.append(a)
    return L

    print(g(1))
    print(g(2))
    print(g(3))

    def f(a, L=None): # non-shared version
    if L is None:
    L = []
    L.append(a)
    return L

    print(f(1))
    print(f(2))
    print(f(3))
    1
    2
    3
    4
    5
    6
    [1]
    [1, 2]
    [1, 2, 3]
    [1]
    [2]
    [3]

    4.7.2. Keyword Arguments

  • Functions can also be called using keyword arguments of the form kwarg=value

  • each argument gets value only once, the order doesn’t matter.

    1
    2
    3
    4
    5
    6
    7
    >>> def f(a1 , a2 =2 , a3 =3 ):
    ... print(a1, a2 ,a3)
    ...
    >>> f(a2 = 4 , a1 = 0 )
    0 4 3
    >>> f(99 , a3 = 123) # In a function call, keyword arguments must follow positional arguments.
    99 2 123
  • arguments can be passed by using tuples and dictionaries

    • *name which receives a tuple containing the positional arguments beyond the formal parameter list.

    • **name is present, it receives a dictionary containing all keyword arguments except for those corresponding to a formal parameter.

    • Note that the order in which the keyword arguments are printed is guaranteed to match the order in which they were provided in the function call.

    1
    2
    3
    4
    5
    6
    >>> def f(defaultarg , *arglist ):
    ... print(defaultarg)
    ... print(arglist , type(arglist))
    >>> f( 0 , 1, 2 , 3 , 's')
    0
    (1, 2, 3, 's') <class 'tuple'>
    1
    2
    3
    4
    5
    6
    7
    8
    9
    >>> def fun(**list):
    ... print(list , type(list))
    ...
    >>> fun( 0 , 1 , 2)
    Traceback (most recent call last):
    File "<stdin>", line 1, in <module>
    TypeError: fun() takes 0 positional arguments but 3 were given
    >>> fun ( a = 0 , b = 1 , c = 3)
    {'a': 0, 'b': 1, 'c': 3} <class 'dict'>
  • / and * can optionally be used as parameters type indicators

    1
    2
    3
    4
    5
    6
    def f(pos1, pos2, /, pos_or_kwd, *, kwd1, kwd2):
    ----------- ---------- ----------
    | | |
    | Positional or keyword |
    | - Keyword only
    -- Positional only

    4.7.3.1. Positional-or-Keyword Arguments

If / and * are not present in the function definition, arguments may be passed to a function by position or by keyword.

4.7.3.2. Positional-Only Parameters

Positional-only parameters are placed before a / (forward-slash).

4.7.3.3. Keyword-Only Arguments

place an * in the arguments list just before the first keyword-only parameter.

4.7.3.4. Function Examples

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
def standard_arg(arg):
print(arg)

def pos_only_arg(arg, /):
print(arg)

def kwd_only_arg(*, arg):
print(arg)

def combined_example(pos_only, /, standard, *, kwd_only):
print(pos_only, standard, kwd_only)

standard_arg("passed by position")
standard_arg(arg = "passed by kwd")
pos_only_arg('Only position is allowed')
kwd_only_arg(arg = 'Only kwd is allowed')
combined_example('pos only' , 'whatever you like' , kwd_only = 'kwd only')
1
2
3
4
5
passed by position
passed by kwd
Only position is allowed
Only kwd is allowed
pos only whatever you like kwd only
  • These separaters are used to avoid ambiguity

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    >>> def foo(name , **kwds):
    ... print(name)
    ... return 'name' in kwds # this can never be True
    ...
    >>> foo(0,a=1,name=3)
    Traceback (most recent call last):
    File "<stdin>", line 1, in <module>
    TypeError: foo() got multiple values for argument 'name'
    >>> foo(name=3)
    3
    False
    >>> foo(0,name=3)
    Traceback (most recent call last):
    File "<stdin>", line 1, in <module>
    TypeError: foo() got multiple values for argument 'name'
    >>> def fo(name , / , **kwds):
    ... print(name)
    ... return 'name' in kwds
    ...
    >>> fo(0,name=3)
    0
    True

    4.7.3.5. Recap

  • Use positional-only if you want the name of the parameters to not be available to the user. This is useful when parameter names have no real meaning, if you want to enforce the order of the arguments when the function is called or if you need to take some positional parameters and arbitrary keywords.
  • Use keyword-only when names have meaning and the function definition is more understandable by being explicit with names or you want to prevent users relying on the position of the argument being passed.
  • For an API, use positional-only to prevent breaking API changes if the parameter’s name is modified in the future.

4.7.4. Arbitrary Argument Lists

  • variadic arguments will be last in the list of formal parameters

  • Any formal parameters which occur after the *args parameter are ‘keyword-only’ arguments

    1
    2
    3
    4
    5
    6
    7
    >>> def concat(*args, sep="/"):
    ... return sep.join(args)
    ...
    >>> concat("earth", "mars", "venus")
    'earth/mars/venus'
    >>> concat("earth", "mars", "venus", sep=".")
    'earth.mars.venus'

    4.7.5. Unpacking Argument Lists

  • write the function call with the * operator to unpack the arguments out of a list or tuple

  • In the same fashion, dictionaries can deliver keyword arguments with the ** operator

1
2
3
4
5
6
7
8
9
>>> tp = (1 , 10)
>>> list(range(*tp))
[1, 2, 3, 4, 5, 6, 7, 8, 9]
>>> def ff(*,kwd1 , kwd2):
... print(kwd1 , kwd2)
...
>>> d = { 'kwd1' : 'I am Key Word' , 'kwd2' : 0 }
>>> ff(**d)
I am Key Word 0

4.7.6. Lambda Expressions

  • Small anonymous functions can be created with the lambda keyword.

    1
    lambda parameter1 , parameter2 , ...  : an expression
  • Lambda functions can be used wherever function objects are required.

  • They are syntactically restricted to a single expression. And they are just syntactic sugar.

1
2
3
4
>>> 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')]
1
2
3
4
5
6
7
>>> x = lambda *,k1 : k1 # the arguments' type separator works, they are just syntactic sugar.
>>> x(2)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: <lambda>() takes 0 positional arguments but 1 was given
>>> x(k1=2)
2

4.7.7. Documentation Strings

  • The first line should always be a short, concise summary of the object’s purpose.

  • This line should begin with a capital letter and end with a period..

  • If there is a second line, the second line should be blank, visually separating the summary from the rest of the description.

  • The following lines should be one or more paragraphs describing the object’s calling conventions, its side effects, etc

  • The first non-blank line after the first line of the string determines the amount of indentation for the entire documentation string.(Just a convention)

  • use __doc__ to check the documentation strings of a function

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    >>> def DOC():
    ... '''Test documentation string
    ...
    ... the first non-blank line
    ... the second non-blank line'''
    ... pass
    ...
    >>> DOC.__doc__
    'Test documentation string\n\n\tthe first non-blank line\n\tthe second non-blank line'
    >>> print(DOC.__doc__)
    Test documentation string

    the first non-blank line
    the second non-blank line
    >>> print(print.__doc__)
    print(value, ..., sep=' ', end='\n', file=sys.stdout, flush=False)

    Prints the values to a stream, or to sys.stdout by default.
    Optional keyword arguments:
    file: a file-like object (stream); defaults to the current sys.stdout.
    sep: string inserted between values, default a space.
    end: string appended after the last value, default a newline.
    flush: whether to forcibly flush the stream.

    4.7.8. Function Annotations

Function annotations are completely optional metadata information about the types used by user-defined functions

  • Annotations are stored in the __annotations__ attribute of the function as a dictionary and have no effect on any other part of the function.

  • Parameter annotations are defined by a colon after the parameter name, followed by an expression evaluating to the value of the annotation.

    • f(arg : type), position argument
    • f(arg : type = value), kwd argument
    • f() -> type, return value
    1
    2
    3
    4
    5
    6
    7
    8
    9
    >>> def f(ham: str, eggs: str = 'eggs') -> str:
    ... print("Annotations:", f.__annotations__)
    ... print("Arguments:", ham, eggs)
    ... return ham + ' and ' + eggs
    ...
    >>> f('spam')
    Annotations: {'ham': <class 'str'>, 'return': <class 'str'>, 'eggs': <class 'str'>}
    Arguments: spam eggs
    'spam and eggs'

    4.8. Intermezzo: Coding Style

PEP 8

  • Use 4-space indentation, and no tabs.
  • Wrap lines so that they don’t exceed 79 characters.
  • Use blank lines to separate functions and classes, and larger blocks of code inside functions.
  • When possible, put comments on a line of their own.
  • Use docstrings.
  • Use spaces around operators and after commas, but not directly inside bracketing constructs: a = f(1, 2) + g(3, 4).
  • Name your classes and functions consistently; the convention is to use UpperCamelCase for classes and lowercase_with_underscores for functions and methods. Always use self as the name for the first method argument.
  • Don’t use fancy encodings if your code is meant to be used in international environments.
  • Likewise, don’t use non-ASCII characters in identifiers if there is only the slightest chance people speaking a different language will read or maintain the code.