C++、Java、C#等语言在实例变量方面有一种奇怪的行为,即在
class {}
块中描述的数据(成员或字段,取决于您属于哪个文化)属于实例,而在同一块中描述的函数(也就是方法,但是C++程序员似乎讨厌这个术语,他们会用“成员函数”代替)则属于类本身。当你真正考虑它时,这很奇怪和令人困惑。
很多人不去思考它,他们只接受并继续前进。但是对于很多初学者来说,这实际上会引起混淆,他们认为块内的所有内容都属于实例。这导致了(对于有经验的程序员来说)非常奇怪的问题和对整个“虚表”实现概念感到困惑。(当然,这主要是教师们没能解释虚表只是一种实现方式,并且没有在第一时间明确区分类和实例的错。)
Python没有这种混淆。由于在Python中,函数(包括方法)是对象,因此编译器做出这样的区分是非常不一致的。因此,在Python中发生的就是您应该直观地期望的:
块内的所有内容都属于类本身。是的,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的房屋,那么它将引发异常。毕竟,请求原谅比请求许可更容易。(请注意,在Python中实际上需要努力才能创建)
至于实际的迭代...在Python中,我们通过实际迭代容器中的对象来进行迭代。Java和C#也有这个概念,您也可以使用C++标准库来获取它(尽管很多人不会费心)。我们不迭代索引,因为它是一个无用和分散注意力的间接。我们不需要为了使用“windows_per_house”值来编号它们;我们只需要依次查看每个值即可。
那么ID号呢?简单。Python为我们提供了一个名为“enumerate”的函数,它在给定元素输入序列的情况下提供(index,element)对。它干净利落,让我们明确解决问题所需的索引(和索引的目的),并且它是一个内置的可以不像 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]
houses = [
House(house_id, [Window(window_id) for window_id in range(window_count)])
for house_id, window_count in enumerate(windows_per_house)
]
for house in houses:
print "House: " + house.ID
for window in house.windows:
print " Window: " + window.ID
for
循环的行。 - Chriszuma