下面是我的"python菜鸟问题"系列,基于另一个问题。
特权
转到http://python.net/~goodger/projects/pycon/2007/adilmatic/messate.html其他语言有变量,并向下滚动到"默认参数值"。您可以在这里找到以下内容:
1 2 3 4 5 6 7 8 9
| def bad_append(new_item, a_list=[]):
a_list.append(new_item)
return a_list
def good_append(new_item, a_list=None):
if a_list is None:
a_list = []
a_list.append(new_item)
return a_list |
在python.org上甚至有一个"重要警告",用同样的例子,但并不是说它"更好"。
一种说法
所以,这里的问题是:为什么在一个已知问题上"好"的语法难看,就像在一个提倡"优雅语法"和"易于使用"的编程语言中一样?
编辑:
换个说法
我不是在问为什么或者如何发生(感谢马克提供链接)。
我在问为什么没有更简单的内置语言替代方案。
我认为更好的方法可能是能够在def本身中做一些事情,其中name参数将附加到def中的"本地"或"新"可变对象。比如:
1 2 3
| def better_append(new_item, a_list=immutable([])):
a_list.append(new_item)
return a_list |
号
我相信有人能提供更好的语法,但我也猜这一定是一个很好的解释为什么没有这样做。
- 好问题。我自己也想过这个。
- 坏的附加和好的附加不是好的也不是坏的。它们都有各自的用途。
- 老实说,我还没有看到像bad-append这样的不错的东西。如果它不是主动的buggy,那么它只是语法滥用(试图摆脱定义对象)。有时它在内部函数中是合理的——但即使在那里,也很少。
- 您可以使用a_list = a_list or []而不是if块。
- @乌特布同意,但不是重点。
- @Piotrlegnica看看@seth与ferg.org的链接——它说你不应该这样做,并给出一些原因。
- 参见stackoverflow.com/questions/1132941/…
- @CAWAS:当然不是直接等价的——如果你想明确检查None而不是布尔值eval(这很少有什么区别),那么使用a_list = [] if a_list is None else a_list。
- 如果我可以这么说的话,问题不是关于append,而是关于作为可变类型实例的默认参数。
- "拉斐尔是真的。正在更改标题。
- 顺便说一下,仍然希望得到一个真正的答案。到目前为止,这里还没有人回答真正的问题:为什么它会一直那么难看?人们基本上都认为这是因为从我的角度来看,"没人想出更好的主意",因为没有一句来自Python创建者的引用。我所能看到的只是对经验丰富的用户的猜测和对其工作原理的解释。
- 答案是,对于事情的工作方式有一个合理的解释,而且不容易解决。看到我的链接。任何修复它的方法都将是一个巨大的错误。
- @尽管这可能不太明显,但我已经把这联系看出来并投了赞成票。感谢您张贴!但我没有得到你的答案"事情按他们的方式运作"。我想要合理的解释。:)
- 对不起,我觉得从接受的回答中可以明显看出。函数是Python中的对象,默认参数是该对象的成员数据。如果它是可变的,那么你可以改变它。要更改行为,您必须创建一个全新的对象类型,每次尝试访问它时都会创建一个自身的副本——这就是您的"本地"属性必须完成的。我知道你的例子是这个问题的典型例子,但我不认为它是现实的——看我的答案。
- 洛尔@马克。我又一次看到了你的答案,甚至对它发表了评论。谢谢你的澄清,但我已经明白为什么会这样。我不明白的是为什么预期的行为没有语法,这就是"好的附加"。我会试着编辑这个问题,也许我能把它弄清楚。
- 我直截了当地说:你的榜样有缺陷。要么您想修改传递的列表,要么您想生成一个新的列表,但是您的示例试图同时执行这两个操作。丑陋是不承认缺陷的直接结果。正如我的答案所述,您要么想要删除默认参数,要么想要复制输入列表——而Python哲学更希望复制是显式的。我欢迎另一个例子,用更现实的眼光来说明这个问题。
- @标记ransom,如果"成员数据"的意思是"属性",则情况并非如此。(总的来说,我不能更同意你所说的话。)
- @Cawas,没有预期的行为;只有你预期的行为。我个人期待实际的行为。这对我来说似乎是非常明显的(因为我非常习惯于Python的语义)。和大多数人一样,当我刚接触到Python时,我的期望可能会因情况而异。很多人在函数调用或算术表达式的情况下发现非延迟表达式计算,即使在所有情况下都不是。有些情况下,一个表达式应该被评估一次,而有些情况下,它应该被评估一次,并且人们
- 对于python来说,新手很容易期望他们现在想要的默认值,而当他们想要的是相反的值时,他们会希望它成为默认值。
- @迈克,我部分同意。但我的观点是,有足够多的人期望这种行为能够证明这种改变是正确的,而不是让它"混乱",因为如果有一种方法可以做到这一点而不损害Python的哲学。也许我这里的问题也可能是,在哪里这样做会伤害到它。
- @标记可修改对象的副本不是显式的。我认为这对我来说没有多大意义,但是,即使这样,语法在交换什么的时候仍然看起来很难看?这里的一些人必须理解这一点,也许这对他们来说是有意义的,所以我要问他们(或者你们)在不损害Python背后的所有概念的情况下,如何更好地编写附加。
- @Cawas,我承认如果这更好的话会很好,但不能想出一个更好的方法。我不认为你是通过这种特殊语法的建议来这样做的,它看起来像一个属性,因为这需要相同的知识,只是使用新的、不立即清晰的语法,而不是不需要新语法的旧模式。像.local这样的东西不能帮助人们更好地理解。
- 麦克酷!如果有人回答(题目)"因为还没有人想出一个好主意,怎么做看起来很好",有一个良好的背景(像一些官方博客,或是从发明家那里谈论Python语法的进化,你知道吗?"表明如果一个精心设计的想法出现,它将得到实施,那么这就是我会接受的答案。
- 充分理解这一问题将揭示为什么不可能有更好的解决方案,尤其是在游戏的这一点上。
- @不管怎样,我只是稍微改变了一下这个例子…希望它实际上也是一个可以替代的修复方法。
- @cawas,python已经有了一个不变的列表,它被称为tuple。当然,因为它是不可变的,所以没有append方法。您要查找的是参数的一些特殊属性,这些属性表示每次调用函数时自动复制参数。这就是为什么我提到Python更喜欢显式的原因——您需要自己显式地复制参数,而不是让Python隐式地复制参数。
- 与讨论相关en.wikipedia.org/wiki/troll_u28internet%29
- 下面是另一个例子:stackoverflow.com/questions/2667688/simple-python-oo-issue。正在使用列表初始化对象。但在这里,即使不使用默认参数,问题仍然存在。
- 补充问题-很好地使用可变默认参数
这被称为"可变默认陷阱"。参见:http://www.ferg.org/projects/python_gotchas.html contents_item_6
基本上,当程序第一次被解释时,不是每次调用函数时(正如您从其他语言所期望的那样),都会初始化a_list。因此,每次调用函数时都不会得到一个新的列表,但会重用同一个列表。
我想问题的答案是,如果你想在一个列表中附加一些东西,就这么做,不要创建一个函数来做。
这是:
1 2
| >>> my_list = []
>>> my_list.append(1) |
比以下内容更清晰易读:
1
| >>> my_list = my_append(1) |
在实际情况下,如果需要这种行为,您可能会创建自己的类,该类有方法来管理它的内部列表。
- 抱歉@cawas,我今天早上发布意识流样式。参阅编辑。
- 另一个需要考虑的问题是,通常最好不要将默认参数设置为空的可变对象,如[]或。
- 你的编辑是在我发表评论7秒钟之后,所以我删除了它;)虽然我想用自己的方式来写得更好,但这里的问题是"为什么Python是那样的",而不是"我如何修复它"。我只是想更好地理解它背后的概念。
- +因为我真的很喜欢学习这个
默认参数在执行def语句时进行评估,这可能是最合理的方法:它通常是需要的。如果不是这样,当环境发生一点变化时,它可能会导致混淆的结果。
用神奇的local方法或类似的方法进行区分,这是不理想的。python试图让事情变得非常简单,而当前的样板文件没有明显的、清晰的替代品,它没有诉诸于干扰python当前具有的相当一致的语义。
- 现在,这更像是我所期待的答案。但是为什么你说Python语义是如此一致?我倾向于认为我们总是有改进的空间。如果有一个不同于def的语句,在执行时不进行评估,使它们在任何类型的可变或不可变对象之后进行评估呢?
- 我没有说没有改进的空间;我认为关于Python有很多东西是次优的。我完全承认这是你必须面对的一件讨厌的小事。然而,在这里要做的艰难决定中,我认为我没有看到一个解决方案在不改变Python工作方式的情况下工作得更好。
- 另一个基本选项是,不将对象作为默认参数传递,而是传递每次调用函数时计算的表达式。这将给python引入一个新的东西,它延迟执行(在此之前,函数是唯一真正执行它的东西),并可能改变一些操作从o(1)到o(n)的算法复杂性(如果一个昂贵的操作一次又一次地执行)。这就需要一个新的模式foo = bar(); def baz(qux=foo)来做与def(qux=sentinel): if qux is sentinel: qux = bar()相反的事情,当它对你的案件很重要的时候。
- 事实上,这种模式可能不太经常出现,这有点令人鼓舞,但会产生意识不足的负面影响。(对于一种广泛使用的语言,如python,要彻底改变这一点也为时已晚。)
- 折衷的做法是,添加语法来说明您到底希望消除哪种情况需要一个模式是值得考虑的,但我不认为诸如"局部"属性之类的选项会接近于值得考虑的任何地方。一个新的关键词必须被引入,语言改变并变得更大,最终的解决方案对我来说似乎没有那么整洁。这并没有比最初的模式更容易解释;也许相反。更重要的是,使用这个模式通常比使用其他模式更早地帮助人们理解Python名称和对象语义。
- 最后一点要注意的是,解决这个问题的一种很酷的方法是使用函数,我说过"做你想做的事情"可以推迟表达式的计算。您可以执行def baz(qux=lambda: bar(spam)),然后在函数内调用qux。这就要求人们把你的函数称为baz(lambda: 3.5),大多数人会认为这在大多数情况下都很奇怪。我不确定我会说这确实是一个好的解决方案(它在这个确切的形式中肯定不太受欢迎),但如果您选择的话,这是一种Python允许您使用它的方法。
- .local的概念来自于定义一种新的可变对象,这种对象在函数上的行为就像不可变对象一样。这里还有一个想法:有一种方法可以使任何对象变为可变的和不可变的?仅从关键词语义上来说,我认为这是可能的,但它似乎是预先定义的,哪个对象是什么样的突变类型。对我来说,这是有意义的,可以很好地解决这个问题,可能会破坏或帮助很多其他的事情,我现在甚至无法想象。:p你说什么?
- 可悲的是,从那以后我就没有用python编码了,所以现在我已经生锈了…但再看一遍,我不知道为什么我以前不接受这个答案!
- @你可以用一个装饰器
- @伊拉扎和我想你可能是想对这个问题发表评论!P
- @CREGOX请避免在堆栈溢出时发誓。
- @凯尔,你只是帮了我一点忙,可能不愿意像伊拉扎那样帮我。再读一遍,我没有骂人,因为我没有攻击任何人。我给你写了一个正确的回复,更多的咒语也像以前一样被正确地使用了——只是作为自我参照——但它立刻被从平台上抹去了。没有警告,什么都没有。它们使它看起来像一只虫子。我还链接到你的视频,这可能更好地解释单词是如何工作的youtube.com/watch?V=0-dasdwmvmi,如果您感兴趣的话。
一个函数的极端特定的用例,它允许您有选择地传递一个要修改的列表,但是除非您特别传递一个列表,否则会生成一个新的列表,这绝对不值得使用特殊的用例语法。说真的,如果您要对这个函数进行多次调用,为什么要对序列中的第一个调用进行特殊说明(只传递一个参数),以便将其与其他调用区分开来(需要两个参数才能不断丰富现有列表)?!例如,考虑这样的事情(当然,假设betterappend做了一些有用的事情,因为在当前的例子中,将它称为直接.append会很疯狂!-)
1 2 3 4 5 6 7 8 9 10
| def thecaller(n):
if fee(0):
newlist = betterappend(foo())
else:
newlist = betterappend(fie())
for x in range(1, n):
if fee(x):
betterappend(foo(), newlist)
else:
betterappend(fie(), newlist) |
这简直是疯了,显然应该是,
1 2 3 4 5 6 7
| def thecaller(n):
newlist = []
for x in range(n):
if fee(x):
betterappend(foo(), newlist)
else:
betterappend(fie(), newlist) |
总是使用两个参数,避免重复,构建更简单的逻辑。
引入特殊情况语法鼓励和支持特殊情况下的用例,而鼓励和支持这种非常特殊的用例真的没有多大意义——现有的、完全规则的语法对于用例极其罕见的好用途是很好的;-)。
- 亚历克斯,第一个代码也不需要if。写相同的第二个,但把betterappend(foo(), newlist)替换为newlist = betterappend(foo())。但感谢您指出这可能是一个"非常具体"的用例…也许这只是重新思考如何编码的问题,比如"变量vs名称"的小里程碑。
- +你说得对。人们通常不会设计一个方法来接受一个参数并返回相同的参数——即使是为了使该参数成为可选参数。知道这个规则的例外:memcpy。
我已经编辑了这个答案,以包含问题中发表的许多评论的想法。
你给出的例子有缺陷。它修改作为副作用传递给它的列表。如果您希望函数是这样工作的,那么使用默认参数是没有意义的。返回更新的列表也没有意义。如果没有默认参数,问题就会消失。
如果目的是返回一个新列表,则需要复制输入列表。python希望事情是明确的,所以由您来复制。
1 2 3 4
| def better_append(new_item, a_list=[]):
new_list = list(a_list)
new_list.append(new_item)
return new_list |
对于稍有不同的内容,可以生成一个生成器,该生成器可以将列表或生成器作为输入:
1 2 3 4
| def generator_append(new_item, a_list=[]):
for x in a_list:
yield x
yield new_item |
我认为您有一种误解,认为Python对可变和不变的默认参数的处理方式不同;这完全不是真的。相反,参数的不可变性使您以一种微妙的方式更改代码,以自动执行正确的操作。以您的示例为例,使其适用于字符串而不是列表:
1 2 3
| def string_append(new_item, a_string=''):
a_string = a_string + new_item
return a_string |
此代码不会更改传递的字符串-它不能更改,因为字符串是不可变的。它创建一个新字符串,并为该新字符串分配一个u字符串。默认参数可以反复使用,因为它不会改变,您在开始时复制了它。
- 这是3个非常好的点!1。这是否比使用None解决方案更好?2。如果目的真的是修改传递的列表,那么我完全同意不使用默认参数,但这难道不只是一个原因,为什么它是一个有缺陷的行为吗?三。我对发电机还真的一无所知(去研究一会儿)。
- 这是对生成器的一个很好的解释:stackoverflow.com/questions/231767/…
如果你不是在谈论列表,而是关于awesomesets,一个你刚刚定义的类呢?您想在每个类中定义".local"吗?
1 2 3 4
| class Foo(object):
def get(self):
return Foo()
local = property(get) |
可能有用,但很快就会变老。很快,"if a is none:a=correctObject()"模式就变成了第二天性,你不会发现它很难看——你会发现它很有启发性。
问题不是语法问题,而是语义问题——默认参数的值是在函数定义时计算的,而不是在函数执行时计算的。
- Sooo,你说的是,我们最终只是如此习惯了丑陋的语法,以至于我们再也看不到它是丑陋的了?正如我在这里说的,".local"可能不是更好的选择,我不知道,但我认为在参数定义中应该有一些东西,因为这是"函数"的定义。再加上看起来好多了。
- 请注意,此属性不执行您想要的操作,因为它仍然被精确计算一次。magic local属性必须是语法,而不是属性。
也许您不应该将这两个函数定义为好函数和坏函数。您可以使用第一个列表或字典来实现对相应对象的就地修改。如果你不知道可变对象是如何工作的,但是你知道你在做什么,这个方法会让你头疼,在我看来这是可以的。
所以您有两种不同的方法来传递提供不同行为的参数。这很好,我不会改变的。
- 我没有给出名字,它们来自python.net
- @我知道,我很同情你的问题。我的回答是你不应该…
这比good_append()好,imo:
1 2
| def ok_append(new_item, a_list=None):
return a_list.append(new_item) if a_list else [ new_item ] |
你也可以格外小心,检查一下清单是不是一个清单…
- 当被称为some_list = []; ok_append(4, some_list)时,这种行为很奇怪,因为它不会附加到我的列表中,而是生成一个新的列表。这就是为什么我总是检查if foo is not None而不是if foo,当我真正指前者的时候。这也很奇怪,因为list.append改变列表并返回None,所以它总是返回None或单个项目列表。
- list的类型检查并不是"特别小心",它只是在Python中的一个糟糕的实践。
我认为你混淆了优雅的语法和句法的制糖。python语法清楚地传达了这两种方法,但与不正确的方法相比,正确的方法显得不那么优雅(就语法行而言)。但是,由于不正确的方法,是……不正确的,它的优雅是无关的。至于为什么像你在《更好的附加》中所展示的东西没有被实现,我想There should be one-- and preferably only one --obvious way to do it.在优雅方面胜过小收益。
- 好吧,我觉得江户记1〔4〕对我来说并不是什么显而易见的东西。
- 我不知道。很明显,好的附加功能会起作用。对我来说,不好的附加功能不起作用并不明显。也许一个显而易见的正确的方法来做一件给定的事情的一个推论是,有任意数量的不明显的错误的方法来做它。
- 准确地说:不清楚为什么bad_append不起作用。这正是good_append不明显的原因,因此不是一种"明显的方法"。
- 这是假设错误的附加是更明显的方法,我不确定它是否是。即使它是语言中不太明显的特性之一,努力寻找明显的特性,它仍然比表示参数的非标准用法的专用语法更明显。
- 考虑到不可变名称的语法,对于可变名称来说,这是更明显的方法。而且不知道不可能使用可选参数,这基本上就是默认参数所做的。我不是建议使用专门的语法,而是建议使用通用语法。不管怎样,这里还没有人回答真正的问题:为什么它会一直那么难看?人们基本上是在暗示这是因为从我的角度来看,"没人想出更好的主意",因为没有一句来自Python创建者的引用,只是猜测有经验的用户。