Python:在对象列表中遍历对象列表
我创建了两个类,一个叫做房子(House),另一个叫做窗户(Window)。然后我做了一个列表,里面包含了四个房子。每个房子都有一个窗户的列表。我想要遍历每个房子里的窗户,并打印出它们的ID。不过,我得到的结果似乎有点奇怪 :S 我非常感谢任何帮助。
#!/usr/bin/env python
# Minimal house class
class House:
ID = ""
window_list = []
# Minimal window class
class Window:
ID = ""
# List of houses
house_list = []
# Number of windows to build into each of the four houses
windows_per_house = [1, 3, 2, 1]
# Build the houses
for new_house in range(0, len(windows_per_house)):
# Append the new house to the house list
house_list.append(House())
# Give the new house an ID
house_list[new_house].ID = str(new_house)
# For each new house build some windows
for new_window in range(0, windows_per_house[new_house]):
# Append window to house's window list
house_list[new_house].window_list.append(Window())
# Give the window an ID
house_list[new_house].window_list[new_window].ID = str(new_window)
#Iterate through the windows of each house, printing house and window IDs.
for house in house_list:
print "House: " + house.ID
for window in house.window_list:
print " Window: " + window.ID
####################
# Desired output:
#
# House: 0
# Window: 0
# House: 1
# Window: 0
# Window: 1
# Window: 2
# House: 2
# Window: 0
# Window: 1
# House: 3
# Window: 0
####################
4 个回答
这是更新后的代码。
# Minimal house class
class House:
def __init__(self, id):
self.ID = id
self.window_list = []
# Minimal window class
class Window:
ID = ""
# List of houses
house_list = []
# Number of windows to build into each of the for houses
windows_per_house = [1, 3, 2, 1]
# Build the houses
for new_house in range(len(windows_per_house)):
# Append the new house to the house list
house_list.append(House(str(new_house)))
# For each new house build some windows
for new_window in range(windows_per_house[new_house]):
# Append window to house's window list
house_list[new_house].window_list.append(Window())
# Give the window an ID
house_list[new_house].window_list[new_window].ID = str(new_window)
#Iterate through the windows of each house, printing house and window IDs.
for house in house_list:
print "House: " + house.ID
for window in house.window_list:
print " Window: " + window.ID
实际的问题是,window_list
这个属性是可变的,也就是说,当不同的实例在使用它的时候,它们会共享同一个列表。通过把window_list
放到__init__
方法里,每个实例就可以拥有自己的列表了。
C++、Java、C#等语言在处理实例变量时有一种很奇怪的行为。在一个class {}
块中,里面定义的数据(成员或字段,具体称呼因文化而异)属于实例,而同样块中的函数(其实是方法,不过C++程序员似乎不喜欢这个词,喜欢叫它“成员函数”)则属于这个类本身。这种设计乍一看让人觉得很奇怪,也容易让人困惑。
很多人对此并不深思,他们只是接受了这个事实,然后继续前进。但这实际上让很多初学者感到困惑,他们以为块内的所有东西都属于实例。这就导致了一些经验丰富的程序员觉得奇怪的问题,比如关于这些方法在每个实例上的开销,以及理解“虚表”(vtable)实现概念的困难。(当然,这主要是老师们的责任,他们没有解释清楚虚表只是其中一种实现方式,也没有明确区分类和实例。)
而Python就没有这种困惑。在Python中,函数(包括方法)都是对象,因此编译器不可能做出这样的区分。在Python中,所有在class
缩进块内的内容都属于类本身。而且,Python的类本身也是对象(这为类属性提供了存放的地方),你不需要通过标准库的复杂操作来反射使用它们。(缺乏显式类型定义在这里是相当解放的。)
那么,如何在实例中添加数据呢?默认情况下,Python并不限制你在任何实例中添加任何东西。它甚至不要求同一类的不同实例包含相同的属性。而且,它也不会预先分配一块内存来存放所有对象的属性。(因为Python是纯引用语义的语言,所以它只能存放引用,而没有C#风格的值类型或Java风格的基本类型。)
但显然,按照这种方式做是个好主意,所以通常的约定是“在实例构造时添加所有数据,然后不要再添加或删除属性”。
那么,“在构造时”是什么意思呢?Python并没有像C++/Java/C#那样的构造函数,因为没有“预留空间”意味着将“初始化”视为与普通赋值分开的任务并没有实际好处——当然,初始化自动发生在新对象上是一个好处。
在Python中,最接近的等价物是魔法方法__init__
,它在新创建的类实例上自动调用。(还有另一个魔法方法__new__
,它更像是构造函数,因为它负责对象的实际创建。然而,在几乎所有情况下,我们只想委托给基类的__new__
,它调用一些内置逻辑,基本上给我们一个可以作为对象的指针,并指向类定义。因此,几乎没有必要担心__new__
。它更像是C++中为类重载operator new
。)在这个方法的主体中(没有C++风格的初始化列表,因为没有预留的数据需要初始化),我们根据给定的参数设置属性的初始值(并可能做其他工作)。
如果我们想让事情更整洁,或者效率是一个真正的问题,我们还有一个小技巧:可以使用类的魔法属性__slots__
来指定类属性的名称。这只是一个字符串列表,没有什么花哨的。然而,这仍然不会预先初始化任何东西; 实例在你赋值之前没有属性。这只是防止你添加其他名称的属性。你甚至仍然可以从指定了__slots__
的类的对象中删除属性。所有发生的事情是,实例会有一个不同的内部结构,以优化内存使用和属性查找。
使用__slots__
要求我们从内置的object
类型派生,这本来就是我们应该做的(尽管在Python 2.x中并不是强制的,这只是为了向后兼容)。
好了,现在我们可以让代码工作了。但如何让它在Python中正确呢?
首先,就像其他语言一样,不断注释解释那些已经显而易见的东西是个坏主意。这会分散用户的注意力,对你作为语言学习者也没有帮助。你应该知道类定义是什么样子的,如果你需要注释来告诉你类定义就是类定义,那么阅读代码注释并不是你需要的帮助。
在这个“鸭子类型”的概念下,在变量(或属性)名称中包含数据类型名称是很不合适的。你可能会抗议:“但我怎么能在没有显式类型声明的情况下跟踪类型呢?” 不要。使用你的窗口列表的代码并不关心你的窗口列表是窗口列表。它只关心它能否遍历窗口列表,从而获得可以以某种方式使用的值。这就是鸭子类型的工作原理:停止思考对象是什么,而关注它能做什么。
你会注意到在下面的代码中,我把字符串转换的代码放在了House和Window的构造函数中。这作为一种原始的类型检查形式,也确保我们不会忘记进行转换。如果有人试图用一个连字符串都无法转换的ID来创建House,那么它会引发异常。毕竟,要求原谅总比请求许可要容易。(注意,在Python中,实际上你得稍微费点劲才能创建)
至于实际的迭代……在Python中,我们通过实际遍历容器中的对象来迭代。Java和C#也有这个概念,你也可以通过C++标准库来实现(尽管很多人不这样做)。我们不通过索引来迭代,因为这是一种无用且分散注意力的间接方式。我们不需要给“每个房子的窗口数量”编号才能使用它们;我们只需要依次查看每个值。
那么ID号呢,我听到你问?很简单。Python提供了一个叫'enumerate'的函数,它给我们(索引,元素)对,基于输入的元素序列。这很简洁,让我们明确了我们需要索引来解决问题(以及索引的目的),而且这是一个内置的,不需要像其他Python代码那样被解释,所以开销不大。(当内存是个问题时,可以使用懒惰求值的版本。)
但即便如此,迭代创建每个房子,然后手动将每个房子添加到一个最初为空的列表中,还是太底层了。Python知道如何构造一个值的列表;我们不需要告诉它怎么做。(而且作为额外好处,通常让它自己处理这部分会获得更好的性能,因为实际的循环逻辑可以在本地C中完成。)我们相反是描述我们想要的列表,使用列表推导式。我们不需要走过“逐个取每个窗口计数,创建相应的房子,并将其添加到列表”的步骤,因为我们可以直接说“一个房子的列表,每个窗口计数对应这个输入列表中的窗口计数”。用英语来说这可能显得笨拙,但在像Python这样的编程语言中更清晰,因为你可以省略很多小词,而且不需要花费精力描述初始列表或将完成的房子添加到列表的过程。你根本不描述过程,只描述结果。量身定制。
最后,作为一个通用的编程概念,尽可能推迟对象的构造,直到我们准备好该对象存在所需的一切是有意义的。“两阶段构造”是丑陋的。因此,我们先为房子制作窗口,然后再制作房子(使用这些窗口)。使用列表推导式,这很简单:我们只需嵌套列表推导式。
class House(object):
__slots__ = ['ID', 'windows']
def __init__(self, id, windows):
self.ID = str(id)
self.windows = windows
class Window(object):
__slots__ = ['ID']
def __init__(self, id):
self.ID = str(id)
windows_per_house = [1, 3, 2, 1]
# Build the houses.
houses = [
House(house_id, [Window(window_id) for window_id in range(window_count)])
for house_id, window_count in enumerate(windows_per_house)
]
# See how elegant the list comprehensions are?
# If you didn't quite follow the logic there, please try **not**
# to imagine the implicitly-defined process as you trace through it.
# (Pink elephants, I know, I know.) Just understand what is described.
# And now we can iterate and print just as before.
for house in houses:
print "House: " + house.ID
for window in house.windows:
print " Window: " + window.ID
现在你使用的是 类属性,而不是实例属性。试着把你的类定义改成下面这样:
class House:
def __init__(self):
self.ID = ""
self.window_list = []
class Window:
def __init__(self):
self.ID = ""
现在你的代码是这样的,所有的 House
实例都在共享同一个 window_list
。