第七章:更加抽象
1:对象的重要优点:多态(Polymorphism)、封装(Encapsulation)、继承(Inheritance)
1)多态:多态意味着就算不知道变量所引用的对象类型是什么,还是能对它进行操作,而它也会根据对象(或类)类型的不同而表现出不同的行为。
(1) 多态和方法——绑定到对象特性上面的函数称为方法。标准库random中包含choice函数,可以从序列中随机选出元素:
>>> from random import choice >>> x=choice(['Hello,world!', [1,2,'e','e',4]]) #x=[1, 2, 'e', 'e', 4] >>> x.count('e') 2
很多函数和运算符都是多态的,唯一能毁掉多态的就是使用函数显式的检查类型,如:type、isinstance以及issubclass 函数等
2):封装:封装是对全局作用域中其他区域隐藏多余信息的原则。听起来像多态——使用对象而不知道其内部细节,两者概念类似,因为它们都是抽象的原则——它们都会帮助处理程序组件而不用过多关心多余细节
但封装不等同于多态。多态可以让用户对于不知道是什么类(或对象类型)的对象进行方法调用,而封装是可以不用关心对象是如何构建的而直接进行使用。
3)继承——继承是另一个懒惰(褒义)的行为。
2:类和类型
1)类:python中,习惯使用单数名词来描述对象的类,如Bird和Lark
2)创建类:新式类的语法中,需要在模块或脚本开始的地方放置语句 __metaclass__ = type
__metaclass__ = type #确定使用新式类 #class语句会在函数定义的地方创建自己的命名空间 class Person: #self是对于对象自身的引用 def setName(self,name): self.name = name def getName(self): return self.name def greet(self): print("Hello,world! I'm %s." % self.name) #samples >>> foo = Person() >>> bar = Person() >>> foo.setName('Luke Skywalker') >>> bar.setName('Anakin Skywalker') >>> foo.greet() Hello,world! I'm Luke Skywalker. >>> bar.greet() Hello,world! I'm Anakin Skywalker.
在调用foo的setName和greet函数时,foo自动将自己作为第一个参数传入函数中——因此形象的命名为self(习惯上叫做self,可以命名为其他)
如果知道foo是Person的实例的话,还可以把foo.greet()看做Person.greet(foo)方便的简写
3)特性、函数、和方法
self 参数事实上正是方法和函数的区别。方法(更专业一点可称为绑定方法)将它们的第一个参数绑定到所属的实例上,因为这个参数可以不必提供。所以可以讲特性绑定到一个普通函数上,这样就不会有特殊的self参数了
>>> class Class: def method(self): #print(self) => <__main__.Class object at 0x02D7AB50> print('I have a self!') >>> def function(): print('I don\'t have a self!') >>> instance = Class() >>> instance.method() I have a self! >>> instance.method = function >>> instance.method() I don't have a self!
注意,self 参数并不取决于调用方法的方式,目前使用的是实例调用方法,可以随意使用引用同一个方法的其他变量:
>>> class Bird: song = 'Squaawk!' def sing(self): print(self.song) >>> bird = Bird() >>> bird.sing() Squaawk! >>> birdsong=bird.sing >>> birdsong() Squaawk!
尽管最后一个方法调用看起来与函数调用十分相似,但是变量birdsong引用绑定方法bird.sing上,也就意味着这还是对self参数的访问(意即它仍旧绑定到类的相同实例上)。
提示:在第九章将会介绍类是如何调用超类的方法的(具体来说就是超类的构造器)。这些方法直接通过类调用,它们没有绑定自己的self参数到任何东西上,所以叫做非绑定方法
再论私有化:有些人(如SmallTalk之父,SmallTalk的对象特性只允许由同一个对象的方法访问)觉得这样破坏了封装的原则,他们认为对象的状态对于外部应该是完全隐藏(不可访问)的。
使用私有(private)特性,这是对象外部无法访问,但getName和setName等访问器(accessor)能够访问的特性——属性是访问器的好选择
Python不直接支持私有方式,可以用一些小技巧达到私有特性的效果:为了让方法或特性变为私有(从外部无法访问),只要在它的名字前面加上双下划线即可
>>> class Secretive: def __inaccessible(self): print("Bet you can't see me...") >>> class Secretive: def __inaccessible(self): print("Bet you can't see me...") def accessible(self): print("The secret message is:") self.__inaccessible() >>> s = Secretive() >>> s.__inaccessible() Traceback (most recent call last): File "<pyshell#124>", line 1, in <module> s.__inaccessible() AttributeError: 'Secretive' object has no attribute '__inaccessible' >>> s.accessible() The secret message is: Bet you can't see me... #在类的内部定义中,双下划线开始的名字被“翻译”成前面加下划线和类名的形式 >>> Secretive._Secretive__inaccessible <function Secretive.__inaccessible at 0x02D7EA50> >>> s._Secretive__inaccessible <bound method Secretive.__inaccessible of <__main__.Secretive object at 0x02D7AD90>> #所以实际上还是能在类外访问这些私有方法的: >>> s._Secretive__inaccessible() Bet you can't see me...
如果不需要使用这种方法但是又想让其他对象不要访问内部数据,那么可以使用单下划线。例如:前面有下划线的名字都不会被带星号的imports语句(from module import *)导入
4)类的命名空间:定义类时,所有位于class语句中的代码都在特殊的命名空间中执行——类命名空间(class namespace)
5)指定超类
>>> class Filter: def init(self): self.blocked = [] def filter(self,sequence): return [x for x in sequence if x not in self.blocked] >>> class SPAMFilter(Filter): # SPAMFilter是Filter的子类 def init(self): #重写Filter超类中的init方法 self.blocked = ['SPAM'] >>> #Filter是个用于过滤序列的通用类,事实上它不能过滤任何东西 >>> f = Filter() >>> f.init() >>> f.filter([1,2,3]) [1, 2, 3] >>> #Filter类的作用是它可以用作其他类的基类(超类),比如SPAMFilter类,可将序列中的“SPAM过滤除去” >>> s = SPAMFilter() >>> s.init() >>> s.filter(['SPAM','SPAM','SPAM','SPAM','eggs','bacon','SPAM']) ['eggs', 'bacon']
6)调查继承:如果想要查看一个类是否是另一个的子类,可以使用内建的issubclass函数
>>> issubclass(SPAMFilter,Filter)
True
如果想要知道已知类的基类,可以直接使用它的特殊特性:__bases__
>>> SPAMFilter.__bases__ (<class '__main__.Filter'>,)
同样还能用isinstance 方法检查一个对象是否是一个类的实例:
>>> s = SPAMFilter() >>> isinstance(s,SPAMFilter) True
注意:使用isinstance并不是个好习惯,使用多态会更好一些
如果想要知道一个对象属于哪个类,可以使用__class__特性:
>>> s.__class__ <class '__main__.SPAMFilter'>
7)多个超类:
class Calculator: def calculate(self,expression): self.value = eval(expression) class Talker: def talk(self): print('Hi,my value is ', self.value) class TalkingCalculator(Calculator, Talker): pass #sample >>> tc = TalkingCalculator() >>> tc.calculate('1+2*3') >>> tc.talk() Hi,my value is 7
子类(TalkingCalculator)自己不做任何事,它从自己的超类继承所有的行为。这种行为称为多重继承(multiple inheritance)。使用多重继承时,如果一个方法从多个超类继承(也就是说有两个相同名字的不同方法),那么必须注意一下超类的顺序(在class语句中):先继承的类中的方法会重写后继承的类中的方法。所以如果前例中的Calculator类也有一个talk方法,那么它就会重写Talker的talk方法(使其不可访问)。如果把它们的顺序掉过来,像这样:
class TalkingCalculator(Talker,Calculator):pass 就会让Talker类中的talk方法可用了。
如果超类们共享一个超类,那么在查找给定方法或者特性时访问超类的顺序称为MRO(Method Resolution Order,方法判定顺序)
8)接口和内省
“接口”的概念和多态有关,在处理多态对象时,只要关心它的接口(或称“协议”)即可——也就是公开的方法和特性。在Python中,不用显示地指定对象必须包含哪些方法才能作为参数接收。不用(像在java中一样)显式地编写接口,可以在使用对象的时候假定它可以实现你所要求的行为。如果它不能实现的话,程序就会失败。
可以检查所需方法是否已经存在:
>>> hasattr(tc,'talk') True >>> hasattr(tc,'fnord') False
还可以检查talk特性是否可调用:
>>> callable(getattr(tc,'talk',None)) True >>> callable(getattr(tc,'fnord',None)) False
getatrr函数允许提供默认值,以便在特性不存在时使用,然后对返回的对象使用callable函数。与getattr相对应的函数是setattr,可以用来设置对象的特性:
>>> setattr(tc,'name','Mr.Gumby') >>> tc.name 'Mr.Gumby'
3:一些关于面向对象设计的思考
1)将属于一类的对象放在一起。如果一个函数操纵一个全局变量,那么两者最好都在类内作为特性和方法出现.
2)不要让对象过于亲密。方法应该只关心自己实例的特性,让其他实例管理自己的状态
3)要小心继承,尤其是多重继承。继承机制有时很有用,但也会在某些情况下让事情变得过于复杂。多重继承难以正确使用,更难调试
4)简单就好。让你的方法小巧。一般来说,多数方法都应能在30秒内被读完(以及理解)
当考虑需要什么类以及类要有什么方法时,应该尝试下面的方法:
1)写下问题的描述(程序要做什么?),把所有名词、动词和形容词加下划线
2)对于所有名词,用作可能的类
3)对于所有动词,用作可能的方法
4)对于所有形容词,用作可能的特性
5)把所有方法和特性分配到类
现在已经有了面向对象模型的草图了。还可以考虑类和对象之间的关系(比如继承或协作)以及它们的作用,可以用以下步骤精炼模型:
1)写下(或者想象)一系列的使用实例——也就是程序应用时的场景,试着包括所有的功能
2)一步步考虑每个使用实例,保证模型包括所有需要的对象。如果有遗漏的话就添加进来。如果某处不太正确则改正。继续,直到满意为止。
当认为已经有了可以应用的模型时,就可以开工了。
4:小结
对象:对象包括特性和方法。特性只是作为对象的一部分的变量,方法则是存储在对象内的函数。(绑定)方法和其他函数的区别在于方法总是将对象作为自己的第一个参数,这个参数一般称为self
类:类代表对象的集合(或一类对象),每个对象(实例)都有一个类。类的主要任务是定义它的实例会用到的方法
多态:多态是实现将不同类型和类的对象进行同样对待的特性——不需要指定对象属于哪个类就能调用方法
封装:对象可以讲它们内部状态隐藏(或封装)起来。在一些语言中,这意味着对象的状态(特性)只对自己的方法可用。在Python中,所有的特性都是公开可用的
继承:一个类可以是一个或者多个类的子类。子类从超类继承所有方法。
接口和内省:程序员可以靠多态调用自己想要的方法。不过如果想哟知道对象到底有什么方法和特性,可以用getattr等函数
面向对象设计
第八章:异常
1:什么是异常——Python用异常对象(exception object)来表示异常情况。遇到错误后,会引发异常。如果异常对象并未被处理或捕捉,程序就会用所谓的回溯(Traceback,一种错误信息)终止执行。事实上,每个异常都是一些类的实例,这些实例可以被引发,并且可以用很多种方法进行捕捉,使得程序可以捉住错误并且对其进行处理,而不是让整个程序失败
2:按自己的方式出错——如何引发异常,甚至创建自己的异常类型
1)raise 语句:为了引发异常,可以使用一个类(应该是Exception的子类)或者实例参数调用raise语句。使用类时,程序会自动创建实例。内建的异常有很多,都在exceptions模块中,可以用dir函数列表模块的内容
2)自定义异常类:class SomeCustomException(Exception):pass
3:捕捉异常:异常最有意思的地方就是可以处理它们(通常叫诱捕或捕捉异常)。这个功能使用try/except实现
try: x=input('Enter the first number: ') y=input('Enter the second number: ') print(int(x)/int(y)) except ZeroDivisionError as err : print('The second number can\'t be zero!')
注意:如果没有捕捉到异常,它会被“传播”到调用的函数中。如果那里依然没有捕获,这写异常就会“浮”到程序的最顶层。也就是说你可以捕捉到在其他人的函数中所引发的异常
如果捕捉到了异常,但是又想重新引发它(也就是说要传递异常),那么可以调用不带参数的raise(还能捕捉到异常时显式地提高具体异常)。举例来说,一个能“屏蔽”ZeroDivisionError(除0错误)的计算器类。如果整个行为被激活,那么计算器就会打印错误信息,而不是让异常传播。如果在与用户进行交互的过程中使用,就很有用了,但是如果是在程序内部使用,引发异常会更好。
>>> class MuffledCalculator: muffled = False def calc(self,expr): try: return eval(expr) except ZeroDivisionError: if self.muffled: print('Division by zero is illegal') else: raise >>> calculator = MuffledCalculator() >>> calculator.calc('10/2') 5.0 #示例:分别打开和关闭了屏蔽 >>> calculator.calc('10/0') Traceback (most recent call last): File "<pyshell#154>", line 1, in <module> calculator.calc('10/0') File "<pyshell#151>", line 5, in calc return eval(expr) File "<string>", line 1, in <module> ZeroDivisionError: division by zero >>> calculator.muffled = True >>> calculator.calc('10/0') Division by zero is illegal
注意:如果除零行为发生而屏蔽机制被打开,那么calc方法会(隐式地)返回None。换句话说,如果打开了屏蔽机制,那么就不应该依赖返回值
4:不止一个except子句
try: x=input('Enter the first number: ') y=input('Enter the second number: ') print(x/y) except ZeroDivisionError : print('The second number can\'t be zero!') except TypeError: print('That wasn\'t a number,was it?')
应该注意到,异常处理不会搞乱原来的代码,而增加一堆if语句检查可能的错误情况会让代码相当难读
5:用一个块捕捉两个异常:
try: x=input('Enter the first number: ') y=input('Enter the second number: ') print(x/y) except (ZeroDivisionError,TypeError,NameError): #注意不能没有括号,因为是元组 print('......')
6:捕捉对象:
如果希望在except子句中访问异常对象本身,可以使用两个参数(注意,就算要捕捉多个异常,也只需向except子句提供一个参数——一个元组)。
try: x=input('Enter the first number: ') y=input('Enter the second number: ') print(x/y) except (ZeroDivisionError,TypeError) as err: print(err)
7:真正的全捕捉
就算程序处理了好几种类型的异常,那有些异常还是会从眼皮底下溜走。比如上面的除法程序,在提示符下直接回车,不输入任何东西,会引发异常,这个异常逃过了try/except语句的检查——很正常。如果想捕捉所有异常,则在except子句中忽略所有的异常类:
try: x=input('Enter the first number: ') y=input('Enter the second number: ') print(x/y) except : print('Something wrong happend')
注意:这样捕捉所有异常是危险的,因为它会隐藏所有程序员未想到并且未做好准备处理的错误。使用except Exception,e会更好,或者对异常对象e进行一些检查
8:万事大吉
有些情况,一些坏事发生时执行一段代码是很有用的,可以像对条件和循环语句那样,给try/except语句加个else子句:
while True: try: x=input('Enter the first number: ') y=input('Enter the second number: ') print(int(x)/int(y)) except Exception as e :
print(e) print('Invalid input,please try again') else: break
9:最后——Finally子句,它可以用来在可能的异常后进行清理。它和try子句联合使用
10:异常和函数
异常和函数能自然地一起工作。如果异常在函数内引发而不被处理,它就会传播至(浮到)函数调用的地方。如果那里也没有处理异常,它就会继续传播,一直到达主程序(全局作用域)。如果还没有异常处理程序,程序就会带着堆栈跟踪中止
11:异常之禅
有时,条件语句可以实现和异常处理同样的功能,但是条件语句可能在自然性和可读性上差些。而从另一方面来看,某些程序中使用if/else实现会比使用try/except要好。
假设有个字典,我们希望打印出存储在特定的键下面的值。如果该键不存在,则说明也不做,代码可能这样:
def describePerson(person): print('Description of', person['name']) print('Age', person['age']) if 'occupation' in person: print('Occupation:', persion['occupation'])
代码很直观,但效率不高(此处效率其实微乎其微)。另外一种方案:
def describePerson(person): print('Description of', person['name']) print('Age', person['age']) try: print('Occupation: ' + person['occupation']) except KeyError:pass
这个程序假定‘occupation’键存在。如果的确存在,取出它的值打印即可——不用额外检查它是否真的存在。如果不存在,则会引发KeyError异常,而被except子句捕捉到
try/except 语句在Python中的表现可以用这样一句话来解释:“请求宽恕易于请求许可”
小结
异常对象——异常情况(如发生错误)可以用异常对象表示。它们可以用几种方法处理,忽略的话,程序就会中止
警告——类似异常,但是(一般来说)仅仅打印错误信息
引发异常——可以使用raise语句引发异常。它接受异常类或异常实例作为参数。还能提供两个参数(异常和错误信息)。如果在except子句中就不要使用参数调用raise,它会“重新引发”该字句捕捉到的异常
自定义异常类——用继承Exception类的方法可以创建自己的异常类
捕捉异常——try/except 子句捕捉异常。如果except子句中部特别指定异常类,那么所有的异常都会被捕捉。异常可以放在元组中以实现多个异常的指定。如果给exceptexcept提供两个参数,第2个参数就会绑定到异常对象上。同样,try/except还可以包含多个except子句
else子句——如果try块中没有引发异常,else子句就会被执行
finally——如果需要确保某些代码不管是否有异常引发都要执行(如清理代码),可以放在finally子句中
异常和函数——在函数中引发异常时,它就会被传播到函数调用的地方(对于方法也是一样)