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
- def func(a, b, c=10, _arg / _, kw1, kw2=100, **kwargs)
- def func(a, b, c=10, _arg / _, kw1, kw2=100, **kwargs)
-
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
- Positional arguments
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
- example
- 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