Есть ли существенная разница между перечислением аргументов и ключевым args

Метод передачи параметров с помощью keyword *args или **kwargs. В качестве примера реализации этих ключевых слов был реализован класс Car:

class Car():
    def __init__(self, *args):
        self.speed = args[0]
        self.color = args[1]

audi = Car(200, 'red')
bmw = Car(250, 'black')
mb = Car(190, 'white')

print(audi.color)
print(bmw.speed)

Как мы видим, при создании объекта класса Car, были переданы параметры в метод __init__ и после присваивание значений из массива args.

Вопрос: Если мы используем вместо args обычное перечисление аргументов, будет ли существенная разница или ничего не произойдет?

UPD: Исходя из ответов и комментарий я подумал об удачных примерах использования этих keywords. В основном, была бы удобна передача всех аргументов в одну кучу и последующей записи или обработки. Например:

import logging
logging.basicConfig(filename='example.log',
                    level=logging.INFO)

def logger(func):
    def log_func(*args):
        logging.info(f'Running "{func.__name__}" with arguments {args}')
        print(func(*args))

    return log_func

def add(x, y):
    return x+y

def sub(x, y):
    return x-y

add_logger = logger(add)
sub_logger = logger(sub)

add_logger(3, 3)
add_logger(4, 5)

sub_logger(10, 5)
sub_logger(20, 10)

Получаем результат:

python .\ClosureFuntcionsPython.py
6
9
5
10

cat .\example.log
INFO:root:Running "add" with arguments (3, 3)
INFO:root:Running "add" with arguments (4, 5)
INFO:root:Running "sub" with arguments (10, 5)
INFO:root:Running "sub" with arguments (20, 10)

Ответы (1 шт):

Автор решения: Ben Puls

Визуальные отличия

Давайте посмотрим, что будет, если мы не передадим ожидаемые аргументы в класс.

audi = Car()

В вашем классе будет вызвано исключение IndexError, что сделает запутанным работу с классом, при работе с классом вам придётся каждый раз вспоминать, что ожидает увидеть на входе класс, или читать код этого класса. И тот, и другой варианты кажутся не практичными.

IndexError: tuple index out of range

А, использовав позиционные аргументы в классе, если забыть указать speed и color будет вызвано исключение TypeError и будет понятно, что было не так сделано.

class Car():
    def __init__(self, speed, color):
        self.speed = speed
        self.color = color

TypeError: Car.__init__() missing 2 required positional arguments: 'speed' and 'color'

Кроме того, как удачно подметил insolor, невозможно заранее узнать, какие аргументы нужно будет передать классу, что недопускает возможности использовать типизацию. Это сделает код нечитабельным и непригодным для дальнейшей поддержки как для самого автора, так и какого-нибудь контрибьютора.

По ту сторону байт-кода

Давайте же посмотрим, какие инструкции выполняются в обоих классах

  1. Рассмотрим первый класс, в котором мы получаем скорость и цвет из args
import dis

class Car:
    def __init__(self, *args):
        self.speed = args[0]
        self.color = args[1]

dis.dis(Car)

Результат:

Disassembly of __init__:
  5           0 RESUME                   0

  6           2 LOAD_FAST                1 (args)
              4 LOAD_CONST               1 (0)
              6 BINARY_SUBSCR
             10 LOAD_FAST                0 (self)
             12 STORE_ATTR               0 (speed)

  7          22 LOAD_FAST                1 (args)
             24 LOAD_CONST               2 (1)
             26 BINARY_SUBSCR
             30 LOAD_FAST                0 (self)
             32 STORE_ATTR               1 (color)
             42 RETURN_CONST             0 (None)

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

Мы загружаем args, загружаем константу (индекс), затем через инструкцию BINARY_SUBSCR мы ищем в кортеже нужный аргумент, такая инструкция имеет свой метод __getitem__, полученный результат присваиваем атрибуту self.speed и тоже самое для self.color.

  1. А как это будет выглядеть в классе с позиционными аргументами?
import dis

class Car:
    def __init__(self, speed, color):
        self.speed = speed
        self.color = color

dis.dis(Car)

Смотрим:

Disassembly of __init__:
 11           0 RESUME                   0

 12           2 LOAD_FAST                1 (speed)
              4 LOAD_FAST                0 (self)
              6 STORE_ATTR               0 (speed)

 13          16 LOAD_FAST                2 (color)
             18 LOAD_FAST                0 (self)
             20 STORE_ATTR               1 (color)
             30 RETURN_CONST             0 (None)

Здесь мы напрямую загружаем значения в наши атрибуты.

Это означает, что класс с неименованными аргументами будет выполнять на несколько инструкций больше.

Гипотеза: класс с неименованными аргументами будет медленнее, чем класс с позиционнными аргументами

Производительность

Естественно, тесты слишком синтетические и результаты этих тестов будут очень далеки от реальности, но для показательного сравнения подойдут.

Замеры будем проводить с помощью timeit:

import timeit

class Car:
    def __init__(self, *args):
        self.speed = args[0]
        self.color = args[1]


class Car1:
    def __init__(self, speed, color):
        self.speed = speed
        self.color = color


def test_car():
    Car(200, "red")

def test_car1():
    Car1(200, "red")

timeit.timeit(test_car), timeit.timeit(test_car1)

Для класса с неименованными аргументами получили 0.13930917900142958 попугаев, а для класса с позиционными аргументами - 0.1009512620003079 попугаев.

Это означает, что гипотеза верна. Однако эта разница настолько микроскопическая в данном контексте, что её можно даже не учитывать. Но другой вопрос, когда у нас в классе будет использоваться в разы больше аргументов.

→ Ссылка