面向对象:多态,封装,绑定与非绑定方法

面向对象第二大特性:多态

一类事物的多种形态

  • 动物的多种形态:人,狗,猪
  • 文件的多种形态:文本文件,可执行文件

1. 多态性

建立在多态的前提下,不考虑实例类型的情况下,直接使用实例

  • 静态多态性

    • 任何类型都可以用运算符+进行运算*
  • 动态多态性

    • peo=People()
      dog=Dog()
      pig=Pig()
      
      #peo、dog、pig都是动物,只要是动物肯定有talk方法
      #于是我们可以不用考虑它们三者的具体是什么类型,而直接使用
      peo.talk()
      dog.talk()
      pig.talk()
      
      #更进一步,我们可以定义一个统一的接口来使用
      def func(obj):
          obj.talk()
      

2.多态性的意义

从上面多态性的例子可以看出,我们并没有增加什么新的知识,也就是说python本身就是支持多态性的,这么做的好处是什么呢?

1.增加了程序的灵活性

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

2.增加了程序额可扩展性

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

3.python崇尚使用鸭子类型来实现多态性

如果看起来像、叫声像而且走起路来像鸭子,那么它就是鸭子

python程序员通常根据这种行为来编写程序。例如,如果想编写现有对象的自定义版本,可以继承该对象

也可以创建一个外观和行为像,但与它无任何关系的全新对象后者通常用于保存程序组件的松耦合度。

  • 就是说不用去创建一个抽象类,使用abc模块,metaclass=abs.ABCMeta,和abstractmethod装饰器这样,然后通过继承这个类来实现相同的函数方法,而是创建一个外观和行为像,但与它无任何关系的全新对象,来保持程序的松耦合度.

例1:利用标准库中定义的各种‘与文件类似’的对象,尽管这些对象的工作方式像文件,但他们没有继承内置文件对象的方法

#二者都像鸭子,二者看起来都像文件,因而就可以当文件一样去用
class TxtFile:
    def read(self):
        pass

    def write(self):
        pass

class DiskFile:
    def read(self):
        pass
    def write(self):
        pass

例2:序列类型有多种形态:字符串,列表,元组,但他们直接没有直接的继承关系

#str,list,tuple都是序列类型
s=str('hello')
l=list([1,2,3])
t=tuple((4,5,6))

#我们可以在不考虑三者类型的前提下使用s,l,t
s.__len__()
l.__len__()
t.__len__()

len(s)
len(l)
len(t)

面向对象第三大特性:封装

1.在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的形式访问到.

#A._A__N是可以访问到的,即这种操作并不是严格意义上的限制外部访问,仅仅只是一种语法意义上的变形

这种自动变形的特点:

  1. 类中定义的__x只能在内部使用,如self.__x,引用的就是变形的结果。
  2. 这种变形其实正是针对外部的变形,在外部是无法通过__x这个名字访问到的。
  3. 在子类定义的__x不会覆盖在父类定义的_x,因为子类中变形成了:_子类名__x,而父类中变形成了:父类名__x,即双下滑线开头的属性在继承给子类时,子类是无法覆盖的。

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

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

2、变形的过程只在类的定义是发生一次,在定义后的赋值操作,不会变形,相当于只有在类内部声明封装的属性或方法才会被隐藏.

  • 在类外面声明一个对象.__属性=变量值 并不会变形,来封装这个属性
  • 在类的定义阶段,内部代码就会运行,并且使封装的属性或方法产生变形.
  • 在内部可以直接访问,是因为在定义阶段解释器自动将所有封装的属性和方法进行了变形,是以变形后的代码进行访问的.

3、在继承中,父类如果不想让子类覆盖自己的方法,可以将方法定义为私有的

  • 因为封装的属性或方法已经产生了变形,并加上了类名,类名_属性或方法.子类会加上子类自己的类名,所以不会被覆盖.

2.封装的意义

封装不是单纯意义的隐藏

  • 封装数据属性的意义
    • 1.明确的区分内外
    • 2.控制外部对隐藏属性的操作行为(通过绑定方法进行访问隐藏属性,来实现打印,更改,删除等等的功能)
      • 限制了
  • 封装方法函数属性的意义
    • 3.隔离复杂度(通过绑定方法将复杂的流程打包成一个方法,对外部开放,外部用户仅仅只需要调用这一个方法就可以实现更为复杂的流程处理)
      • 简单例子:例如将一个复杂的流程分解为多个隐藏方法的步骤,打包成一个非封装的普通绑定方法,对于使用者来说只需要调用这个普通绑定方法即可,不用去管内部如何实现.从另一个角度来说给使用者提供了一个接口,其他都是隐藏起来的.

3.封装与扩展性

使用封装进行复杂度隔离之后,对方法的扩展和改动非常的简单,甚至不需要更改用户调用的方法就能实现其功能的改动.

提示:在编程语言里,对外提供的接口(接口可理解为了一个入口),可以是函数,称为接口函数,这与接口的概念还不一样,接口代表一组接口函数的集合体。

4.特性(property)

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

  • 某些对象的属性(名字,名称类属性)是需要通过一系列处理计算得出的,如果直接拿函数返回值来实现,会造成用户调用混乱,所以需要使用property装饰器来伪装方法函数
  • 使用property装饰器,被装饰的方法函数在对象调用时,不需要加括号来调用,而是可以像调用对象绑定属性那样进行访问
  • 被property装饰的函数返回值不能被赋值(虽然看起来是一个属性的样子,但是并不能被赋值,因为本质上还是一个方法,不是属性)

为什么要用property

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

补充:
ps:面向对象的封装有三种方式:
【public】
这种其实就是不封装,是对外公开的
【protected】
这种封装方式对外不公开,但对朋友(friend)或者子类(形象的说法是“儿子”,但我不知道为什么大家 不说“女儿”,就像“parent”本来是“父母”的意思,但中文都是叫“父类”)公开
【private】
这种封装对谁都不公开

使用property,setter,deleter来触发相应的方法

class Foo:
    def __init__(self,val):
        self.__NAME=val #将所有的数据属性都隐藏起来

    @property
    def name(self):
        return self.__NAME #obj.name访问的是self.__NAME(这也是真实值的存放位置)

    @name.setter
    def name(self,value):
        if not isinstance(value,str):  #在设定值之前进行类型检查
            raise TypeError('%s must be str' %value)
        self.__NAME=value #通过类型检查后,将值value存放到真实的位置self.__NAME

    @name.deleter
    def name(self):
        raise TypeError('Can not delete')

f=Foo('egon')
print(f.name)
# f.name=10 #抛出异常'TypeError: 10 must be str'
del f.name #抛出异常'TypeError: Can not delete'

# 注意@property 和 @name.setter  @name.deleter 装饰的名字是一致的!!!!!

绑定方法与非绑定方法

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

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

  1. 绑定到类的方法:用classmethod装饰器装饰的方法。
    • 为类量身定制
    • 类.boud_method(),自动将类当作第一个参数传入
    • (其实对象也可调用,但仍将类当作第一个参数传入)
  2. 绑定到对象的方法:没有被任何装饰器装饰的方法。
    • 为对象量身定制
    • 对象.boud_method(),自动将对象当作第一个参数传入
    • (属于类的函数,类可以调用,但是必须按照函数的规则来,没有自动传值那么一说)

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

  1. 不与类或对象绑定,类和对象都可以调用,不会自动传值。就是一个普通方法函数而已

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

练习

练习1:定义MySQL类

要求:

1.对象有id、host、port三个属性

2.定义工具create_id,在实例化时为每个对象随机生成id,保证id唯一

3.提供两种实例化方式,方式一:用户传入host和port 方式二:从配置文件中读取host和port进行实例化

4.为对象定制方法,save和get_obj_by_id,save能自动将对象序列化到文件中,文件路径为配置文件中DB_PATH,文件名为id号,保存之前验证对象是否已经存在,若存在则抛出异常,;get_obj_by_id方法用来从文件中反序列化出对象

import conf
import pickle,hashlib,time,os


class MySQL:
    def __init__(self, host, port):
        self.host = host
        self.port = port
        self.__id = self.create_id()

    @staticmethod
    def create_id():
        m = hashlib.md5()
        m.update(bytes(str(m), encoding='utf-8'))
        return m.hexdigest()

    def tell_info(self):
        print('id:%s\nhost:%s\nport:%s' % (self.__id, self.host, self.port))

    def save(self):
        id_filename = os.path.join(conf.DB_PATH, self.__id)
        print(id_filename)
        with open(id_filename, 'wb') as f:
            pickle.dump(self, f)

    @classmethod
    def get_obj_by_id(cls, filename):
        id_filename = os.path.join(conf.DB_PATH,filename)
        with open(id_filename, 'rb') as f:
            return pickle.load(f)

    @classmethod
    def id_from_conf(cls):
        return cls(conf.host, conf.port)


# 用户传入host和port
user1 = MySQL("127.0.0.1", 80)
user1.tell_info()

# save能自动将对象序列化到文件中
# user1.save()

# 从配置文件中读取host和port进行实例化
user2 = MySQL.id_from_conf()
user2.tell_info()

# get_obj_by_id方法用来从文件中反序列化出对象
user3 = MySQL.get_obj_by_id('d23589d1bae0bc4f492e6a8e58604c03')
user3.tell_info()

你可能感兴趣的:(面向对象:多态,封装,绑定与非绑定方法)