Python面向对象编程

理解Python面向对象

Python中的类

在Python中。类使用class语句来定义。在类的代码中包含了一系列语句,用赋值语句创建变量,用def定义函数等。Python中的类只要有下面几个特点:

  • 一个类可以有多个实例对象,每个实例对象拥有自己的命名空间。
  • 类支持继承,通过继承对类进行扩展。
  • 支持运算符重载。通过内置的特定方法,可以使类的对象支持内置类型的各种运算。

Python中的对象

类对象

类对象具有如下特点:

  • Python执行class语句时创建一个类对象和一个变量(名称就是类名称),变量引用对象。导入类模块时,class语句被执行,创建类对象。
  • 类的数据(变量)用“对象名.属性名”格式来访问。
  • 类的方法属性用“对象名.方法名()”格式来访问。
  • 类的数据(变量)和方法由所有实例对象共享

实例对象

实例对象具有如下特点:

  • 实例对象通过调用类对象来创建。
  • 每个实例对象继承类对象的属性,并获得自己的命名空间。

实例对象拥有私有属性。类的方法函数的第一个参数默认为self,表示引用方法对象的实例。在方法中对self属性赋值才会创建属于实例对象的属性。

定义和使用类

类定义的基本格式如下:

1
2
3
4
5
6
7
class 类名
赋值语句
赋值语句
……
def语句定义函数
def语句定义函数
……

各种语句的先后顺序没有关系。

定义类

下面的代码演示了定义了一个testclass类:

1
2
3
4
5
6
7
class testclass:
data=100
def setpdata(self,value):
self.pdata = value
def showpdata(self):
print("self.pdata=",self.pdata)
print("类testclass加载完成!")

使用类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
type(testclass) #输出<class 'type'> Python的所有类对象都是type类型
testclass.data #输出100
testclass.showpdata() #类的方法需要实例对象来调用 否则会报错
类的方法不能通过类对象直接调用,因为含有参数self,它代表实例对象。调用方式如下:
x = testclass() #调用类创建第一个实例对象
x.setpdata('abc')#调用方法给实例对象的数据属性pdata设置值
x.showpdata() #输出self.pdata=abc,调用方法显示实例对象的属性值
y=testclass() #调用类创建第二个实例对象
x.setpdata('123')#调用方法给实例对象的数据属性pdata设置值
x.showpdata() #输出self.pdata=123,调用方法显示实例对象的属性值
类对象的属性可以实例对象共享,例如:
x.data,y.data #输出(100,100)
testclass.data = 200 #通过类对象修改共享属性
x.data,y.data #输出(200,200)

对象的属性和方法

对象的属性

在Python中实例对象继承了类对象的所有属性和方法,可以通过dir()方法来查看。

1
dir(testclass)  # 输出['__class__', '__delattr__', '__dict__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__gt__', '__hash__', '__init__', '__le__', '__lt__', '__module__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', '__weakref__', 'data', 'setpdata', 'showpdata']

以双下划线开始和结尾的变量名属于内置属性。

共享属性

类对象的数据属性是全局的,即默认情况下它属于类对象,并可以通过实例变量来引用。例如:

1
2
3
4
5
6
7
x = testclass()
y = testclass()
testclass.data,x.data,y.data #输出(100,100,100)
testclass.data = 200 #对全局属性data赋值
testclass.data,x.data,y.data #输出(200,200,200)
x.data = 300 #对实例变量的data属性赋值
testclass.data,x.data,y.data #输出(200,300,200)

在没有给实例对象赋值时,它引用的是类对象的同名属性。给实例对象的属性赋值时,使其引用了新对象,不再与类对象的同名属性相关联。所以出现了x的data属性值与testclass和y的属性值不一样。实例对象和类对象的关系如下所示:
在这里插入图片描述

实例对象的“私有”属性

实例对象的“私有”属性指类的函数中以”self.属性值=值”格式进行赋值创建的属性。“私有”属性只属于当前实例对象。例如:

1
2
3
4
x = testclass()
x.pdata #会报AttibuteError 'testclass' object has no attribute 'pdata'错误
x.setpdata(1234)
x.pdata #可以访问

对象的属性具有动态性

对于类对象或者实例对象而言,当给不存在的属性赋值时,Python为其创建属性。例如:

1
2
3
4
x = testclass()
testclass.data2 = 'aaa'
testclass.data3 = [1,2,3]
testclass.data2,x.data2,x.data3 #输出('aaa','aaa',[1, 2, 3])

可以看出实例对象也自动拥有了为类对象动态添加的属性。

对象的方法

实例对象没有自己的方法,只是通过继承的方法名变量来引用属于类对象的方法。通过实例对象调用方法时,当前实例对象作为一个参数传递给方法,所以在定义供实例对象调用的方法时,通常第一个参数名称为self(也可以用其他的名称)。
例如:

1
2
3
4
5
6
7
8
9
10
11
class test:
def add(a,b):
return a+b
def add2(self,a,b):
return a+b

print(test.add(2,3)) #输出5,通过类对象调用方法
print(test.add2(2,3,4)) #输出7,通过类对象调用方法
x = test() #创建实例对象
x.add(2,3) #会报错,显示需要2个参数但传递了3个
print(x.add2(3,4)) #输出7,通过实例对象调用方法

可以看出类对象可以调用类中的所有方法,只是需要记住参数个数。在执行“test.add2(2,3,4)”时第一个参数在函数中未使用。

类的”伪私有”属性和方法

在模块中使用双下划线作为变量名前缀,可以避免变量在使用from…import*语句时被导入。类似地,在类中使用双下划线作为变量名前缀,这些变量名不能直接在类外使用。例如:

1
2
3
4
5
6
7
8
9
10
11
class test:
data = 100
__data2 = 200
def add(a,b):
return a+b
def __sub(a,b):
return a-b
test.data #100
test.add(2,3) #5
test. __data2 #报AttributeError: type object 'test' has no attribute 'data2'错
test.__sub(2,3) #AttributeError: type object 'test' has no attribute '__sub'

可以看出类对象不能直接访问双下划线作为变量名前缀的属性和方法。之所以成为”伪私有”,是Python在处理这类变量名时自动在带双下划线前缀的变量名前加上”_类名”。比如上面的例子可以通过test._test__data2访问__data2变量,test._test__sub访问__sub方法

构造函数和析构函数

类的构造函数和析构函数名称是由Python预设的,__init__为构造函数名,__del__为析构函数名。构造函数在调用类创建实例对象时自动被调用,完成对实例对象的初始化。析构函数在实例对象被回收时调用。在定义类时,可以不定义构造函数和析构函数。

类的继承

通过继承,子类(新类)可以获得父类(超类)的属性和方法。在子类中可以定义新的属性和方法,从而实现对父类的扩展。基本代码格式如下:

1
2
class 类名(超类名): #如果超类有多个用,分隔
子类中的语句

简单继承

下面的代码定义一个空的子类来说明子类继承了超类的所有属性和方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
class supper_class:
data=100
__data2=200
def showinfo(self):
print("超类showinfo()方法调用")
def __showinfo(self):
print("超类__showinfo()方法调用")
class sub_class(supper_class):pass #定义空的子类,pass表示空操作
print(sub_class.data) #输出100
print(sub_class._supper_class__data2) #输出200
x = sub_class() #创建子类的实例对象
x.showinfo() #输出“超类showinfo()方法调用”
x._supper_class__showinfo() #输出“超类__showinfo()方法调用”

定义子类的属性和方法

如果子类定义的方法和属性与父类的方法和属性重名,那么子类实例对象调用子类中定义的方法和属性。这在java中叫方法重写。例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class supper_class:
data1=10
data2=20
def show1(self):
print("超类show1()方法调用")
def show2(self):
print("超类show2()方法调用")
class sub_class(supper_class): #定义子类
data1=111
def show1(self):
print("子类show1()方法调用")

x = sub_class() #创建子类的实例对象
print(x.data1) #输出111
print(x.data2) #输出20
x.show1() #输出“子类show1()方法调用”
x.show2() #输出“超类show2()方法调用”

Python中允许子类方法中通过类对象直接调用父类的方法。例如:

1
2
3
4
5
6
class sub_class(supper_class): #定义子类
data1=111
def show1(self):
print("子类show1()方法调用")
supper_class.show1(self) #调用超类的方法
supper_class.show2(self) #调用超类的方法

调用超类的构造函数

在使用构造函数对实例对象进行初始化时,可以在子类的构造函数中调用超类的构造函数。例如:

1
2
3
4
5
6
7
8
9
10
11
12
class supper:
def __init__(self,a):
self.supper_data = a

class sub(supper):
def __init__(self,a,b):
self.sub_data = b
supper.__init__(self,a) #调用超类的构造方法

x = sub(5,6)
print(x.sub_data) #输出6
print(x.supper_data) #输出5

多重继承

多重继承指子类可以同时继承多个超类。如果超类中存在同名的属性或方法,Python按照从左到右的顺序在超类中搜索,如果左边的超类中找到了就不再从右边找。例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
class supperone:
one = 10
two = 20
def showone(self):
print("supperone中的showone()方法调用")
def showtwo(self):
print("supperone中的showtwo()方法调用")

class suppertwo:
one = 100
three = 30
def showtwo(self):
print("suppertwo中的showtwo()方法调用")
def showthree(self):
print("suppertwo中的showthree()方法调用")

class sub(supperone,suppertwo): pass #定义子类

x = sub()
print(x.one) #输出10
print(x.two) #输出20
print(x.three) #输出30
x.showone() #输出"supperone中的showone()方法调用"
x.showtwo() #输出"supperone中的showtwo()方法调用"
x.showthree() #输出"suppertwo中的showthree()方法调用"

运算符重载

重载运算符就是在类中定义相应的方法,当使用实例对象执行相关运算时,则调用对应方法。

部分运算符重载方法表格如下
方法 说明 何时调用方法
__add __ 加法运算 对象加法:x+y、x+=y
__sub __ 减法运算 对象减法:x-y、x-=y
__mul __ 乘法运算 对象乘法:x*y、x*=y
__div __ 除法运算 对象除法:x/y、x/=y
__mod __ 取模运算 对象取模:x%y、x%=y
__contains __ 成员测试 item in x
__getitem __ 索引、分片 x[i]、x[i:j]、没有__iter__的for循环等
__setitem __ 索引赋值 x[i]=值、x[i:j]=序列对象
__len __ 求长度 len(x)
__iter __、__next __ 迭代 Iter(x)、next(x)、for循环等

加法运算符重载

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class test:
def __init__(self,a): #构造方法
self.data = a[:]
def __add__(self, obj): #实现加法运算方法的重载,将两个列表对应元素相加(列表长度需要一样)
x = len(self.data)
y = len(obj.data)
max = x if x>y else y
nl = []
for n in range(max):
nl.append(self.data[n]+obj.data[n])
return test(nl[:])
x = test([1,2,4])
y = test([5,7,9])
z = x + y
print(z.data) #输出 [6, 9, 13]

__getitem__方法

在对实例执行索引、分片或for循环时,调用__getitem__方法。例如:

1
2
3
4
5
6
7
8
9
10
class test:
def __init__(self,a): #构造方法
self.data = a[:]
def __getitem__(self,index):#定义索引、分片重载方法
return self.data[index]

x = test([10,20,30,40])
print(x[2]) #输出30
print(x[:]) #输出[10, 20, 30, 40]
print(x[:3]) #输出[10, 20, 30],分片返回部分值

setitem方法

在通过赋值语句给索引或分片赋值时,调用__setitem__方法,实现对序列对象的修改。例如:

1
2
3
4
5
6
7
8
9
10
11
class test:
def __init__(self,a): #构造方法
self.data = a[:]
def __setitem__(self,index,value):#重载索引、分片赋值运算方法
self.data[index] = value

x = test([10,20,30,40])
x[0] = 100 #修改列表第一个值
print(x.data) #输出[100, 20, 30, 40]
x[1:3] = ['a','b','c'] #将列表中的分片[1:3]替换
print(x.data) #输出[100, 'a', 'b', 'c', 40]