Python从入门到精通之面向对象

面向过程与面向对象

面向过程的程序设计:核心是过程二字,过程指的是解决问题的步骤,即先干什么再干什么……面向过程的设计就好比精心设计好一条流水线,是一种机械式的思维方式。
优点是:复杂度的问题流程化,进而简单化(一个复杂的问题,分成一个个小的步骤去实现,实现小的步骤将会非常简单)
缺点是:改一个组件,牵一发而动全身。
应用场景:一旦完成基本很少改变的场景,著名的例子有Linux內核,git,以及Apache HTTP Server等。

面向对象的程序设计:核心是对象二字。对象是数据与功能函数的结合体。
优点:解决了程序的扩展性。对某一个对象单独修改,会立刻反映到整个体系中,如对游戏中一个人物参数的特征和技能修改都很容易。

缺点:

  1. 编程的复杂度远高于面向过程,不了解面向对象而立即上手基于它设计程序,极容易出现过度设计的问题。一些扩展性要求低的场景使用面向对象会徒增编程难度,比如管理linux系统的shell脚本就不适合用面向对象去设计,面向过程反而更加适合。

  2. 无法向面向过程的程序设计流水线式的可以很精准的预测问题的处理流程与结果,面向对象的程序一旦开始就由对象之间的交互解决问题,即便是上帝也无法准确地预测最终结果。于是我们经常看到对战类游戏,新增一个游戏人物,在对战的过程中极容易出现imba的技能,一刀砍死3个人,这种情况是无法准确预知的,只有对象之间交互才能准确地知道最终的结果。

应用场景:需求经常变化的软件,一般需求的变化都集中在用户层,互联网应用,企业内部软件,游戏等都是面向对象的程序设计大显身手的好地方

Python中对象的属性查找

类有两种属性:数据属性和函数属性

  1. 类的数据属性是所有对象共享的
  2. 类的函数属性是绑定给对象用的
class A:
    x = 1
    def foo(self):
        print('foo')

a = A()
b = A()
print(id(a.x), id(b.x)) # 10919424 10919424
print(a.foo, b.foo) 
# <bound method A.foo of <__main__.A object at 0x7f0e4f1d6fd0>>
# <bound method A.foo of <__main__.A object at 0x7f0e51a95be0>>
# 类的函数属性是绑定给对象使用的,obj.method称为绑定方法,内存地址都不一样

在obj.name会先从obj自己的名称空间里找name,找不到则去类中找,类也找不到就找父类…最后都找不到就抛出异常 。
注意:绑定到对象的方法的这种自动传值的特征,决定了在类中定义的函数都要默认写一个参数self,self可以是任意名字,但是约定俗成地写出self。如:

a.foo() # 相当于A.foo(a)

继承与派生

继承是一种创建新类的方式,新建的类可以继承一个或多个父类(python支持多继承),父类又可称为基类或超类,新建的类称为派生类或子类。子类会“”遗传”父类的属性,从而解决代码重用问题。

class A:
    pass

class B:
    pass

class C(A): # 单继承
    pass

class D(A, B):  # 多继承
    pass

查看继承

print(C.__bases__)
print(D.__bases__)
# __base__只查看从左到右继承的第一个子类,__bases__则是查看所有继承的父类

继承与抽象
继承描述的是子类与父类之间的关系,是一种什么是什么的关系。要找出这种关系,必须先抽象再继承
抽象

继承:是基于抽象的结果,通过编程语言去实现它,肯定是先经历抽象这个过程,才能通过继承的方式去表达出抽象的结构。
继承
子类可以访问呢父类的数据属性和函数,继承可以很好的解决代码重用问题。

派生
当然子类也可以添加自己新的属性或者在自己这里重新定义这些属性(不会影响到父类),需要注意的是,一旦重新定义了自己的属性且与父类重名,那么调用新增的属性时,就以自己为准了。

class Mylist(list):
    def append(self, value):
        super().append(value)
        print('添加【%s】 成功'%value)

l = Mylist()
l.append(100)
print(l)

组合与重用性

软件重用的重要方式除了继承之外还有另外一种方式,即:组合。
组合指的是,在一个类中以另外一个类的对象作为数据属性,称为类的组合。

>>> class Equip: #武器装备类
...     def fire(self):
...         print('release Fire skill')
... 
>>> class Riven: #英雄Riven的类,一个英雄需要有装备,因而需要组合Equip类
...     camp='Noxus'
...     def __init__(self,nickname):
...         self.nickname=nickname
...         self.equip=Equip() #用Equip类产生一个装备,赋值给实例的equip属性
... 
>>> r1=Riven('rock')
>>> r1.equip.fire() #可以使用组合的类产生的对象所持有的方法
release Fire skill

组合与继承都是有效地利用已有类的资源的重要方式。但是二者的概念和使用场景皆不同,

  1. 继承的方式
    通过继承建立了派生类与基类之间的关系,它是一种’是’的关系,比如白马是马,人是动物。
    当类之间有很多相同的功能,提取这些共同的功能做成基类,用继承比较好,比如老师是人,学生是人
  2. 组合的方式
    用组合的方式建立了类与组合的类之间的关系,它是一种‘有’的关系,比如教授有生日,教授教python和linux课程,教授有学生s1、s2、s3…
# 组合和继承的运用

class People:
    def __init__(self, name, age, sex):
        self.name=name
        self.age=age
        self.sex=sex

class Course:
    def __init__(self, name, period, price):
        self.name=name
        self.period=period
        self.price=price
    def tell_info(self):
        print('<%s %s %s>'%(self.name, self.period, self.price))

class Teacher(People):
    def __init__(self, name, age, sex, job_tittle):
        super().__init__(name, age, sex)
        self.job_tittle=job_tittle
        self.course=[]
        self.students=[]

class Student(People):
    def __init__(self, name, age, sex):
        super().__init__(name, age, sex)
        self.course=[]

lee = Teacher('Lee', 24, 'male', 'Python')
s1 = Student('Sam', 18, 'male')

python = Course('python', '3mons', 3000.0)
linux = Course('linux', '1mons', 1000)

lee.course.append(python)
s1.course.append(linux)

lee.students.append(s1)
s1.course.append(python)

for stu in lee.students:
    for cour in stu.course:
        cour.tell_info() 

注意: 当类之间有显著不同,并且较小的类是较大的类所需要的组件时,用组合比较好。

抽象类

抽象类是一个特殊的类,其特殊性在于只能被继承,不能被直接实例化。
Python中的抽象类写法:

import abc

class File(metaclass=abc.ABCMeta):
    all_type = 'file'
    @abc.abstractmethod
    def read(self):
        pass    
    @abc.abstractmethod
    def write(self):
        pass


class Txt(File):
    def read(self):
        print('文本文件读取')

    def write(self):
        print('文本文件写入')


class Mem(File):
    def read(self):
        print('内存文件读取')

    def write(self):
        print('内存文件写入')

txt_file = Txt()
mem_file = Mem()
txt_file.read()
txt_file.write()
mem_file.read()
mem_file.write()
+++++++++++++++++++++++++++++++++++++++++++++++++++
>>> file = File()   # 抽象类不可实例化
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: Can't instantiate abstract class File with abstract methods read, write

抽象类与接口
抽象类的本质还是类,指的是一组类的相似性,包括数据属性(如all_type)和函数属性(如read、write),而接口只强调函数属性的相似性,是功能函数的集合。
抽象类是一个介于类和接口直接的一个概念,同时具备类和接口的部分特性,可以用来实现归一化设计。
继承顺序
Python3中的类为新式类,在多继承情况下,查找属性按照广度优先的方式查找。

class A:
    def foo(self):
        print('from A')

class B(A):
    def foo(self):
        print('from B')

class C(A):
    def foo(self):
        print('from C')

class D(B):
    def foo(self):
        print('from D')

class E(C,D):
    def foo(self):
        print('from E')

e = E()
print(E.mro())
# [<class '__main__.E'>, <class '__main__.C'>, <class '__main__.D'>, <class '__main__.B'>, <class '__main__.A'>, <class 'object'>]
# 注: 只有出现菱形继承时,才会按照广度优先的方式查找,若无菱形继承,还是以深度优先的方式查找!

继承原理
python到底是如何实现继承的,对于你定义的每一个类,python会计算出一个方法解析顺序(MRO)列表,这个MRO列表就是一个简单的所有基类的线性顺序列表,例如:

>>> F.mro() #等同于F.__mro__
[<class '__main__.F'>, <class '__main__.D'>, <class '__main__.B'>, <class '__main__.E'>, <class '__main__.C'>, <class '__main__.A'>, <class 'object'>]

为了实现继承,python会在MRO列表上从左到右开始查找基类,直到找到第一个匹配这个属性的类为止。
而这个MRO列表的构造是通过一个C3线性化算法来实现的。我们不去深究这个算法的数学原理,它实际上就是合并所有父类的MRO列表并遵循如下三条准则:
1. 子类会先于父类被检查
2. 多个父类会根据它们在列表中的顺序被检查
3. 如果对下一个类存在两个合法的选择,选择第一个父类

子类中调用父类的方法

方法一: 按照 父类名.父类方法的形式

class A:
    def __init__(self, name):
        self.name = name


class B(A):
    def __init__(self, name, age):
        A.__init__(self, name)
        self.age = age

方法二: super()

class A:
    def __init__(self, name):
        self.name = name


class B(A):
    def __init__(self, name, age):
        super().__init__(name)  # 相当于super(B, self),首先找到B的父类A,然后将self也就是B对象转化成A对象,然后调用A对象的__init__方法
        self.age = age

了解部分
即使没有直接继承关系,super仍然会按照mro继续往后查找

class A:
    def test(self):
        super().test()
class B:
    def test(self):
        print('from B')
class C(A,B):
    pass

c=C()
c.test() #打印结果:from B

print(C.mro())
#[<class '__main__.C'>, <class '__main__.A'>, <class '__main__.B'>, <class 'object'>]

方法一与方法二的区别:

# 方法一
class A:
    def __init__(self):
        print('A的构造方法')
class B(A):
    def __init__(self):
        print('B的构造方法')
        A.__init__(self)


class C(A):
    def __init__(self):
        print('C的构造方法')
        A.__init__(self)


class D(B,C):
    def __init__(self):
        print('D的构造方法')
        B.__init__(self)
        C.__init__(self)

    pass
f1=D() #A.__init__被重复调用
'''
D的构造方法
B的构造方法
A的构造方法
C的构造方法
A的构造方法
'''


# 方法二 使用super()
class A:
    def __init__(self):
        print('A的构造方法')
class B(A):
    def __init__(self):
        print('B的构造方法')
        super(B,self).__init__()


class C(A):
    def __init__(self):
        print('C的构造方法')
        super(C,self).__init__()


class D(B,C):
    def __init__(self):
        print('D的构造方法')
        super(D,self).__init__()

f1=D() #super()会基于mro列表,往后找
'''
D的构造方法
B的构造方法
C的构造方法
A的构造方法
'''

当你使用super()函数时,Python会在MRO列表上继续搜索下一个类。只要每个重定义的方法统一使用super()并只调用它一次,那么控制流最终会遍历完整个MRO列表,每个方法也只会被调用一次(注意注意注意:使用super调用的所有属性,都是从MRO列表当前的位置往后找,千万不要通过看代码去找继承关系,一定要看MRO列表)。

封装

属性的隐藏
在python中用双下划线开头的方式将属性隐藏起来(设置成私有的)

#其实这仅仅这是一种变形操作且仅仅只在类定义阶段发生变形
#类中所有双下划线开头的名称如__x都会在类定义时自动变形成:_类名__x的形式:

class A:
    __x=0 #类的数据属性就应该是共享的,但是语法上是可以把类的数据属性设置成私有的如__x,会变形为_A__x
    def __init__(self):
        self.__x=10 #变形为self._A__x
    def __foo(self): #变形为_A__foo
        print('from A')
    def bar(self):
        self.__foo() #只有在类内部才可以通过__foo的形式访问到

# 在外部,A._A__x是可以访问到的,但无法通过__x这个名字访问到。

注意:
1. 这种机制也并没有真正意义上限制我们从外部直接访问属性,知道了类名和属性名就可以拼出名字:_类名_、_属性,然后就可以访问了,如a._A__x,即这种操作并不是严格意义上的限制外部访问,仅仅只是一种语法意义上的变形,主要用来限制外部的直接访问。
2. 变形的过程只在类的定义时发生一次,在定义后的赋值操作,不会变形。

>>> a.__y=9
>>> a.__dict__
{'__y': 9}
  1. 在继承中,父类如果不想让子类覆盖自己的方法,可以将方法定义为私有的

封装并不是单纯意义上的隐藏
封装的真谛在于明确地区分内外,封装的属性可以直接在内部使用,而不能被外部直接使用,然而定义属性的目的终归是要用,外部要想用类隐藏的属性,需要我们为其开辟接口,让外部能够间接地用到我们隐藏起来的属性,那这么做的意义何在???
1. 封装数据:将数据隐藏起来这不是目的。隐藏起来然后对外提供操作该数据的接口,然后我们可以在接口附加上对该数据操作的限制,以此完成对数据属性操作的严格控制。
2. 封装方法:目的是隔离复杂度,让用户不去纠结内部实现。

拓展知识:
python并不会真的阻止你访问私有的属性,模块也遵循这种约定,如果模块名以单下划线开头,那么from module import *时不能被导入,但是你from module import _private_module依然是可以导入的,仅仅是打破了规范。

property
property是一种特殊的属性,访问它时会执行一段功能(函数)然后返回值。

>>> class Room:
...     def __init__(self, x, y):
...             self.x = x
...             self.y = y
...     @property
...     def area(self):
...             return self.x*self.y
...
>>> r = Room(4,5)
>>> r.area
20

C++中的属性有public、protect和private三个级别,python并没有在语法上把它们三个属性级别内建到自己的class机制中,在C++里一般会将所有的所有的数据都设置为私有的,然后提供set和get方法(接口)去设置和获取,在python中通过property方法可以实现:

class Foo:
    def __init__(self, val):
        self.__NAME = val
    @property
    def name(self):
        return self.__NAME
    @name.setter
    def name(self, val):
        self.__NAME = val
    @name.deleter
    def name(self):
        del self.__NAME
        print('删除属性成功!')

f = Foo('lee')
print(f.name)
f.name = 'sam'
del f.name

绑定方法与非绑定方法

绑定方法
绑定给谁,谁来调用就自动将它本身当作第一个参数传入。包括:
1. 绑定到类的方法:@classmethod
为类量身定制,类.boud_method():自动将类当作第一个参数传入(其实对象也可调用,但仍将类当作第一个参数传入)
2. 绑定到对象的方法:没有被任何装饰器装饰的方法
为对象量身定制,对象.boud_method(),自动将对象当作第一个参数传入

非绑定方法
@staticmethod
不与类或对象绑定,类和对象都可以调用,但是没有自动传值那么一说,只是一个普通的函数。

猜你喜欢

转载自blog.csdn.net/u010525694/article/details/80225173
今日推荐