Что такое *args и **kwargs в определении функции?

Продолжаем сериал. Сами авторы опросника пишут, что "*args и **kwargs — это специальные параметры в Python, которые позволяют передавать переменное количество аргументов в функцию", что может ввести в заблуждение.

Думаете, args и kwargs - это специальные ключевые слова?

А вот и нет.

Есть только * и **, и нет никаких специальных параметров args и kwagrs. Они так названы только для удобства и в 99% случаев вы увидите эти названия в учебниках, но это сделано только потому, что "так принято". И это не ключевые слова.

Вот, смотрите:

class SomeClass():

····def __init__(self, *bbargs, **zzargs):

········print(f'{bbargs=}; {zzargs=}')

if __name__ == '__main__':

····c = SomeClass(1,2,3, a='a', c='d')

>>>bbargs=(1, 2, 3); zzargs={'a': 'a', 'c': 'd'}

Операторы * и ** используются всего лишь для упаковки и распаковки коллекций в Python, и вовсе не являются какими-то "специальными операторами для функций"!

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

a, *b = (1, 2, 3, 4)

# первое значение будет a, все остальные списком станут b

# a == 1, b == [2,3,4]

*a, b = (1, 2, 3, 4)

# наоборот, сколько-то первых будут a, а последнее - точно b

# a == [1,2,3], b == 4

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

params = {'a':1, 'b':2}

c = {'c':3, **params}

print(c)

>>>{'c': 3, 'a': 1, 'b': 2}

Хорошая ли практика передавать аргументы через args, kwargs?

Я считаю, что в большинстве случаев - нет.

Вот вам пример, допустим нам в классе нужен параметр адреса.

class SomeClass():

····def __init__(self, *bbargs, **zzargs):

········self.addr = zzargs.get('addr', '127.0.0.1') # 1

········self.addr = zzargs.get('addr', None) or '127.0.0.1' # 2

········print(self.addr)

c = SomeClass(1,2,3, a='a', c='d', addr='192.168.1.1')

Красиво, читаемо? Я считаю, что нет.

Кстати, вот ещё пример за гранью добра и зла:

class SomeClass():

····def __init__(self, *bbargs, **zzargs):

········self.__dict__.update(zzargs)

········# Так делать нельзя!

········print(self.addr) # '192.168.1.1' ;)

c = SomeClass(1,2,3, a='a', c='d', addr='192.168.1.1')

Для меня самый правильный вариант - по возможности принципиально избегать использования * и ** до тех пор, пока это не станет абсолютно необходимым:

class SomeClass():

····def __init__(self, addr:str='127.0.0.1') -> None:

········self.addr: str = addr

········print(self.addr)

Потому что так - красивей!

Это кросспост из моего Telegram-канала "Не Ван Россум", где я прямо сейчас пишу сериал "101 вопрос про Python" с описанием подводных камней, неочевидностей и загвоздок.