面向对象
一、编程思维
1.1、面向过程编程
核心是过程二字, 过程指的是解决问题的步骤,即先干什么在干什么
基于该思想编写程序好比在设计一条流水线,是一种机械式的思维方式
优点: 复杂的问题流程化,进而简单化
缺点: 扩展性差
1.2、面向对象编程
面向对象编程:核心是对象二字, 而对象是特征与技能的结合体
优点: 可扩展性强
缺点: 编程的复杂度要高于面向过程
对象不仅包含一系列数据(自己独有的+与其它对象共有的),还专门包含操作该数据的方法
二、类
2.1、什么是类
# 什么是类
类即类别、种类,是面向对象设计最重要的概念,对象是特征与技能的结合体,而类则是一系列对象相似的特征与技能的结合体, 强调:站的角度不同,其总结出的类必然不同。
在程序中:务必保证先定义类,后产生对象
这与函数的使用是类似的,先定义函数,后调用函数,类也是一样的,在程序中需要先定义类,后调用类
不一样的是,调用函数会执行函数体代码返回的是函数体执行的结果,而调用类会产生对象,返回的是对象
# 1、先定义类
# 类体代码会在类定义阶段就立刻执行,会产生类的名称空间
类.__dict__ 查看类属性, 类的本身其实就是一个容器/名称空间,用来存放名字
python为我们提供专门访问属性(名称空间中的名字)的语法,点后的都是属性
# 后调用类产生的对象,调用类的过程,又称为类的实例化,实例化的结果称为类的对象/实例
调用类会得到一个返回值,该返回值就是类的一个具体存在的对象/实例
2.2、 __init__
为对象定义独有的属性
# 类.属性, 就能直接添加类的属性, 对象本质就是一个名称空间,对象的名称空间是用来存放对象自己对象独有的属性
# 类的名称空间中存放的是对象们共有的属性
# 推荐使用方式三
方式一
### 创建一个类
class Teacher(object):
name = "xiong"
def tclass(self):
print("班级")
t1 = Teacher() # 生成类对象
t1.name = "hei"
t1.age = 100
t1.sex = "male" # 创建属性
print(t1.__dict__) # 查看属性内容 {'name': 'hei', 'age': 100, 'sex': 'male'}
方式二
# 方式二、为对象初始化自己独有的特征
def init(obj, x, y, z):
obj.name = x
obj.age = y
obj.sex = z
t2 = Teacher() # 创建两个空对象,
t3 = Teacher()
init(t2, "bai", 100, "male") # 初始化属性 {'name': 'bai', 'age': 100, 'sex': 'male'}
init(t3, "hong", 100, "female") # 初始化属性 {'name': 'hong', 'age': 100, 'sex': 'female'}
print(t2.__dict__)
print(t3.__dict__)
方式三
# 方式三、 __init__ 初始化属性, 在定义类之初要求初始属性,为每个对象创建自己独有的属性
class Teacher(object):
def __init__(self, name, age ,sex):
self.name =name
self.age = age
self.sex = sex
t1= Teacher("大黑熊", 100, "male")
t2= Teacher("大白熊", 100, "female")
t3= Teacher("大灰熊", 100, "female")
print(t1.__dict__) # {'name': '大黑熊', 'age': 100, 'sex': 'male'}
print(t2.__dict__) # {'name': '大白熊', 'age': 100, 'sex': 'female'}
print(t3.__dict__) # {'name': '大灰熊', 'age': 100, 'sex': 'female'}
# 强调: __init__
# 1、该方法内可以有任意的python代码
# 2、一定不能有返回值
2.3、属性查找
# 先从对象自己的名称空间找,没有则从类中找,如果类也没有就报错
# 类名称空间中,类中定义的数据属性和函数属性都是共享给所有对象使用的
# 对象名称空间中定义的只有数据属性,而且是对象独有的属性
类有两种属性:数据属性和函数属性
1. 类的数据属性是所有对象共享的
2. 类的函数属性是绑定给对象用的
class Tests:
count = 0
def __init__(self, name):
self.name = name
Tests.count += 1 # 绑定类属性, 每次创建类对象时都会加1, 通用属性
t1 = Tests("大黑熊")
t2 = Tests("大白熊")
t3 = Tests("大灰熊")
print(t3.count) # 创建几个对象就是多少、3
2.4、绑定关系
class Test:
def __init__(self, name):
self.name = name
def tcls(self):
print("这是test的绑定关系")
类中定义的函数是类的函数属性,类可以使用,但其实使用的就是一个普通的函数,意味着需要完全遵循函数的参数规则,该值几个值传几个
Test.tcls() # TypeError: tcls() missing 1 required positional argument: 'self'
Test.tcls("123") # 类可以用,但是有几个值就需要传几个值
类中定义的函数是类的函数属性,对象也可以使用,而且是绑定给对象用的, 绑定的效果: 绑定给谁,就应该由谁来调用 ,谁调用就将谁当作第一个参数自动传递, self会将调用的对象 自动传递(绑定方法)
t1 = Test("x")
t2 = Test("y")
print(id(t1.tcls), id(t2.tcls)) # 1986257763528 1986257763528
t1.tcls() 运行时会自动将t1做为属性传递给tcls(self)
# 补充:类中定义的函数,类确实可以使用,但其实类定义的函数大多情况下都是绑定给对象用的,所以在类中定义的函数都应该自带一个参数self
# 在python3中统一了类与类型的概念,类就是类型
三、继承与派生
# 什么是继承
继承是一种新建类的方式,新建的类称为字类或派生类,被继承的类称为父类 或超类或基类
继承的特性: 子类可以 “ 继承 ” 父类的全部属性
继承是类与类之间的关系 (子类与父类的区别)
# 为什么用继承
目的: 减少代码冗余
继承是面向对象三大基本特征之一(继承,封装,多态),继承就是子类继承父类的特征和行为,使得子类对象(实例)具有父类的实例域和方法,或子类从父类继承方法,使得子类具有父类相同的行为
# 如何使用继承
在python中支持一个类可以同时继承多个父类,
在Python3中如果一个类没有继承类,则默认继承Object类
Python2中一个类没有继承任何类,不会继承object类
使用
class Foo(object): pass 父类
class Bra(Foo): pass 子类
__bases__ 可以查看Python中子类继承的父类
class Foo:
def __init__(self, name):
self.name = name
def running(self):
print("{} 喜欢跑步".format(self.name))
class Hei(Foo):
pass
he = Hei("小黑")
he.running()
类区分
print(Hei.__bases__) # (<class '__main__.Foo'>,)
print(Foo.__bases__) # (<class 'object'>,) # 默认继承object类
新式类:
继承了Object的类以及该类的子类,都是新式类, Python3都会默认继承Object类
经典类
没有继承了Object的类以及该类的子类,都是经典类
派生
# 子类中新定义的属性,子类在使用时始终以自己为准
在子类派生出的新功能中重用父类功能的方式有两种
1、直接指定访问某一个类的函数,该方式与继承无关
class run:
def __init__(self, name, size, heiht):
self.name = name
self.size = size
self.heiht = heiht
def weight(self):
print("{0} 喜欢举重 体重{1} 身高{2}".format(self.name, self.size, self.heiht))
class Poo(run):
def __init__(self,name, size, heiht, level):
run.__init__(self, name, size, heiht) # 直接指定某一个类的函数
self.level = level
def running(self):
print("{}喜欢跑步".format(self.name))
p1 = Poo("xiong", 180, 180, 10)
print(p1.__dict__) # {'name': 'xiong', 'size': 180, 'heiht': 180, 'level': 10}
p1.running()
p1.weight()
3.1、菱形继承-mro
当一个子继承多个父类时,多个父类最终继承了同一个类,称为菱形继承
菱形继承,在不同类型下的查找区别
经典类下查找属性: 深度优先查找
新式类下查找属性: 广度优先查找
python到底是如何实现继承的,对于你定义的每一个类,python会计算出一个方法解析顺序(MRO)列表,这个MRO列表就是一个简单的所有基类的线性顺序列表,例如
示例
class G:
def test(self):
print("test G")
class D(G):
def test(self):
print("test D")
class F(G):
def test(self):
print("test F")
class C(F):
def test(self):
print("test C")
class E(G):
def test(self):
print("test E")
class B(E):
def test(self):
print("test B")
class A(B,C,D):
def test(self):
print("test A")
a=A()
a.test()
print(A.mro())
# 继承了object的就是新式类, 广度优先查找 [<class '__main__.A'>, <class '__main__.B'>, <class '__main__.E'>, <class '__main__.C'>, <class '__main__.F'>, <class '__main__.D'>, <class '__main__.G'>, <class 'object'>]
# 如果是老式类则是 深度优先 A --> B --> E --> G --> C --> F --> D
为了实现继承,python会在MRO列表上从左到右开始查找基类,直到找到第一个匹配这个属性的类为止。
而这个MRO列表的构造是通过一个C3线性化算法来实现的。我们不去深究这个算法的数学原理,它实际上就是合并所有父类的MRO列表并遵循如下三条准则:
- 子类会先于父类被检查
- 多个父类会根据它们在列表中的顺序被检查
- 如果对下一个类存在两个合法的选择,选择第一个父类
3.2、派生子类中调用父类的方法
方式一: 与继承无关, 直接用: 类名.函数名
class Bro:
def __init__(self, name, age):
self.name = name
self.age = age
class Stauts(Bro):
def __init__(self, name, age, data,):
Bro.__init(name,age)
self.data = data
def info(self):
print("name: {0} data: {1}".format(self.name, self.data))
sta1 = Stauts("xiong", 100, "2018")
sta1.info()
方式二: 严格以来继承属性查找关系
super()会得到一个特殊的对象,该对象就是专门用来访问父类中的属性(按照继承的关系),只能在python3中使用,完整写法 super(类名,self) 注意是逗号,super().方法()
注意super 是按照mro的列表为准
class Bro:
def __init__(self, name, age):
self.name = name
self.age = age
class Stauts(Bro):
def __init__(self, name, age, data,):
# super().__init__(name, age) # 一个特殊的对象,该对象就是专门用来访问父类中的属性
super(Stauts,self).__init__(name,age) # 完整写法
self.data = data
def info(self):
print("name: {0} data: {1}".format(self.name, self.data))
sta1 = Stauts("xiong", 100, "2018")
sta1.info()
即使没有直接继承关系,super仍然会按照mro继续往后查找
class C:
def testc(self):
print("test C")
class B:
def test(self):
super().testc()
print("test B")
class A(B,C):
pass
a=A()
a.test()
四、组合
1. 组合就是一个类的对象具备某一个属性,该属性的值是指向另外一个类的对象
2. 组合是通过对现有对象进行拼装即组合产生新的具有更复杂的功能
3. 组合也是用来解决类与类直接代码冗余
继承与组合之间的区别
当类与类之间存在明显的从属关系 就应该用继承,
示例
样式demo
#coding:utf-8
#
# 继承
class Prople:
def __init__(self, name, age, sex):
self.name = name
self.age = age
self.sex = sex
# 组合
class Course:
def __init__(self, cname, price):
self.cname = cname
self.price = price
def course_info(self):
print("cname: {0} price: {1}".format(self.cname, self.price))
class Teacher(Prople):
def __init__(self, name, age, sex,level):
Prople.__init__(self, name,age,sex)
self.level = level
def teacher(self):
print("this teacher name: {0} age: {1} sex:{2} level: {3}".format(self.name, self.age, self.sex, self.level))
# 生成一个老师 类对象
tea1 = Teacher("gg", 100, "male",100)
# 创建c2课程
c2 = Course("python", 100)
sta1.course = c1 # 将课程对象指向老师,让老师具备这个课程的所有属性,让一个类的对象具备另一个类对象的属性
print(sta1.__dict__)
print(tea1.__dict__)
# {'name': 'gg', 'age': 100, 'sex': 'male', 'course': <__main__.Course object at 0x0000000002201A20>}
sta1.course.course_info() # cname: python price: 100
五、多态
父类是用来指定一个标准, 不能被实例化, 多态性就是一种标准上的统一
多态指的是同一种事物的多种形态
多态性:
继承同一个类的多个子象中有相同的方法名,那么子类产生的对象就可以不用考虑具体的类型而直接调用功能
import abc
父类是用来制定标准化的,无法被初始化
示例
class File:
def read(self):
pass
class Txt(File): # 每个类都继承父类的方法,形成了标准上的统一
pass
class Sql(File):
pass
# 初始化每个类都能直接不用考虑具体类型而能够直接调用
################# 示例2 ################# 示例1不能达到固定上的每个标准统一
# 不推荐使用, 每个类都强制规定死了类型,耦合度太高
import abc
class File(metaclass=abc.ABCMeta):
@abc.abstractmethod
def read(self):
pass
class Txt(File):
def read(self):
pass
class Sql(File):
pass
# TypeError: Can't instantiate abstract class File with abstract methods read
# f = File()
# TypeError: Can't instantiate abstract class Txt with abstract methods read
t1 = Txt() # 但Txt这个类没有与File基类达到统一时会报错
################ 示例3 ################
# 推荐使用, 达到意识上的统一,每个类在调用时 也都无需考虑具体类型而能直接调用, 同时也节省了记各种方法名
class Txt:
def read(self):
pass
class Sql:
def read(self):
pass
class Office:
def read(self):
pass
六、封装
# 为啥要封装
封: 属性对外是隐藏,但对内是开放的
装: 申请一个名称空间,往里装入各种属性/方法
# 隐藏: 在属性前加上__开头
这种隐藏仅仅只是一种语法上的变形操作
这种语法上的变形只在类定义阶段发生一次
这种隐藏是对外不对内的,即在类的内部可以直接访问,而在类的外部无法直接访问,原因是在类定义阶段,类体内代码统一发生了一次变形
# 为什么要封装
1. 封装数据属性:
定义属性就是为了给类外部的使用
隐藏之后是为了不让外部直接使用, 需要类内部开辟一个接口,然后让类外部的使用通过接口来间接操作隐藏的属性 我们可以在接口这上附加任意逻辑,从而严格控制使用者对属性的操作
2. 封装函数属性
定义属性就是为了给类外部的使用使用的
隐藏函数属性是为了不让外部直接使用,需要类内部开辟一个接口
然后在接口内去调用隐藏的功能,精髓在于:隔离了复杂度
示例
class Foo:
__country = "china" # _Foo__country 对外隐藏
def __init__(self, name, age):
self.__name = name # 类在初始化执行的时候变形, 前面会加上 _Foo__name
self.__age = age
def tell_info(self):
print("{0}:{1}".format(self.__name,self.__age))
# 隐藏属性是内部开辟了一个接口, 控制使用者对内部属性的操作
f=Foo("xiong",111)
f.tell_info()
########################## 示例2 变形 #####################################
class Foo:
def __f1(self):
print("foo.f1")
def f2(self):
print("foo.f2")
self.__f1() # 仅在类定义会会产生变形, 所以在访问的时候就是 _Foo__f1
class Bar(Foo):
def f1(self):
print("bar.f1")
b1= Bar()
b1.f2() # foo.f2 foo.f1
七、property
property装饰器用于将被装饰的方法伪装成一个数据属性,在使用时不加括号而直接引用
class Human:
'''
bmi 用于计算肥胖值, bmi=体重(kg) / 身高(m)的平方
'''
def __init__(self, name, weight, height):
self.name = name
self.weight = weight
self.height = height
@property
def bmi(self):
return self.weight / (self.height ** 2)
h=Human("xiong", 91,1.76)
# 像这种数据结果返回的直接以类似数据属性的写法会更好就可以用到 property了
# print(h.bmi())
print(h.bmi)
CUDR
property 删修查操作
class Status:
def __init__(self, name):
self.__name = name
@property # 查看, 定义这个只允许查看
def name(self):
return "name:{0}".format(self.__name)
@name.setter # property 定义之后如果需要修改要定义setter装饰器
def name(self, name):
if not isinstance(name, str):
raise TypeError("定义的属性需要为str类型")
self.__name = name
@name.deleter
def name(self):
raise PermissionError("无法删除")
# del self.__name # 如果真要删除可直接定义del, 但如果都添加上 封装有何意义?
s1 = Status("xiong")
print(s1.name)
# 当定义property 没有定义@name.setter 方法时会报错AttributeError: can't set attribute
s1.name = "123"
print(s1.name)
del s1.name
八、类方法与对象方法
8.1、绑定方法-classmethod
特性: 绑定给谁就应该由谁来调用,谁调就会将谁的第一个参数自动传入, 一般用于配置文件绑定使用
绑定方法分为两类:
1.1 绑定给对象方法
在类内部定义的函数(没有被任何装饰器修饰的),默认就是绑定给对象的
1.2 绑定给类的方法
在类内部定义的函数如果被装饰器@classmethod装饰, 那么则是绑定给类的,应该由类来调用,类来调用就自动将第一个参数自动传入
示例
class Foo:
@classmethod # 绑定给类的方法
def f1(cls):
print(cls)
def f2(self): # 绑定给对象的方法
print(self)
f=Foo()
# 绑定给类的应该由类来调用, 但对象也可以直接调用, 只不过传入的仍然是类
print(f.f2) # <bound method Foo.f2 of <__main__.Foo object at 0x000000000221CBA8>>
print(Foo.f1) # <bound method Foo.f1 of <class '__main__.Foo'>>
# 如果不加@classmethod那么就是绑定给对象的, 类在调用函数的时候就是一个普通的函数调用
# functions xxxxxxxx
8.2、非绑定方法 staticmethod
一次性函数使用
类中定义的函数如果被装饰器staticmethod装饰,那么该函数就变成了非绑定关系
即不与类绑定,也不与对象绑定,意味着类与对象都能调用,都不会传值的效果,就是一个普通函数
8.3、应用
如果函数体代码需要用外部传入的类,则应该将函数定义成给类的方法
如果函数体代码需要用外部传入的对象,则应该将函数定义成给对象的方法
如果函数体代码即不需要用外部传入的对象也不需要外部传入的类,则应该将函数定义成非绑定关系/普通函数