Python面向对象小结

一、初阶使用

1.1 基本概念

我们将一个类型及其关联的一组操作组成的整体叫做,并称这些操作为类的属性。如果类的属性是函数的话,我们也称它为类的方法。当我们用类创建了一个对象时,称这个对象为类的实例

1.2 基本语法

1.2.1 类的定义

类在Python中通过class关键字定义,最简单的类就是空类:

class Firstclass:
	pass

上述代码创建了一个类,类名叫做Firstclass(在Python中,模块名一般小写字母开头,类名一般大写字母开头,请尽量遵循这一习惯),类的内部什么也没有,当我们在调试面板上调试该脚本时,会产生以下结果:

>>> x=Firstclass
>>> x
<class '__main__.Firstclass'>
>>> y=Firstclass()
>>> y
<__main__.Firstclass object at 0x000001E4350DAA58>

可以看到,Firstclassclass对象,而Firstclass()才是object对象。也就是说,Firstclass是类,Firstclass()是类的实例。

1.2.2添加属性

Python规定,只要是在类内部的顶层赋值的任何变量,都会成为类的属性(需要提醒的是,def也是一种赋值运算),比如以下脚本:

class Firstclass:
    version='1.0.0'
    def setdata(self,value):
        self.data=value
    def display(self):
        print(self.data)

当我们在调试面板上调试该脚本时,会产生以下结果:

>>> x=Firstclass()
>>> x.version
'1.0.0'
>>> x.setdata(2.71828)
>>> x.display()
2.71828
>>> y=Firstclass()
>>> y.version
'1.0.0'
>>> y.setdata('python')
>>> y.display()
python

我们给Firstclass类写了三个属性,version,setdatadisplay,其中,version是字符串对象,剩下两个都是函数对象。而在setdata方法内,我们又通过传入的self参数给Firstclass的实例(即self代表的对象)增加了一个data属性,data对象的类型由传入的value参数决定。
值得一提的是,这两个函数的self参数都不是必须叫self的,只不过按照惯例这样取而已。Python规定,在类方法函数内,第一个参数会引用正处理的实例对象,对self的属性做赋值运算会创建或修改实例内的数据。所以类的方法在定义的时候至少要有一个参数,且最好写为self

1.2.3 使用属性

从刚才的调试代码中可以看出,Python中使用点号来访问类的属性。当解释器遇到object.attribute句式时,就会在类object中寻找是否存在attribute属性。如果存在,修改它,否则创建它。因此,在刚才的调试代码之后还可以追加:

>>> x.new_attri=3
>>> x.new_attri
3

这样做是不会报错的。

二、类的继承

2.1 概念

除了通过刚才的方式为类编写属性之外,类也可以引入其他类来进行定义。我们称被引用的类为父类,引用父类的类为父类的子类。由子类产生的实例会继承父类的属性,我们称这一行为为类的继承。在Python当中,实例继承自类,而类继承自超类。类在继承其他类以后还可以自行添加新的属性,或者修改父类的属性,我们称这样的修改为重载。请注意,重载不会修改父类的内容。

2.2 语法

2.2.1 如何继承

Python规定,要继承另一个类的属性,需要把该类列在class语句开头的括号中。如以下代码:

class Firstclass:
    version='1.0.0'
    def setdata(self,value):
        self.data=value
    def display(self):
        print(self.data)
class Secondclass(Firstclass):
    new_version='2.0.0'

调试代码如下:

>>> x=Secondclass()
>>> x.version
'1.0.0'
>>> x.new_version
'2.0.0'
>>> x.setdata(3.14)
>>> x.display()
3.14

我们可以看到,创建的Secondclass()实例拥有Firstclass()的所有属性,还可以添加自己新的属性new_version

2.2.2 如何重载

上文提到,在Python中访问类的属性是通过查找的方式,而查找会优先查找当前类,当前类没有才会到父类里面找。因此,如果我们要在子类中重载父类的属性,只要赋一个与该属性相同的变量名即可。如以下代码:

class Firstclass:
    version='1.0.0'
    def setdata(self,value):
        self.data=value
    def display(self):
        print(self.data)
class Secondclass(Firstclass):
    new_version='2.0.0'
    def display(self):
        print('data='+self.data)

调试代码如下:

>>> x=Secondclass()
>>> x.setdata('6.62')
>>> x.display()
data=6.62

通过在Secondclass中也定义一个display()的方式,即可对Firstclassdisplay属性进行重载。

三、运算符重载

除了可以重载父类的属性,我们还可以重载内置类型的运算,如加法、切片、打印等。通过重载运算符,我们可以使得自己编写的类拥有类似于内置类型那样的行为。

3.1 基础知识

内置类型的运算也是通过其类的方法来实现的,在Python中,这些方法会以双下划线命名(即__X__的形式)以示区分。当实例对象继承了这些方法时,内置运算就会调用这些方法。比如,如果一个实例继承了__add__方法,则当对象出现在+表达式时,__add__方法就会被调用。如果一个实例继承了__init__方法,则当新的实例对象被构造时,__init__方法就会被调用。
比如以下代码:

class Firstclass:
    version='1.0.0'
    def setdata(self,value):
        self.data=value
    def display(self):
        print(self.data)
class Secondclass(Firstclass):
    new_version='2.0.0'
    def display(self):
        print('data='+self.data)
class Thirdclass(Secondclass):
    def __init__(self,value):
        self.data=value

调试代码如下:

>>> x=Thirdclass(6.67)
>>> x.data
6.67

可以看到,Thirdclass在生成实例时会传递一个参数(例如6.67),这是传给__init__构造函数内的参数value的,而__init__会将其赋值给self.data。直接效果是,Thirdclass在构建时自动设置data属性,而不需要构建之后请求setdata调用。
目前为止,我们已经基本写出了一个“像模像样”的类了,因为一般情况下,由于我们需要让类立即在其新建的实例内添加属性,几乎每个实际的类都会出现一个__init__方法的重载。此外,我们也需要注意,尽量不要给自己的类起双下划线命名的属性,这可能造成隐藏的错误。
再补充几点,在Python中,__X__定义的是特殊方法,一般是系统定义的变量 ;_X表示的是保护类型的变量,只能允许其本身与子类进行访问,不能用于 from module import *__X表示的是私有类型变量, 只允许这个类本身进行访问。

3.2 常用的运算符重载

方法重载调用
__init__构造函数X=Class(args)
__add__运算符+X+Y,X+=Y
__or__运算符|X|Y,X|=Y
__str__打印print(X)
__getitem__索引运算X[key],X[i:j]
__setitem__索引赋值X[key]=value,X[i:j]=sequence

3.2.1 索引和分片

如果在类中定义了(或继承了)__getitem____setitem__方法的话,解释器就可以在类的实例进行索引和索引赋值时拦截其操作,并调用用户自己写的方法。例如,以下的代码定义了一个类,它的索引将返回索引值的平方。

class Squares:
	def __getitem__(self,idx):
		return idx**2

调试代码如下:

>>> x=Squares()
>>> x[2]
4

不仅如此,我们还可以用同样的操作拦截切片。为了理解这一点,首先我们需要了解到,切片操作[i:j:k]本质上是一种语法糖,它真正传入__getitem__的参数是一个分片对象slice(i,j,k)。因此我们可以编写以下类,它将返回data列表中的对应索引或切片:

class Indexer:
    data=[1,2,3,4,5,6]
    def __getitem__(self,idxs):
        return self.data[idxs]

调试代码如下:

>>> x=Indexer()
>>> x[2]
3
>>> x[1:5:2]
[2, 4]

3.2.2 索引迭代

其实,__getitem__也是Python中一种重载迭代的方式,如果定义了这个方法,那么当for循环语句调用到这个类的实例的时候,for循环内的每次循环都会调用类的__getitem__,并持续传递更高的偏移值。对于刚才的Indexer类,如果我们将其生成的实例放在for循环中,像这样:

x=Indexer()
for i in x:
    print(i,end=' ')

会产生如下输出:

1 2 3 4 5 6 

在Python中,任何支持for循环的类也会自动支持Python所有的迭代环境,例如成员关系测试in、列表解析、内置函数map等等。

3.2.3 迭代器对象

尽管__getitem__技术很有效,但在Python中,所有的迭代环境都会优先尝试__iter__方法,再尝试__getitem__。所以,一般来说,我们应该优先使用__iter__
从技术角度来讲,迭代环境是通过调用内置函数iter去尝试寻找__iter__方法来实现的,这个方法返回的内容是一个迭代器。如果找到了,Python就会重复调用这个迭代器的next方法,直到遇到StopIteration异常。而如果没有找到,才会改用__getitem__机制,直到遇到IndexError异常。
下面,我们将使用__iter__机制来完成刚才生成平方值的操作。

class Squares:
    def __init__(self,start,stop):
        self.value=start-1
        self.stop=stop
    def __iter__(self):
        return self
    def __next__(self):
        if self.value==self.stop:
            raise StopIteration
        self.value+=1
        return self.value**2
x=Squares(1,5)
for i in x:
	print(i,end=' ')

输出:

1 4 9 16 25 

此外,由于__iter__对象能够在调用的过程中保留状态信息(如果我们不用for循环而手动不断__next__也是可以实现相同的功能的,但这样的方法却不能用在__getitem__上),所以__iter____getitem__有更好的通用性。
但是,在有些时候,__iter__也有缺点。首先来说,__iter__只用于迭代,而不重载索引表达式,因而我们无法使用Squares(1,5)[0]的方式来调用它。其次,__iter__只会迭代一次,之后返回的都是空。比如以下的测试代码:

>>> x=Squares(1,5)
>>> [n for n in x]
[1, 4, 9, 16, 25]
>>> [n for n in x]
[]

四、实战

下面,我们将用以上知识编写一个程序。该程序包含一个数列类基类以及它的三个子类,等差数列类,等比数列类和斐波那契数列类。代码如下:

class Array:
    def __init__(self,start=0):
        self.start=start
    def _advance(self):
        return self.cur+1
    def __iter__(self):
        return self
    def __next__(self):
        if self.len>0:
            self.len-=1
            buf=self.cur
            self.cur=self._advance()
            return buf
        else:
            raise StopIteration
    def show(self,num):
        self.cur=self.start
        self.next=None
        self.len=num
        print(list(self))
class Arithmetic(Array):
    def __init__(self,start,increment):
        Array.__init__(self,start)
        self.increment=increment
    def _advance(self):
        return self.cur+self.increment
class Geometric(Array):
    def __init__(self,start,base):
        Array.__init__(self,start)
        self.base=base
    def _advance(self):
        return self.cur*self.base
class Fibonacci(Array):
    def __init__(self,first,second):
        Array.__init__(self,first)
        self.second=second
    def _advance(self):
        if self.next==None:
            self.next=self.second
        self.cur,self.next=self.next,self.cur+self.next
        return self.cur
a=Array(4)
a.show(9)
b=Arithmetic(3,2)
b.show(10)
c=Geometric(1,2)
c.show(3)
d=Fibonacci(2,1)
d.show(7)
d.show(5)

##输出如下:
##[4, 5, 6, 7, 8, 9, 10, 11, 12]
##[3, 5, 7, 9, 11, 13, 15, 17, 19, 21]
##[1, 2, 4]
##[2, 1, 3, 4, 7, 11, 18]
##[2, 1, 3, 4, 7]

下面,我们一个类一个类的分析一下这个程序:
首先先看基类:

class Array:
    def __init__(self,start=0):
        self.start=start
    def _advance(self):
        return self.cur+1
    def __iter__(self):
        return self
    def __next__(self):
        if self.len>0:
            self.len-=1
            buf=self.cur
            self.cur=self._advance()
            return buf
        else:
            raise StopIteration
    def show(self,num):
        self.cur=self.start
        self.next=None
        self.len=num
        print(list(self))

在这个类的构造函数中,我们初始化了类属性start,用来存储数列的首项。中间三个方法暂时不看,当我们调用了这个类的show方法时,我们会给这个类的实例先增加三个属性:curnextlen,分别用来保存数列的当前值(用于输出和向后迭代)、后继值(为了妥协斐波那契数列)以及数列长度(用于控制什么时候迭代结束)。然后,我们就要调用list(self)。那么这个self是什么呢?由于我们重载了__iter__方法,因此这里的self是一个迭代器对象,而当我们使用到这个迭代器对象时,我们知道,它会不断调用__next__方法。那么再来看__next__,我们会发现,它会首先检查len数据是否合法,如果不合法,则抛出StopIteration异常停止迭代;否则,它会先将len,即数组的长度减一,再用buf缓冲变量保存当前值cur,然后用_advance方法更新cur,最后返回buf的值,也就是原本的cur值。至于为什么要如此大费周章而不是直接返回self._advance(),是因为我们无法先returncur的值再去更新cur(这里的实现确实有不够优美的地方,暂时还没想到解决方法)。
看完了最长的基类,下面几个类就很简单了。可以看到,子类都只是重载了__init__方法和_advance方法,用于它们各自需要的更新操作。等差数列的更新方式就是当前值cur加上公差increment;等比数列就是cur乘以公比base;斐波那契数列就是后继项next,并更新当前项curnextnextcurnext

从这个程序中我们也可以看出,面向对象的继承和多态思想的恰当使用能够有效的降低代码的耦合度,提高代码的复用率,这一点确实解决了面向过程编程中可能或业已出现的许多问题。

  • 8
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 8
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 8
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值