9.3.3 实例对象
现在我们可以用实例对象作什么?实例对象唯一可用的操作就是属性引用。有两种有效的属性名,数据属性和方法。
数据属性相当于Smalltalk中的“实例变量”或C++中的“数据成员”。
和局部变量一样,数据属性不需要声明,在第一次给它们赋值时就会生成。
例如,如果 x 是前面创建的 MyClass 实例,下面这段代码会打印出 16 ,而在堆栈中不再留下痕迹:
x.counter =1
whilex.counter
print(x.counter)
delx.counter
为了更好理解,我将程序做了修改,并实际运行:
classMyClass:
"""A simple example class"""
i = 12345
deff(self):
return'hello world'
x = MyClass()
x.counter = 1
whilex.counter < 10:
x.counter = x.counter * 2
print(x.counter)
print(x.counter)
delx.counter
运行结果
另一种实例引用的属性是方法。方法是(“属于”一个对象的)函数。
(在Python中,方法不止是类实例所独有:其它类型的对象也可有方法。
例如,列表对象有append,insert,remove,sort等方法。然而,在后面的介绍中,除非特别说明,我们提到的方法特指类方法)
实例对象的有效方法名依赖于它的类。按照定义,类中所有的那些是函数对象的属性定义了对应的实例方法。所以在我们的例子中,x.f是一个有效的方法引用,因为MyClass.f是一个函数。但x.i不是有效的方法引用,因为MyClass.i不是函数。不过x.f和MyClass.f并不是一回事,x.f是一个方法对象,不是一个函数对象。
9.3.4 方法对象
通常,方法恰在它被绑定时被调用:
x.f()
在MyClass示例中,这会返回字符串 'hello world'。
然而,也不是一定要在绑定时马上就要调用方法。x.f是一个方法对象,
它可以存起来在以后被调用。例如:
xf = x.f
while True:
print(xf())
会不断的打印 hello world。
为了搞清楚实例化,绑定等的意思,我修改了程序
classMyClass:
"""A simple example class"""
i = 12345
deff(self):
return'hello world'
x = MyClass()
xf = x.f
print(MyClass)
print(MyClass())
print(x)
print(x.f)
print(x.f())
print(xf)
print(xf())
运行结果:
hello world
hello world
是不是可以这样理解,实例化类的效果就是产生了一个有地址的对象?
调用方法时到底发生了什么?你可能注意到调用x.f()时没有引用前面标出的变量,尽管在f()的函数定义中指明了一个参数。这个参数怎么了?事实上如果函数调用中缺少参数,Python 会抛出异常--即使这个参数实际上没什么用……
实际上,你可能已经猜到了答案:方法的特别之处是将实例对象作为第一个参数传给了函数。在我们的例子中,调用x.f()相当于MyClass.f(x)。通常, 调用一个带n个参数的方法时,先将方法的对象插入到这个参数列表的最前面,然后用这个参数列表去调用相应的函数。
如果你还是不理解方法的工作原理,了解一下它的实现也许会理清头绪。当引用非数据属性的实例属性时,会搜索自己的类。如果这个命名确认是一个有效的类属性(即一个函数对象)。就会将实例对象和函数对象封装(实例对象指向函数对象)进一个抽象对象:这就是方法对象。当调用一个有参数列表的方法对象时, 用实例对象和这个参数列表构造一个新的参数列表,然后调用函数时就用这个新的参数列。
9.3.5 类和实例变量
一般来说,实例变量是每个实例特有的数据,类变量是由类的所有实例共享的属性和方法:
classDog:kind='canine'# class variable shared by all instancesdef__init__(self,name):self.name=name# instance variable unique to each instance
>>>d=Dog('Fido')
>>>e=Dog('Buddy')
>>>d.kind# shared by all dogs
'canine'
>>>e.kind# shared by all dogs
'canine'
>>>d.name# unique to d
'Fido'
>>>e.name# unique to e
'Buddy'
正如在话说名称和对象讨论的, 因引入可变对象,例如列表和字典,共享数据可能带来意外的效果。例如,以下代码中的tricks列表不能用作类变量,因为所有Dog实例都共享了单个列表:
classDog:tricks=[]# mistaken use of a class variabledef__init__(self,name):self.name=namedefadd_trick(self,trick):self.tricks.append(trick)
>>>d=Dog('Fido')
>>>e=Dog('Buddy')
>>>d.add_trick('roll over')
>>>e.add_trick('play dead')
>>>d.tricks# unexpectedly shared by all dogs
['roll over', 'play dead']
正确的类设计应该使用一个实例变量来代替:
classDog:def__init__(self,name):self.name=nameself.tricks=[]# creates a new empty list for each dogdefadd_trick(self,trick):self.tricks.append(trick)
>>>d=Dog('Fido')
>>>e=Dog('Buddy')
>>>d.add_trick('roll over')
>>>e.add_trick('play dead')
>>>d.tricks
['roll over']
>>>e.tricks
['play dead']
9.4 补充说明
数据属性会覆盖同名的方法属性;这在大型程序中可能带来极难发现的bug,为了避免意外的命名冲突,使用一些减少冲突机会的约定是明智的。可能的约定包括,大写方法命名,用小写的唯一字符串(可能只是下划线)给数据属性名称添上前缀,或者方法属性用动词,数据属性用名词。
数据属性既可以由方法引用,也可以由一个对象的普通用户(“客户端”)引用。换句话说,类不能用于实现纯抽象数据类型。事实上,Python中没有什么可以强制数据隐藏 ---这是基于约定。(另一方面,用C语言编写的Python实现可以完全隐藏实现细节,并在必要时控制对象的访问;这可以由C语言的Python扩展使用。)
客户端应该谨慎使用数据属性--客户可能会因为乱改数据属性而破坏了本来由方法维护的不变量。注意,只要避免了命名冲突,客户端可以将自己的数据属性添加到实例对象,而不会影响方法的有效性--再次强调,命名约定可以在这里节省很多麻烦。
从方法内部引用数据属性(或其他方法)并没有快捷方式。我发现这实际上提高了方法的可读性:当浏览一个方法时,就不会混淆局部变量和实例变量。
通常,方法的第一个参数为。这仅仅是一个约定:名字对Python而言绝对没有任何特殊含义。但请注意,不遵循约定,你的代码对其他Python程序员的可读性可能较差,而且也应该料到有些类查看器程序也可能是遵循此约定编写的。
类属性的任何函数对象都为那个类的实例定义了一个方法。函数定义文本代码不一定非得定义在本类中:也可以将一个函数对象赋值给类中的一个局部变量。例如:
# Function defined outside the class
deff1(self,x,y):
returnmin(x,x+y)
class C:
f=f1
defg(self):
return'hello world'
h=g
现在,和都是类的属性,引用的都是函数对象,因此它们都是实例的方法--严格等于。请注意,这种做法通常只会使阅读程序的人产生困惑。
通过 参数的方法属性,方法可以调用其它的方法:
classBag:
def__init__(self):
self.data = []
defadd(self, x):
self.data.append(x)
defaddtwice(self, x):
self.add(x)
self.add(x)
方法可以像引用普通的函数那样引用全局命名。与方法关联的全局作用域是包含类定义的模块。(类本身永远不会做为全局作用域使用。)尽管少有好的理由在方法中使用全局数据,全局作用域确有很多合法的用途:其一是方法既可以调用通过import导入全局作用域的函数和方法,也可以调用定义在全局作用域中的类和函数。通常,包含此方法的类本身也定义在这个全局作用域,在下一节我们会了解为何一个方法要引用自己的类。
每个值都是一个对象,因此每个值都有一个 类(class) (也称为它的 类型(type) ),它存储为
9.5 继承
当然,一个语言的类如果不支持继承特性,就不值得称作“类”。派生类定义的语法如下所示:
classDerivedClassName(BaseClassName):
.
.
.
命名(示例中的基类名)必须与派生类定义在一个作用域内。除了类,还可以用任意的表达式,此法当基类定义在另一个模块中时非常有用:
classDerivedClassName(modname.BaseClassName):
派生类定义的实行过程和基类是一样的。构造派生类对象时,就记住了基类。这在解析属性引用的时候尤其有用:如果在本类中找不到要调用的属性,就搜索基类。如果基类是由别的类派生而来,这个规则会递归的应用上去。
派生类的实例化没有什么特殊之处:创建一个新的类实例。方法引用按如下规则解析:搜索对应的类属性,必要时沿基类链逐级搜索,如果找到了函数对象这个方法引用就是合法的。
派生类可能会覆盖其基类的方法。因为方法调用同一个对象中的其它方法时没有特权,基类的方法调用同一个基类的方法时,可能实际上最终调用了派生类中的覆盖方法。(对于 C++ 程序员来说,Python中的所有方法本质上都是方法。)
派生类中的覆盖方法实际上可能是想要扩充而不是简单的替代基类中的同名方法。有一个简单的办法可以直接调用基类方法,只是调用:
BaseClassName.methodname(self,arguments).有时这对于客户端也很有用
(要注意,只有在同一全局作用域定义或导入时才能这样用。)
Python有两个内建函数用于继承:
。使用来检查实例类型:只有是 或者是从 派生的类时才为 。
。使用来检查类的继承:是 因为 是 .的子类。然而, 为 ,因为 不是 的子类
准确理解—与曾工一块学习
领取专属 10元无门槛券
私享最新 技术干货