Python零基础速成班-第10讲-Python面向对象编程(下),Property属性、特殊方法、设计模式、链表应用
学习目标
- 面向对象编程 接上一讲:Property属性、特殊方法、设计模式
- 面向对象示例:链表应用
- 课后作业(3必做1扩展)
友情提示:将下文中代码拷贝到JupyterNotebook中直接执行即可,部分代码需要连续执行。
1、面向对象编程 接上一讲
1.1 Property属性
在面向对象编程时,对于实体类,我们一般需要get或者set方法来对类进行赋值和取值。而Python不需要getter和setter,其内置的@property装饰器就是负责把一个方法变成属性调用的,@property本身又创建了另一个装饰器@state.setter,负责把一个setter方法变成属性赋值。
1.@property其实就是实现了getter功能; @xxx.setter实现的是setter功能;
2.定义方法的时候 @property必须在 @xxx.setter之前,且二者修饰的方法名相同;
3.如果只实现了 @property(而没有实现@xxx.setter),那么该属性为只读属性,则该类中的属性不允许被修改。
@property广泛应用在类的定义中,可以让调用者写出简短的代码,同时保证对参数进行必要的检查,这样,程序运行时就减少了出错的可能性。
如下例,我们定义了一个商品类 Goods(),包含商品名称 name和商品价格 price两个属性,常规方式,我们构建了一个get_name()方法用于商品名取值,构建了一个set_name()方法用于商品名称赋值。
而利用@property我们可以快速实现商品价格取值,@price.setter实现商品价格赋值。在apple = Goods(‘apple’,8.5)实例化后,我们可以直接apple.price用于商品价格取值,apple.price = 9用于商品价格赋值。
class Goods():
def __init__(self,name,price):
self.name = name
self._price = price
def get_name(self):#取值
return self.name
def set_name(self,name):#赋值
self.name = name
@property#类似get方法
def price(self):
return self._price
@price.setter#类似set方法
def price(self,newprice):
self._price = newprice
apple = Goods('apple',8.5)
print("商品名:",apple.get_name())
print("商品原价格:",apple.price)
apple.price = 9
print("商品现价格:",apple.price)
商品名: apple
商品原价格: 8.5
商品现价格: 9
进阶提示:使用@property时,属性名与方法名一定要区分开,不然会进入死循环,如上述例子,价格属性值为_price,方法名为price。
1.2 特殊方法Special Methods
在Python中 ,除了构建函数和析构函数之外,还有大量的特殊方法支持更多的功能,例如,运算符重载就是通过重写特殊函数来实现的。在自定义类时如果重写了某个特殊方法即可支持对应的运算符,具体实现什么工作则完全可以根据需要来定义。
接下来我们通过部分运算符重载的案例来详细讲解。
例1:完成一个类,实现字符串忽略大小写并比较是否相同的功能。w1、w2对象实例化后,w1==w2则会自动调用__eq__方法,对两个字符串进行比较。
class Word():
def __init__(self,text):
self.text= text
def __eq__(self,word2):
return self.text.lower() == word2.text.lower()
w1 = Word("Test")
w2 = Word("test")
w3 = Word("tes")
print(w1==w2)
print(w2==w3)
True
False
例2:完成一个类,实现两个数组内元素相加并返回一个新数组的功能。L1、L2实例化后,L1 + L2执行加入运算,实际是调用__add__方法,把两个列表对应的元素进行相加,并返回一个新的数组。
class ListAdd():
def __init__(self,list1):
self.data = list1
def __add__(self,list2):
return list(map(lambda x,y:x+y,self.data,list2.data))
L1 = ListAdd([1,2,3,4,5])
L2 = ListAdd([2,3,4,5,6])
print(L1 + L2)
[3, 5, 7, 9, 11]
特殊方法汇总:
比较运算方法:
__eq__:==
__ne__:!=
__gt__:>
__lt__:<
__le__:<=
__ge__:>=
数字运算方法:
__add__:+
__sub__:-
__mul__:*
__mod__:%
__pow__:**
__truediv__:/
__floordiv__://
其他非常有用的方法:
__str__:str(self) 输出字符串本身
__repr__:repr(self) 将对象转化为供解释器读取的形式,例如将对象转化为可读的字符串,也可以作为对象解释器
__len__:len(self) 输出字符串长度
class Word():
def __init__(self,text):
self.text =text
def __str__(self):
return self.text
def __repr__(self):
return"这是一个字符串对象:( "+self.text+" )"
def __len__(self):
return len(self.text)
w =Word('明天会更好')
print(w)
print(len(w))
w
明天会更好
5
这是一个字符串对象:( 明天会更好 )
1.3 面向对象设计模式Design pattern
- 单一职责原则:每个类都只有一个职责,修改一个类的理由只有一个。
- 开放-封闭远程(OCP):开放是指可拓展性好,封闭是指一旦一个类写好了,就尽量不要修改里面的代码,通过拓展(继承,重写等)来使旧的类满足新的需求,而不是修改一个类里面的代码。
- 依赖倒转原则:高层模块不应该依赖底层模块,两个都应该依赖抽象;抽象不应该依赖细节,细节应该依赖抽象。底层模块例如很多工具类,例如专门用于管理sql连接的类,管理文件,管理socket连接的类,高层类指具体实现需求的类。高层类和底层类都不应该相互依赖,不能出现高层类改变了,底层类就不能用了这种情况,两个都依赖抽象是指连个都依赖与调用的api,即接口,在开发的过程中,一旦接口设计后就不要轻易改变,所以接口的设计是尤为重要的。细节应该依赖抽象,是指一个类里面功能的实现(细节)要依赖于调用该类的接口。
- 里氏转换原则:子类必须能够替换掉他们的父类,即父类可以实现的功能,子类一定可以实现。
补充知识:线性存储结构
- Queue队列:先进先出,先来先服务,常用方法为 Put & Get。
- Stack栈:先进后出,常用方法为 Push & Pop。
- Linked List链表:像一条铁链,将数据连接在一起,每一个节点保存了数据以及指向下一个节点的指针,链表头指针指向第一个节点,如果链表为空,则头指针为空或者为 null。
- Tree 树:分层的数据结构,由节点和连接节点的边组成,被广泛应用在人工智能和一些复杂算法中,用来提供高效的存储结构。
2、面向对象示例:链表应用Linked List
链表的特点:
- 因为结点的内存地址不需要连续,所以相比顺序表,对于内存的利用更高效。
- 同时管理器只需要存储第一个结点的地址即可,对于后续结点,也只需要前一个结点有指针即可。
- 根据下标的查询操作只能从第一个结点依次往后进行。
- 越靠近头部的操作时间复杂度越低,越靠近尾部的时间复杂度越高。
如下图所示,链表中每一个数据结点,除了保存当前结点的基本数据值,还要保存后面相邻节点的地址,从而形成从头到尾的一个单向指针结构。而整个单向链表的变量名,存储的是第一个结点的地址。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-26WWJ8UZ-1653894579880)(attachment:image.png)]
2.1 链表常用方法
方法名 | 方法说明 |
---|---|
isEmpty | 链表是否为空,返回Bool |
add(val) | 链表头部添加节点 |
remove(val) | 删除链表某个节点 |
__str__ | 输出链表节点对象的值 |
__repr__ | 输出链表节点对象解释器值 |
index | 输出某个节点在链表中的下标位置,如不存在则返回-1 |
contain | 输出某个节点是否在链表中,返回布尔Bool |
insert(idx,val) | 在idx位置插入节点 |
len | 求链表的长度 |
2.2 节点类Node
节点类包含两个属性val和nex,val表示当前节点的内容,nex表示链接指针,节点的最后一个位置标识为None。
如下例,我们创建一个3节点链表,Node1–>Node3–>Node4–>None
class Node():
def __init__(self,val,nex):
self.val=val
self.nex=nex
n1=Node("1",Node("3",Node("4",None)))
print("头节点值是:",n1.val)
print("第二节点值是:",n1.nex.val)
print("第三节点对象是:",n1.nex.nex)
print("第三节点节点值是:",n1.nex.nex.val)
print("最后一个节点值是:",n1.nex.nex.nex)
头节点值是: 1
第二节点值是: 3
第三节点对象是: <__main__.Node object at 0x0000020ED76DC910>
第三节点节点值是: 4
最后一个节点值是: None
加入__str__输出链表节点对象的值方法,使用print()则会直接输出对象的值,如print(n1.nex)。
__repr__输出链表节点对象解释器值,直接读取对象则会输出对象解释器的值,如n1.nex。
class Node():
def __init__(self,val,nex):
self.val=val
self.nex=nex
def __str__(self):
return str(self.val)
def __repr__(self):
return "节点对象("+self.val+")"
n1=Node("1",Node("3",Node("4",None)))
print(n1)
print(n1.nex)
print(n1.nex.nex)
print(n1.nex.nex.nex)
n1.nex
1
3
4
None
节点对象(3)
2.2 链表头部添加节点add(val)方法
从头部添加节点比较容易,即将新的头部head指向老的头部即可。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-hMCFqHUq-1653894579881)(attachment:image.png)]
- 我们创建一个链表类LinkedList(),包含一个头部属性self.head,默认为None最后一个节点。
- 我们创建一个链表头部添加节点的方法add(self,val),self.head[新的头部] = Node(val,self.head[老的头部])
- 在链表类初始化方法__init__(self,list1=None)中,用循环的方式将传入进来数组list1中的值分别设置为链表中的节点,如传入进来为[1,2,3],则链表为3->2->1->None。
- 链表中的__str__方法,则是通过循环的方式将链表中的每个节点分别输出出来,可以清晰的看到整个链表的结构。
n2 =LinkedList([“1”,“2”,“3”,“4”,“5”])表示按照数组顺序初始化一个链表。
print(n2)则会将整个链表输出出来。
print(n2.head.nex.val)表示输出第二个节点的值。
n2.head.nex.nex表示输出第三个节点对象解释器的值。
class Node():
def __init__(self,val,nex):
self.val=val
self.nex=nex
def __str__(self):
return str(self.val)
def __repr__(self):
return "节点对象("+self.val+")"
class LinkedList():
def __init__(self,list1=None):
self.head = None
if list1 != None:
for listval in list1:
self.add(listval)
def add(self,val):
self.head = Node(val,self.head)
def __str__(self)->"5->4->3->2->1->None":
p=self.head
s=''
while p != None:
s+= p.val+'->'
p = p.nex
return s+'None'
n2 =LinkedList(["1","2","3","4","5"])
print(type(n2.head))
print(type(n2.head.val))
print(type(n2.head.nex))
print(n2)
print(n2.head.nex.val)
n2.head.nex.nex
<class '__main__.Node'>
<class 'str'>
<class '__main__.Node'>
5->4->3->2->1->None
4
节点对象(3)
2.3 删除链表某个节点remove(val)方法
删除结点应该算是所有方法中的难点和重点,因为删除以后要将删除结点的前后连接起来,所以游标必须停留在说删除结点的前一个结点,这样才能对前一个结点的next做赋值操作。此时就得考虑两种特殊情况,如删除空链表时或者删除头部节点时。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-dXMOnIjE-1653894579882)(attachment:image.png)]
- 当删除的是None的时候即空链表,直接返回。
- 当删除头部head节点时,下一个节点设置为头部self.head即可。
- 当删除中间位置节点时,需要循环遍历链表,如果当前节点的下一级等于要删除的,则将当前节点指向下下级。
n3 =LinkedList([“1”,“2”,“3”,“4”,“5”])表示按照数组顺序初始化一个链表。
print(n3)则会将整个链表输出出来。
n3.remove(“4”)表示删除值为4的节点。
print(n3)再将删除后的链表输出出来。
class Node():
def __init__(self,val,nex):
self.val=val
self.nex=nex
def __str__(self):
return str(self.val)
def __repr__(self):
return "节点对象("+self.val+")"
class LinkedList():
def __init__(self,list1=None):
self.head = None
if list1 != None:
for listval in list1:
self.add(listval)
def add(self,val):
self.head = Node(val,self.head)
def remove(self,val):
p=self.head
if p==None:#当删除的是None的时候
return
if p.val==val:#当删除的head节点的时候
p.nex=self.head
while p != None and p.val != val:
if p.nex.val == val:#如果当前节点的下一级等于要删除的,则将当前节点指向下下级
p.nex = p.nex.nex
break
p = p.nex
def __str__(self)->"5->4->3->2->1->None":
p=self.head
s=''
while p != None:
s+= p.val+'->'
p = p.nex
return s+'None'
n3 =LinkedList(["1","2","3","4","5"])
print(n3)
n3.remove("4")
print(n3)
5->4->3->2->1->None
5->3->2->1->None
2.4 isEmpty()方法,index(val)方法,len()方法,insert(index,val)方法,in(contain)方法
- isEmpty()链表是否为空,这个比较简单,只需要判断self.head是否为None即可,如是则为空,如不是则不为空。
- index(val)输出某个节点在链表中的下标位置,即循环遍历链表,从0累加,直到找到节点的值为止,如如不存在则返回-1。
- len()求链表长度,即循环遍历链表,每增加一个结点,中间变量size的值加1,最后得到的总数,就是链表的长度。
- insert(index,val)在idx位置插入节点,此时就得考虑三种情况:
- 当插入位置是0或者空链表时,直接调用add方法。
- 当插入位置超过链表长度时,则直接在尾部节点插入,指向None。
- 当在链表中插入时,则首先定位到插入值前一位,即index >1(遍历链表,每向后移动一位,index-1),再将前一位的指针p.nex指向当前需要插入的Node对象,插入对象的指针指向原来指针,即插入值前一位的p.nex,语法如下p.nex=Node(val,p.nex)。
- contain(val)输出某个节点是否在链表中,返回布尔Bool,这个比较简单,只需要判断self.index(val) != -1即可,当为-1时,则表示不在链表中。
n4 =LinkedList([“1”,“2”,“3”,“4”,“5”])表示按照数组顺序初始化一个链表。
print(n4.index(‘3’))表示3在链表中的位置;print(n4.index(‘6’)) = -1 则表示没有在链表中找到6。
print(n4.len())则输出链表长度。
n4.insert(3,“7”)表示在链表第三个位置插入7;在print(n4)遍历输出链表,print(n4.contain(“7”))最后判断7是否在链表中。
class Node():
def __init__(self,val,nex):
self.val=val
self.nex=nex
def __str__(self):
return str(self.val)
def __repr__(self):
return "节点对象("+self.val+")"
class LinkedList():
def __init__(self,list1=None):
self.head = None
if list1 != None:
for listval in list1:
self.add(listval)
def add(self,val):
self.head = Node(val,self.head)
def isEmpty(self):#1、链表是否为空
return self.head ==None
def index(self,val):#2、输出某个节点在链表中的下标位置
p=self.head
idx =0 #从0号开始
while p != None:
if p.val == val:
return idx
idx+=1
p = p.nex
return -1
def len(self):#3、求链表长度
p=self.head
size=0
while p !=None:
size+=1
p =p.nex
return size
def insert(self,index,val):#4、在idx位置插入节点
if index ==0 or self.isEmpty():#当位置是0或者空链表时,则直接add
self.add(val)
elif index > self.len():#当插入位置超过长度时,则直接在尾部节点插入,指向None
p=self.head
while p.nex != None:#不等于尾部
p=p.nex
p.nex = Node(val,None)
else:
p =self.head
while p!=None and index >1:# >1是因为要定位到插入值的前一位
index-=1
p=p.nex
p.nex=Node(val,p.nex)#插入值的前一位的指针nex指向插入节点对象Node,该None再指向后一位
def contain(self,val):#5、求链表的长度
return self.index(val) != -1
def __str__(self)->"5->4->3->2->1->None":
p=self.head
s=''
while p != None:
s+= p.val+'->'
p = p.nex
return s+'None'
n4 =LinkedList(["1","2","3","4","5"])
print(n4)
print(n4.index('3'))
print(n4.index('6'))
print(n4.len())
n4.insert(3,"7")
print(n4)
print(n4.contain("7"))
5->4->3->2->1->None
2
-1
5
5->4->3->7->2->1->None
True
3、课后作业,答案在下一讲
1、使用以下属性创建名为Point的类:
x:float
y:float
包含以下四种方法:
__str__():返回字符串,如:(1.0,2.0)
__repr__():返回字符串,如:Point(1.0,2.0)
get_x:返回x值
get_y:返回y值
您的代码:
2、使用以下属性创建名为Circle的类,继承Point:
center:Point
radius:float
包含以下两种方法:
__str__():返回字符串,如:Circle at(1.0,2.0),r = 5.0
__repr__():返回字符串,如:Circle(center=Point(1.0,2.0),r = 5.0)
您的代码:
3、使用以下属性创建名为Triangle的类,继承Point:
vertx:tuple of Point
包含以下两种方法:
__str__():返回字符串,如:Triangle at((0.0,0.0),(0.0,10.0),(5.0,5.0))
__repr__():返回字符串,如Triangle(vertx=(Point(0.0,0.0),Point(0.0,10.0),Point(5.0,5.0)))
和计算方法:
area:area= ∣ A x ( B y − C y ) + B x ( C y − A y ) + C x ( A y − B y ) 2 ∣ \left| \frac{A_x(B_y-C_y)+B_x(C_y-A_y)+C_x(A_y-B_y)}{2} \right| ∣∣∣2Ax(By−Cy)+Bx(Cy−Ay)+Cx(Ay−By)∣∣∣
提示:
__str__()方法可以使用语法:"Triangle at(%s,%s,%s)" %self.vertx
您的代码:
*(扩展)4、编程实践项目
模拟计算器的操作输出(从小学起大家就知道,先乘除,再加减,有括号先算括号里的内容,那么计算机是如何实现的呢?)
如下例,我们如何实现下面计算式在计算机中的运算呢?我们首先需要将中缀运算式转化为计算机可以执行的后缀运算式。
2 - (5 + 3 * 2 +1) / 3
简单介绍一下前缀、中缀、后缀表达式
- 前缀表达式(Prefix Notation)是指将运算符写在前面操作数写在后面的不包含括号的表达式,而且为了纪念其发明者波兰数学家Jan Lukasiewicz(卢卡西维兹发明于1929),所以前缀表达式也叫做“波兰表达式”。
- 后缀表达式(Postfix Notation)与之相反,是指运算符写在操作数后面的不包含括号的算术表达式,也叫做逆波兰表达式。
- 中缀表达式(Infix Notation)就是常用的将操作符放在操作数中间的算术表达式。前缀表达式和后缀表达式相对于中缀表达式最大的不同就是去掉了表示运算符优先级的括号。
任务第一步:将中缀转化为后缀
模拟一下中缀转后缀过程:从左到右遍历中缀表达式,数字则输出,成为后缀表达式的一部分,若是遇到符号,判断其余栈顶符号的优先级,是有括号或优先级不高于栈顶符号则栈顶元素依次出栈并输出,如下例:
2 - (5 + 3*2 +1) / 3
2
2 [-]
2 [-,(]
25 [-,(]
25 [-,(,+]
253 [-,(,+]
253 [-,(,+,*]
2532 [-,(,+,*]
2532*+ [-,(,+]
2532*+1+ [-]
2532*+1+ [-,/]
2532*+1+3 [-,/]
2532*+1+3/-
您的代码:
任务第二步:计算后缀字符串
模拟一下后缀字符串计算过程 从左到右遍历后缀表达式,遇到数字就进栈,遇到是符号,就将处于栈顶两个数字出栈,进行运算,运算结果进栈,一直到最终获得结果,如下例:
2532*+1+3/-
[2]
[2,5]
[2,5,3]
[2,5,3,2]*
[2,5,6]+
[2,11]
[2,11,1]+
[2,12]
[2,12,3]/
[2,4]-
[-2]
您的代码:
4、上一讲Python零基础速成班-第9讲-Python面向对象编程(上),对象和类、初始化、继承、重写、多态、类方法、组合 课后作业及答案
1、创建一个Dog类,再创建一个FlyDog类,FlyDog继承Dog类中的方法。Dog类拥有方法eat(self)和bark(self),eat方法输出"我要吃东西!“bark方法输出"汪汪汪!”,FlyDog类拥有方法fly(self),fly方法输出"我能飞" ,最后创建一个mydog=FlyDog(),执行mydog.bark()和mydog.fly()
class Dog(object):
def eat(self):
print("我要吃东西")
def bark(self):
print("汪汪汪")
class FlyDog(Dog):
def fly(self):
print("我能飞")
mydog = FlyDog()
mydog.bark()
mydog.fly()
汪汪汪
我能飞
2、创建一个Date类,设置year、month、day三种属性,利用@classmethod创建一个新的类方法new_Date,该方法是将新传入的日期参数newDate(格式:yyyy-mm-dd,如2024-12-31)拆分成年、月、日,重新赋值一个新的Date类,并分别输出year、month、day。
提示:日期拆分可以使用year,month,day = map(int,date_from_str.split(‘-’))实现
class Date(object):
def __init__(self,day,month,year):
self.year = year
self.month = month
self.day = day
@classmethod
def new_Date(cls,newDate):
year,month,day = map(int,newDate.split('-'))
newobject = cls(day,month,year)
return newobject
newdate = Date.new_Date("2024-12-31")
print(newdate.year)
print(newdate.month)
print(newdate.day)
2024
12
31
3、创建一个Date类,设置year、month、day三种属性,利用@staticmethod创建一个新的静态方法validate_Date,该方法是将新传入的日期参数newDate(格式:yyyy-mm-dd,如2025-10-30)拆分并进行校验,当同时满足year<2999、month<=12、day<=31时,返回True,否则返回False,最后实例化对象并输出结果。
class Date(object):
def __init__(self,day,month,year):
self.year = year
self.month = month
self.day = day
@staticmethod
def validate_Date(newDate):
year,month,day = map(int,newDate.split('-'))
return year<2999 and month<=12 and day<=31
Date.validate_Date("2025-10-30")
True