创建自定义类
终于要创建自定义类了!下面是一个简单的示例:
class Person:def set_name(self, name):self.name = namedef get_name(self):return self.namedef greet(self):print("Hello, world! I'm {}.".format(self.name))
这个示例包含三个方法定义,它们类似于函数定义,但位于class语句内。Person当然是类的名称。class语句创建独立的命名空间,用于在其中定义函数。一切看起来都挺好,但你可能想知道参数self是什么。它指向对象本身。那么是哪个对象呢?下面通过创建两个实例来说明这一点。
foo = Person()bar = Person()foo.set_name('Luke Skywalker’)bar.set_name("Anakin Skywalker’)foo.greet()Hello, world! I'm Luke Skywalker.bar. greet()Hello, world! I'm Anakin Skywalker.
这个示例可能有点简单,但澄清了self是什么。对foo调用set name和greet时,foo都会作为第一个参数自动传递给它们。我将这个参数命名为self,这非常贴切。实际上,可以随便给这个参数命名,但鉴于它总是指向对象本身,因此习惯上将其命名为self.显然,self很有用,甚至必不可少。如果没有它,所有的方法都无法访问对象本身–要操作的属性所属的对象。与以前一样,也可以从外部访问这些属性。
foo. nameLuke Skywalkerbar.name ='Yodabar.greet()Hello, world! I'm Yoda.
提示 如果foo是一个Person实例,可将foo.greet0)视为Person.greet(foo)的简写,但后者的多态性更低。
属性、函数和方法
实际上,方法和函数的区别表现在前面提到的参数self上。方法(更准确地说是关联的方法)将其第-个参数关联到它所属的实例,因此无需提供这个参数。无疑可以将对象的属性关联到一个普通函数,但这样就没有特殊的self参数了。
class Class:def method(self):...print('I have a self!’)def function():print("I don't...")instance = Class()instance.method() instance.method = functioninstance.method() I don't...
请注意,有没有参数self并不取决于是否以刚才使用的方式(如instance.method)调用方法。实际上完全可以让另一个变量指向同一个方法。
class Bird:song =’Squaawk!’def sing(self):print(self. song)bird = Bird()bird. sing()Squaawk!birdsong = bird.singbirdsong()Squaawk!
虽然最后一个方法调用看起来很像函数调用,但变量birdsong指向的是关联的方法bird.sing,这意味着它也能够访问参数self(即它也被关联到类的实例)。
再谈隐藏
默认情况下,可从外部访问对象的属性。再来看一下前面讨论封装时使用的示例。
c.nameSir Lancelotc.name ='Sir Gumby’c.get name()Sir Gumby
有些程序员认为这没问题,但有些程序员(如Smalltalk雅之父)认为这违反了封装原则。他们认为应该对外部完全隐藏对象的状态(即不能从外部访问它们)。你可能会问,为何他们的立场如此极端?由每个对象管理自己的属性还不够吗?为何要向外部隐藏属性?毕竟,如果能直接访问ClosedObject(对象c所属的类)的属性name,就不需要创建方法setName和getName了,关键是其他程序员可能不知道(也不应知道)对象内部发生的情况。例如,ClosedObject可能在对象修改其名称时向管理员发送电子邮件。这种功能可能包含在方法set_name中。但如果直接设置c.name,结果将如何呢?什么都不会发生–根本不会发送电子邮件。为避免这类问题,可将属性定义为私有。私有属性不能从对象外部访问,而只能通过存取器方法(如get name和set name)来访问。
Python没有为私有属性提供直接的支持,而是要求程序员知道在什么情况下从外部修改属性是安全的,。毕竟,你必须在知道如何使用对象之后才能使用它。然而,通过玩点小花招,可获得类似于私有属性的效果。要让方法或属性成为私有的(不能从外部访问),只需让其名称以两个下划线打头即可。
class Secretive:def __inaccessible(self):print("Bet you can't see me ...")def accessible(self):print( The secret message is:")self.__inaccessible()
现在从外部不能访问 inaccessible,但在类中(如accessible中)依然可以使用它
s= Secretive()s.inaccessible()s.accessible()The secret message is:Bet you can't see me ..
虽然以两个下划线打头有点怪异,但这样的方法类似于其他语言中的标准私有方法。然而,幕后的处理手法并不标准:在类定义中,对所有以两个下划线打头的名称都进行转换,即在开头加上一个下划线和类名。
Secretive.__Secretive__inaccessible
只要知道这种幕后处理手法,就能从类外访问私有方法,然而不应这样做。
s. __Secretive__inaccessible()Bet you can't see me .
总之,你无法禁止别人访问对象的私有方法和属性,但这种名称修改方式发出了强烈的信号,让他们不要这样做。
类的命名空间
下面两条语句大致等价:
def foo(x): return x*xfoo=lambda x:x*x
它们都创建一个返回参数平方的函数,并将这个函数关联到变量foo。可以在全局(模块)作用域内定义名称foo,也可以在函数或方法内定义。定义类时情况亦如此:在class语句中定义的代码都是在一个特殊的命名空间(类的命名空间)内执行的,而类的所有成员都可访问这个命名空间。类定义其实就是要执行的代码段,并非所有的Python程序员都知道这一点,但知道这一点很有帮助。例如,在类定义中,并非只能包含def语句。
class C:print('Class C being defined...’)Class C being defined.
这有点傻,但请看下面的代码:
class MemberCounter:members=0def init(self):MemberCounter.members += 1m1 = MemberCounter()ml.init()MemberCounter.members1m2 = MemberCounter()m2.init()2MemberCounter.members
上述代码在类作用域内定义了一个变量,所有的成员(实例)都可访问它,这里使用它来计算类实例的数量。注意到这里使用了init来初始化所有实例,每个实例都可访问这个类作用域内的变量,就像方法一样。
m1.membersm2.members
如果你在一个实例中给属性members赋值,结果将如何呢?
m1.members ='Two'm1.membersTwom2.members2
新值被写入m1的一个属性中,这个属性遮住了类级变量。
指定超类
本章前面讨论过,子类扩展了超类的定义。要指定超类,可在class语句中的类名后加上超类名,并将其用圆括号括起。
class Filter:def init(self):self.blocked=[]def filter(self,sequence):return [x for x in sequence if x not in self.blocked# SPAMFilter是Filter的子类class SPAMFilter(Filter):def init(self):#重写超类Filter的方法initself.blocked = ['SPAM’]
Filter是一个过滤序列的通用类。实际上,它不会过滤掉任何东西。
f = Filter()f.init ()f.filter([1, 2,3])[1,2,3]
Filter类的用途在于可用作其他类(如将’SPAM’从序列中过滤掉的SPAMFilter类)的基类(超类)
s=SPAMFilter()s.init()s.filter(['SPAM'.'SPAM','SPAM','SPAM','eggs’,’SPAM,’bacon '])bacon’"eggs
请注意SPAMFilter类的定义中有两个要点。
口 以提供新定义的方式重写了Filter类中方法init的定义
口 直接从Filter类继承了方法filter的定义,因此无需重新编写其定义。
第二点说明了继承很有用的原因:可以创建大量不同的过滤器类,它们都从Filter类派生而来,并且都使用已编写好的方法filter。这就是懒惰的好处。
面向对象设计总结:
口将相关的东西放在一起。如果一个函数操作一个全局变量,最好将它们作为一个类的属性和方法,。
口 不要让对象之间过于亲密。方法应只关心其所属实例的属性,对于其他实例的状态,让它们自己去管
理就好了。
口慎用继承,尤其是多重继承。继承有时很有用,但在有些情况下可能带来不必要的复杂性。要正确地
使用多重继承很难,要排除其中的bug更难。
口 保持简单。让方法短小紧凑。一般而言,应确保大多数方法都能在30秒内读完并理解。对于其余的方法,尽可能将其篇幅控制在一页或一屏内。
确定需要哪些类以及这些类应包含哪些方法时,尝试像下面这样做,
(1)将有关问题的描述(程序需要做什么)记录下来,并给所有的名词、动词和形容词加上标记
(2)在名词中找出可能的类。
(3)在动词中找出可能的方法。
(4)在形容词中找出可能的属性
(5)将找出的方法和属性分配给各个类.
有了面向对象模型的草图后,还需考虑类和对象之间的关系(如继承或协作)以及它们的职责。为进-步改进模型,可像下面这样做。
(1)记录(或设想)一系列用例,即使用程序的场景,并尽力确保这些用例涵盖了所有的功能,
(2)透彻而仔细地考虑每个场景,确保模型包含了所需的一切。如果有遗漏,就加上;如果有不太对的地方,就修改。不断地重复这个过程,直到对模型满意为止。
有了你认为行之有效的模型后,就可以着手编写程序了。你很可能需要修改模型或程序的某些部分,所幸这在Python中很容易,请不用担心。只管按这里说的去做就好。