Почему foo (* arg, x) не разрешено в Python?
Посмотрите на следующий пример
point = (1, 2)
size = (2, 3)
color = 'red'
class Rect(object):
def __init__(self, x, y, width, height, color):
pass
Было бы очень заманчиво позвонить:
Rect(*point, *size, color)
Возможные обходные пути:
Rect(point[0], point[1], size[0], size[1], color)
Rect(*(point + size), color=color)
Rect(*(point + size + (color,)))
Но почему Rect(*point, *size, color)
не разрешено, есть ли какая-либо смысловая двусмысленность или общий недостаток, о котором вы могли бы подумать?
EDIT: конкретные вопросы
Почему множественные расширения * arg не допускаются при вызове функций?
Почему позиционные аргументы не допускаются после расширений * arg?
Ответы
Ответ 1
Насколько я знаю, это был дизайнерский выбор, но, похоже, есть логика.
РЕДАКТИРОВАТЬ: нотация *args
в вызове функции была сконструирована таким образом, чтобы вы могли передать кортеж переменных произвольной длины, которые могут меняться между вызовами. В этом случае наличие чего-то типа f (* a, * b, c) не имеет смысла в качестве вызова, как если бы a
изменяло длину, все элементы b присваивались неправильным переменным, а c не в нужном месте.
Сохранение языка простым, мощным и стандартизированным - это хорошо. Хранение его в синхронизации с тем, что на самом деле происходит при обработке аргументов, тоже очень хорошо.
Подумайте, как язык распаковывает ваш вызов функции. Если несколько *arg
разрешены в любом порядке, например Rect(*point, *size, color)
, обратите внимание, что все, что имеет значение для правильной распаковки, состоит в том, что точка и размер имеют в общей сложности четыре элемента. Поэтому point=()
, size=(1,2,2,3)
и color='red')
позволяют Rect(*point, *size, color)
работать как правильный вызов. В принципе, язык, когда он анализирует * точку и * размер, обрабатывает его как один комбинированный кортеж *arg
, поэтому Rect(*(point + size), color=color)
является более точным представлением.
Никогда не должно быть двух кортежей аргументов, переданных в форме *args
, вы всегда можете представлять их как один. Поскольку назначение параметров зависит только от порядка в этом объединенном списке *arg
, имеет смысл определить его как таковое.
Если вы можете выполнять вызовы функций, такие как f (* a, * b), язык почти попросит вас определить функции с несколькими * args в списке параметров, и они не могут быть обработаны. Например.
def f(*a, *b):
return (sum(a), 2*sum(b))
Как будет обрабатываться f(1,2,3,4)
?
Я думаю, что именно поэтому для синтаксической конкретности языковые силы вызывают вызовы функций и определения в следующем конкретном виде; как f(a,b,x=1,y=2,*args,**kwargs)
, который зависит от порядка.
Все, что имеет определенное значение в определении функции и вызове функции. a
и b
являются параметрами, определенными без значений по умолчанию, следующие x
и y
являются параметрами, заданными по умолчанию (это может быть пропущено, поэтому приходите после параметров по умолчанию). Затем *args
заполняется как кортеж со всеми аргументами, заполненными остальными параметрами, из вызова функции, не являющегося параметрами ключевого слова. Это происходит после других, поскольку это может изменить длину, и вы не хотите что-то, что может изменить длину между вызовами, чтобы повлиять на назначение переменных. В конце ** kwargs принимает все аргументы ключевого слова, которые не были определены в другом месте. С этими конкретными определениями вам никогда не нужно иметь несколько *args
или **kwargs
.
Ответ 2
Я не буду говорить, почему многократная распаковка не является частью Python, но я укажу, что вы не соответствуете вашему классу своим данным в вашем примере.
У вас есть следующий код:
point = (1, 2)
size = (2, 3)
color = 'red'
class Rect(object):
def __init__(self, x, y, width, height, color):
self.x = x
self.y = y
self.width = width
self.height = height
self.color = color
но лучший способ выразить свой объект Rect будет следующим:
class Rect:
def __init__(self, point, size, color):
self.point = point
self.size = size
self.color = color
r = Rect(point, size, color)
В общем случае, если ваши данные находятся в кортежах, ваш конструктор принимает кортежи. Если ваши данные указаны в dict, попросите своего конструктора взять dict. Если ваши данные являются объектами, пусть ваш конструктор возьмет объект и т.д.
В общем, вы хотите работать с идиомами языка, а не пытаться обойти их.
ИЗМЕНИТЬ
Видя, насколько популярен этот вопрос, я дам вам декоратор, который позволяет вам вызвать конструктор, как вам нравится.
class Pack(object):
def __init__(self, *template):
self.template = template
def __call__(self, f):
def pack(*args):
args = list(args)
for i, tup in enumerate(self.template):
if type(tup) != tuple:
continue
for j, typ in enumerate(tup):
if type(args[i+j]) != typ:
break
else:
args[i:i+j+1] = [tuple(args[i:i+j+1])]
f(*args)
return pack
class Rect:
@Pack(object, (int, int), (int, int), str)
def __init__(self, point, size, color):
self.point = point
self.size = size
self.color = color
Теперь вы можете инициализировать свой объект любым способом.
r1 = Rect(point, size, color)
r2 = Rect((1,2), size, color)
r3 = Rect(1, 2, size, color)
r4 = Rect((1, 2), 2, 3, color)
r5 = Rect(1, 2, 2, 3, color)
Хотя я бы не рекомендовал использовать это на практике (это нарушает принцип, что у вас должен быть только один способ сделать это), он служит для демонстрации того, что обычно есть способ сделать что-либо в Python.
Ответ 3
*point
говорит, что вы передаете целую последовательность элементов - что-то вроде всех элементов в списке, но не как список.
В этом случае вы не можете ограничить, сколько элементов передано. Поэтому интерпретатору нет способа узнать, какие элементы последовательности являются частью *points
и которые имеют значение *size
Например, если вы передали в качестве входных данных следующее: 2, 5, 3, 4, 17, 87, 4, 0
, можете ли вы сказать мне, какое из этих чисел представлено *points
и которое по *size
? Это та же проблема, с которой столкнулся бы и интерпретатор
Надеюсь, что это поможет
Ответ 4
Python полон этих тонких сбоев. Например, вы можете:
first, second, last = (1, 2, 3)
И вы не можете делать:
first, *others = (1, 2, 3)
Но в Python 3 вы теперь можете.
Ваше предложение, вероятно, будет предложено в PEP и интегрировано или отклонено в один прекрасный день.
Ответ 5
Ну, в Python 2 вы можете сказать:
point = 1, 2
size = 2, 3
color = 'red'
class Rect(object):
def __init__(self, (x, y), (width, height), color):
pass
Затем вы можете сказать:
a_rect= Rect(point, size, color)
заботясь о том, чтобы первые два аргумента были последовательностями len == 2.
NB: эта возможность была удалена из Python 3.