Python&数据结构 抽象数据类型 Python类机制和异常
Posted
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Python&数据结构 抽象数据类型 Python类机制和异常相关的知识,希望对你有一定的参考价值。
这篇是《数据结构与算法Python语言描述》的笔记,但是大头在Python类机制和面向对象编程的说明上面。我也不知道该放什么分类了。。总之之前也没怎么认真接触过基于类而不是独立函数的Python编程,借着本次机会仔细学习一下。
抽象数据类型
最开始的计算机语言,关注的都是如何更加有效率地计算,可以说其目的是计算层面的抽象。然而随着这个行业的不断发展,计算机不仅仅用于计算,开发也不仅只关注计算过程了,数据层面的抽象也变得同样重要。虽然计算机语言一开始就有对数据的抽象,但是那些都只是对一些最基本的数据类型而不包括我们想要用的,多种多样的数据。
数据类型:
程序处理的数据,通常是不同的类型的。只有事先约定好的不同类型的数据的存储方式,计算机才能正确理解逻辑上不同的数据类型。所有编程语言都会有一组内置的基本数据类型。另外在实际工作过程中,或早或晚总会碰到一些没法用现有数据类型解决的问题,这时就需要自定义一些数据类型来解决。像Python这样比较高级的语言的话,在基本类型的基础上还添加了一些额外的数据结构如tuple,list,dict(这些广义上来说也算是Python的数据类型)。
■ 抽象数据类型
以上基本数据类型都是比较simple,naive的结构,而且有一点很违和的是,以上数据结构都把数据暴露在外。如果一个人有了对某个变量的权限的话他就可以看到这个变量代表的数据结构中的所有数据。为了解决这个问题,必须要有一种数据类型,它可以让使用者只需要考虑如何使用这种类型的对象,而不需要(或者根本不能)去关注对象内部的实现方式以及数据的表示等等。这样的对象和类型从概念上来说就是抽象数据对象和抽象数据类型了。
抽象数据类型的基本想法是把数据定义为抽象的数据对象集合,只为他们定义可用的合法操作而不暴露内部实现的具体细节,不论是操作细节还是数据的存储细节。在这样的思想指导下,一般而言的抽象数据类型应该具有下列三种操作:
1. 构造操作(比如python类中的__init__方法)
2. 解析操作(getxx方法)
3. 变动操作(setxx方法)
看到这三种操作之后,根据这三个性质可以区分出数据类型的变动性。如果一个类型只有1和2两种操作那么就是不可变的类型,如果一个类型具备三种操作,那么就是一个可变的类型。在Python中,对象分成可变和不可变的,从抽象数据类型的角度来看就是看这个类型有没有变动操作的一个判断。
■ Python的类
Python中的类就是一种抽象数据类型的实现,定义好的一个类就像是一个系统内部类型,可以产生该类型的对象(或者也可以叫它实例),实例具有这个类所描述的行为。实际上,Python的内置类型也都可以看做是类的一种从而进行一些类似“类”的操作。
关于类如何定义,一些基本的方法看下基本教程就懂了。之前有接触过一点java,总体来说,python的类的定义方法和java是类似的,而且比java要简单一点(比如python中定义类和类中的方法时不必指出类的公用性有多大,比如是public,private还是其他什么标志)
下面讲些稍微高端一点的类定义的规范和方法
● 关于内部使用的属性和方法
在java里面,在类内部使用的方法和属性通常要加上修饰符private使得外部的调用者没办法直接访问这些方法和属性。Python中也有类似的机制,分成两种形式。一是把这种只提供给内部使用的属性(方法)的名字前面加上一个下划线以提示其私有的性质,这样的写法并不是语言规定而是人们约定俗成的,也就是说如果你想要通过实例直接访问一个下划线开头的属性或者属性方法也是可行的只不过不鼓励这么做。第二种形式是以两个下划线开头作为属性(方法)的名字,当然它不能同时以两个下划线结尾,这样就变成了魔法方法了。这种两个下划线的形式的属性是和java中的private一样的,如果从类的外部去访问这个属性的话会抛出AttributeError提示找不到相关属性。
● 关于类属独立属性
有时候,可以在类中的所有属性方法外面添加一些属性。这些严格来说都已经不算是类的属性了,因为他们和类的实例是完全不搭界的。对于这类“属性”几点想说:
1. 因为这些属性常常写在类的最上面,有时候可能会受到java的影响而下意识的以为这些属性是跟实例关联的,实则不然。就像函数参数的默认值一样,在函数被定义好的时候就被初始化好并保存在内存中的特定地址里不会随着调用函数次数的变化而变化一样,类属独立属性是在类被定义好的时候就被保存了起来,不会因为类被实例化了多少次而被初始化多少次。所以在比如类需要统计一共被实例化了多少次的场景中,可以在类中的所有方法外写一个count = 0然后在类的__init__方法中添加一句count+=1。这样每次实例化调用__init__的时候会让count加上1而不是初始化回0的状态。
2. 这种属性也不能理解成类的局部变量。在这个类的方法中我们不能直呼其名地调用这些属性,而是得像在类外面一样通过圆点的形式来调用相关属性。比如:
class Test(): count = 0 def __init__(self): count += 1 #这会报错 Counter.count += 1 #必须这么写
当然如果是类方法的话可以在方法中用cls.attribute的形式调用相关属性。下面也会有提到。
3. self.attribute是不能写在外面的!总是把self误认为是类对象,其实应该是调用时的实例对象。换句话说,在所有方法外面写self.attribute的话,self是什么东西解释器是不知道的。唯有在写方法中(which的第一个参数是self),在调用方法的时候解释器可以把调用方法的实例作为参数约束给self,这样self才会有意义。
● 关于静态方法
在java中,可以用提示符static来表明某个方法是独立于其他同一个类中其他方法的静态方法。所谓静态方法就是说要调用这个方法可以不必通过类的实例而直接通过类名来调用。在Python中,类本身也是一个对象,通过类名本身来调用一个方法看起来似乎合情合理,为了满足这种需求,Python的类中可以通过添加修饰符@staticmethod来使得一个方法变成类的静态方法。静态方法的参数中没有self并且也可以通过类的实例来调用。从某种意义上说,静态方法其实算是类里面定义的普通函数,是一个类的局部函数。
● 关于类方法
和静态方法类似的,类方法用@classmethod修饰符来表示。类方法和普通的属性方法一样,一般自带一个参数叫cls,在方法中代表调用这个类方法的类对象(通常是正在定义的这个类或者其父类或子类),然后在方法体中就可以用cls
来refer to这个类本身啦。比如书上有这样一个例子
class Counter(object): count = 0 def __init__(self): Counter.count += 1 @classmethod def get_count(cls): return cls.count x = Counter() print x.get_count() y = Counter() print y.get_count() ######结果是 1 2
这个例子说明了两个问题。一,对于类方法而言,其参数cls在调用时确实约束到了调用它的那个类对象上。二,对于属于类本身的独立属性,其并不根据实例的初始化而初始化。
● 类中的魔法方法
关于魔法方法的说明可以参考魔法方法那篇笔记,这里不多提。想说的是一个小技巧,比如在一个类中要定义一个比大小的魔法方法,而比较的类的一个属性的时候,下意识的总会写
def __lt__(self,another): if self._attribute < another._attribute: return True else: return False
但是实际上可以这么写更简洁,而且因为拿来作比较外部实例的不一定也有_attribute这个属性,最好还能加上一个异常排除的过程:
def __lt__(self,another): try: return self._attribute < another._attribute except AttributeError as e: raise e
● 关于__init__和构造方法
如果一个类中定义了__init__方法,那么在创建这个类的实例时解释器后自动调用这个方法初始化这个对象。之前一直认为python类中的__init__方法就是java中的构造方法。其实这两者还是有些微妙的区别的。比如java的一个类中不能没有构造方法(好像是这样吧= =),但是python的一个类中可以没有__init__方法,没有__init__方法时所有基于这个类创建实例的动作都会创建出一个空实例,此时解释器实际上调用的是object()方法,而object是Python中所有类的父类。
■ 类的使用和对象(实例)
某个程序基于类C创建了实例o然后用o以调用属性方法的形式调用了方法m,这整个过程中,python解释器是这样工作的。创建一个空方法对象,约束对象o和方法m到这个方法对象上去。正如大家所知,类中的属性方法在定义的时候通常第一个参数是self,这是因为需要把一个实例作为一个参数传递到方法中去这就是为什么方法对象约束的不仅仅是m还有o的原因。当o被作为第一个参数传递给m之后,m里面的self指的就是o这个实例了。
对于静态方法,在定义的时候就没有要写self,所以自然就没必要约束调用它的实例,这也是为什么它可以用类名直接调用的原因了。
其实从上面的说明中已经不难看出,一般属性方法调用时的o.m()其实等价于C.m(o)
● 关于增删属性
python的类的实例的属性都维护在实例的__dict__这个隐藏属性中。因为它本身就是一个字典,所有我们可以动态地对某个实例的属性做出增删操作。操作具体不用通过__dict__这个变量,而是直接通过o.new_attribute = "new_value"的形式。当o已经存在new_attribute这个名称的属性的时候,这个属性的内容会被新的赋值语句覆盖掉。而删除操作可以通过del(o.attribute)来实现。
● 在一个属性方法中调用另一个属性方法
同一个类中如果出现这种情况,就可以通过self.another_method()的形式来调用实现,而不是直接another_method()。另外,如果调用这个语句的不是本类的实例而是一个子类的实例,然后这个子类还没有重写这个语句所在的方法但是却重写了another_method这个方法的话,这就导致了一个问题(我靠我都晕了,实例看下):我应该执行哪个类中的another_method,是父类还是子类的。
class Parent(): def f(self): self.g() def g(self): print "this is method g in Parent" class Child(Parent): def g(self): print "this is method g in Child" c = Child() c.f() ##结果是 #this is method g in Child
这种现象说好听一点叫动态约束,因为c在调用方法f的时候传递给f的参数self的是c实例本身,而通过self调用的g自然是通过实例c调用的g,也就是类Child中定义的g方法了。
■ 类的继承
上面这个例子其实已经提到了类的继承了。python中类的继承机制和java也都差不多,不同的是python中支持多类继承一类也支持一类继承多类,后者在java中好像是不行的吧(记不太清了。。)。还是讲一下在实际运用过程中可能会碰到的一些问题
● issubclass和isinstance
isinstance函数用来检查某个对象是不是某个类的实例,issubclass用来检查一个类是不是另一个类的子类。对于多层继承,比如class A(),class B(A), class C(B)的情况,issubclass(C,A)返回True。同时子类的实例也被默认为也是父类的实例,所以isinstance(C(),A)和isinstance(C(),B)也都是返回True的。
● 在子类中调用父类的初始化方法 super函数
在java中,似乎是用super()指代父类的构造方法的。python中也有super方法不过用法不太一样:在python中如果想要在子类中调用父类的初始化构造方法有两种写法,分别是
Parent.__init__(self,...)和super().__init__(...)。前一种很好理解,相当于是通过父类对象来调用其初始化方法,把子类初始化时的那个实例作为父类初始化方法的参数来执行。至于第二个,在python中的super函数其实是返回一个父类的实例,通过实例来调用自然就不用self参数了。就调用父类的初始化方法而言,还是建议用前面一种,因为毕竟用一个创建后立刻销毁的对象作为父类初始化方法的self参数总感觉有点不太对劲。而对于父类中的其他方法,如果想要调用那么可以super().method(...)这样是很自然的。(这部分存疑。。书上是这么说的但是实验一下通不过,报错说在python2中super()必须要有一个参数,而书上用的是python3)当然也可以通过Parent.method(self,...)的形式来调用。
super函数还有另外一种写法,就是super(Class,object).method(...),这个语句可以出现在程序的任何地方而不一定要是在类的属性方法定义中。这个语句的意思是从Class类的父类开始逐级向上搜索前辈类,在类中找到属性方法method之后把object这个Class的实例作为method的self参数传递过去,然后执行method。比如上面那个动态约束的例子,在Child类中重写一下f方法:
def f(self): super(Child,self).g() #这样运行的结果就变成了 #this is method g in Parent
在这个f方法中,通过super函数强行把g方法关联到父类中的g方法而不是动态约束到子类中的g方法。
● 回溯父类查找方法
如果从一个子类的一个实例出发去调用一个属性的话,python解释器需要确定应该调用哪个属性,即这个属性在哪里定义。查找过程从实例所属的类开始,如果在本类中没有找到相应的属性定义的话就沿着继承关系依次向父类寻找。在某个前辈类中找到了相关属性的话就把这个属性拿过来用。如果找遍了所有前辈类都没有找到想要的属性的话那么就爆出AttributeError异常
python异常
Python中的所有异常都是作为一种类而存在的,Python系统给出了很多系统自带的常用的异常。一般如果用户需要自定义异常的话可以选择某一个异常作为父类来衍生出一个自定义异常。所有异常的总父类是Exception类。
所有的异常类在初始化的时候都可以接受一个字符串作为错误信息,比如在自定义一个异常类的时候可以:
class MyException(Exception): def __init__(self): Exception.__init__(self,"My Exception is Raised") raise MyException #结果 # Traceback (most recent call last): # File "D:/PycharmProjects/TestProject/test.py", line 8, in <module> # raise MyException # __main__.MyException: My Excpetion is Raised
如果对错误判断没有太多要求的话可以方便地raise Exception("some error message")来抛出一个有提示文字的错误。但是在更多情况下,我们可能会根据具体的业务要求来对代码运行做一些逻辑判断,比如某些情况下可以抛出我们的自定义异常,然后在调用这些代码的时候except我们的自定义异常,这样就可以做到符合业务逻辑的异常捕获和处理了。
● 异常的传播和捕获
如果异常发生在一个try语句块里面,那么解释器将按照顺序先检查这个try语句块相应的except语句块有没有为这个异常准备好处理器,如果没有的话那么就把这个异常交给更外层的try语句(如果有的话),这样逐层传播异常,如一直传播到这个异常所在的函数的最外层也没能找到相关的处理器的话那么这个函数就将异常中止运行,程序也因此整个中止。
如果在搜索过程中找到了相关的异常处理器的话,那么把执行点从异常语句的地方跳到处理器头部(也就是说跳过了try语句块中从异常位置到最后部分的所有代码)。在处理器的代码中还可能遇到新的异常,也可以在处理器代码中抛出异常。
● 实例
书中提到了实现一个大学人事管理系统框架的实例,当然是一个很简朴的东西,逻辑也不复杂,不过其中有些面向对象编程的常识性的知识和技巧值得一看。我决定照样子把这段代码全部都抄过来,在有价值的 地方注释一下。
其基本思路是这样的:
实现一个公共人员的类,包含一些人的基本信息属性
创建学生和教职人员两个类,分别继承公共人员类。再根据学生和教职人员的属性不同来实现不同的类。
首先是两个本例中可能会用到的异常类型:
class PersonTypeError(TypeError): pass class PersonValueError(ValueError): pass
然后是公共人员类:
class Person(object): #实现一个公共的人员类 _num = 0 #用来记录创建过的总人数 def __init__(self,name,sex,birthday,ident): if not (isinstance(name,str)) and sex in ("女","男"): raise PersonValueError try: birth = datetime.date(*birthday) except: raise PersonTypeError("Wrong Date:{0}".format(birthday)) self._name = name self._sex = sex self._id = ident self._birthday = birth Person._num += 1 def id(self): return self._id def name(self): return self._name def sex(self): return self._sex def birthday(self): return self._birthday def age(self): return datetime.date.today() - self._birthday.year def set_name(self,new_name): if not isinstance(new_name,str): raise PersonValueError("Wrong New Name:{0}".format(new_name)) self._name = new_name #其他的set方法不一一列举了,反正都是差不多的 def __lt__(self,other): #定义一个比较魔法方法,当两个人员对象比较时默认比较他们的id号的大小 if not isinstance(other,Person): raise PersonTypeError(other) return self._id < other._id @classmethod def num(cls): #定义一个获取目前为止总的注册人数的方法 return cls._num def __str__(self): #定义当print此类对象时的操作 return " ".join((self._id,self._name,self._sex,self._birthday))
然后是学生类,学生类中需要有学号这个id,但是学号应该有一套生成的规则,这个规则应该放在Student这个类中维护,所以这个类中应该额外加一个学号生成的方法:
class Student(Person): """学生类主要考虑增加院系,入学年份以及课程情况的三种信息 """ _id_num = 0 @classmethod def _id_gen(cls): cls._id_num += 1 year = datetime.date.today().year return "1{:04}{:05}".format(year, cls._id_num) # 生成一个类似于 1201300001的学号,1代表学生,2013是入学年份,00001是学生编号 def __init__(self, name, sex, birthday, depart): Person.__init__(self, name, sex, birthday, self._id_gen()) self._department = depart self._enroll_year = datetime.date.today().year self._courses = {} def set_course(self, course): # 模拟选课,选课刚开始还没有成绩 self._courses[course] = None def set_score(self, course, score): #模拟给分 if course not in self._courses: raise PersonValueError("The Course is not Selected:{0}".format(course)) elif not isinstance(score, float) or score > 100.0 or score < 0.0: raise PersonValueError("Score for Course {0} is Invalid".format(course)) self._courses[course] = score def scores(self): #获得一个学生全部课程分数情况 return [(course,self._courses[course]) for course in self._courses]
最后是实现教职人员的类,教职人员相比于公共人员类要增加院系,员工号,工资等。操作和学生类是类似的就不再重复了。只是在它的set_salary方法中我看到了一个以前没想到的表达。。
if type(amount) is not int: xxxxx #这句判断的意图在于判断所给参数是不是一个合法的int类型,之前没想过直接用is关键字+int类型名就能够进行判断了。。我之前都是这样做的: if type(amount) is not type(1): xxxxx
以上是关于Python&数据结构 抽象数据类型 Python类机制和异常的主要内容,如果未能解决你的问题,请参考以下文章