python算法类型_数据结构与算法之8——抽象数据类型与python类

就算你是特别聪明,也要学习,从头学起!——(俄国)屠格涅夫

本篇文章要说的主要是数据结构与算法和python中关于类(Class)以及异常(Error)的一些基础,虽然很简单,但是必须非常重视。只有在牢牢掌握了这些基础的前提下,我们才能学得更快,无论是找工作还是以后在工作岗位上,扎实的基础都会给我们带来事半功倍的效益。千里之行始于足下,我们在各行各业,无论涉及什么领域,都不要忘了基础的重要性。

开始正题!

抽象数据类型

按照抽象的思维,设计者在考虑一个程序部件时,首先应该给出一个清晰的边界,通过一套接口描述说明这一程序部分可用的功能,但并不限制功能的实现方法。从使用者的角度来看,一个程序部件实现了一种功能,如果适合实际需要,就可以通过其接口使用它,并不需要知道实现的具体细节。就像python函数一样,其也是一种功能部件,其头部定义了它的接口,描述了函数的名字及其对参数的要求。使用者只需要考虑函数的功能是否满足需要,保证调用式符合函数头部的需求,并不需要知道函数实现的任何细节。

抽象数据类型的基本思想是把数据定义为抽象的对数集合,只为它们定义可用的合法操作,并不暴露其内部实现的具体细节,不论是其数据的表示细节还是操作的实现细节。比如python里的str是一个典型例子,字符串对象有一种内部表示方式(无须对外宣布),人们用python编程序时并不依赖于实际的表示;str提供了一组操作供编程使用,每个操作具有明确的抽象意义,不依赖于内部的具体实现技术。

作为数据类型,特别是比较复杂的数据类型,有一个很重要的性质称为变动性,表示该类型的对象在创建之后是否允许变化。一旦创建之后不会变化的称为不变数据类型,这种类型在程序里只能构建新对象或者取得已有对象的特性,不能修改。反之,可变数据类型的对象不发生改变,但是内容可变,下面经常将不变数据类型和可变数据类型简称为不变类型和可变类型。在python中的number、str、tuple和frozenset是不变数据类型,而list、set和dict是可变数据类型。

说到可变数据类型和不变数据类型,就顺便说一说python中深拷贝和浅拷贝问题吧

作为‘面向对象’的语言,python的一切变量都是对象,变量在存储时,采用了引用语义的方式,存储的是在这个变量的内存地址,而不是变量本身,这点和值语义不一样的地方是,值语义的语言在赋值时,存储的是变量本身。

比如在值语义中,a=1;b=a,那么b存储了a的值1,b和a两个变量就不会有联系了。在引用语义中,a=1;b=a,a变量存储的是1的内存地址,a赋值b的时候,b得到的也是a的内存地址,那么a和b将指向相同的内存地址。

python是引用语义机制,因此赋值操作不会开辟新的内存空间,它只是复制了源对象的引用罢了。引用语义:两个变量赋值时,只是指向同一个内存引用

搞懂了python的赋值机制,接下来谈一谈浅拷贝问题。拷贝有三种操作方式:切片操作,工厂函数以及copy函数。

完全切片操作:

list1=[1,2,3,4,5]

list2=list1[:]

print(list1,id(list1))

print(list2,id(list2))

[1, 2, 3, 4, 5] 1886748369416

[1, 2,3,4,5] 1886748369480

list1[1]=[1,2]

print(list1,id(list1))

print(list2,id(list2))

[1, [1, 2], 3, 4, 5] 1886748369416

[1, 2,3,4,5] 1886748369480

从上面可以分析出:1.列表在改变自身元素时,内容发生改变,但不会改变内存地址;2.切片操作会另辟一个新的内存空间给变量list2,list1和list2之间不再有联系,不会相互影响。

工厂函数:

list1=[1,2,3,4,5]

list2=list(list1)

print(list1,id(list1))

print(list2,id(list2))

[1, 2, 3, 4, 5] 3113769918984

[1, 2, 3, 4, 5] 3113770401416

list1和list2内容和类型一样,但内存地址不同,表示list2新开辟一个内存空间存储list1的复制,后面无论list1和list2如何操作都不会相互影响。

copy函数:

import copy

list1=[1,2,3,4,[5,6]]

list2=list1

list3=copy.copy(list1) #浅拷贝 list3=list1.copy() (python自建函数)

list4=copy.deepcopy(list1) #深拷贝

list1.append(8)

list1[4].append(7)

print(list2);print(list3);print(list4)

[1, 2, 3, 4, [5, 6, 7], 8]

[1, 2, 3, 4, [5, 6, 7]]

[1, 2, 3, 4, [5, 6]]将list1赋值给list2,即list2和list1引用同一个内存地址,因此该内存地址内容发生改变,则两个变量一起改变。

list3是对list1的浅拷贝,即拷贝了list1这个对象(两个list内存地址不同,但是list1和list3中每个元素的内存地址相同),因此在list1外层发生改变时,list3并不会改变,但是当list1中的元素(嵌套列表)发生改变时,由于列表是可变对象,内容改变内存地址不变,list1和list3的元素共享地址,因此list3中嵌套列表元素同样发生改变。

深拷贝是另辟一个新的内存空间,拷贝了整个list1和其中元素。因此无论list1中发生何种改变,都不会影响到list3。

python类

执行一个类定义将创建一个类对象,这种对象主要支持两种操作:属性访问和实例化(创建这个类的实例对象),类的属性分为数据属性和函数属性,在调用属性时的操作为classname.att or classname.method()。

python基本类的创建这里就不多说啦,基础不够的朋友去看看廖雪峰官方网站的python教程噢。这里总结一些比较不常见的但仍然很重要的知识点。

类里的修饰符def行前加修饰符@staticmethod,静态方法实际就是普通函数,只是由于某种原因需要定义在类中,静态方法的参数可以根据需要定义,不需要有self参数,但是可以用类名去调用这个静态函数。例如classname.staticmethod(),或者self.staticmethod()。无论采用哪种调用形式,参数表里都必须为每个形参提供实参,这里没有自动使用的self。

def行前加修饰符@classmethod,类方法,调用类的参数,类方法也是类的属性,可以以属性访问的形式调用。类方法执行时,调用它的类将自动约束到方法的cls参数(类参数),可通过这个参数访问其他属性。人们通常用类方法实现于本类的所有对象有关的操作。

class Countable:

counter=0 #类参数

def __init__(self):

Countable.counter+=1

@classmethod

def get_count(cls): #类方法自动绑定类属性cls,类似于self

return Countable.counter

'''类参数不在初始化函数中,因此每实例化一次该类,类参数就会+1,并刷新原类的值,该功能可以计数实例化了多少次该类。'''def行前加修饰符@property,装饰器,利用间接的代码对类里的参数进行检查

#传统方式

class Student(object):

def get_score(self):

return self._score

def set_score(self, value): #传统方法检查参数必须通过调用函数方法来实现

if not isinstance(value, int):

raise ValueError('score must be an integer!')

if value < 0 or value > 100:

raise ValueError('score must between 0 ~ 100!')

self._score = value

#加有装饰器

class Student:

@property

def score(self): #

return self.__score

@score.setter 定义装饰器属性的函数方法

def score(self,value):

if not isinstance(value,int):

raise ValueError('score must be an integer')

if value<0 or value>100:

raise ValueError('score is out of range')

'''第一个score(self)是get方法,用@property装饰,第二个score(self, score)是set方法,用@score.setter装饰,@score.setter是前一个@property装饰后的副产品。'''

l=Student()

l.score=999

print(l.score)

ValueError: score is out of range

私有变量

我们知道类创建的属性是可以通过外部访问和修改的,如果有时候有些很重要的属性不能访问和修改时,必须得限制外部的访问和修改权利。这是一种信息隐藏机制。一般属性加两个下划线,例如self._attr,这就是私有属性,当调用类对象时,在外部是无法找到和访问到该属性的,不过一般我们不会这么做。当你有不想让别人访问的属性时,可以在属性前加1个下划线,self._attr,这种写法实际上仍然是有外部访问和修改的权限的,但是告诉别人这个属性不应该被访问和修改,因此全靠自觉。

继承、基类和派生类

继承的主要作用有2个:一是可以基于已有的类定义新类,通过继承的方式复用已有类的功能,重复利用已有的代码,减少定义新类的工作量,简化新功能的开发,提高工作效率;另一个作用是建立一组类之间的继承关系,利用这种关系可能更好地组织和构造复杂的程序。

通过继承定义出的新类称为所列已有类的派生类(子类),被继承的已有类则称为这个派生类的基类(父类)。派生类可以继承基类所有的属性和方法,也可以修改一些功能,可以根据需要扩充新功能(定义新的属性或函数)。

一个类可能是其他类的派生类,它有可能被用作基类去定义新的派生类,这样在一个程序里可能会根据继承关系形成一种层次结构。python有一个内置的类object,其中定义了一些所有类都需要的基本功能。如果一个类没有定义说明基类,那么该类就自动以object为基类,换句话说,任何自定义的类都是object的派生类。python内置函数issubclass()检查两个类是否具有继承关系,包括直接或间接地继承关系,返回bool值。

派生类常需要重新定义_init_函数,完成该实例的初始化,不仅要初始化自己定义的属性也需要定义所继承的父类的所有属性。

class basedclass:

def __init__(self):

pass

class subclass(basedclass):

def __init__(self):

basedclass.__init__(self,....) #初始化父类的属性操作

..... #初始化子类的额外属性

标准函数super():super() 函数是用于调用父类(超类)的一个方法。super 是用来解决多重继承问题的,直接用类名调用父类方法在使用单继承的时候没问题,但是如果使用多继承,会涉及到查找顺序(MRO)、重复调用(钻石继承)等种种问题。

super().m()...,从该类的基类(可能存在多个基类,按照MRO顺序查找)开始搜索方法m()。或者super(class,self).m(),即在指定的基类去寻找方法。super()._init_(),初始化所有基类属性

python异常

如果程序中出现异常,无论是解释器发生异常或者是raise语句引发的异常,非常执行流程都会终止,解释器转入异常处理模式,查找能处理所发生异常的处理器。若找不到则会输出错误信息结束当前执行。python中常见异常有ValueError,TypeError,ZeroDivisionError等。

python中的异常都是类,运行中产生异常就是生成相应类的实例对象,全体内部异常类构成了一个树形结构,所有异常类的基类是BaseException,最主要的子类是Exception,内置异常类都是这个类的直接或简介派生类。如果自定义异常,就应该从系统异常类中选择一个合适的异常,派生出自己的异常类:

class RelationError(ValueError):

pass

运行中发生的异常与处理器匹配按面向对象的方式处理。假设运行中发生的异常是e,如果一个异常处理器头部列有异常名E,且isinstance(e,E)为真,那么这个处理器就能捕捉并处理异常e。如果引发了RelationError异常,某个处理器头部列出了RelationError,或者ValueError或者Exception,该处理器就能捕捉这个异常,匹配ValueError的处理器还能捕捉和处理其他异常,匹配Exception的处理器能捕捉和处理各种主要异常。

异常的传播和捕捉:假设在函数f的执行中发生异常e,当前执行立即中断转入异常处理模式首先在发生异常的函数体中查找处理器:如果发生异常的语句位于一个try语句体里,首先顺序检查这个try语句后部的各except子句,检查是否存在能处理e的处理器。

如果发生异常的try语句的所有异常处理器都不能处理e,解释器转去查看包围着该try语句外围try语句(如果存在),检查是否存在能与e匹配的异常处理器。

如果e不能在函数f里处理,则f的执行异常终止,e在函数f这次执行的调用点重新引发,导致又一轮处理器查找工作。查找规则与上面一样。

如果上面的查找过程在某一步找到了与e匹配的处理器,解释器就去执行该except子句的体。执行完该段代码后,解释器回到正常执行模式,从该异常处理器所在的try语句之后继续执行。

上述查找过程可能导致函数一层层以异常方式退出,有可能一直退到当前模块的最上层也没有找到与之匹配的处理器:如果程序是在解释器的交互方式下执行,python解释器终止该模块执行并回到交互状态,输出错误信息后等待下一个命令。

如果程序是自主执行,该程序立即终止。

标准异常类都是Exception的直接或间接派生类。

接下来介绍一些关于python中类的一些知识和技巧:

访问限制:私有属性

在前面已经提及到,设置为私有的属性在外部是无法获得访问和修改权限的,如果一定要访问和修改的话,就必须设定专门的方法。

class student:

def __init__(self,name,score):

self.__name=name #私有属性,外部无法直接访问和修改

self.__score=score #私有属性,外部无法直接访问和修改

def get_name(self): # 外部无法访问私有属性,可通过定义get_name(),get_score()函数来调用

return self.__name

def get_score(self):

return self.__score

def set_score(self,score): # 外部无法修改私有属性,可通过定义set_name(),set_score()函数来调用

self.__score = score

获取对象的信息

dir():#返回对象的所有属性和方法

hasattr,getattr,setattr:#判断是否有,得到,添加或设置某属性

L=class()

hasattr(L,'x') #L类是否有x属性

getattr(L,'x') #返回L类x属性的值,等价于L.x

setattr(L,'x',4) #将L类x属性的值设置为4 等价于L.x=4

限制类在外部动态添加属性和方法(_slots_)

#__slots__ 限制类在外部动态添加属性和方法

class father:

__slots__ = ('name','age') #只允许在外部动态添加指定的属性

f=father();f.name='job' #在外部动态添加属性和类

f.score=100 #添加属性失败

print(f.name);print(f.score) #AttributeError,无法添加score属性,被__slot__限制住了

class son(father):

__slots__ = father.__slots__ #子类继承父类的__slot__必须特别指定

定制类:几种特殊函数在类中的使用

__str__

class F:

def __init__(self,name,age):

self.name=name

self.age=age

def double_age(self):

self.age=self.age*self.age

return self.age

def __str__(self): #为属性或函数方法的打印而服务

return 'your name is%s,age is%d'%(self.name,self.double_age())

print(F('miaceal',27)) #实例化,直接print就行,打印__str__函数返回值

__iter__/__next__

如果类想被用于迭代循环,那么该方法返回一个迭代对象,然后利用__next__的方法来不断得到循环的值

class Feibo: #斐波那契数列

def __init__(self):

self.a,self.b=0,1

def __iter__(self): #返回一个迭代对象,将类实例改为一个可迭代对象

return self

def __next__(self):

self.a,self.b=self.b,self.a+self.b

if self.a>20:

raise StopIteration #迭代停止条件

return self.a

for n in Feibo():

print(n)

__getitem__

__iter__虽然将类变为可迭代对象,但始终不是列表,不能进行切片引用,此时用到__getitem__,注意这是适用于单个切片索引。

class Feibo: #斐波那契数列

def __getitem__(self, item):

a,b=1,1

for x in range(item):

a,b=b,a+b

return a

print(Feibo()[2]) #单个切片索引,如果是多索引,例如[1:6],上面的__getitem__会报错

要想可以多索引可以对__getitem__类进行改进:

class Feibo:

def __getitem__(self, item):

if isinstance(item,int): #分别对切片索引类型进行判断

a,b=1,1

for i in range(item):

a,b=b,a+b

return a

if isinstance(item,slice): #对切片索引类型进行判断

start=item.start

stop=item.stop

step=item.step

if start is None:

start=0

a,b=1,1

L=[]

for i in range(stop):

if i>=start:

L.append(a)

a,b=b,a+b

return L

print(Feibo()[3:8])

__getattr__

当类里没有某属性并且在外面动态调用时会出错,此时需要利用__getattr__函数来增添未有的属性。

#传统方式

class fun:

def __init__(self,name):

self.name=name

l=fun('micheal')

print(l.name) #micheal

print(l.score) #AttributeError: 'fun' object has no attribute 'score'

l.score=37 #外部动态添加score属性

print(l.score) #访问新添加的属性 :37

#__getattr__函数

class fun:

def __init__(self,name):

self.name=name

def __getattr__(self, item): #构造__getattr__函数,当访问类中所没有的属性时,返回函数方式

if item is 'score':

item=99

return item

raise AttributeError('no attr named\'%s\'in class'%item) #在__getattr__函数中仍然没有的属性就会抛出异常

l=fun('micheal')

print(l.name) #micheal

print(l.score) #99

print(l.fei) #定义了__getattr__函数的类在没有定义某属性,但在外部仍然调用到时,会返回None,此时一般抛出错误

程序的调试和错误处理

# 程序的调试与错误处理

# try ...except...finally...

'''用try来运行这段代码,如果执行出错,则后续代码不会继续执行,而是直接跳转至错误处理代码,即except语句块,执行完except后,如果有finally语句块,则执行finally语句块,至此,执行完毕'''

try:

r=10/0 #发现错误

print('result:',r) #后续自动截断,不会再执行

except Exception as e: #except 捕捉到错误信息并执行

print('Error:',e) #执行except

finally:print('finally...') #执行finally

''' 使用try...except捕获错误还有一个巨大的好处,就是可以跨越多层调用,比如函数main()调用foo(),foo()调用bar(),结果bar()出错了,这时,只要main()捕获到了,就可以处理:'''

import logging #导入错误日志函数

def foo(s):

return 10 / int(s)

def bar(s):

return foo(s) * 2

def main():

try: #在主函数中调用try,可以检查另外两个子函数的错误

bar('0')

except Exception as e:

logging.exception(e) #记录错误信息,并返回错误堆栈

print('Error:', e)

finally:

print('finally...')

main()

# 记录错误信息

from functools import reduce

import logging

def str2num(s):

return int(s)

def calc(exp):

ss = exp.split('+')

ns = map(str2num, ss)

return reduce(lambda acc, x: acc + x, ns)

def main():

try:

r = calc('100 + 200 + 345')

print('100 + 200 + 345 =', r)

r = calc('99 + 88 + 7.6')

print('99 + 88 + 7.6 =', r)

except Exception as e:

print('Error:',e)

logging.exception(e)

main()

# logging有相应的四个级别一般在首几行进行配置

# import logging

# logging.basicConfig(level=logging.INFO) #另外还有lWARNING, DEBUG,ERROR模式

# 对应于日志函数:logging.info(),logging.error(),logging.warning(),logging.debug()

# s='0';n=int(s);

# logging.info('n=%d'%n)

# print(10/n)

参考书籍:《数据结构与算法—python语言描述》—裘宗燕

你可能感兴趣的:(python算法类型)