I have a Python framework that I'm trying to get to know. One of the classes has the following __init__()
:
def __init__(self, identity, direction, type, app_name, *aspects):
Usually (for clarity for myself), I try to call functions using their keywords, such as:
destination = RNS.Destination(
identity=identity,
direction=RNS.Destination.IN,
type=RNS.Destination.SINGLE,
app_name=APP_NAME,
aspects='minimalsample',
)
The exception I get when instantiating the class as above is Destination.__init__() got an unexpected keyword argument 'aspects'
.
I think this is because the aspects
argument is not really a keyword argument but rather a list of additional positional arguments.
Ok, so it isn't really a kwarg, so I tried passing the 'minimalsample' argument as the first:
destination = RNS.Destination(
'minimalsample',
identity=identity,
direction=RNS.Destination.IN,
type=RNS.Destination.SINGLE,
app_name=APP_NAME,
)
But then I get
TypeError: got multiple values for argument 'identity'
I think I see what is wrong here, but I have no idea how to call the function while retaining the clarity of using the keywords. Is there a good way?
This might have been asked before (plenty of times even), but I can't seem to find the answer with the search terms I'm using.
The documented signature is pretty clear.
RNS.Destination(identity, direction, type, app_name, *aspects)
It requires you to call it like this:
import RNS
from RNS.Destination import IN, SINGLE
aspects: list[str] = ['minimalsample', 'largesample']
dest = RNS.Destination(identity, IN, SINGLE, APP_NAME, *aspects)
That approach would work just the same if aspects
was a single-element list.
Here are equivalent ways of passing in 1 or 2 aspects:
... , APP_NAME, 'minimalsample')
... , APP_NAME, 'minimalsample', 'largesample')
The *args
notation indicates we're passing in a list of zero or more arguments.
It happens that this particular library wants more than zero arguments.
The authors of this package could have chosen to accept
an aspects: list[str]
argument,
or even aspects: list[str] | str
,
but that's not what they implemented.
The 1st alternative forces callers to use more [
]
brackets
than they might prefer for a common single-aspect use case.
The 2nd alternative trades convenience against simple type-safety
arguments.
Sometimes you'll see a signature like this:
def foo(a, b, c, d=None, e=0):
You could call that with
args = [a, b, c]
kwargs = dict(d=None, e=0)
foo(*args, **kwargs)
Since Destination
accepts a variable number of
aspect strings, you don't have a choice, you need
to list four positional args,
then one-or-more aspects,
followed by zero keyword args,
if you want the bytecode to push
all the right things onto the stack
by the time the constructor executes.
If you want to better understand the details, disassemble the bytecode of some calling function.
When a library author is designing a Public API, there's more than one way to force callers to use positional args.
What does the slash(/) in the parameter list of a function mean?
A slash in the argument list of a function denotes that the parameters prior to it are positional-only. Positional-only parameters are the ones without an externally usable name.
The documentation refers to PEP-570, which explains
When designing APIs, library authors try to ensure correct and intended usage of an API
and continues with a discussion of the careful choosing of names and several challenges.
Something you're likely to see more often in
signatures is a bare *
star, which introduces
keyword-only args.
For example, a library author who is concerned
about (x, y) vs (y, x) confusion,
stemming from (lat, long) vs (long, lat),
might choose a signature like this:
def gis_location(*, x, y):
This prevents attempted (incorrect!) calls such as
lhr = gis_location(51.48, -0.46)
and requires a more explicit
lhr = gis_location(y=51.48, x=-0.46)