面向对象编程

一、什么是面向对象的程序设计及为什么要有他

  面向对象的程序设计的核心是对象,要理解对象为何物,必须把自己当成上帝,上帝眼里世间存在的万物皆为对象,不存在的也可以创造出来。

  优点:

  解决了程序的扩展性,对某一个对象单独修改,会立刻反应到整个体系中,如对游戏中一个人物参数的特征和技能修改都很容易。

  缺点:

  可控性差,无法像面向过程的程序设计流水线式的可以很精准的预测问题的处理流程与结果,面向对象的程序一旦开始就由对象之间的交互解决问题,即便是上帝也无法预测最终结果。于是我们经常看到一个游戏英雄某一参数的修改极有可能导致硬霸的技能出现,一刀砍死好几个人,这个游戏就市区平衡。

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

面向对象的程序设计并不是全部,对于一个软件质量来说,面向对象的程序设计只是用来解决扩展性

 

二、类和对象

  1.什么是类和对象

  python中一切皆为对象,类型就是类,对象是特征(变量)与技能(函数)的结合体,而类是一系列对象共同的特征与技能的结合体。

  2.类相关知识

  (1)在python中声明函数与声明类很相似

  声明函数

def functionname(args):
    '函数文档字符串'
    函数体

  声明类

 
 1 '''
 2 class 类名:
 3     '类的文档字符串'
 4     类体
 5 '''
 6 
 7 #我们创建一个类
 8 class Data:
 9     pass 
 

  (2)类有两种作用:属性引用和实例化

  创建一个类

class Garen:        #定义英雄盖伦的类,不同的玩家可以用它实例出自己英雄;
    camp='Demacia'  #所有玩家的英雄(盖伦)的阵营都是Demacia;
    def attack(self,enemy):   #普通攻击技能,enemy是敌人;
        enemy.life_value-=self.aggressivity #根据自己的攻击力,攻击敌人就减掉敌人的生命值。

  1)属性引用

 
>>> Garen.camp #引用类的数据属性,该属性与所有对象/实例共享
'Demacia'
>>> Garen.attack #引用类的函数属性,该属性也共享
<function Garen.attack at 0x101356510>
>>> Garen.name='Garen' #增加属性
>>> del Garen.name #删除属性
 

  2)实例化

  类名加括号就是实例化,会自动触发__init__函数的运行,可以用它来为每个实例定制自己的特征

 
class Garen:        #定义英雄盖伦的类,不同的玩家可以用它实例出自己英雄;
    camp='Demacia'  #所有玩家的英雄(盖伦)的阵营都是Demacia;
    def __init__(self,nickname,aggressivity=58,life_value=455): #英雄的初始攻击力58...;
        self.nickname=nickname  #为自己的盖伦起个别名;
        self.aggressivity=aggressivity #英雄都有自己的攻击力;
        self.life_value=life_value #英雄都有自己的生命值;
    def attack(self,enemy):   #普通攻击技能,enemy是敌人;
        enemy.life_value-=self.aggressivity #根据自己的攻击力,攻击敌人就减掉敌人的生命值。

  实例化:类名+括号

>>> g1=Garen('草丛伦') #就是在执行Garen.__init__(g1,'草丛伦'),然后执行__init__内的代码g1.nickname=‘草丛伦’等

  self的作用是在实例化时自动将对象/实例本身传给__init__的第一个参数,self可以是任意名字,但是瞎几把写别人就看不懂了。

  这种自动传递的机制还体现在g1.attack的调用上,后续会介绍。

   查看类的属性

一:我们定义的类的属性到底存到哪里了?有两种方式查看
dir(类名):查出的是一个名字列表
类名.__dict__:查出的是一个字典,key为属性名,value为属性值

二:特殊的类属性
类名.__name__# 类的名字(字符串)
类名.__doc__# 类的文档字符串
类名.__base__# 类的第一个父类(在讲继承时会讲)
类名.__bases__# 类所有父类构成的元组(在讲继承时会讲)
类名.__dict__# 类的字典属性
类名.__module__# 类定义所在的模块
类名.__class__# 实例对应的类(仅新式类中)

  (3)对象相关知识

>>> g1=Garen('草丛伦') #类实例化得到g1这个实例,基于该实例我们讲解实例相关知识
>>> type(g1) #查看g1的类型就是类Garen
<class '__main__.Garen'>
>>> isinstance(g1,Garen) #g1就是Garen的实例
True

  1)对象只有一种作用:属性引用

#对象/实例本身其实只有数据属性
>>> g1.nickname
'草丛伦'
>>> g1.aggressivity
58
>>> g1.life_value
455
'''
查看实例属性
同样是dir和内置__dict__两种方式
特殊实例属性
__class__
__dict__
....
'''

  对象/实例本身只有数据属性,但是python的class机制会将类的函数绑定到对象上,称为对象的方法,或者叫绑定方法,绑定方法唯一绑定一个对象,同一个类的方法绑定到不同的对象上,属于不同的方法,内存地址都不会一样

>>> g1.attack #对象的绑定方法
<bound method Garen.attack of <__main__.Garen object at 0x101348dd8>>

>>> Garen.attack #对象的绑定方法attack本质就是调用类的函数attack的功能,二者是一种绑定关系
<function Garen.attack at 0x101356620>

 

  对象的绑定方法的特别之处在于:obj.func()会把obj传给func的第一个参数。

  (4)对象之间的交互

  我们可以仿照garen类再创建一个Riven类

class Riven:
    camp='Noxus'  #所有玩家的英雄(锐雯)的阵营都是Noxus;
    def __init__(self,nickname,aggressivity=54,life_value=414): #英雄的初始攻击力54;
        self.nickname=nickname  #为自己的锐雯起个别名;
        self.aggressivity=aggressivity #英雄都有自己的攻击力;
        self.life_value=life_value #英雄都有自己的生命值;
    def attack(self,enemy):   #普通攻击技能,enemy是敌人;
        enemy.life_value-=self.aggressivity #根据自己的攻击力,攻击敌人就减掉敌人的生命值。 

  实例出一个Riven来

>>> r1=Riven('锐雯雯')

  交互:锐雯雯攻击草丛伦,反之一样

>>> g1.life_value
455
>>> r1.attack(g1)
>>> g1.life_value
401 

  (5)类名称空间与对象名称空间

  创建一个类就会创建一个类的名称空间,用来存储类中定义的所有名字,这些名字称为类的属性

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

  其中类的数据属性是共享给所有对象的

>>> id(r1.camp) #本质就是在引用类的camp属性,二者id一样
4315241024
>>> id(Riven.camp)
4315241024

  而类的函数属性是绑定到所有对象的:

>>> id(r1.attack) 
4302501512
>>> id(Riven.attack)
4315244200

'''
r1.attack就是在执行Riven.attack的功能,python的class机制会将Riven的函数属性attack绑定给r1,r1相当于拿到了一个指针,指向Riven类的attack功能

除此之外r1.attack()会将r1传给attack的第一个参数
'''

  创建一个对象/实例就会创建一个对象/实例的名称空间,存放对象/实例的名字,称为对象/实例的属性

  在obj.name会先从obj自己的名称空间里找name,找不到则去类中找,类也找不到就找父类...最后都找不到就抛出异常  

 三、继承与派生

  1.什么是继承

  继承是用来创建新的类的一种方式,好处是解决代码重用的问题,减少代码冗余

  继承是类与类之间的关系,是一种什么‘是’什么的关系

  python中类的继承分为:单继承和多继承

class ParentClass1: #定义父类
    pass
class ParentClass2: #定义父类 pass
class SubClass1(ParentClass1): #单继承,基类是ParentClass1,派生类是SubClass pass
class SubClass2(ParentClass1,ParentClass2): #python支持多继承,用逗号分隔开多个继承的类 pass

  查看继承

SubClass1.__bases__ #__base__只查看从左到右继承的第一个子类,__bases__则是查看所有继承的父类
(<class '__main__.ParentClass1'>,)
SubClass2.__bases__
(<class '__main__.ParentClass1'>, <class '__main__.ParentClass2'>)

提示:如果没有指定基类,python的类会默认继承object类,object是所有python类的基类,它提供了一些常见方法(如__str__)的实现。

>>> ParentClass1.__bases__
(<class 'object'>,)
>>> ParentClass2.__bases__
(<class 'object'>,)
继承:是基于抽象的结果,通过编程语言实现它,首先得通过抽象这个过程,才能通过继承的方式表达出抽象的结构
class People:    #定义父类
    def __init__(self,name,age,sex):
        self.name=name
        self.age=age
        self.sex=sex
    def walk(self):
        print('%s is walking' %self.name)
    def foo(self):
        print('from father %s' %self.name)

class Teachar(People):  #单继承,People是基类,Teachar是派生类
    school = 'oldboy'
    def __init__(self,name,age,sex,level,salary):
        People.__init__(self,name,age,sex)
        self.level=level
        self.salary=salary
    def teach(self):
        print('%s is teaching' %self.name)
    def foo(self):
        People.foo(self)
        print('from teacher')

class Student(People):
    def __init__(self,name,age,sex,group):
        People.__init__(self,name,age,sex)
        self.group=group
    def study(self):
        print('%s is studying' %self.name)
t=Teachar('sam',18,'male',10,3000)

  

  组合

  组合指的是,在一个类中以另外一个类的对象作为数据属性,称为类的组合
  当类之间有显著不同,并且较小的类是较大的类所需要的组件时,用组合比较好
class People:
    def __init__(self,name,age,year,mon,day):
        self.name=name
        self.age=age
        self.birth=Date(year,mon,day)
    def walk(self):
        print('%s is walking' %self.name)

class Date:
    def __init__(self,year,mon,day):
        self.year=year
        self.mon=mon
        self.day=day
    def tell_birth(self):
        print('出生于<%s>年<%s>月<%s>日' %(self.year,self.mon,self.day))

class Teachar(People):
    def __init__(self,name,age,year,mon,day,level,salary):
        People.__init__(self,name,age,year,mon,day)
        self.level=level
        self.salary=salary
    def teach(self):
        print('%s is teaching' %self.name)

class Student(People):
    def __init__(self,name,age,year,mon,day,group):
        People.__init__(self,name,age,year,mon,day)
        self.group=group
    def study(self):
        print('%s is studying' %self.name)

  抽象类

  父类要限制
  1.子类必须要有父类的方法
  2.子类实现的方法必须跟父类的方法的名字一样
import abc  #利用abc模块实现抽象类
class File(metaclass=abc.ABCMeta):
    @abc.abstractmethod  #定义一个抽象类,无需实现功能
    def read(self):
    #子类必须有read功能 pass @abc.abstractmethod def write(self):
    #子类必须有write功能 pass class Txt(File): def read(self):  #子类继承抽象类,但必须定义read和write方法 pass def write(self): pass t=Txt()

   继承实现的原理

  1.继承顺序

  python中可以继承多个类,继承多个类之后寻找方法有两种,分别是深度优先和广度优先

  当类是经典类时,多继承情况下,会按照深度优先方式查找

  当类是新式类时,多继承情况下,会按照广度优先方式查找

复制代码
class A(object):
    def test(self):
        print('from A')

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

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

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

class E(C):
    def test(self):
        print('from E')

class F(D,E):
    # def test(self):
    #     print('from F')
    pass
f1=F()
f1.test()
print(F.__mro__) #只有新式才有这个属性可以查看线性列表,经典类没有这个属性

#新式类继承顺序:F->D->B->E->C->A
#经典类继承顺序:F->D->B->A->E->C
#python3中统一都是新式类
#pyhon2中才分新式类与经典类

继承顺序
复制代码

  2.继承原理

  python是通过一个叫做mro的算法解析出顺序列表,这个列表就是一个简单的所有基类的线性顺序列表例如:

[<class '__main__.H'>, <class '__main__.E'>, <class '__main__.B'>, <class '__main__.F'>, <class '__main__.C'>, <class '__main__.G'>, <class '__main__.D'>, <class '__main__.A'>, <class 'object'>]

  python会在mro列表上从左到右开始查找基类,知道找到第一个匹配这个属性的类为止。

  而这个mro列表的构造是通过一个c3线性化算法来实现的,我们不去深究这个算法的数学原理,它实际上就是合并所有父类的mro列表并遵循下列三条准则:

  1.子类会先于父类被检查

  2.多个父类会根据它们在列表中的顺序被检查

  3.如果下一个类存在两个合法的选择,选择第一个父类

   子类中调用父类方法

  方法一:父类名.父类方法()

class Vehicle: #定义交通工具类
     Country='China'
     def __init__(self,name,speed,load,power):
         self.name=name
         self.speed=speed
         self.load=load
         self.power=power

     def run(self):
         print('开动啦...')

class Subway(Vehicle): #地铁
    def __init__(self,name,speed,load,power,line):
        Vehicle.__init__(self,name,speed,load,power)
        self.line=line

    def run(self):
        print('地铁%s号线欢迎您' %self.line)
        Vehicle.run(self)

line13=Subway('中国地铁','180m/s','1000人/箱','电',13)
line13.run()
 
   方法二:super()
 
class People:
    def __init__(self,name,age,sex):
        self.name=name
        self.age=age
        self.sex=sex
    def foo(self):
        print('from parent')

class Teacher(People):
    def __init__(self,name,age,sex,level,salary):
        #在python3中
        super().__init__(name,age,sex)#调用父类的__init__功能,实际上用的是绑定方法

        #在python2中
        super(Teacher,self).__init__(name,age,sex)

        self.level=level
        self.salary=salary
    def foo(self):
        super().foo()
        print('from child')

t=Teacher('egon',18,'male',10,3000)
t.foo()
 

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

 五、多态与多态性

   1.多态
  多态指的是一类事物有多种形态,(一个抽象类有多个子类,因而多态的概念依赖于继承)
import abc
class Animal(metaclass=abc.ABCMeta): #同一类事物:动物
    @abc.abstractmethod
    def talk(self):
        pass

class People(Animal): #动物的形态之一:人
    def talk(self):
        print('say hello')

class Dog(Animal): #动物的形态之二:狗
    def talk(self):
        print('say wangwang')

class Pig(Animal): #动物的形态之三:猪
    def talk(self):
        print('say aoao')

  2.多态性

  (1)什么是多态性

  多态性是指具有不同功能的函数可以使用相同的函数名,这样就可以用一个函数名调用不同功能的函数。

  (2)为什么要用多态性

  1)增加了和程序的灵活性

  以不变应万变,不论对象千变万化,使用者都是同一种形式去调用

  2)增加了程序的可扩展性

  通过继承类穿件了一个新的类,使用者无需更改自己的代码,还是用func(animal)去调用

六、封装

   1.为什么要封装
  封装不是单纯意义的隐藏:
  (1)封装数据的主要原因是:保护隐私
  (2)封装方法的主要原因是:隔离复杂度
  2.封装分为两个层面
  第一个层面的封装(什么都不用做):创建类和对象会分别创建二者的马明成空间,我们只能用类名,或者obj.的方式去访问里面的名字,这本身就是一种封装
  ps:对于这一层面的封装(隐藏),类名.和实例名.就是访问隐藏属性的接口
 
  第二个层面 的封装:类中把某些属性和方法隐藏起来(或者说定义成私有的),只在类的内部使用,外部无法访问,或者留下少量接口(函数)供外部访问。
   
  在python中用双下划线的方式实现隐藏属性(设置成私有的)
  类中所有双下划线开头的名称如__x都会自动变形成:_类名__x的形式:
class A:
    __N=0 #类的数据属性就应该是共享的,但是语法上是可以把类的数据属性设置成私有的如__N,会变形为_A__N
    def __init__(self):
        self.__X=10 #变形为self._A__X
    def __foo(self): #变形为_A__foo
        print('from A')
    def bar(self):
        self.__foo() #只有在类内部才可以通过__foo的形式访问到.
class Teacher:
    def __init__(self,name,age):
        self.__name=name
        self.__age=age

    def tell_info(self):
        print('姓名:%s,年龄:%s' %(self.__name,self.__age))
    def set_info(self,name,age):
        if not isinstance(name,str):
            raise TypeError('姓名必须是字符串类型')
        if not isinstance(age,int):
            raise TypeError('年龄必须是整型')
        self.__name=name
        self.__age=age

t=Teacher('egon',18)
t.tell_info()

t.set_info('egon',19)
t.tell_info()

这种自动变形的特点:

  1.类种定义的__x只能在内部使用,如self.__x,引用的就是变形的结果。

  2.这种变形其实正是针对外部的变形,在外部是无法通过__x这个名字访问到的。

  3.在子类定义的__x不会覆盖在父类定义的__x,因为子类中变形成了:_子类名__x,而父类中变形成了:_父类名__x,即双下划线开头的属性在继承给子类时,子类是无法覆盖的。

  ps:对于这一层面的封装(隐藏),我们需要在类中定义一个函数(接口函数)在它内部访问被隐藏的属性,然后外部就可以使用了

这种变形需要注意的问题是:

  1.这种机制也没有真正意义上限制我们从外部直接访问属性,知道了雷鸣和属性名就可以拼出名字:_类名__属性,然后皆可以访问了,如a._A__N

  2.变形的过程只在类的定义时放生一次,在定义后的赋值操作,不会变形
  3.在继承中,父类如果不想让子类渡改自己的方法,可以将方法定义为私有的

  python并不会真的阻止你访问私有的属性,模块也遵循这种约定,如果模块名以单下划线开头,那么from module import *时不能被导入,但是你from module import _private_module依然是可以导入的其实很多时候你去调用一个模块的功能时会遇到单下划线开头的(socket._socket,sys._home,sys._clear_type_cache),这些都是私有的,原则上是供内部调用的,作为外部的你,一意孤行也是可以用的,只不过显得稍微傻逼一点点

  python要想与其他编程语言一样,严格控制属性的访问权限,只能借助内置方法如__getattr__,详见面向对象进阶

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

  例一:BMI指数(bmi是计算而来的,但很明显它听起来像是一个属性而非方法,如果我们将其做成一个属性,更便于理解)

  成人的BMI数值:
  过轻:低于18.5
  正常:18.5-23.9
  过重:24-27
  肥胖:28-32
  非常肥胖, 高于32
  体质指数(BMI)=体重(kg)÷身高^2(m)
  EX:70kg÷(1.75×1.75)=22.86
 
 
class People:
    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)

p1=People('sam',75,1.85)
print(p1.bmi)
 

例二:圆的周长和面积

 
import math
class Circle:
    def __init__(self,radius): #圆的半径radius
        self.radius=radius

    @property
    def area(self):
        return math.pi * self.radius**2 #计算面积

    @property
    def perimeter(self):
        return 2*math.pi*self.radius #计算周长

c=Circle(10)
print(c.radius)
print(c.area) #可以向访问数据属性一样去访问area,会触发一个函数的执行,动态计算出一个值
print(c.perimeter) #同上
'''
输出结果:
10
314.1592653589793
62.83185307179586
'''

  2.为甚要用property

  将一个类的函数定义成特性以后,对象再去使用的时候obj.name,根本无法差距自己的name是执行了一个函数后计算出来的,这种特性的使用方式遵循了同意访问的原则,

 
import pickle
class Teacher:
    school='oldboy'
    def __init__(self,name,age,sex,year,mon,day,level,salary):
        self.name=name
        self.age=age
        self.sex=sex
        self.level=level
        self.salary=salary
        self.__birth=Date(year,mon,day)

    @property
    def birth(self):
        print('name:%s birth:%s' % (self.name,self.__birth.Birth()))
    @property
    def save(self):
        with open('teacherdb.pkl','ab') as f:
            pickle.dump(self,f)

class Date:
    def __init__(self,year,mon,day):
        self.year=year
        self.mon=mon
        self.day=day
    def Birth(self):
        return '%s-%s-%s' %(self.year,self.mon,self.day)

alex=Teacher('alex',84,'male',1990,3,45,1,300000)
alex.birth
alex.save
 

4.封装与扩展性

封装在于明确区分内外,使得类实现者可以修改封装内的东西而不影响外部调用者的代码,二外部使用者只知道一个接口(函数),只要接口(函数)名、参数不变,使用者的代码永远无需改变。这就提供一个良好的合作基础——或者说,只要接口这个基础约定不变,则代码改变不足为虑

 
#类的设计者
class Room:
    def __init__(self,name,owner,width,length,high):
        self.name=name
        self.owner=owner
        self.__width=width
        self.__length=length
        self.__high=high
    def tell_area(self): #对外提供的接口,隐藏了内部的实现细节,此时我们想求的是面积
        return self.__width * self.__length
 
#使用者
>>> r1=Room('卧室','sam',20,20,20)
>>> r1.tell_area() #使用者调用接口tell_area
400
 
 
#类的设计者,轻松的扩展了功能,而类的使用者完全不需要改变自己的代码
class Room:
    def __init__(self,name,owner,width,length,high):
        self.name=name
        self.owner=owner
        self.__width=width
        self.__length=length
        self.__high=high
    def tell_area(self): #对外提供的接口,隐藏内部实现,此时我们想求的是体积,内部逻辑变了,只需求修该下列一行就可以很简答的实现,而且外部调用感知不到,仍然使用该方法,但是功能已经变了
        return self.__width * self.__length * self.__high
 

对于仍然在使用tell_area接口的人来说,根本无需改动自己的代码,就可以用上新功能

r1.tell_area()
8000

 七、绑定方法与非绑定方法

  类中定义的函数分成两大类:

  一、绑定方法(绑定给谁,谁来调用就自动将它本身当作第一个参数传入):

  1.绑定到类的方法:用classmethod装饰器装饰的方法,为类量身定制

  类.boud_method(),自动将类当作第一个参数传入(其实对象也可调用,但仍将类当作第一个参数传入)

   2.绑定到对象的方法:没有被任何装饰器装饰的方法,为对象量身定制

  对象.boud_method(),自动将对象当做第一个参数传入

  (属于类的函数,类可以调用,但是必须按照函数的规则来,没有自动传值name一说)

   二、非绑定方法:用staticmethod装饰器装饰的方法

  不与类或对象绑定,类和对象都可以调用,但是没有自动传值的功能,就是一个普通工具而已

  ps:在类中直接定义的函数,没有被任何装饰器装饰的,都是绑定到对象的方法,可不是普通函数,对象调用该方法会自动传值,二staticmethod装饰的方法,不管谁来调用,都没有自动传值一说

  

1.staticmethod

statimethod不与类或对象绑定,谁都可以调用,没有自动传值效果,python为我们内置了函数staticmethod来把类中的函数定义成静态方法

 
import hashlib
import time
class MySQL:
    def __init__(self,host,port):
        self.id=self.create_id()
        self.host=host
        self.port=port
    @staticmethod
    def create_id(): #就是一个普通工具
        m=hashlib.md5(str(time.clock()).encode('utf-8'))
        return m.hexdigest()


print(MySQL.create_id) #<function MySQL.create_id at 0x0000000001E6B9D8> #查看结果为普通函数
conn=MySQL('127.0.0.1',3306)
print(conn.create_id) #<function MySQL.create_id at 0x00000000026FB9D8> #查看结果为普通函数
 

2.classmethod

  classmehtod是给类用的,即绑定到类,类在使用时会将类本身当做参数传给类方法的第一个参数(即便是对象来调用也会将类当作第一个参数传入),python为我们内置了函数classmethod来把类中的函数定义成类方法

 

 
import settings
import hashlib
import time
class MySQL:
    def __init__(self,host,port):
        self.host=host
        self.port=port

    @classmethod
    def from_conf(cls):
        print(cls)
        return cls(settings.HOST,settings.PORT)

print(MySQL.from_conf) #<bound method MySQL.from_conf of <class '__main__.MySQL'>>
conn=MySQL.from_conf()

print(conn.host,conn.port)
conn.from_conf() #对象也可以调用,但是默认传的第一个参数仍然是类

posted @ 2017-11-21 15:17  PengDa  阅读(163)  评论(0编辑  收藏  举报