13.6 实例属性
实例仅拥有数据属性(方法严格来说是类属性),后者只是与某个类的实例相关联的数据值,并且可以通过句点属性标识法来访问。这些值独立于其他实例或类。当一个实例被释放后,它的属性同时也被清除了。
13.6.1 “实例化”实例属性(或创建一个更好的构造器)
设置实例的属性可以在实例创建后任意时间进行,也可以在能够访问实例的代码中进行。构造器init()是设置这些属性的关键点之一。
核心笔记:实例属性
能够在“运行时”创建实例属性,是Python类的优秀特性之一,从C++或Java转过来的人会被小小地震惊一下,因为C++或Java中所有属性在使用前都必须明确定义/声明。Python不仅是动态类型,而且在运行时,允许这些对象属性的动态创建。这种特性让人爱不释手。当然,我们必须提醒读者,创建这样的属性时,必须谨慎。一个缺陷是,属性在条件语句中创建,如果该条件语句块并未被执行,属性也就不存在,而你在后面的代码中试着去访问这些属性,就会有错误发生。故事的精髓是告诉我们,Python让你体验从未用过的特性,但如果你使用它了,你还是要小心为好。
1. 在构造器中首先设置实例属性
构造器是最早可以设置实例属性的地方,因为init()是实例创建后第一个被调用的方法。再没有比这更早的可以设置实例属性的机会了。一旦init()执行完毕,返回实例对象,即完成了实例化过程。
2. 默认参数提供默认的实例安装
在实际应用中,带默认参数的init()提供一个有效的方式来初始化实例。在很多情况下,默认值表示设置实例属性的最常见的情况,如果提供了默认值,我们就没必要显式给构造器传值了。我们在11.5.2节中也提到默认参数的常见好处。需要明白一点,默认参数应当是不变的对象;像列表(list)和字典(dictionary)这样的可变对象可以扮演静态数据,然后在每个方法调用中来维护它们的内容。
例13.1描述了如何使用默认构造器行为来帮助我们计算在美国一些大都市中的旅馆中寄宿时,租房总费用。
代码的主要目的是来帮助某人计算出每日旅馆租房费用,包括所有州销售税和房税。缺省为旧金山附近的普通区域,它有8.5%销售税及10%的房间税。每日租房费用没有缺省值,因此在任何实例被创建时,都需要这个参数。
例13.1 使用缺省参数进行实例化
定义一个类来计算这个假想旅馆租房费用。init()构造器对一些实例属性进行初始化。calcTotal()方法用来决定是计算每日总的租房费用还是计算全部的租房费。
设置工作是由init()在实例化之后完成的,如上面的第4~8行,其余部分的核心代码是calcTotal()方法,从第10~14行。init()的工作即是设置一些参数值来决定旅馆总的基本租房费用(不包括住房服务,电话费,或其他偶发事情)。calcTotal()可以计算每日所有费用,如果提供了天数,那么将计算整个旅程全部的住宿费用。内建的round()函数可以大约计算出最接近的费用(两个小数位)。下面是这个类的用法:
最开始的两个假想例子都是在旧金山,使用了默认值,然后是在西雅图,这里我们提供了不同的销售税和房间税率。最后一个例子在华盛顿特区。经过计算更长的假想时间,来扩展通常的用法:停留5个工作日,外加一个周六,此时有特价,假定是星期天出发回家。
不要忘记,函数所有的灵活性,比如默认参数,也可以应用到方法中去。在实例化时,可变长度参数也是一个好的特性(当然,这要根据应用的需要)。
3. init()应当返回None
你也知道,采用函数操作符调用类对象会创建一个类实例,也就是说这样一种调用过程返回的对象就是实例,下面示例可以看出:
如果定义了构造器,它不应当返回任何对象,因为实例对象是自动在实例化调用后返回的。相应地,init()就不应当返回任何对象(应当为None);否则,就可能出现冲突,因为只能返回实例。试着返回非None的任何其他对象都会导致TypeError异常:
13.6.2 查看实例属性
内建函数dir()可以显示类属性,同样还可以打印所有实例属性:
与类相似,实例也有一个dict特殊属性(可以调用varsO并传入一个实例来获取),它是实例属性构成的一个字典:
13.6.3 特殊的实例属性
实例仅有两个特殊属性(见表13.2)。对于任意对象I:
现在使用类C及其实例C来看看这些特殊实例属性:
你可以看到,c现在还没有数据属性,但我们可以添加一些再来检查dict属性,看是否添加成功了:
dict属性由一个字典组成,包含一个实例的所有属性。键是属性名,值是属性相应的数据值。字典中仅有实例属性,没有类属性或特殊属性。
核心风格:修改dict
对类和实例来说,尽管dict属性是可修改的,但还是建议你不要修改这些字典,除非你知道你在干什么。这些修改可能会破坏你的OOP,造成不可预料的副作用。使用熟悉的句点属性标识来访问及操作属性会更易于接受。需要你直接修改dict属性的情况很少,其中之一是你要重载setattr特殊方法。实现setattr()本身是一个冒险的经历,满是圈套和陷阱,例如无穷递归和破坏实例对象。这个故事还是留到下次说吧。
13.6.4 建类型属性
内建类型也是类,它们有没有像类一样的属性呢?那实例有没有呢?对内建类型也可以使用dir(),与任何其他对象一样,可以得到一个包含它属性名字的列表:
既然我们知道了一个复数有什么样的属性,我们就可以访问它的数据属性,调用它的方法了:
试着访问dict会失败,因为在内建类型中,不存在这个属性:
13.6.5 实例属性vs类属性
我们已在13.4.1节中描述了类数据属性。这里简要提一下,类属性仅是与类相关的数据值,和实例属性不同,类属性和实例无关。这些值像静态成员那样被引用,即使在多次实例化中调用类,它们的值都保持不变。不管如何,静态成员不会因为实例而改变它们的值,除非实例中显式改变它们的值(实例属性与类属性的比较,类似于自动变量和静态变量,但这只是笼统的类推。在你对自动变量和静态变量还不是很熟的情况下,不要深究这些)。
类和实例都是名字空间。类是类属性的名字空间,实例则是实例属性的。
关于类属性和实例属性,还有一些方面需要指出。你可采用类来访问类属性,如果实例没有同名的属性的话,你也可以用实例来访问。
1. 访问类属性
类属性可通过类或实例来访问。下面的示例中,类C在创建时,带一个version属性,这样通过类对象来访问它是很自然的了,比如C.version。当实例c被创建后,对实例c而言,访问c.version会失败,不过Python首先会在实例中搜索名字version,然后是类,再就是继承树中的基类。本例中,version在类中被找到了:
然而,我们只有当使用类引用version时,才能更新它的值,像上面的C.version递增语句。如果尝试在实例中设定或更新类属性会创建一个实例属性c.version,后者会阻止对类属性C.versioin的访问,因为第一个访问的就是c.version,这样可以对实例有效地“遮蔽”类属性C.version,直到c.version被清除掉。
2. 从实例中访问类属性须谨慎
与通常Python变量一样,任何对实例属性的赋值都会创建一个实例属性(如果不存在的话)并且对其赋值。如果类属性中存在同名的属性,有趣的副作用即产生(经典类和新式类都存在)。
在上面的代码片段中,创建了一个名为version的新实例属性,它覆盖了对类属性的引用。然而,类属性本身并没有受到伤害,仍然存在于类域中,还可以通过类属性来访问它,如上例可以看到的。好了,那么如果把这个新的version删除掉,会怎么样呢?为了找到结论,我们将使用del语句删除c.version。
所以,给一个与类属性同名的实例属性赋值,我们会有效地“隐藏”类属性,但一旦我们删除了这个实例属性,类属性又重见天日。现在再来试着更新类属性,但这次,我们只尝试一下“无辜”的增量动作:
还是没变。我们同样创建了一个新的实例属性,类属性原封不动(深入理解Python相关知识:属性已存于类字典[dict]中。通过赋值,其被加入到实例的dict中了)。赋值语句右边的表达式计算出原类的变量,增加0.2,并且把这个值赋给新创建的实例属性。注意下面是一个等价的赋值方式,但它可能更加清楚些:
但……在类属性可变的情况下,一切都不同了:
3. 类属性持久性
静态成员,顾名思义,任凭整个实例(及其属性)的如何进展,它都不理不睬(因此独立于实例)。同时,当一个实例在类属性被修改后才创建,那么更新的值就将生效。类属性的修改会影响到所有的实例:
核心提示:使用类属性来修改自身(不是实例属性)
正如上面所看到的那样,使用实例属性来试着修改类属性是很危险的。原因在于实例拥有它们自已的属性集,在Python中没有明确的方法来指示你想要修改同名的类属性,比如,没有global关键字可以用来在一个函数中设置一个全局变量(来代替同名的局部变量)。修改类属性需要使用类名,而不是实例名。