So, this is all about the innocent little ✱ (star or asterisk) as a versatile syntax element in Python. Depending on the context, it fulfills quite a few different roles. Here is a piece of code that uses all of them (as far as I know).
In [2]:
# Yes. The code makes no sense. Thanks for pointing it out.
from os import *
def append(*, end=linesep):
def _append(function):
def star_reporter(*args, **kwargs):
print(*args, **kwargs, end=end)
return function(*args, **kwargs)
return star_reporter
return _append
@append(end=" ❇❇❇" + linesep)
def wrapped(stars, bars):
first, *middle, last = stars
for elem in [*middle, last, *bars]:
first *= 2 ** elem
print(f"answer: {first} (don't know the question though)")
In [3]:
2 * 2
Out[3]:
In [4]:
2 ** 4
Out[4]:
In [5]:
'spam' * 3
Out[5]:
Although it is not even trying to make sense, the star-spangled code example further up actually works. If you look at the wrapped
function, ✱ is used as a boring old mathematical operator here: first *= 2 ** elem
(which is using an augmented assignment and is the same as first = first * 2 ** elem
).
If we run wrappped
, we won't get a useful result but at least we see that the code executes:
In [6]:
wrapped([1, 2, 3, 4], (23, 42))
If your function needs 23 arguments, you have a big problem anyway but you can at least alleviate it a bit by making calls to that function more readable. Passing some or all arguments as keyword arguments usually helps. Problem is: the caller normally has the choice how to pass the arguments. You can even call a "keyword only" function like this:
In [7]:
def kw_only_you_wish(spam=None, eggs=None, lobster=None):
return spam + eggs * lobster
kw_only_you_wish(2, 3 ,4)
Out[7]:
With Python 3.0 a new syntax was introduced to make enforcement of so called "keyword-only arguments" possible. This is used in the definition of the append
function above. When using this, everything after the [, ]*,
has to be passed as keyword argument or you get into trouble.
Trying to decorate a function with append
and not passing end
as a keyword parameter results in a friendly TypeError
exception:
In [8]:
@append("❈❈❈")
def badly_wrapped():
pass
This goes back to at least Python 2.0. In this case ✱ and ✱✱ are syntax elements to be used as prefix, when defining or calling functions. The idea is usually that you want to pass through parameters to an underlying function without having to care about what or even how many they are. In this example we have a function that is just passing through arguments without needing to now anything about them:
In [ ]:
def passing_things_through_function(*args, **kwargs):
print(f"passing through {args=} and {kwargs=}")
the_actual_function(*args, **kwargs)
def the_actual_function(a, b, c=None, d=None):
print(f"passed arguments: {a=}, {b=}, {c=}, {d=}")
passing_things_through_function(*[1, 2], **dict(c=3, d=4))
A case where this is particularly useful is when creating decorators that are not opinionated about the kind of function they decorate (like append
). They just need to pass through whatever the decorated function needs to be called with.
In pre Python3 days so-called tuple unpacking was already supported. Here is the classic example of swapping assignments between two names:
In [ ]:
a = 1
b = 2
print(f"before: {a=}, {b=}")
a, b = b, a
print(f"after: {a=}, {b=}")
PEP 3132 - extended iterable unpacking brought the star into the "classic" tuple unpacking (which was never restricted to tuples but that name somehow stuck):
In [ ]:
for iterable in [
"egg",
[1, 2, 3],
(1, 2, 3),
{1, 2, 3},
{1: 'a', 2: 'b', 3: 'c'}
]:
print(f"{iterable} ({type(iterable)}):")
a, b, c = iterable
print(f"a, b, c -> {a} {b} {c}")
*a, b = iterable
print(f"*a, b = iterable -> {a} {b}")
a, *b = iterable
print(f"a, *b = iterable -> {a} {b}\n")
This syntax to merge iterables was implemented via PEP 448 (additional unpacking generalizations) in Python 3.5.1
In [ ]:
a, b = [1, 2, 3], [4, 5, 6]
[*a, *b]
In [ ]:
a, b = {1: 2, 2: 3, 3: 4}, {1: 4, 4: 5, 5: 6}
{**a, **b}
In [ ]:
a, b = {1, 2 ,3}, {3, 4 ,5}
{*a, *b}
This is the more "natural" approach for sets though (union):
In [ ]:
a | b
As the underlying functionality only cares about whether something is iterable, you can mix and match. This creates a tuple from a list and a set:
In [ ]:
(*[1, 2 ,3], *{3, 4 ,5})
Be aware though that merging maps like this is not recursive. Later keys overwrite earlier ones. Here foo
will contain the second dict after merging:
In [1]:
a = {"a": 1, "foo": { "a": 1}}
b = {"a": 1, "foo": { "b": 2, "c": 3}}
{**a, **b}
Out[1]:
The last star shines a bit dimly as this is usually an antipattern and it looks like this:
In [ ]:
from os import *
this is usually not a good idea because:
open
or some other inbuilt)linesep
come from?)If a package (or module)1 is explicitly designed to be imported like this, this is usually documented and the authors defined the special module attribute __all__
that explicitly lists the names that should be imported when using from <module or package> import *
I'm either not seeing it or the Python documentation is omitting that __all__
also works for modules. It does though ... I tried it.↩
That's all the stars I can think of for now. If you know any more: please let me know.