In Python 3, we can define a function with keyword-only arguments and in Python 3.8, we can further define with positional-only arguments. The syntax is easy, as follows:
Syntax:
def func(x, /, y, z=0, *, a, b):
...
which, the *
signifies whatever afterwards are specified as keywords only.
And the /
signifies whatever before are positional only. This means the
function defined above cannot be invoked with:
func(x=3)
func(3, 4, 5, 6, 7)
for the first violated the requirement that argument x
must be positional
only and the second violated that a
and b
must be keyword only. These
works, however:
func(3, 4, 5, a=6, b=7)
func(3, 4, z=5, a=6, b=7)
because argument z
(also y
) can be specified either as positional or
keyword argument
In this definition, z
has a default value. We can legally define a function
without default values for a
and b
because they are keyword only.
Otherwise, we must specify their default value for them if we provided one for
z
since they can be positional.
While we do not provide a default for the two keyword-only arguments, they are not required. That is, we cannot invoke with
func(3, 4, a=5)
but must provide both as
func(3, 4, a=5, b=6)
Why we need them? One example is to make decorators. Consider a decorator that
can be used differently, such as functools.lru_cache
:
@lru_cache
def my_function():
...
@lru_cache(maxsize=32):
def my_other_function():
...
This decorator can be invoked using @lru_cache
as well as
@lru_cache(maxsize=32)
. Recall that a decorator in Python simply means
def my_function():
...
def my_other_function():
...
my_function = lru_cache(my_function)
my_other_function = lru_cache(maxsize=32)(my_other_function)
So in the first example, lru_cache
is a function that takes another function
as argument, and in the second example, lru_cache(maxsize=32)
returns a
function that does the same. If we look at the code, it is indeed implemented
this way:
def lru_cache(maxsize=128, typed=False):
if isinstance(maxsize, int):
# Negative maxsize is treated as 0
if maxsize < 0:
maxsize = 0
elif callable(maxsize) and isinstance(typed, bool):
# The user_function was passed in directly via the maxsize argument
user_function, maxsize = maxsize, 128
wrapper = _lru_cache_wrapper(user_function, maxsize, typed, _CacheInfo)
wrapper.cache_parameters = lambda : {'maxsize': maxsize, 'typed': typed}
return update_wrapper(wrapper, user_function)
elif maxsize is not None:
raise TypeError(
'Expected first argument to be an integer, a callable, or None')
def decorating_function(user_function):
wrapper = _lru_cache_wrapper(user_function, maxsize, typed, _CacheInfo)
wrapper.cache_parameters = lambda : {'maxsize': maxsize, 'typed': typed}
return update_wrapper(wrapper, user_function)
return decorating_function
So some isinstance()
and callable()
is used to see if lru_cache
is used
as a decorator or it is used as lru_cache(maxsize=32)
.
We can indeed write in this way (which we assume we forbid the usage of
lru_cache(32)
as decorator):
def lru_cache(func=None, /, *, maxsize=128, typed=False):
def decorating_function(user_function):
wrapper = _lru_cache_wrapper(user_function, maxsize, typed, _CacheInfo)
wrapper.cache_parameters = lambda : {'maxsize': maxsize, 'typed': typed}
return update_wrapper(wrapper, user_function)
if func is None:
return decorating_function
else:
return decorating_function(func)
The way it works is as follows: If @lru_cache
is used as decorator, it will be
invoked with the function to decorate as positional argument. But if
@lru_cache(maxsize=32)
is used as decorator, we assume a decorator should
return as a result of lru_cache(maxsize=32)
.