Python范围/静态误解
Posted
技术标签:
【中文标题】Python范围/静态误解【英文标题】:Python Scoping/Static Misunderstanding 【发布时间】:2010-12-11 21:59:00 【问题描述】:我真的很困惑为什么下面的代码块 1 会导致输出 1 而不是输出 2?
代码块 1:
class FruitContainer:
def __init__(self,arr=[]):
self.array = arr
def addTo(self,something):
self.array.append(something)
def __str__(self):
ret = "["
for item in self.array:
ret = "%s%s," % (ret,item)
return "%s]" % ret
arrayOfFruit = ['apple', 'banana', 'pear']
arrayOfFruitContainers = []
while len(arrayOfFruit) > 0:
tempFruit = arrayOfFruit.pop(0)
tempB = FruitContainer()
tempB.addTo(tempFruit)
arrayOfFruitContainers.append(tempB)
for container in arrayOfFruitContainers:
print container
**Output 1 (actual):**
[apple,banana,pear,]
[apple,banana,pear,]
[apple,banana,pear,]
**Output 2 (desired):**
[apple,]
[banana,]
[pear,]
此代码的目标是遍历一个数组并将每个数组包装在一个父对象中。这是我的实际代码的简化,它将所有苹果添加到一袋苹果等等。我的猜测是,出于某种原因,它要么使用相同的对象,要么就像水果容器使用静态数组一样。我不知道如何解决这个问题。
【问题讨论】:
不是您问题的答案,但也值得注意:“while len(arrayOfFruit) > 0:”相当于“while arrayOfFruit:”。至少根据 Python 样式指南,后者更可取。 【参考方案1】:比传入 None 更好的解决方案 - 在这个特定的例子中,而不是一般情况下 - 将 __init__ 的 arr 参数视为一组可枚举的项目来预初始化 FruitContainer,而不是使用数组内部存储:
class FruitContainer:
def __init__(self, arr=()):
self.array = list(arr)
...
这将允许您传入其他可枚举类型来初始化您的容器,这是更高级的 Python 用户期望能够做到的:
myFruit = ('apple', 'pear') # Pass a tuple
myFruitContainer = FruitContainer(myFruit)
myOtherFruit = file('fruitFile', 'r') # Pass a file
myOtherFruitContainer = FruitContainer(myOtherFruit)
它还将消除另一个潜在的别名错误:
myFruit = ['apple', 'pear']
myFruitContainer1 = FruitContainer(myFruit)
myFruitContainer2 = FruitContainer(myFruit)
myFruitContainer1.addTo('banana')
'banana' in str(myFruitContainer2)
对于此页面上的所有其他实现,这将返回 True,因为您不小心为容器的内部存储设置了别名。
注意:这种方法总是正确答案:“如果不是无”在其他情况下会更好。问问自己:我是传入一组对象还是可变容器?如果我传递对象的类/函数改变了我给它的存储,那会是(a)令人惊讶还是(b)可取?在这种情况下,我认为它是(a);因此, list(...) 调用是最好的解决方案。如果(b),“如果不是无”将是正确的方法。
【讨论】:
您好,我想我是“某人”。我不反对以这种方式编写此示例。在此示例中,您将传入要添加到 FruitContainer 类的内部存储中的水果。用户并没有传入一个列表对象,而是用一些水果初始化容器。现在,在一般情况下,我认为默默地强制用户提供的东西不是一个好主意。我认为我们彼此交谈的原因是我专注于一般情况,而您正在考虑这个非常具体的情况。 一般而言, 不要通过强制类型来破坏鸭子类型。 优秀。我重写了我的最后一段以反映这一点。感谢您抽出宝贵时间让我更清楚地了解您的观点! 很抱歉,我没有早点弄清楚您来自哪里。我猜我当时正处于完全学究模式。 :-)【参考方案2】:您的代码有一个用于初始化类的默认参数。默认参数的值在编译时评估一次,因此每个实例都使用相同的列表进行初始化。改成这样:
def __init__(self, arr=None):
if arr is None:
self.array = []
else:
self.array = arr
我在这里更全面地讨论了这个问题:How to define a class in Python
【讨论】:
【参考方案3】:正如 Ned 所说,问题在于您使用列表作为默认参数。还有更多细节here。解决方法是修改__init__
函数如下:
def __init__(self,arr=None):
if arr is not None:
self.array = arr
else:
self.array = []
【讨论】:
【参考方案4】:您不应该将可变值(如 [])用作方法的默认参数。该值计算一次,然后用于每次调用。当您使用空列表作为默认值时,每次在不带参数的情况下调用方法时都会使用相同的列表,即使该值已被先前的函数调用修改。
改为这样做:
def __init__(self,arr=None):
self.array = arr or []
【讨论】:
我真的不希望看到您通过查看值是否为 false 来测试None
。最好使用is None
测试。调用者可以合法地为初始化程序传递一个空列表,并且您的代码将丢弃该空列表并创建一个新的空列表......只有当有人试图使该类的多个实例都共享相同时才会出现我猜最初是空列表,但这是可能的。无论如何,使用is
测试None
是一个好习惯。
哦,一个更好的例子来说明你的代码如何失败:有人可以创建一个具有一些所需额外属性的列表子类,将其作为初始化程序传递,如果它评估为 false,你的代码将丢弃它喜欢一个空的、标准的列表。
更好的是,保留 arr=[] 并使用 self.array = list(arr)。就目前而言,用户不能在不破坏类的情况下将生成器或元组传递给构造函数。
@chrispy,这是个坏主意。如果用户想将一个迭代器展开为一个列表,用户可以调用FruitContainer(list(iterable))
。但是如果用户想传入一个列表的子类,带有副作用日志或其他东西怎么办?您的 list(arr)
将摆脱子类对象并迫使它成为一个无聊的列表。有一个标准的、经过时间考验的 Python 习惯用法,它是对 is None
的测试,如果不是 None
,则存储提供的参数。
@steveha,用户想要关注类的内部存储的可能性有多大?以上是关于Python范围/静态误解的主要内容,如果未能解决你的问题,请参考以下文章