Разберем префиксные операторы * и **, которые ставятся перед переменными для распаковки или упаковки коллекций.

Распаковка в переменные

Распаковка кортежей

Кортеж можно распаковать в отдельные элементы:

a, b = (1, 2)
print(a)
print(b)
#1
#2

Но если в кортеже будет больше значений, чем переменных, то будет ошибка:

a, b = (1, 2, 3, 4)
print(a)
print(b)
#ValueError: too many values to unpack (expected 2)

Можно воспользоваться оператором распаковки *:

a, *b = (1, 2, 3, 4)
print(a)
print(b)
#1
#[2, 3, 4]

В результате первый элемент кортежа будет присвоен первой переменной a, а все остальные элементы (уже в виде списка) — переменной b.

Также можно сделать и наоборот:

*a, b = (1, 2, 3, 4)
print(a)
print(b)
#[1, 2, 3]
#4

Переменной b будет присвоен последний элемент кортежа, а все остальные будут присвоены переменной a в виде списка.

Или так:

a, *b, c = (1, 2, 3, 4)
print(a)
print(b)
print(c)
#1
#[2, 3]
#4

Крайние элементы кортежа присваиваются переменным a и c, а все остальное попадает в переменную b.

Распаковка списков

a, *b = [1, 'два', 3, False]
print(a)
print(b)
#1
#['два', 3, False]

Распаковка строк

a, *b, c = 'fullstacker.ru'
print(a)
print(b)
print(c)
#f
#['u', 'l', 'l', 's', 't', 'a', 'c', 'k', 'e', 'r', '.', 'r']
#u

Распаковка множеств

s = {1, 2, 4}
a, *b = s
print(a)
print(b)
#1
#[2, 4]

Распаковка словарей

d1 = {'key1': 'val1', 'key2': 'val2', 'key3': 'val3'}
a, *b = d1
print(a)
print(b)
#key1
#['key2', 'key3']

Получим ключи словаря.

Для получения значений, следует использовать метод values:

d1 = {'key1': 'val1', 'key2': 'val2', 'key3': 'val3'}
a, *b = d1.values()
print(a)
print(b)
#val1
#['val2', 'val3']

Для получения кортежей с ключами и значениями — метод items:

d1 = {'key1': 'val1', 'key2': 'val2', 'key3': 'val3'}
a, *b = d1.items()
print(a)
print(b)
#('key1', 'val1')
#[('key2', 'val2'), ('key3', 'val3')]

Распаковка кортежей без переменных

d = 5, 10
print(*d)
#5 10

Зачем это вообще может понадобиться?

К примеру, нам нужно получить числовой диапазон из значений кортежа.

Создадим кортеж из двух значений:

d = 5, 10
print(d)
#(5, 10)

Если мы в функцию range() передадим кортеж d, то получим ошибку:

d = 5, 10
print(range(d))
#TypeError: 'tuple' object cannot be interpreted as an integer

Поскольку range() ожидает в качестве аргументов 2 значения, а не кортеж из двух значений.

Но мы можем распаковать кортеж с помощью оператора *:

d = 5, 10
print(range(*d))
#range(5, 10)

Преобразовав в список, получим числовой диапазон из переданного кортежа:

d = 5, 10
print(list(range(*d)))
#[5, 6, 7, 8, 9]

Или таким образом:

d = 5, 10
print([*range(*d)])
#[5, 6, 7, 8, 9]

Распаковка словарей

d = {'key1': 'val1', 'key2': 'val2', 'key3': 'val3'}
print(*d)
#key1 key2 key3

Мы получили ключи словаря.

А если использовать метод values:

d = {'key1': 'val1', 'key2': 'val2', 'key3': 'val3'}
print(*d.values())
#val1 val2 val3

Получим значения словаря.

Если же использовать метод items:

d = {'key1': 'val1', 'key2': 'val2', 'key3': 'val3'}
print(*d.items())
#('key1', 'val1') ('key2', 'val2') ('key3', 'val3')

Получим кортежи из ключей и значений словаря.

Распаковка списков

lst = [1, 2, 3, 4]
print(*lst)
#1 2 3 4

Объединение итерируемых объектов

С помощью оператора *, можно не только распаковывать, но и объединять итерируемые объекты:

d = 5, 10
e = ('один', 'два', 'три')
f = [True, False]
print([*range(*d), *e, *f])
#[5, 6, 7, 8, 9, 'один', 'два', 'три', True, False]

Точно также объединение работает и с другими типами данных:

my_tuple = (1, 2, 3)
print((0, *my_tuple, 4))
my_set = {1, 2, 3}
print({0, *my_set, 4})
#(0, 1, 2, 3, 4)
#{0, 1, 2, 3, 4}

Объединили в один список — числовой диапазон, кортеж и список.

Объединение двух словарей с помощью **

d1 = {'key1': 'val1', 'key2': 'val2'}
d2 = {'key4': 'val4', 'key5': 'val5'}
d3 = {**d1, **d2}
print(d3)
#{'key1': 'val1', 'key2': 'val2', 'key4': 'val4', 'key5': 'val5'}

В итоге получился словарь, который содержит элементы и первого и второго словаря.

Упаковка в функциях

Операторами * и ** также удобно пользоваться и в функциях. Это дает возможность вызывать функцию с произвольным количеством позиционных аргументов и именованных аргументов.

def my_function(*args, **kwargs):
    print(args)
    print(kwargs)
    
my_function(1, 2, 3, a=10, b=20)
#(1, 2, 3)
#{'a': 10, 'b': 20}

После вызова функции мы получили кортеж args позиционных аргументов и словарь kwargs именованных аргументов.