Functions

Scope rules


In [ ]:
a = 0
m = 0
n = 0
x = 0

print('PRINT 0:  a =', a,
      ' m =', m, ' n =', n,
      '        x =', x)

def f_1():
    m = 1
    global n
    n = 1
    x = 1
    y = 1
    z = 1
    print('PRINT 1:  a =', a,
          ' m =', m, ' n =', n,
          '        x =', x, ' y =', y, ' z =', z)
    
    def f_2():
        global m
        m = 2
        # Cannot write:
        # nonlocal n
        global n
        n = 2
        global p
        p = 2
        x = 2
        nonlocal y
        y = 2
        # Cannot write:
        # nonlocal u
        print('PRINT 2:  a =', a,
              ' m =', m, ' n =', n, ' p =', p,
              ' x =', x, ' y =', y, ' z =', z)

        def f_3():
            nonlocal x
            x = 3
            nonlocal y
            y = 3
            nonlocal z
            z = 3
            print('PRINT 3:  a =', a,
                  ' m =', m, ' n =', n, ' p =', p,
                  ' x =', x, ' y =', y, ' z =', z)

        f_3()
        print('PRINT 4:  a =', a,
              ' m =', m, ' n =', n, ' p =', p,
              ' x =', x, ' y =', y, ' z =', z)

    f_2()
    print('PRINT 5:  a =', a,
          ' m =', m, ' n =', n, ' p =', p,
          ' x =', x, ' y =', y, ' z =', z)

f_1()
print('PRINT 6:  a =', a,
      ' m =', m, ' n =', n, ' p =', p,
      ' x =', x)

In [ ]:
x = 0

def f():
    print(x)
    x = 1

f()

In [ ]:
def f():
   m = 0
   class C:
       m = 1
       def g(self):
           print(m)
   C().g()
f()

In [ ]:
i = 1
bad_increment = lambda x: x + i
i = 0
print(bad_increment(2))

i = 1
good_increment = lambda x, i = i: x + i
i = 0
print(good_increment(2))

Closures (factory functions)


In [ ]:
def v1_multiply_by(m):
    def multiply(n):
        return n * m
    return multiply

multiply_by_7 = v1_multiply_by(7)
print(multiply_by_7(4))

def v2_multiply_by(m):
    return lambda n: n * m

multiply_by_7 = v2_multiply_by(7)
print(multiply_by_7(4))

def multiplications_between_0_and_9():
    multiply_by = []
    for m in range(10):
        # If "lambda n, m = m: n * m" is replaced by "lambda n, m: n * m"
        # then all mulplications are by 9
        multiply_by.append(lambda n, m = m: n * m)
    return multiply_by

multiply_by = multiplications_between_0_and_9()
multiply_by_7 = multiply_by[7]
print(multiply_by_7(4))

Function states


In [ ]:
from random import randrange

def randomly_odd_or_even_random_digit():
    odd = randrange(2)
    if odd:
        def random_odd_or_random_even_digit():
            return randrange(1, 10, 2)
    else:
        def random_odd_or_random_even_digit():
            return randrange(0, 10, 2)
    random_odd_or_random_even_digit.odd = odd
    return random_odd_or_random_even_digit

for i in range(10):
    random_odd_or_random_even_digit = randomly_odd_or_even_random_digit()
    if random_odd_or_random_even_digit.odd:
        print('Will be a random odd digit.... ', random_odd_or_random_even_digit())
    else:
        print('Will be a random even digit... ', random_odd_or_random_even_digit())

Function parameters:

  • first parameters without default values, if any,
  • then parameters with default values, if any,
  • then, possibly,
    • either a starred parameter to
      • gather values and assign them to parameters of the first and second type beyond the longest initial segment of those that are otherwise assigned an argument, if any, provided none of those parameters is assigned a keyword argument,
      • and to store an arbitray number of positional arguments beyond those that have been assigned to a parameter, if any,
    • or only a star,
  • if a starred parameter or only a star is present, then parameters for required keyword arguments (so called "keyword-only arguments"), if any, with or without defaults (actually the defaults make the associated keyword-only arguments not truly required and these parameters could be part of the second group),
  • then a double starred parameter to store an arbitray number of keyword arguments, if any.

Function arguments:

  • positional arguments precede keyword arguments and double starred ones, and
  • starred arguments precede double starred ones.

In [ ]:
def f1(a, b, c = 3, d = 4, e = 5, f = 6):
    print(a, b, c, d, e, f)

f1(11, 12, 13, 14, 15, 16)
f1(11, 12, 13, *(14, 15, 16))
f1(11, *(12, 13, 14), **{'f': 16, 'e': 15})
f1(11, 12, 13, e = 15)
f1(11, c = 13, b = 12, e = 15)
f1(11, c = 13, *(12,), e = 15)
f1(11, *(12, 13), e = 15)
f1(11, e = 15, *(12, 13))
f1(11, f = 16, e = 15, b = 12, c = 13)
f1(11, f = 16, **{'e': 15, 'b': 12, 'c': 13})
f1(11, *(12, 13), e = 15, **{'f': 16, 'd': 14})
f1(11, e = 15, *(12,), **{'f': 16, 'd': 14})
f1(11, f = 16, *(12, 13), e = 15, **{'d': 14})

In [ ]:
def f2(*x):
    print(x)

f2()
f2(11)
f2(11, 12, *(13, 14, 15))

In [ ]:
def f3(*x, a, b = -2, c):
    print(x, a, b, c)

f3(c = 23, a = 21)
f3(11, 12, a = 21, **{'b': 22, 'c': 23})
f3(11, *(12, 13), c = 23, a = 21)
f3(11, 12, 13, c = 23, *(14, 15), **{'a': 21})

In [ ]:
def f4(*, a, b = -2, c):
    print(a, b, c)

f4(c = 23, a = 21)
f4(**{'a': 21, 'b': 22, 'c': 23})
f4(c = 23, **{'a': 21})
f4(a = 21, **{'c': 23, 'b': 22})

In [ ]:
def f5(**x):
    print(x)

f5()
f5(a = 11, b = 12)
f5(**{'a': 11, 'b': 12, 'c': 13})
f5(a = 11, c = 12, e = 15, **{'b': 13, 'd': 14})

In [ ]:
def f6(a, b, c, d = 4, e = 5, *x, m, n = -2, o, **z):
    print(a, b, c, d, e, x, m, n, o, z)

# Cannot replace "*(12,)" by "*(12, 21)"
f6(11, t = 40, e = 15, *(12,), o = 33, c = 13, m = 31, u = 41,
   **{'v': 42, 'w': 43}) 
# Cannot replace "*(13, 14)" by "*(13, 14, 21)"
f6(11, 12, u = 41, m = 31, t = 40, e = 15, *(13, 14), o = 33,
   **{'v': 42, 'w': 43}) 
f6(11, u = 41, o = 33, *(12, 13, 14, 15, 21, 22), n = 32, t = 40, m = 31,
   **{'v': 42, 'w': 43}) 
f6(11, 12, 13, n = 32, t = 40, *(14, 15, 21, 22, 23), o = 33, u = 41, m = 31,
   **{'v': 42, 'w': 43})

Function annotations


In [ ]:
def f(w: str, a: int, b: int = -2, x: float = -3.) -> int:
    if w == 'incorrect_return_type':
        return '0'
    return 0

from inspect import signature

def type_check(function, *args, **kwargs):
    '''Assumes that "function" has nothing but variables possibly with defaults
    as arguments and has type annotations for all arguments and the returned value.
    Checks whether a combination of positional and default arguments is correct,
    and in case it is whether those arguments are of the appropriate types,
    and in case they are whether the returned value is of the appropriate type.
    '''
    good_arguments = True
    argument_type_errors = ''
    parameters = list(reversed(function.__code__.co_varnames))
    if len(args) > len(parameters):
        print('Incorrect sequence of arguments')
        return
    for argument in args:
        parameter = parameters.pop()
        if not isinstance(argument, function.__annotations__[parameter]):
            argument_type_errors += ('{} should be of type {}\n'
                              .format(parameter, function.__annotations__[parameter]))
            good_arguments = False
    for argument in kwargs:
        if not argument in parameters:
            print('Incorrect sequence of arguments')
            return
        if not isinstance(kwargs[argument], function.__annotations__[argument]):
            argument_type_errors += ('{} should be of type {}\n'
                              .format(argument, function.__annotations__[argument]))
            good_arguments = False
        parameters.remove(argument)
    # Make sure that all parameters left are given a default value.
    if any([parameter for parameter in parameters
               if signature(function).parameters[parameter].default is
                signature(function).parameters[parameter].empty]):
        print('Incorrect sequence of arguments')
        return
    if good_arguments:
        if isinstance(function(*args, **kwargs), function.__annotations__['return']):
            print('All good')
        else:
            (print('The returned value should be of type {}'
                   .format(function.__annotations__['return'])))
    else:
        print(argument_type_errors, end = '')

for args, kwargs in [(('0', 1, 2, 3.), {}),
                     (('0', 1, 2), {'x': 3.}),
                     (('0', 1), {'b': 2, 'x': 3.}),
                     (('0',), {'x': 3., 'a': 1, 'b': 2}),
                     ((), {'x': 3., 'w': '0', 'a': 1}),
                     (('0', 1, 2), {}),
                     (('0',), {}),
                     (('0'), {'x': 3.}),
                     (('0', 1, 2, 3., 4), {}),
                     (('incorrect_return_type', 1, 2, 3.), {'x' : 3}),
                     (('incorrect_return_type', 1, 2), {'y': 3}),
                     (('0', 1), {'x': 3, 'c': 2}),
                     ((), {'a': 1, 'b': 2,'x': 3}),
                     ((0, 1, 2, 3.), {}),
                     (('0', 1., 2, 3), {'w': 'incorrect_return_type'}),
                     (('incorrect_return_type', 1, 2), {'x': 3}),
                     ((0, 1), {'b': 2., 'x': 3.}),
                     ((0,), {'x': 3, 'a': 1., 'b': 2.}),
                     ((), {'x': 3, 'w': 0, 'a': 1.}),
                     (('incorrect_return_type', 1, 2, 3.), {})]:
    print('Testing {}, {}:'.format(args, kwargs))
    type_check(f, *args, **kwargs)
    print()

Mutable versus immutable default values


In [ ]:
def append_one_v1(L = []):
    L.append(1)
    return L

def append_one_v2(L = None):
    if L == None:
        L = []
    L.append(1)
    return L

for i in range(5):
    print(append_one_v1([0]))
print()
for i in range(5):
    print(append_one_v1())
print()
for i in range(5):
    print(append_one_v2([0]))
print()
for i in range(5):
    print(append_one_v2())

In [ ]:
_nothing = object()

def f(x = _nothing):
    if x is _nothing:
        print('Nothing')
    else:
        print('Something')

f(0), f(1), f([]), f([1]), f(None)
print()
f()