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: |