Essence and usage of asterisks in Python

Keywords: Python Asterisk Programming Ruby

Translator: Python Developer - Yiting, English: Trey Hunner

http://blog.jobbole.com/114655/

Python Developer

 

There are many places to see * and ** in Python. In some cases, both operators can be a bit mysterious for novice programmers or for people migrating from many other programming languages that do not have exactly the same operators. So I want to discuss the nature of these operators and how they are used.

Over the years, the functions of * and ** operators have been increasing. In this article, I'll discuss all the current usage of these operators and point out which ones can only be used in the current Python version. So if you've learned how to use * and ** in Python 2, I recommend that you at least browse through this article, because Python 3 adds many new uses of * and **.

If you are new to Python and are not familiar with keyword parameters (also known as naming parameters), I suggest you read my article on keyword parameters in Python first.

What does not fall within the scope of our discussion

In this article, when I discuss * and **, I mean * and ** prefix operators, not infix operators.

That is to say, I am not talking about multiplication and exponential operations:

>>> 2 * 5

10

>>> 2 ** 5

32

So what are we talking about?

We are talking about * and ** prefix operators, which are * and ** operators used before variables. For example:

>>> numbers = [2, 1, 3, 4, 7]

>>> more_numbers = [*numbers, 11, 18]

>>> print(*more_numbers, sep=', ')

2, 1, 3, 4, 7, 11, 18

 

Two uses of * are shown in the above code, but the use of ** is not shown.

These include:

  1. Transfer parameters to functions using * and **

  2. Use * and ** to capture parameters passed into functions

  3. Use * to accept parameters that contain only keywords

  4. Use * to capture items when unpacking tuples

  5. Use * to extract iteration items into lists/tuples

  6. Use ** to extract the dictionary into other dictionaries

 

Even if you think you're familiar with all the ways in which * and ** are used, I recommend that you look at each block of code below to make sure it's all familiar to you. Over the past few years, Python core developers have been adding new functions to these operators, and it's easy for users to overlook new uses of * and **.

The asterisk is used to split the iterative objects and to take them as function parameters, respectively.

When a function is called, the * operator can be used to decompress an iteration item into a parameter in the function call:

>>> fruits = ['lemon', 'pear', 'watermelon', 'tomato']

>>> print(fruits[0], fruits[1], fruits[2], fruits[3])

lemon pear watermelon tomato

>>> print(*fruits)

lemon pear watermelon tomato

 

The print(*fruits) line passes all items in the fruits list as independent parameters to the print function call, even without knowing how many parameters are in the list.

* Operators are much more than grammatical sugar here. It is impossible to transfer all items as independent parameters with a specific iterator without using * unless the length of the list is fixed.

Here is another example:

def transpose_list(list_of_lists):

    return [

        list(row)

        for row in zip(*list_of_lists)

    ]

 

Here we accept a two-dimensional list and return a "transposed" two-dimensional list.

>>> transpose_list([[1, 4, 7], [2, 5, 8], [3, 6, 9]])

[[1, 2, 3], [4, 5, 6], [7, 8, 9]]


The ** operator performs a similar operation, using only keyword parameters. The ** operator allows us to get a key-value dictionary and decompress it into a keyword parameter in a function call.

>>> date_info = {'year': "2020", 'month': "01", 'day': "01"}

>>> filename = "{year}-{month}-{day}.txt".format(**date_info)

>>> filename '2020-01-01.txt' `

 

In my experience, it is not common to use ** to decompress keyword parameters into function calls. I see it most often when implementing inheritance: calls to uper() usually include * and **.

As in Python 3.5, both * and ** can be used multiple times in function calls.

Sometimes it's convenient to use * multiple times:

>>> fruits = ['lemon', 'pear', 'watermelon', 'tomato']

>>> numbers = [2, 1, 3, 4, 7]

>>> print(*numbers, *fruits)

2 1 3 4 7 lemon pear watermelon tomato `


Similar results can be achieved with multiple use of **:

>>> date_info = {'year': "2020", 'month': "01", 'day': "01"}

>>> track_info = {'artist': "Beethoven", 'title': 'Symphony No 5'}

>>> filename = "{year}-{month}-{day}-{artist}-{title}.txt".format(

...     **date_info,

...     **track_info,

... )

>>> filename

'2020-01-01-Beethoven-Symphony No 5.txt'

 

However, special care should be taken when using ** many times. Functions in Python cannot specify the same keyword parameters multiple times, so keys used with ** in each dictionary must be distinguishable from each other, otherwise exceptions will be raised.

Asterisks are used to compress parameters passed into functions

When defining a function, the * operator can be used to capture the position parameters passed to the function. The number of location parameters is not limited, and they are captured and stored in a tuple.

from random import randint  

 

def roll(*dice):    

    return sum(randint(1, die) for die in dice)


The number of parameters accepted by this function is unlimited:

>>> roll(20)

18

>>> roll(6, 6)

9

>>> roll(6, 6, 6)

8

 

The number of position parameters accepted by Python's print and zip functions is unlimited. * This parameter compression usage allows us to create functions like print and zip, accepting any number of parameters.

The ** operator also has another function: when we define a function, we can use ** to capture any keyword parameter of the incoming function into a dictionary:

def tag(tag_name, **attributes):

    attribute_list = [

        f'{name}="{value}"'

        for name, value in attributes.items()

    ]    

    return f"<{tag_name} {' '.join(attribute_list)}>"

 

** Any keyword parameters that we pass into this function will be captured and put into a dictionary that will refer to attributes parameters.

>>> tag('a', href="http://treyhunner.com")

'<a href="http://treyhunner.com">'

>>> tag('img', height=20, width=40, src="face.jpg")

'<img height="20" width="40" src="face.jpg">'

 

Location parameters with keyword parameters only

In Python 3, we now have a special grammar to accept function parameters with only keywords. Only keyword parameters are function parameters that can only be specified by keyword grammar, which means that they cannot be specified by location.

In defining functions, in order to accept keyword-only parameters, we can put named parameters after *:

def get_multiple(*keys, dictionary, default=None):

    return [

        dictionary.get(key, default)

        for key in keys

    ]

 

The above functions can be used like this:

>>> fruits = {'lemon': 'yellow', 'orange': 'orange', 'tomato': 'red'}

>>> get_multiple('lemon', 'tomato', 'squash', dictionary=fruits,default='unknown')

['yellow', 'red', 'unknown']

 

The parameters dictionary and default follow * keys, which means that they can only be specified as keyword parameters. If we try to specify them by location, we get an error:

>>> fruits = {'lemon': 'yellow', 'orange': 'orange', 'tomato': 'red'}

>>> get_multiple('lemon', 'tomato', 'squash', fruits, 'unknown')

Traceback (most recent call last):

File "<stdin>", line 1, in <module>

TypeError: get_multiple() missing 1 required keyword-only argument: 'dictionary'

 

This behavior is introduced into Python through PEP 3102.

Parameters without location parameter keywords

It's cool to use only keyword parameters, but what if you want to use only keyword parameters without capturing infinite location parameters?

Python uses a somewhat odd separate* grammar to implement the following:___________

def with_previous(iterable, *, fillvalue=None):

    """Yield each iterable item along with the item before it."""    

    previous = fillvalue    

    for item in iterable:        

        yield previous, item        

        previous = item

 

This function accepts an iterator parameter, which can be specified by location or name (as the first parameter) and the keyword parameter fillvalue, which only uses keywords. This means that we can call with_previous as follows:

 

>>> list(with_previous([2, 1, 3], fillvalue=0))

[(0, 2), (2, 1), (1, 3)]


But like this, we can't:

>>> list(with_previous([2, 1, 3], 0))

Traceback (most recent call last):  

File "<stdin>", line 1, in <module>

TypeError: with_previous() takes 1 positional argument but 2 were given `

 

This function accepts two parameters, in which the fillvalue parameter must be specified as a keyword parameter.

I usually use keyword parameters only when I get any number of location parameters, but sometimes I use this * to force a parameter to be specified by location.

In fact, Python's built-in sorted function uses this approach. If you look at sorted help information, you will see the following information:

>>> help(sorted)

Help on built-in function sorted in module builtins:

 

sorted(iterable, /, *, key=None, reverse=False)    

    Return a new list containing all items from the iterable inascending order.  

    A custom key function can be supplied to customize the sort order, and the    

    reverse flag can be set to request the result in descending order.

 

In the official description of sorted, there is a separate * parameter.

Asterisks for tuple unpacking

Python 3 also adds a new use of the * operator, which is only related to the way in which functions are defined and invoked above.

Now, the * operator can also be used for tuple unpacking:

>>> fruits = ['lemon', 'pear', 'watermelon', 'tomato']

>>> first, second, *remaining = fruits

>>> remaining

['watermelon', 'tomato']

>>> first, *remaining = fruits

>>> remaining

['pear', 'watermelon', 'tomato']

>>> first, *middle, last = fruits

>>> middle

['pear', 'watermelon']

 

If you want to know under what circumstances you can use it in your own code, check out my example of tuple unpacking in Python. In that article, I'll show you how to use the * operator as an alternative to sequence slicing.

Usually when I teach * I tell you that you can only use one * expression in multiple assignment statements. This is actually incorrect because you can use two * (I discussed nested unpacking in the tuple unpacking article):

>>> fruits = ['lemon', 'pear', 'watermelon', 'tomato']

>>> first, second, *remaining = fruits

>>> remaining

['watermelon', 'tomato']

>>> first, *remaining = fruits

>>> remaining

['pear', 'watermelon', 'tomato']

>>> first, *middle, last = fruits

>>> middle

['pear', 'watermelon']

 

But I've never seen any practical use of it, and even if you look for an example because it looks a little mysterious, I don't recommend it.

The PEP added to Python 3.0 is PEP 3132, which is not very long.

Asterisks in list text

Python 3.5 introduced a large number of new * related features through PEP 448. One of the biggest new features is the ability to dump iterators into new lists using *.

Suppose you have a function that takes any sequence as input and returns a list in which the sequence is connected to the reverse order of the sequence:

def palindromify(sequence):  

    return list(sequence) + list(reversed(sequence))

 

This function needs to convert the sequence to a list many times to connect the list and return the result. In Python 3.5, we can write functions like this:

 

def palindromify(sequence):  

    return [*sequence, *reversed(sequence)]

 

This code avoids unnecessary list calls, so our code is more efficient and readable.

Here is another example:

def rotate_first_item(sequence):    

    return [*sequence[1:], sequence[0]]

 

This function returns a new list in which the first item in a given list (or other sequence) is moved to the end of the new list.

* This use of operators is a good way to connect different types of iterators together. * Operators are suitable for linking any kind of iterator, whereas + operators are only applicable to specific sequences of the same type.

In addition to creating list storage iterators, we can dump iterators into new tuples or collections:

>>> fruits = ['lemon', 'pear', 'watermelon', 'tomato']

>>> (*fruits[1:], fruits[0])

('pear', 'watermelon', 'tomato', 'lemon')

>>> uppercase_fruits = (f.upper() for f in fruits)

>>> {*fruits, *uppercase_fruits}

{'lemon', 'watermelon', 'TOMATO', 'LEMON', 'PEAR','WATERMELON', 'tomato', 'pear'}

 

Notice that the last line above uses a list and a generator and dumps them into a new collection. Until then, there was no easy way to do this in a single line of code. There was once a way to do this, but it was not easy to remember or discover:

Two asterisks for dictionary text

PEP 448 also extends the ** operator by allowing key/value pairs to be dumped from a dictionary to a new dictionary:

>>> date_info = {'year': "2020", 'month': "01", 'day': "01"}

>>> track_info = {'artist': "Beethoven", 'title': 'Symphony No 5'}

>>> all_info = {**date_info, **track_info}

>>> all_info

{'year': '2020', 'month': '01', 'day': '01', 'artist': 'Beethoven', 'title':'Symphony No 5'}

 

I also wrote another article: the usual way to merge dictionaries in Python.

However, the ** operator can be used more than merging two dictionaries.

For example, we can add a new value while copying a dictionary:

>>> date_info = {'year': '2020', 'month': '01', 'day': '7'}

>>> event_info = {**date_info, 'group': "Python Meetup"}

>>> event_info

{'year': '2020', 'month': '01', 'day': '7', 'group': 'Python Meetup'}

 

Or rewrite specific values while copying/merging dictionaries:

 

>>> event_info = {'year': '2020', 'month': '01', 'day': '7', 'group':'Python Meetup'}

>>> new_info = {**event_info, 'day': "14"}

>>> new_info

{'year': '2020', 'month': '01', 'day': '14', 'group': 'Python Meetup'}

 

Python's asterisk is very powerful

Python's * and ** operators are not just grammatical sugar. * Some operations allowed by the ** and ** operators can be implemented in other ways, but are often more cumbersome and resource-intensive. Moreover, some of the features provided by * and ** operators are not implemented in an alternative way: for example, functions cannot accept any number of location parameters without using *.

After reading all the features of * and ** operators, you may want to know the names of these strange operators. Unfortunately, their names are not concise. I've heard that * is called the "packing" and "unpacking" operators. I've also heard of it being called "splat" (from Ruby World) and simply "star".

I tend to call these operators "stars" and "binaries" or "stars". This term does not distinguish them from their affix relationships (multiplication and exponential operations), but it is usually clear from the context that we are discussing the prefix operator or the affix operator.

Don't remember all the uses of * and ** operators without understanding them! These operators have many uses. It's not important to remember the specific usage of each operator. It's important to know when you can use them. I recommend using this article as a memo sheet or making your own to help you use understanding* and ** in Python.

Do you like my teaching style?

Want to know more about Python? I share my favorite Python resources and answer Python questions every week through real-time chat.

Shang Xue School launched< 13 days to fix Python crawler > Video tutorial, learn to become a Python crawler engineer, pay bars!

Posted by helpmeplease1234 on Sun, 12 May 2019 01:49:06 -0700