面向对象思想
我们先来介绍一下两大编程思想,其一是面向过程,它强调功能上的封装,运用的是简单的线性思维,二是面向对象,它主要是对属性和行为上的封装,处理复杂的事物。
面向过程的典型语言是C语言,面向对象的典型代表就是Python和Java。
面向过程和面向对象都是解决实际问题的一种思维方式,二者是相辅相成的。通过面向对象我们可以从宏观上把握事物之间复杂的关系,方便我们分析整个系统,而具体到每个微观操作,仍旧要用面向过程的方法。
类与对象
接下来讲解一下类的定义:由多个对象抽取出“像”的属性和行为从而归纳总结出来的一种类别。
在Python中一切皆对象!
1、自定义数据类型的语法结构是:
class 类名():
pass
这里的类名是自己取定的,注意首字母要大写。类名后的小括号可以省略。
2、创建对象的语法架构为:
对象名=类名()
实际地来理解,类就是模板,而对象则是具体实例。看几个简单的例子:
class Person(): # 自定义类
pass
class Student():
pass
per=Person() # 创建对象
stu=Student()
print(type(per))
print(type(stu))
# 输出结果:
# <class '__main__.Person'>
# <class '__main__.Student'>
类的组成
现在我们详细说明一下类的组成:
1、类属性:直接定义在类中,方法外的变量
2、实例属性:定义在__init__方法中,使用self打点的变量
3、实例方法:定义在类中的函数,且自带参数self
4、静态方法:使用装饰器@staticmethod修饰的方法
5、类方法:使用装饰器@classmethod修饰的方法
下面我们开一个定义的比较完整的类及其调用:
class Student:
# 类属性,定义在类中,方法外的变量
school='上海university'
# 初始化方法
def __init__(self, xm, age): # xm,age是方法的参数,是局部变量,xm,age的作用域是整个__init__方法
self.name=xm # =左侧是实例属性,xm是局部变量,将局部变量的值xm赋值给实例属性self.name
self.age=age # 实例的名称和局部变量的名称可以相同
# 类中的函数称为方法
def show(self):
print(f'我叫:{self.name}.今年:{self.age}岁了')
# 静态方法
@staticmethod
def sm():
print('这是一个静态方法,不能使用实例属性和实例方法')
# 这部分的代码若使用例如self.name将会报错
# 类方法
@classmethod
def cm(cls):
print('这是一个类方法,不能使用实例属性和实例方法')
# 创建类的对象
stu=Student('abc', 18) # 传两个参数是因为__init__方法中有两个形参
# 实例属性,使用对象名进行打点调用
print(stu.name, stu.age)
# 类属性,直接使用类名打点调用
print(Student.school)
# 实例方法,使用对象名打点调用
stu.show()
# 类方法,类名打点调用
Student.cm()
# 静态方法,类名打点调用
Student.sm()
接下来再补充一个用类模板创建多个对象的方法:
# 这里我们要继续使用上面定义的student类(不重复展示)
stu1=Student('abc', 18)
stu2=Student('Peter', 25)
stu3=Student('march', 17)
stu4=Student('xing', 100)
print(type(stu1)) # 类型都是一样的
Student.school='×××大学'
lst=[stu1, stu2, stu3, stu4]
for item in lst:
item.show()
# 输出:
# <class '__main__.Student'>
# 我叫:abc.今年:18岁了
# 我叫:Peter.今年:25岁了
# 我叫:march.今年:17岁了
# 我叫:xing.今年:100岁了
动态绑定属性与方法
使用类创建多个对象时,虽然每个对象的属性名称相同,但是属性值不同。我们可以为每个对象绑定独有的属性或者方法。
举个简单的例子,我们继续使用上文定义的Student类,假设我们创建一个对象:
stu=Student('abc', 18)
现在想要多加一个性别的属性,可以直接 stu.gender='man' 此时就能直接调用stu.gender。
即:可直接进行 stu.属性/方法 = ...... 即是这个属性或方法未在类中定义。
面向对象的三大特征
现在我们介绍这一部分的主要知识:面向对象的三大特征。希望以此与面向过程区别开来。
①封装:隐藏内部细节,对外提供操作方式。②继承:是在函数调用时,使用“形参名称=值”的方式进行传参,传递参数顺序可以与定义时参数的顺序不同。③多态:是在函数定义时,直接对形式参数进行赋值,在调用时如果该参数不传值,将使用默认值,如果该参数传值,则使用传递的值。
一、封装。
讲到封装,即隐藏内部细节,就要讲到外界访问的权限控制。Python中的权限控制,是对属性/方法添加单下划线、双下划线、首尾双下划线来实现的。
单下划线开头:表示protected的成员,被视为仅供内部使用,允许类本身和子类访问(子类的部分将在后面说明)。PS:实际上也还是可以被外部访问的。
双下划线开头:表示private的成员,只允许定义该属性或方法的类本身访问。
首尾双下划线:一般表示特殊的方法。
这里我们再重新定义的一个student类来展示一下权限控制:
class Student():
def __init__(self, name, age, gender):
self._name=name # 受保护,只能本类,子类访问
self.__age=age # 私有的,只能类本身访问
self.gender=gender # 普通的实例属性
def _fun1(self):
print('子类及本身可以访问')
def __fun2(self):
print('只有定义的类可以访问')
def show(self):
self._fun1()
self.__fun2()
print(self._name)
print(self.__age)
stu=Student('abc', 18, 'nan')
print(stu._name)
# print(stu.__age) # 报错AttributeError: 'Student' object has no attribute '__age'. Did you mean: '_name'?
stu._fun1()
# stu.__fun2() # 报错AttributeError: 'Student' object has no attribute '__fun2'. Did you mean: '_fun1'?
# 私有的强行访问
print(stu._Student__age)
stu._Student__fun2()
print(dir(stu)) # 这里dir()函数可用于展示对象中所有的属性和方法
上述代码中使用了 print(stu._Student__age)去强行访问该类的私有属性,这种方法并不十分可取,在Python中我们可以使用装饰器来改善这一做法,这里就讲到Python中的属性设置:
①使用@property修改方法将方法转化成属性使用。举例说明:
class Student:
def __init__(self, name, gender):
self.name=name
self.__gender=gender # 这里的self.__gender是私有的实例属性
# 使用@property修改方法,将方法转成属性使用
@property
def gender(self):
return self.__gender
stu=Student('abc', 'man')
print(stu.name, '的性别是:', stu.gender) # stu.gender就会执行stu.gender()
如果尝试修改属性值,例如stu.gender='woman' 将会报错AttributeError,但可以——
②@属性.setter 将属性设置为可写属性。
现在只需要在上述定义的类中加入下段代码,之后可以修改gender属性:
# 将gender这个属性设置为可写属性
@gender.setter
def gender(self, value):
if value != 'man' and value != 'woman':
print('性别有误,默认为man')
self.__gender='man'
else:
self.__gender=value
二、继承
首先说明程序设计中子类与父类的概念:被继承的类称为父类(或者基类),新的类称为子类(或者派生类),子类如若继承了父类,就用拥有了父类所有的公有成员以及受保护的成员。
Python中的继承有如下三个特点:①在Python中一个子类可以继承多个父类;②一个父类可拥有多个子类;③若一个类未继承任何类,默认继承object类。
继承的语法结构为:
class 类名(父类1, 父类2,...,父类N):
pass
这里我们用一个例子展示一下继承(实际是单一继承)的代码:
class Person: # 默认继承了object
def __init__(self, name, age):
self.name=name
self.age=age
def show(self):
print(f'大家好,我叫:{self.name},我今年:{self.age}岁')
# Student继承Person类
class Student(Person):
# 初始化方法
def __init__(self, name, age, stuno):
super().__init__(name, age) # 调用父类的初始化方法
self.stuno=stuno # 进行赋值
# doctor继承Person类
class Doctor(Person):
def __init__(self, name, age, department):
super().__init__(name, age)
self.department=department
# 创建子类对象
stu=Student('abc', 18, '0731')
stu.show()
doctor=Doctor('zbc', 48, '外科')
doctor.show()
与其他编程语言不一样的一点,是Python中单一子类可继承多个父类(用逗号隔开),这即是Python的多继承:
class FatherA():
def __init__(self, name):
self.name=name
def showA(self):
print('父类A中的方法')
class FatherB():
def __init__(self, name):
self.name = name
def showB(self):
print('父类B中的方法')
# 多继承
class Son(FatherA, FatherB):
def __init__(self, name, age, gender):
# 需要调用两个父类的初始化方法
FatherA.__init__(self, name)
FatherB.__init__(self, name)
self.gender=gender
son=Son('abc', 19, '男') # 调用Son类中的__init__执行
son.showA()
son.showB()
子类继承了父类就拥有了父类中公有成员和受保护的成员,但父类的方法不一定完全适合子类的需要,此时子类也可以重写父类的方法。这即是Python的方法重写。
子类在重写父类的方法时,要求方法的名称必须与父类方法的名称相同,在子类重写后的方法中可以通过super().xxx()调用父类中的方法。
我们再看到继承的例子,实际上上述代码中学生的学号、医生的科室是没有被展示出来的,因为父类的方法中没有第三个参数的展示,此时可以方法重写:
class Person: # 默认继承了object
def __init__(self, name, age):
self.name=name
self.age=age
def show(self):
print(f'大家好,我叫:{self.name},我今年:{self.age}岁')
# Student继承Person类
class Student(Person):
# 初始化方法
def __init__(self, name, age, stuno):
super().__init__(name, age) # 调用父类的初始化方法
self.stuno=stuno # 进行赋值
def show(self):
super().show() # 调用父类中的方法
print(f'我来自***大学,学号是:{self.stuno}')
# doctor继承Person类
class Doctor(Person):
def __init__(self, name, age, department):
super().__init__(name, age)
self.department=department
def show(self):
# 此处同样可选择调用父类中的方法
print(f'大家好,我叫:{self.name},我今年{self.age}岁,我的工作科室是:{self.department}')
# 创建第一个子类对象
stu=Student('abc', 18, '0731')
stu.show() # 调用的都是子类自己的show()方法
三、多态
多态指的就是“多种形态”,即便不知道一个变量所引用的对象到底是什么类型,任然可以通过这个变量调用对象的方法。
在程序运行过程中我们可以根据所引用对象的数据类型,动态决定调用哪个对象中的方法。
Python语言在的多态,根本不关心对象的数据类型,也不关心类之间是否存在继承关系,只关心对象的行为(方法)。只要不同的类中有相同的方法,即可实现多态。
class Person():
def eat(self):
print('人,吃五谷杂粮')
class Cat():
def eat(self):
print('猫,喜欢吃鱼')
class Dog():
def eat(self):
print('狗,喜欢吃骨头')
# 这三个类都有一个同名的方法
# 编写一个函数
def fun(obj): # obj是函数的形参,定义时不知道它的数据类型
obj.eat() # 直接通过变量obj(对象)调用eat方法
# 创建三个类的对象
per=Person()
cat=Cat()
dog=Dog()
# 调用fun函数,Python中的多态,不关心对象的数据类型,只关心是否具有同名方法
fun(per)
fun(cat)
fun(dog)
object类
接着我们讲解一下上文已经多次出现过的object类。在Python中,它是所有类直接或间接的父类,所有类都拥有object类的属性和方法。先介绍几个object类中的特殊方法:
object类中的特殊方法 | 功能描述 |
__new__() | 由系统调用,用于创建对象 |
__init__() | 创建对象时手动调用,用于初始化对象属性值 |
__str__() | 对象的描述,返回值是str类型,默认输出对象的内存地址 |
下面我们用dir()函数展示一下object类的所有属性和方法:
class Person(object):
def __init__(self, name, age):
self.name=name
self.age=age
def show(self):
print(f'大家好,我叫:{self.name},我今年:{self.age}岁了')
# 创建Person类的对象
per=Person('abc', 18) # 创建对象时会自动调用__init__方法
print(dir(per))
print(per) # 自动调用了__str__方法
# 输出结果:
# ['__class__', '__delattr__', '__dict__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__',
# '__getattribute__', '__getstate__', '__gt__', '__hash__', '__init__', '__init_subclass__',
# '__le__', '__lt__', '__module__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__',
# '__setattr__', '__sizeof__', '__str__', '__subclasshook__', '__weakref__', 'age', 'name', 'show']
# <__main__.Person object at 0x000001646EA2F110>
有关__str__()对对象的描述,格式可能无法达到我们的需求,此时涉及到方法重写:
# str方法重写
class Person(object):
def __init__(self, name, age):
self.name=name
self.age=age
# 方法重写
def __str__(self):
return '这是一个人,具有name和age两个实例属性'
# 创建Person类的对象
per=Person('abc', 18)
print(per) # 不再输出内存地址,直接输出对象名,实际是调用__str__方法
print(per.__str__()) # 手动调用的写法,但实际是默认调用的
对象的特殊方法、属性
在Python中首尾双下划线的方法都是特殊方法,实际上还有很多运算或者说运算符也可以用特殊方法实现,下面我们来总结一些:
运算符 | 特殊方法 | 功能描述 |
+ | __add__() | 执行加法运算 |
- | __sub__() | 执行减法运算 |
<, <=, == | __lt__(), __le__(), __eq__() | 执行比较运算 |
>, >=, != | __gt__(), __ge__(), __ne__() | 执行比较运算 |
*, / | __mul__(), __truediv__() | 执行乘法运算,非整除运算 |
%, // | __mod__(), __floordiv__() | 执行取余运算,整除运算 |
** | __pow__() | 执行幂运算 |
以下代码可以简单进行演示:
a=10
b=20
print(dir(a)) # Python中一切皆对象
print(a+b)
print(a.__add__(b))
print(a.__sub__(b))
print(a.__lt__(b))
print(a.__le__(b))
print(a.__eq__(b))
print(a.__gt__(b))
print(a.__ge__(b))
print(a.__ne__(b))
print(a.__mul__(b))
print(a.__truediv__(b))
print(a.__mod__(b))
print(a.__floordiv__(b))
print(a.__pow__(b))
同样,首尾双下划线定义的属性称为特殊属性:
特殊属性 | 功能描述 |
obj.__dict__ | 对象的属性字典 |
obj.__class__ | 对象所属的类 |
class.__bases__ | 类的父类元组 |
class.__base__ | 类的父类 |
class.__mro__ | 类的层次结构 |
class.__subclasses__() | 类的子类列表 |
依旧给出一段代码用作演示,读者可自己尝试查看结果:
class A:
pass
class B:
pass
class C(A, B):
def __init__(self, name, age):
self.name=name
self.age=age
a=A()
b=B()
c=C('abc', 18)
print('对象a的属性字典:', a.__dict__)
print('对象b的属性字典:', b.__dict__)
print('对象c的属性字典:', c.__dict__)
print('对象a所属的类:', a.__class__)
print('对象b所属的类:', b.__class__)
print('对象c所属的类:', c.__class__)
print('A类的父类元组:', A.__bases__)
print('B类的父类元组:', B.__bases__)
print('C类的父类元组:', C.__bases__)
print('A的父类:', A.__base__)
print('B的父类:', B.__base__)
print('C的父类:', C.__base__) # 如果父类继承了很多,结果只显示第一个父类
# 层次结构,C类继承了A类,B类,间接继承了object类
print('A类的层次结构:', A.__mro__)
print('B类的层次结构:', B.__mro__)
print('C类的层次结构:', C.__mro__)
类的深拷贝与浅拷贝
最后我们来讲一下类的深拷贝与浅拷贝,为了理解这一部分内容,先要说明变量的赋值。变量赋值时,只是形成两个变量,实际上还是指向同一个对象。下面给出演示:
# 两个相互独立的类
class CPU():
pass
class Disk():
pass
class Computer():
# 计算机有CPU和硬盘
def __init__(self, cpu, disk):
self.cpu=cpu
self.cpu=cpu
self.disk=disk
cpu=CPU() # 创建了一个CPU对象
disk=Disk() # 创建了一个硬盘对象
# 创建了一个计算机对象
com=Computer(cpu, disk)
# 变量(对象)的赋值
com1=com
print(com, '子对象的内存地址', com.cpu, com.disk)
print(com1, '子对象的内存地址', com1.cpu, com1.disk)
可以得到输出结果是:
<__main__.Computer object at 0x000002E81C24F410> 子对象的内存地址 <__main__.CPU object at 0x000002E81C24F470> <__main__.Disk object at 0x000002E81C24F3E0>
<__main__.Computer object at 0x000002E81C24F410> 子对象的内存地址 <__main__.CPU object at 0x000002E81C24F470> <__main__.Disk object at 0x000002E81C24F3E0>
发现对com和赋了com值的com1,自身及子对象的地址都是相同的。(更进一步理解,创建对象时,com和com1所指向的内存空间是一样的)
浅拷贝:
拷贝时,对象包含的子对象内容不拷贝,因此,源对象与拷贝对象会引用同一个子对象。
深拷贝:
使用copy模块的deepcopy函数,递归拷贝对象中包含的子对象,源对象和拷贝对象所有的子对象也不相同。
下部分演示代码需要先导入模块copy,使用的CPU对象等沿用上文代码。
# 类对象的浅拷贝
import copy
com2=copy.copy(com) # com2是新产生的对象,com2的子对象,cpu,disk不变
print(com, '子对象的内存地址', com.cpu, com.disk)
print(com2, '子对象的内存地址', com2.cpu, com2.disk)
# 输出后可以看到com和com2的地址不同,而子对象的仍然是相同的
# 类对象的深拷贝
print('-'*30)
com3=copy.deepcopy(com) # com3是新产生的对象,com3的子对象,cpu,disk也会重新创建
print(com, '子对象的内存地址', com.cpu, com.disk)
print(com3, '子对象的内存地址', com3.cpu, com3.disk)
# 输出后可以看到com和com3及子对象的地址均不同
总结一下,就是变量的赋值不会产生新对象,浅拷贝会产生新对象,但子对象不变,深拷贝不仅会创建新对象,子对象也会重新创建。
以上就是Python中面向对象的全部基本内容了。