Python 基础
面向对象
面向对象(Object Oriented)的英文缩写是OO,它是一种设计思想。我们经常听说的面向对象编程(Object Oriented Programming,即OOP)就是主要针对大型软件设计而提出的,它可以使软件设计更加灵活,并且能更好地进行代码复用。
面向对象中的对象(Object),通常是指客观世界中存在的对象,这个对象具有唯一性,对象之间个不相同,各有各的特点,每一个对象都有自己的运动规律和内部状态;对象和对象之间又是可以相互联系、相互作用的。
对象:是一个抽象概念,英文称为Object,表示任意存在的事物。世间万物皆对象,现实世间中随处可以的一种事物就是对象,对象是事物存在的实体,如一个人。
通常将对象划分为两个部分:静态部分和动态部分。静态部分被称为【属性】,任何对象都具备自身属性,这些属性不仅是客观存在的,而且是不能被忽视的。如人的性别。动态部分指的是对象的行为,即对象执行的动作,如人可以行走。
类和实例
面向对象最重要的概念就是类(Class)和实例(Instance),必须牢记类是抽象的模板,比如Student类,而实例是根据类创建出来的一个个具体的“对象”,每个对象都拥有相同的方法,但各自的数据可能不同。
代码示例
1 | class Student(object): |
- 通过
class关键字来定义类,类名通常要大写,后面的括号里面表示该类继承于哪个类,所有类最终继承于object类,这类似于Java - python允许对实例变量绑定任何数据,因此同一个类的不同实例拥有的变量名称可能会不同
- 特殊的
__init__方法可以把必须绑定给类实例的属性填写进去,它的第一个参数永远是self,表示创建的实例本身,在创建实例的时候,必须传入与init方法匹配的参数,self除外 - 实际上,Python中类中定义的函数的第一个参数都必须是
self,调用时不用传递该参数
对Student类使用实例代码:
1 | chen = Student('chen', 13) |
输出结果
1 | chen: 13 |
访问限制
- 如果要让内部属性不被外部访问,可以把属性的名称前加上两个下划线
__,在Python中,
实例的变量名如果以__开头,就变成了一个私有变量(private),只有内部可以访问,外部不能访问:1
2
3
4
5
6
7
8
9
10
11
12
13
14class Student(object):
def __init__(self, name, score):
self.__name = name
self.__score = score
def print_score(self):
print('%s: %s' % (self.__name, self.__score))
>>> bart = Student('Bart Simpson', 59)
>>> bart.__name
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
AttributeError: 'Student' object has no attribute '__name'
这样就确保了外部代码不能随意修改对象内部的状态,这样通过访问限制的保护,代码更加健壮。
但是如果外部代码要获取name和score怎么办?可以给Student类增加get_name和get_score这样的方法:
1 | class Student(object): |
如果又要允许外部代码修改score怎么办?可以再给Student类增加set_score方法:
1 | class Student(object): |
你也许会问,原先那种直接通过bart.score = 99也可以修改啊,为什么要定义一个方法大费周折?因为在方法中,可以对参数做检查,避免传入无效的参数:
1 | class Student(object): |
需要注意的是,在Python中,变量名类似xxx的,也就是以双下划线开头,并且以双下划线结尾的,是特殊变量,特殊变量是可以直接访问的,不是private变量,所以,不能用name、score这样的变量名。
以一个下划线开头的变量_name外部可以访问,但是最好把它视为私有变量,不要随便访问
python解释器对外把__name改成了_Student__name,因此可以通过_Student__name来访问私有的内部属性(强烈建议不要采用这种方式):
1 | >>> bart._Student__name |
我们在前面发现无法输出bart.__name,会报错Student类不含有此成员,但是bart.__name = 'New Name'语
句是不会报错的,随后再输出bart.__name也不会再报错了:
1 | >>> bart = Student('Bart Simpson', 59) |
这是一种错误的写法,这样设置的__name变量并不是类内部的__name变量,相当于给bart新增了一个变量
继承和多态
在OOP程序设计中,当我们定义一个class的时候,可以从某个现有的class继承,新的class称为子类(Subclass),而被继承的class称为基类、父类或超类(Base class、Super class)。
比如,我们已经编写了一个名为Animal的class,有一个run()方法可以直接打印:
1 | class Animal(object): |
当我们需要编写Dog和Cat类时,就可以直接从Animal类继承:
1 | class Dog(Animal): |
对于Dog来说,Animal就是它的父类,对于Animal来说,Dog就是它的子类。Cat和Dog类似。
继承有什么好处?最大的好处是子类获得了父类的全部功能。由于Animial实现了run()方法,因此,Dog和Cat作为它的子类,什么事也没干,就自动拥有了run()方法:
1 | dog = Dog() |
运行结果如下:
1 | Animal is running... |
当然,也可以对子类增加一些方法,比如Dog类:
1 | class Dog(Animal): |
继承的第二个好处需要我们对代码做一点改进。你看到了,无论是Dog还是Cat,它们run()的时候,显示的都是Animal is running...,符合逻辑的做法是分别显示Dog is running...和Cat is running...,因此,对Dog和Cat类改进如下:
1 | class Dog(Animal): |
再次运行,结果如下:
1 | Dog is running... |
当子类和父类都存在相同的run()方法时,我们说,子类的run()覆盖了父类的run(),在代码运行的时候,总是会调用子类的run()。这样,我们就获得了继承的另一个好处:多态。
要理解什么是多态,我们首先要对数据类型再作一点说明。当我们定义一个class的时候,我们实际上就定义了一种数据类型。我们定义的数据类型和Python自带的数据类型,比如str、list、dict没什么两样:
1 | a = list() # a是list类型 |
判断一个变量是否是某个类型可以用isinstance()判断:
1 | >>> isinstance(a, list) |
看来a、b、c确实对应着list、Animal、Dog这3种类型。
但是等等,试试:
1 | >>> isinstance(c, Animal) |
看来c不仅仅是Dog,c还是Animal!
不过仔细想想,这是有道理的,因为Dog是从Animal继承下来的,当我们创建了一个Dog的实例c时,我们认为c的数据类型是Dog没错,但c同时也是Animal也没错,Dog本来就是Animal的一种!
所以,在继承关系中,如果一个实例的数据类型是某个子类,那它的数据类型也可以被看做是父类。但是,反过来就不行:
1 | >>> b = Animal() |
Dog可以看成Animal,但Animal不可以看成Dog。
要理解多态的好处,我们还需要再编写一个函数,这个函数接受一个Animal类型的变量:
1 | def run_twice(animal): |
当我们传入Animal的实例时,run_twice()就打印出:
1 | >>> run_twice(Animal()) |
当我们传入Dog的实例时,run_twice()就打印出:
1 | >>> run_twice(Dog()) |
当我们传入Cat的实例时,run_twice()就打印出:
1 | >>> run_twice(Cat()) |
看上去没啥意思,但是仔细想想,现在,如果我们再定义一个Tortoise类型,也从Animal派生:
1 | class Tortoise(Animal): |
当我们调用run_twice()时,传入Tortoise的实例:
1 | >>> run_twice(Tortoise()) |
你会发现,新增一个Animal的子类,不必对run_twice()做任何修改,实际上,任何依赖Animal作为参数的函数或者方法都可以不加修改地正常运行,原因就在于多态。
多态的好处就是,当我们需要传入Dog、Cat、Tortoise……时,我们只需要接收Animal类型就可以了,因为Dog、Cat、Tortoise……都是Animal类型,然后,按照Animal类型进行操作即可。由于Animal类型有run()方法,因此,传入的任意类型,只要是Animal类或者子类,就会自动调用实际类型的run()方法,这就是多态的意思:
对于一个变量,我们只需要知道它是Animal类型,无需确切地知道它的子类型,就可以放心地调用run()方法,而具体调用的run()方法是作用在Animal、Dog、Cat还是Tortoise对象上,由运行时该对象的确切类型决定,这就是多态真正的威力:调用方只管调用,不管细节,而当我们新增一种Animal的子类时,只要确保run()方法编写正确,不用管原来的代码是如何调用的。这就是著名的“开闭”原则:
对扩展开放:允许新增Animal子类;
对修改封闭:不需要修改依赖Animal类型的run_twice()等函数。
继承还可以一级一级地继承下来,就好比从爷爷到爸爸、再到儿子这样的关系。而任何类,最终都可以追溯到根类object,这些继承关系看上去就像一颗倒着的树。比如如下的继承树:
1 | ┌───────────────┐ |
获取对象信息
使用type()来判断对象类型,它返回对应的Class类型。
基本类型可以使用type()来判断:
1 | >>> type(123) |
函数和类也可以使用type()来判断:
1 | >>> class Person: |
利用type来作为if的判断条件,当类型不是int,str等基本类型时,需要使用types模块中定义的常量:
1 | >>> import types |
使用isinstance()来判断对象是否是某种类型
isinstance()判断的是一个对象是否是该类型本身,或者位于该类型的父继承链上
能用type()判断的基本类型也可以用isinstance()判断:
1 | >>> isinstance('a', str) |
isinstance不仅可以判断是否是某种类型,还可以判断是否是某些类型的一种:
1 | >>> isinstance([1, 2, 3], (list, tuple)) |
使用dir()来获得一个对象的所有属性和方法
获取str的所有属性和方法:
1 | >>> dir('ABC') |
利用getattr()、setattr()以及hasattr(),我们可以直接操作一个对象,不过这几个函数是在不知道对象信息的时候使用的,在了解对象信息时没有必要使用:
1 | >>> class MyObject(object): |
对于getattr()函数,可以在传入的属性参数后面加一个默认的参数,这样在对象不含有这个属性时就会返回默认参数,而不是抛出异常了:
1 | # 获取属性'z',如果不存在,返回默认值404 |
getattr()函数还可以用于获取方法,把获取的方法赋值给一个变量,那个变量就指向这个方法,调用那个变量就相当于调用了这个方法:
1 | >>> getattr(obj, 'power') # 获取属性'power' |
实例属性和类属性
实例属性我们在之前就已经使用过了,我们也了解到在python中可以给一个类实例绑定任何属性,方法是在类方法中利用self或者直接通过实例来绑定。
要给类绑定一个属性的话,可以在类中直接定义它:
1 | class Student(object): |
也可以把类属性成为类的静态成员变量,这个属性是归类所有的,所有实例可以共享它。
需要注意的是,虽然name属性归类Student所有,但是类的所有实例都可以访问到,并且实例属性的优先级比类属性高,所以如果实例绑定了一个与类属性同名的实例属性时,优先调用的是实例属性:
1 | >>> class Student(object): |
异常处理
异常处理
捕捉异常可以使用try/except语句。
try/except语句用来检测try语句块中的错误,从而让except语句捕获异常信息并处理。
如果你不想在异常发生时结束你的程序,只需在try里捕获它。
语法:
以下为简单的try....except...else的语法:
1 | try: |
try的工作原理是,当开始一个try语句后,python就在当前程序的上下文中作标记,这样当异常出现时就可以回到这里,try子句先执行,接下来会发生什么依赖于执行时是否出现异常。
- 如果当
try后的语句执行时发生异常,python就跳回到try并执行第一个匹配该异常的except子句,异常处理完毕,控制流就通过整个try语句(除非在处理异常时又引发新的异常)。 - 如果在
try后的语句里发生了异常,却没有匹配的except子句,异常将被递交到上层的try,或者到程序的最上层(这样将结束程序,并打印缺省的出错信息)。 - 如果在
try子句执行时没有发生异常,python将执行else语句后的语句(如果有else的话),然后控制流通过整个try语句。
实例
下面是简单的例子,它打开一个文件,在该文件中的内容写入内容,且并未发生异常:
1 | #!/usr/bin/python |
以上程序输出结果:
1 | $ python test.py |
实例
下面是简单的例子,它打开一个文件,在该文件中的内容写入内容,但文件没有写入权限,发生了异常:
1 | #!/usr/bin/python |
在执行代码前为了测试方便,我们可以先去掉 testfile 文件的写权限,命令如下:
1 | chmod -w testfile |
再执行以上代码:
1 | $ python test.py |
使用except而不带任何异常类型
你可以不带任何异常类型使用except,如下实例:
1 | try: |
以上方式try-except语句捕获所有发生的异常。但这不是一个很好的方式,我们不能通过该程序识别出具体的异常信息。因为它捕获所有的异常。
使用except而带多种异常类型
你也可以使用相同的except语句来处理多个异常信息,如下所示:
1 | try: |
try-finally 语句
try-finally 语句无论是否发生异常都将执行最后的代码。
1 | try: |
实例
1 | #!/usr/bin/python |
如果打开的文件没有可写权限,输出如下所示:
1 | $ python test.py |
同样的例子也可以写成如下方式:
1 | #!/usr/bin/python |
当在try块中抛出一个异常,立即执行finally块代码。
finally块中的所有语句执行后,异常被再次触发,并执行except块代码。
参数的内容不同于异常。
异常的参数
一个异常可以带上参数,可作为输出的异常信息参数。
你可以通过except语句来捕获异常的参数,如下所示:
1 | try: |
变量接收的异常值通常包含在异常的语句中。在元组的表单中变量可以接收一个或者多个值。
元组通常包含错误字符串,错误数字,错误位置。
实例
以下为单个异常的实例:
1 | #!/usr/bin/python |
以上程序执行结果如下:
1 | $ python test.py |
触发异常
我们可以使用raise语句自己触发异常
raise语法格式如下:
1 | raise [Exception [, args [, traceback]]] |
语句中 Exception 是异常的类型(例如,NameError)参数标准异常中任一种,args 是自已提供的异常参数。
最后一个参数是可选的(在实践中很少使用),如果存在,是跟踪异常对象。
实例
一个异常可以是一个字符串,类或对象。 Python的内核提供的异常,大多数都是实例化的类,这是一个类的实例的参数。
定义一个异常非常简单,如下所示:
1 | def functionName( level ): |
注意:为了能够捕获异常,”except“语句必须有用相同的异常来抛出类对象或者字符串。
例如我们捕获以上异常,”except“语句如下所示:
1 | try: |
实例
1 | #!/usr/bin/python |
执行以上代码,输出结果为:
1 | $ python test.py |
用户自定义异常
通过创建一个新的异常类,程序可以命名它们自己的异常。异常应该是典型的继承自Exception类,通过直接或间接的方式。
以下为与RuntimeError相关的实例,实例中创建了一个类,基类为RuntimeError,用于在异常触发时输出更多的信息。
在try语句块中,用户自定义的异常后执行except块语句,变量 e 是用于创建Networkerror类的实例。
1 | class Networkerror(RuntimeError): |
在你定义以上类后,你可以触发该异常,如下所示:
1 | try: |