第三屆 Python Deep Dive I - Keyword Arguments

Keyword Arguments - Lecture

  • Keyword Argument

  • Mandatory Keyword Arguments

  • Omit or force no positional arguments

  • Putting it all together

    • 無預設值則為 Mandatory,有預設值則為 Optional
    • *args 表示 可傳入零至多個 positional arguments
    • 若僅有 * ,則表示不得傳入任何一個 positional argument
    • 在最後一個 positional augument 後,全數都必須是 keyword(named) argument

Keyword Arguments - Coding

def func1(a, b, c):
    print(a, b, c)

func1(1, 2, 3)
func1(1, c=3, b=2)
func1(c=3, b=2, a=1)
1 2 3
1 2 3
1 2 3
def func2(a, b, *args): # a, b are mandatory positional arguments
    print(a, b, args)

func2(1, 2, 3, 4)
func2(1, 2)
func2(1, 2, 3, 4, 5, 6, 7, 8, 9)
1 2 (3, 4)
1 2 ()
1 2 (3, 4, 5, 6, 7, 8, 9)
def func3(a, b, *args, d): # the way to force users to use keyword argument for d
    print(a, b, args, d)

func3(1, 2, d=4)
func3(1, 2, 3, d=4)
func3(1, 2, 3, 4, d=5)
func3(1, 2, 3, 4, 5, 6, 7, 8, 9, d=10)
# func3(1, 2, 3, 4)  ->TypeError -> missing 1 required keyword-only argument: 'd'
1 2 () 4
1 2 (3,) 4
1 2 (3, 4) 5
1 2 (3, 4, 5, 6, 7, 8, 9) 10
def func4(*args, d): # We can have zero to several positional arguments
    print(args, d)

func4(d=10)
func4(1, d=10)
func4(1, 2, d=10)
func4(1, 2, 3, 4, 5, 6, 7, 8, 9, d=10)
() 10
(1,) 10
(1, 2) 10
(1, 2, 3, 4, 5, 6, 7, 8, 9) 10
def func5(*, d): # no positional argument is allowed
    print(d)

func5(d=10)
# func5(1, d=10)
# func5(1, 2, d=10)
10
def func6(a, b, *, d):  # Only two mandatory positional arguments are allowed + one keyword argument
    print(a, b, d)

func6(1, 2, d=4)
# func6(1, d=4)  # TypeError: func6() missing 1 required positional argument: 'b'
# func6(1, 2, 3, d=4) # TypeError: func6()takes 2 positional arguments but 3 positional arguments were given
1 2 4
def func7(a, b=2, *args, d):  #  b with default value -> make it an optional positional argument
    print(a, b, args, d)

func7(1, d=9)
func7(1, 3, 5, d=9)
func7(1, 3, 5, 7, d=9)
1 2 () 9
1 3 (5,) 9
1 3 (5, 7) 9
# b with default value -> make it an optional positional argument
# d with default value -> make it an optional keyword argument
def func8(a, b=2, *args, d=0, e):
    print(a, b, args, d, e)

func8(1, e=9) # 1, 2, (), 0, 9
func8(1, 3, e=9) # 1, 3, (), 0, 9
func8(1, 3, d=8, e=9) # 1, 3 ,(), 8, 9
func8(1, 3, 5, d=8, e=9) # 1, 3, (5,), 8, 9
func8(1, 3, 5, 7, d=8, e=9) # 1, 3, (5, 7), 8, 9
func8(1, 3, 5, 7, e=9) # 1, 3, (5, 7), 0, 9
func8(1, "m/s", 5, "mph", d="key_arg1", e = "key_arg2")
1 2 () 0 9
1 3 () 0 9
1 3 () 8 9
1 3 (5,) 8 9
1 3 (5, 7) 8 9
1 3 (5, 7) 0 9
1 m/s (5, 'mph') key_arg1 key_arg2

**kwargs

  • \color{tomato}{*args} is used to scoop up variable amount of remaining \color{tomato}{positional} arguments → \color{tomato}{tuple}
  • \color{tomato}{**kwargs} is used to scoop up variable amount of remaining \color{tomato}{keyword} arguments → \color{tomato}{dictionary}
  • The parameter name args or kwargs is just the conventional name.
  • No parameter can come after **kwargs
def func(**kwarg):
    print(kwarg)

func(a=1, b=2, c=3)
{'a': 1, 'b': 2, 'c': 3}
def func(*arg, **kwarg):
    print(arg, kwarg, sep=" | ")

func(1, 2, c=3, d=4, e=5)
(1, 2) | {'c': 3, 'd': 4, 'e': 5}
def func(a, b, *, d, **kwargs):
    print(a, b, d, kwargs, sep="     ")

func(1, 2, d=20, e=45, f=55, g=65)
1     2     20     {'e': 45, 'f': 55, 'g': 65}
def func(a, b, **kwargs):
    print(a, b, kwargs, sep="     ")

func(1, 2)
func(1, 2, c=3, d=4, e=5)
1     2     {}
1     2     {'c': 3, 'd': 4, 'e': 5}

Putting it all Together - Lecture

  • Recap
    • Positional arguments
      • with \color{tomato}{specific\ position}, and may have default values
      • \color{tomato}{*args} => \color{tomato}{collects\ and\ exhausts} remaining positional arguments => \color{tomato}{tuple}
      • \color{tomato}{*} => indicates the end of positional arguments
    • Keyword arguments (or Named arguments)
      • with \color{tomato}{specific\ name}, and may have default values
      • after positional arguments have been exhausted
      • \color{tomato}{**kwargs} => \color{tomato}{collects} any remaining keyword arguments => \color{tomato}{dictionary}
    • The sequence of the arguments

    • Some examples

      • def func(a, b=10)
      • def func(a, b, *args)
      • def func(a, b, *args, kw1, kw2=100)
      • def func(a, b=10, *, kw1, kw2=100)
      • def func(a, b, *args, kw1, kw2=100, **kwargs)
      • def func(a, b=10, *, kw1, kw2=100, **kwargs)
      • def func(*args)
      • def func(**kwargs)
      • def func(*args, **kwargs)
    • Typical use cases

      • help(print) => print(*args, sep=’ ‘, end=’\n’, file=None, flush=False)
      • Often, keyword arguments are used to \color{tomato}{modify\ the\ default\ behavior} of a function
      • Other times, keyword arguments might be used to make things clearer
help(print)
Help on built-in function print in module builtins:

print(*args, sep=' ', end='\n', file=None, flush=False)
    Prints the values to a stream, or to sys.stdout by default.

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

Putting it all Together - Coding

def func(a, b, *args):
    print(a, b, args)

func(1, 2)
func(1, 2, 'x')
func(1, 2, 'x', 4, 5)
# func(1, b=2, 'x', 4, 5) # SyntaxError: positional argument follows keyword argument
1 2 ()
1 2 ('x',)
1 2 ('x', 4, 5)
def func(a, b=2, c=3,  *args):
    print(a, b, c, args)

func(1)
func(1, 3, 'x', 7, 9)
# func(1, c=3, 'x', 7, 9) # SyntaxError: positional argument follows keyword argument
1 2 3 ()
1 3 x (7, 9)
def func(a, b=2, *args, d=4, e):
    print(a, b, args, d, e)

func(1, e =5)
func(1, 3, 'x', 7, e=9)
print("_" * 50)
# func(1, 'x', 'y','z', b=7, e=9) # TypeError: func() got multiple values for argument 'b'
func(1, 'x', 'y','z', e=9)
1 2 () 4 5
1 3 ('x', 7) 4 9
__________________________________________________
1 x ('y', 'z') 4 9
def func(a, b, *args, c=10, d=4, **kwargs):
    print(a, b, args, c, d, kwargs)

func(1, 2, 'x', 'y','z', c=100, d=200, x=0.1, y=0.2)

1 2 ('x', 'y', 'z') 100 200 {'x': 0.1, 'y': 0.2}
# help(print) - Typical usage of keyword arguments
# print(*args, sep=' ', end='\n', file=None, flush=False)
# default -> (1)以 space 區隔,(2)執行後換行,(3)輸出至 sys.stdout,(4)不做 flush
print(1, 2, 3)
print(4, 5, 6)
print("_" * 50)
print(1, 2, 3, sep='-', end=" *** ")
print(4, 5, 6, sep='-')
1 2 3
4 5 6
__________________________________________________
1-2-3 *** 4-5-6
  • X \color{tomato}{or} Y → If X is truthy, returns X, otherwise evaluates and returns Y
  • X \color{tomato}{and} Y → If X is falsy, returns X, otherwise evaluates and returns Y
def calc_hi_lo_avg(*args, log_to_console=False):
    hi = int(bool(args)) and max(args)
    lo = int(bool(args)) and min(args)
    avg = (hi + lo) / 2
    if  log_to_console:
        print("high={0}, low={1}, avg={2}".format(hi, lo, avg))
    return avg

is_debug = True
avg1 = calc_hi_lo_avg(1, 2, 3, 4, 5)
print(avg1)
avg2 = calc_hi_lo_avg()
print(avg2)
print("_" * 50)
avg3 = calc_hi_lo_avg(1, 2, 3, 4, 5, log_to_console=is_debug)
print(avg3)
3.0
0.0
__________________________________________________
high=5, low=1, avg=3.0
3.0

A Simple Timer

import time
def time_it(fn, *args, **kwargs):
    print(args, kwargs)

time_it(print, 1, 2, 3, sep= " - ", end= " ***")
(1, 2, 3) {'sep': ' - ', 'end': ' ***'}
import time
def time_it(fn, *args, **kwargs):
    fn(args, kwargs) # just print the tuple and dictionary, but not use them as arguments

time_it(print, 1, 2, 3, sep=" - ", end=" ***")
(1, 2, 3) {'sep': ' - ', 'end': ' ***'}
import time
def time_it(fn, *args, **kwargs): # pack auguments -> into a tuple, and a dictionary
    fn(*args, **kwargs) # unpack the tuple and dictionary -> into individual arguments

time_it(print, 1, 2, 3, sep=" - ", end=" ***")
1 - 2 - 3 ***
import time
def time_it(fn, *args, rep=1, **kwargs):
    for i in range(rep):
        fn(*args, **kwargs)

time_it(print, 1, 2, 3, sep=" - ", end=" ***\n", rep=5)
1 - 2 - 3 ***
1 - 2 - 3 ***
1 - 2 - 3 ***
1 - 2 - 3 ***
1 - 2 - 3 ***
import time

def time_it(fn, *args, rep=1, **kwargs):
    start = time.perf_counter()
    for i in range(rep):
        fn(*args, **kwargs)
    end = time.perf_counter()
    return (end - start) / rep # average run time

time_it(print, 1, 2, 3, sep=" - ", end=" ***\n", rep=5)
1 - 2 - 3 ***
1 - 2 - 3 ***
1 - 2 - 3 ***
1 - 2 - 3 ***
1 - 2 - 3 ***





8.095999946817755e-05
def compute_powers_1(n, *, start=1, end):
    # using a for loop
    results = []
    for i in range(start, end):
        results.append(n**i)
    return results

compute_powers_1(2, end=5)
[2, 4, 8, 16]
def compute_powers_2(n, *, start=1, end):
    # using a list comprehension
    return[n**i for i in range(start, end)] # very concise and pythonic

compute_powers_2(2, end=5)
[2, 4, 8, 16]
def compute_powers_3(n, *, start=1, end):
    # using a generator expression
    return (n**i for i in range(start, end))  # very concise and pythonic

x = compute_powers_3(2, end=5) # we actually get a generator object instead of a list
list(x) # using the list constructor and passing it the generator
[2, 4, 8, 16]
time_it(compute_powers_1, 2, start=0, end=20000, rep=5)
1.6312298199976794
time_it(compute_powers_2, 2, start=0, end=20000, rep=5)
1.280659900000319
time_it(compute_powers_3, 2, start=0, end=20000, rep=5)
4.760001320391893e-06

Parameter Defaults - Beware!

  • At run-time, when a module is loaded: all code is executed immediately
  • default value specified is created when the function is loaded before the function is actually called
  • it causes some problems in certain circumstance
    • example
      • def log(msg, *, dt=datetime.utcnow()):
      • To fix it → def log(msg, *, dt=\color{tomato}{None}):, and check if dt is None in the function
  • In general, always beware of using a mutable object(or a callable) for an argument default
from datetime import datetime
datetime.utcnow()
print(datetime.utcnow()) # it is an object, but it also has string representation
print(datetime.utcnow())
2024-06-07 09:10:49.905302
2024-06-07 09:10:49.910297
from datetime import datetime

def log(msg, *, dt=datetime.utcnow()):
    print('{0}: {1}'.format(dt, msg))

log('message 01', dt='2024-06-06 17:10:33.909808')
log('message 02', dt='2024-06-06 18:20:44.909808')
log('message 03', dt='2024-06-06 19:30:55.909808')
print("_" * 50)
log('message 04') # without specifing default dt
log('message 05')
log('message 06')
2024-06-06 17:10:33.909808: message 01
2024-06-06 18:20:44.909808: message 02
2024-06-06 19:30:55.909808: message 03
__________________________________________________
2024-06-07 09:19:28.586251: message 04
2024-06-07 09:19:28.586251: message 05
2024-06-07 09:19:28.586251: message 06
# fixed function
from datetime import datetime

def log1(msg, *, dt=None):
    # if not dt:
    #    dt = datetime.utcnow()
    dt = dt or datetime.utcnow() # a more concise way without using if clause
    print('{0}: {1}'.format(dt, msg))

log1('message 07', dt='2024-06-06 20:15:33.009808')
log1('message 08', dt='2024-06-06 21:25:44.009808')
log1('message 09', dt='2024-06-06 22:35:55.009808')
print("_" * 50)
log1('message 10')  # without specifing default dt
2024-06-06 20:15:33.009808: message 07
2024-06-06 21:25:44.009808: message 08
2024-06-06 22:35:55.009808: message 09
__________________________________________________
2024-06-07 09:30:59.810886: message 10
log1('message 11')
2024-06-07 09:31:16.103737: message 11
log1('message 12')
2024-06-07 09:31:30.079254: message 12
# use a mutable object as the default value -> unexpected result
my_list = [1, 2, 3]

def func(a=my_list):
    print(a)

func()
func(['a','b'])

my_list.append(4)
func() # default value changed
[1, 2, 3]
['a', 'b']
[1, 2, 3, 4]
# Fixed program -> use immutable tuple instead
my_tuple = (1, 2, 3)

def func(a=my_tuple):
    print(a)

func()
func(['a', 'b'])
print("_" * 50)
# my_tuple.append(4) # -> AttributeError: 'tuple' object has no attribute 'append'
func()  # default value never changed
(1, 2, 3)
['a', 'b']
__________________________________________________
(1, 2, 3)

Parameter Defaults - Pitfall

  • Techinically, when we pass a mutable object to a function, it is a better approach that we don’t return it .
def add_item(name, quantity, unit, grocery_list):
    grocery_list.append("{0} ({1} {2})".format(name, quantity, unit))
    return grocery_list

store_1 = []
store_2 = []

add_item('banana', 2, 'units', store_1)
add_item('milk', 1, 'liter', store_1)
print(store_1)

add_item('Python', 1, 'medium-rare', store_2)
print(store_2)
['banana (2 units)', 'milk (1 liter)']
['Python (1 medium-rare)']
# add a default value for grocery_list
def add_item(name, quantity, unit, grocery_list=[]):
    grocery_list.append("{0} ({1} {2})".format(name, quantity, unit))
    return grocery_list

store_3 = add_item('banana', 2, 'units')
add_item('milk', 1, 'liter', store_3)
print(store_3)

store_4 = add_item('Python', 1, 'medium-rare')
print(store_4)
['banana (2 units)', 'milk (1 liter)']
['banana (2 units)', 'milk (1 liter)', 'Python (1 medium-rare)']
print(store_3)
store_3 is store_4
['banana (2 units)', 'milk (1 liter)', 'Python (1 medium-rare)']

True
# quick fix -> change default value to None, and check if the default value is none at beginning of the function
def add_item(name, quantity, unit, grocery_list=None):
    if not grocery_list:
        grocery_list= []
    grocery_list.append("{0} ({1} {2})".format(name, quantity, unit))
    return grocery_list

store_3 = add_item('banana', 2, 'units')
add_item('milk', 1, 'liter', store_3)
print(store_3)

store_4 = add_item('Python', 1, 'medium-rare')
print(store_4)
['banana (2 units)', 'milk (1 liter)']
['Python (1 medium-rare)']
# However, here is an example we could leverage this pitfall
def factorial(n): # 階乘函數
    if n < 1:
        return 1
    else:
        print('calculating {0}!'.format(n))
        return n * factorial(n-1)

factorial(3)
print("_" * 50)
factorial(3)
calculating 3!
calculating 2!
calculating 1!
__________________________________________________
calculating 3!
calculating 2!
calculating 1!

6
# However, here is an example we could leverage this pitfall
# Use a dictionary to cache the result to prevent re-calculation
cache = {}
def factorial_new(n, *, cache):
    if n < 1:
        return 1
    elif n in cache:
        return cache[n]
    else:
        print('calculating {0}!'.format(n))
        result = n * factorial_new(n-1, cache=cache)
        cache[n] = result
        return result

factorial_new(3, cache=cache)
print("_" * 50)
print(cache)
print("_" * 50)
print(factorial_new(3, cache=cache))
print("_" * 50)
factorial_new(4, cache=cache)
calculating 3!
calculating 2!
calculating 1!
__________________________________________________
{1: 1, 2: 2, 3: 6}
__________________________________________________
6
__________________________________________________
calculating 4!

24
def factorial_new1(n, cache1={}):
    if n < 1:
        return 1
    elif n in cache1:
        return cache1[n]
    else:
        print('calculating {0}!'.format(n))
        result = n * factorial_new1(n-1)
        cache1[n] = result
        return result

factorial_new1(3)
print("_" * 50)
factorial_new1(5)
calculating 3!
calculating 2!
calculating 1!
__________________________________________________
calculating 5!
calculating 4!

120
1 Like