为什么Python不允许foo(*arg, x)?

28 投票
5 回答
759 浏览
提问于 2025-04-16 19:47

看看下面这个例子

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) 这样的写法不被允许呢?你能想到有什么语义上的模糊或者其他缺点吗?

编辑:具体问题

为什么在函数调用中不允许多个 *arg 展开?

为什么在 *arg 展开之后不允许再有位置参数?

5 个回答

0

*point 的意思是你传入了一整串的东西——就像是一个列表里的所有元素,但不是以列表的形式传入。

在这种情况下,你无法限制传入多少个元素。因此,解释器无法知道哪些元素属于 *points,哪些属于 *size

举个例子,如果你传入了这些数字:2, 5, 3, 4, 17, 87, 4, 0,你能告诉我,哪些数字是 *points 的,哪些是 *size 的吗?这也是解释器会遇到的同样问题。

希望这能帮到你。

11

我不打算讨论为什么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)

一般来说,如果你的数据是元组,就让构造函数接收元组。如果你的数据是字典,就让构造函数接收字典。如果你的数据是对象,就让构造函数接收对象,等等。

总的来说,你应该遵循语言的习惯用法,而不是试图绕过它们。


编辑

看到这个问题这么受欢迎,我给你一个装饰器,可以让你以任何你喜欢的方式调用构造函数。

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中通常有办法做到任何事情。

9

据我了解,这是一个设计上的选择,但背后似乎有一些逻辑。

编辑:在函数调用中使用*args的写法是为了让你可以传入一个长度不固定的元组,这个长度在每次调用时可能会变化。在这种情况下,像f(*a, *b, c)这样的调用就不太合理,因为如果a的长度变化了,b中的所有元素就会被分配到错误的变量上,c的位置也会不对。

保持语言简单、强大和标准化是件好事。让它与实际处理参数的方式保持一致也是非常重要的。

想想语言是如何解析你的函数调用的。如果允许多个*arg以任意顺序出现,比如Rect(*point, *size, color),那么要正确解析,pointsize的元素总数必须是四个。所以point=()size=(1,2,2,3)color='red'就可以让Rect(*point, *size, color)正常工作。基本上,当语言解析*point*size时,它把它们当作一个组合的*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),这依赖于顺序。

在函数定义和函数调用中,所有的内容都有特定的含义。ab是没有默认值的参数,接下来xy是有默认值的参数(可以被跳过,所以放在没有默认值的参数后面)。接着,*args作为一个元组,包含了函数调用中所有没有指定关键字的参数。这些放在后面,因为长度可能会变化,你不希望在调用之间长度变化的东西影响变量的赋值。最后,**kwargs接收所有没有在其他地方定义的关键字参数。通过这些明确的定义,你永远不需要有多个*args**kwargs

撰写回答