Python面向对象篇(3)-封装、多态、反射及描述符

一、 多态

  多态,最浅显的意识就是同一事物具备多种形态,这很好理解,动物是一个大类,猫类属于动物类,狗类属于动物类,人也是属于动物类,那能理解成,猫、狗、人是同样的吗?固然不是,还有,水,分为液体、固体、气体,但它们都是H20构成的,这也是多态。意味着对于不一样的类的对象,能够执行相同的操做,html

  多态是面向对象语言的一个基本特性,多态就意味着变量不须要知道引用的对象是什么,经过一个统一的接口,对引入的不一样对象表现出不一样的行为方式。python

  下面这个例子编程

 1 import abc
 2 class Animal(metaclass=abc.ABCMeta):
 3     @abc.abstractmethod
 4     def yell(self):
 5         pass
 6 class Dog(Animal):
 7     def yell(self):
 8         print("汪汪汪~")
 9 class Cat(Animal):
10     def yell(self):
11         print("喵喵喵")

  在上面这段代码中,类Animal做为一个大类,定义了yell方法,但对于Animal来讲,有不少不一样的形态表示,猫,狗、人等等都属于Animal类,而每一个类都有不一样的叫声,这就是同一类,多种形态的表示方式。函数

二、 封装

  面向对象的程序设计中,某个类把所须要的数据(也能够说是类的属性)和对数据的操做(也能够说是类的行为)所有都封装在类中,分别称为类的成员变量和方法(或成员函数)。这种把成员变量和成员函数封装在一块儿的编程特性称为封装。封装,其实就是为了对外部隐藏对象的代码逻辑。测试

       封装在概念上和多态很类似,可是不等同于多态。多态可让实例对不知道是什么的类的对象进行属性、方法的调用,而封装是能够不一样关心对象是如何构建就能够直接调用的。spa

  在python中用双下划线开头的方式,将属性隐藏起来。双下划线+属性名就是私有的标识符,会自动变形为_类名__属性名的形式,即__x都会变造成_类名__x的形式,能够从类的属性字典中很明显的看到这种变形。这种变换不会关注标识符的语法位置,所以能够用来定义类私有的实例和类变量、方法、全局变量。设计

  看下面这个例子:建立一个类Person,类变量,全局变量,和方法都有经过双下划线__设置的私有变量代理

 1 class Person:
 2     __skin = "yellow"  #设置类的数据属性为私有的
 3     hair = "black"
 4     def __init__(self,name,age,height):
 5         self.__name = name    #变形为self._Person_name的形式
 6         self.age = age
 7         self.height = height
 8     def __dance(self):        #变形为_Person__dance的形式
 9         print("%s正在跳舞"%self.name)
10     def sing(self):
11         print("%s正在唱歌"%self.name)
12 p_1 = Person("Tony",18,160)

      在这个例子中,__skin、__self.name、__dance都是私有变量,外部的实例是没法直接访问的,经过实例p_1.属性\方法的形式,是没法调用这3个属性的,这就是私有变量的意义所在。code

      但其实,这只是一种python的约定,若是非想在外部的实例调用其私有变量,也并非不可能。若是知道了类名和想调用的私有属性名,就能够调用。htm

即p_1._Person__skin这种方式,(注意:类名前是一个下划线,私有属性前是双下划线),调用方法p_1._Person__dance()

      封装并不只仅是为了将类中的属性隐藏起来,更多的需求是将私有属性做为外部调用的一个接口,用来对传入的数据作相应的限制,以下面这个例子:

 1  class Person:
 2     def __init__(self,name,age):
 3         self.__name = name
 4         self.__age = age
 5     def person_info(self):
 6         print("My name is %s,My age is %s"%(self.__name,self.__age))
 7     def set_person_info(self,name,age):
 8         if not isinstance(name,str):
 9             raise TypeError("名字必须是字符串类型")
10         if not isinstance(age,int):
11             raise TypeError("年龄必须是整数类型")
12             self.__name = name
13             self.__age = age
14 p_1 = Person("Tony",23)
15 p_1.person_info()
16 p_1.set_person_info(11,"An")
17 p_1.person_info()

    在上面这个例子中,想要调用person_info()这个方法来显示出一我的的姓名和年龄,可是若是想要限制用户输入的姓名和年龄的数据类型,就要经过set_person_info()这个方法来做为用户输入的一个接口。

    单下划线定义的模块名,不用from..import..的方式导入

    受权是封装的一个特性,

三、 反射

    反射(或自省)主要是指程序能够访问、检测、和修改它自己状态或行为的一种能力,在python中,经过字符串的形式操做对象相关的属性,python中的一切事物都是对象,即均可以使用反射。

     hasattr、getattr、setattr、delattr

    四个能够实现自省的函数:

hasattr(object,name)

判断对象是否包含对应的属性。

object -- 对象

name   -- 字符串,属性名

getattr(object,name,[default])

getattr() 函数用于返回一个对象属性值。

object -- 对象。

name   -- 字符串,对象属性。

default -- 默认返回值,若是不提供该参数,在没有对应属性时,将触发 AttributeError。

setattr(object,name,value)

setattr 函数对应函数 getatt(),用于设置属性值,该属性必须存在。

object – 对象

name    - 字符串,属性名

value   - 属性值

 

delattr(object, name)

delattr 函数用于删除属性。

delattr(x, 'foobar') 相等于 del x.foobar。

object -- 对象。

name -- 必须是对象的属性。

   

        经过下面这个实例,来演示一下4个函数的使用方法:

 1 class Person:
 2     skin = "yellow"
 3     hair = "black"
 4     def __init__(self,name,age):
 5         self.name = name
 6         self.age = age
 7     def dance(self):
 8         print("%s正在跳舞"%self.name)
 9 p_1 = Person("Tony",18)
10 #判断Person类中是否含有dance属性,
11 print(hasattr(Person,"dance"))
12 #获得Person类中的skin属性的值,若是传入的是方法名,返回的就是方法的内存地址
13 print(getattr(Person,"skin"))
14 print(getattr(Person,"skin11"))   #属性不存在,程序报错
15 #修改Person类中skin属性的值,类属性字典中也会作相应改变,经过Person.__dict__查看
16 setattr(Person,"skin","black")
17 print(getattr(Person,"skin"))
18 #删除Person类中hair这个属性,类的属性字典中也会删除
19 delattr(Person,"hair")
20 delattr(Person,"hair11")         #属性不存在,报错  

   反射有什么好处?反射就能够事先定义好接口,这意味这什么呢?就是能够先写好主要的逻辑,而后再去后期实现实现的功能。

   __getattr__、__setattr__、__delattr__

 1 class Attr:
 2     def __init__(self,x):
 3         self.x = x
 4     def __getattr__(self, item):
 5         print("--->getter")
 6     def __setattr__(self, key, value):
 7         print("--->setattr")
 8     def __delattr__(self, item):
 9         print("--->delattr")

  在这个例子中,重写了__getattr__、__setattr__、__delattr__这三个方法。这三个方法的存在会对类发生什么改变呢?

  • __getattr__:

  只有在实例调用属性且调用的属性不存在才会运行

1 print(a_1.x)
2 运行结果:
3 --->setattr    #只要赋值操做,就会触发__setattr__方法
4 6
5 print(a_1.y)
6 运行结果:
7 --->setattr
8 --->getter
9 None

  还有一个__getattribute__方法,不管调用的属性是否存在,都会触发__getattribute__方法。当__getattribute__和__getattr__同时存在的话,只会执行__getattribute__,除非__getattribute__在执行的过程当中抛出异常

  • __setattr__:
    1 a_1 = Attr(6)
    2 print(a_1.__dict__)
    3 运行结果:
    4 {'x': 6}
    5 删除注释,再次执行:
    6 运行结果:
    7 --->setattr
    8 {}

    先将这两行注销,而后建立一个实例并打印这个实例的属性字典:

 

    建立了实例,可是属性确没有写进属性字典中,这是为何?

    这是由于,凡是赋值的操做,都会触发__setter__()方法的运行,而在上面代码中,重写了__setattr__方法,代码逻辑仅仅有打印“--> setattr”操做,根本就没有将属性值写入实例属性的字典的操做,天然不会被写入,除非直接操做属性字典,不然根本没法往里面写入值。那么就须要进行以下修改:

 1 class Attr:
 2     def __init__(self,x):
 3         self.x = x
 4     def __getattr__(self, item):
 5         print("--->getter")
 6        self.__dict__[key]=value
 7     def __setattr__(self, key, value):
 8         print("--->setattr")
 9     def __delattr__(self, item):
10         print("--->delattr")

    直接将实例化传入的值,写入实例的属性字典,有些人很容易将这步操做写成

    self.key = value ,这条语句是错误的,会致使程序无限递归。

  • __delattr__

    与__setattr__实质相同,在重写了__delattr__方法后,也须要定义相应的代码来从实例的属性字典中删除。即:

 1 class Attr:
 2     def __init__(self,x):
 3         self.x = x
 4     def __getattr__(self, item):
 5         print("--->getter")
 6        self.__dict__[key]=value
 7     def __setattr__(self, key, value):
 8         print("--->setattr")
 9     def __delattr__(self, item):
10         print("--->delattr")
11        self.__dict__.pop(item)

    能够先经过a_1.__dict__["a"]=1在实例的属性字典中添加属性a,进行删除测试,一样的,不能够在__delattr__()后经过del self.item的方式来删除,这样一样会引发无限递归。

四、 描述符

    描述符是Python语言中很深奥也很重要的一个概念,简而言之,描述符也是一个对象,该对象表明了一个属性的值,这就意味着,若是一个类中有一个属性name,若是给该类添加了描述符,那么描述符就是另外一个能表明属性name的对象。描述符协议中定义了_get_、_set_、_del_这些特殊的方法,描述符是实现其中的一个或多个方法的对象。这样说你们可能没有办法理解,仍是经过代码讲解吧。

    __get__():调用一个属性时,触发
    __set__():为一个属性赋值时,触发
    __delete__():采用del删除属性时,触发

    定义一个描述符:

1 class Descriptor:
2     def __get__(self, instance, owner):
3         pass
4     def __set__(self, instance, value):
5         pass
6     def __delete__(self, instance):
7         pass

    前面说过了,描述符就是一个类,若是一个类中定义了这三个方法,这个类就被称为一个描述符。

    在前面讲过了__getattr__、__setattr__、__delattr__、方法,在由类产生实例的过程当中会相应的触发这三个方法,而对_get_、_set_、_delete_来讲,并不会被触发,那么触发这三个函数的条件又是什么?

 1 class Descriptor:             #建立描述符Descriptor
 2     def __get__(self, instance, owner):
 3         print("--->get")
 4     def __set__(self, instance, value):
 5         print("--->set")
 6     def __delete__(self, instance):
 7         print("--->delete")
 8  
 9 
10 class Person:
11     name = Descriptor()        #属性name被Descriptor描述符描述
12     def __init__(self,name):
13         self.name = name

    首先,描述符对描述符自身是没有用的,显而易见,之因此叫描述符,是用来描述其余对象的(类也是对象),从上段程序的运行结果能够明显的看出,name属性被描述符代理后,经过实例调用、设置、删除该属性,实质上执行的都是描述符Descriptor中的逻辑。描述符不能够被定义到构造函数中,必须赋值给类属性。这样写有什么意义呢?

 1 p_1 = Person("Tony")
 2 print(p_1.__dict__)
 3 p_1.name = "An"
 4 print(p_1.__dict__)
 5 p_1.age = 18
 6 print(p_1.__dict__)
 7 运行结果:
 8 --->set
 9 {}
10 --->set
11 {}
12 {'age': 18}

    建立了一个Person类的实例p_1,首先会触发_set_()方法,可是p_1的name=Tony这个属性并无写入到它的属性字典中,修改name的属性为An,一样没有写入。这是由于,将Person类中的name这个属性经过name = Descriptor()  的方式被描述符代理了,因此调用该属性的时候,实质上就会去触发描述符Descriptor中的_set_方法,因此就没法写入。在最后添加了一个属性age,成功的写入到了实例p_1的属性字典中,是由于age这个属性没有被其余描述符代理,使用的就是类默认的逻辑来执行。

若是想要自定义的描述中也具有属性写入的功能,能够将Descriptor中的_set_()修改以下:

1 def __set__(self, instance, value):
2         print("--->set")
3         instance.__dict__["name"]=value

    经过这个例子,能够看到描述符的强大之处,有不少时候,咱们须要控制某些变量属性,就能够经过描述符来自定义想要的功能。

    描述符分为两种:

    数据描述符:至少实现了_get_()方法和_set_()方法。

    非数据描述符:没有实现_set_()方法。

    在调用的时候,应遵循如下优先级:类属性-->数据描述符-->实例属性-->非数据描述符-->__getattr__()方法(若是找不到属性的时候触发)

    __str__()和__repr__()方法:

1 class Person:
2     def __init__(self,name):
3         self.name = name
4     def __str__(self):
5         return ("my name is %s"%self.name)
6 p_1 = Person("An")
7 print(p_1)

    __str__发放就是能够自定义对象的字符串返回形式,若是不自定义__str__()方法,返回结果是:An,定义后,会按照定义的形式输出结果,即:my name is An。

       __repr__()方法能够理解为__str__()方法的替代品,若是__str__没有被定义,就会使用__repr__()方法来代替输出。

         __slots__属性:

    __slots__实际上是一个类变量,访问类或实例的时候,实质上就是在访问类和实例的属性字典__dict__,(类的属性字典是共享的,即全部所属该类的实例均可以访问,实例的属性字典是是独立的)。

1 p_1 = Person()
2 p_1.name = "Tony"
3 #p_1.age = 18           #报错
4 #print(p_1.__dict__)   #报错,定义了__slots__,就再也不有dict
5 print(p_1.__slots__)
6 运行结果:
7 name

    可是字典是很占用计算机内存的,当类的属性不多,可是须要不少所属该类的实例的时候,就能够用__slots__来代替__dict__,__slots__中定义的变量就变成了类的描述符,类的实例只能拥有这些变量,可是不会拥有属于本身的属性字典,所以也就是不能增长新的变量。

       经过__slots__,能够限制实例的属性,即减少的占用的内存空间,也会大大提高属性的访问速度。

五、 迭代器协议

    __next__和__iter__实现迭代器协议。

 1 class Iterator():
 2     def __init__(self,x):
 3         self.x = x
 4     def __iter__(self):
 5         return self
 6     def __next__(self):
 7         
 8         self.x+=1
 9         return self.x
10 i_1 = Iterator(1)
11 for i in i_1:
12     print(i)

    这样就经过__iter__和__next__方法实现了迭代器。

    斐波那契数列:

 1 class Fibo():
 2     def __init__(self,x,y):
 3         self.x = x
 4         self.y = y
 5     def __iter__(self):
 6         return self
 7     def __next__(self):
 8         if self.x > 20:
 9             raise StopIteration("超出终止")
10         self.x = self.y
11         self.y = self.x + self.y
12         return self.x
13 f_1= Fibo(0,1)
14 for i in f_1:
15     print(i)

六、 上下文管理协议:__enter__和__exit__

    在管理文件操做的时候,能够经过with..open的方式来自动释放文件的内存,在类中,若是想让对象兼容with语句,必须在这个类中声明__enter__和__exit__方法

 1 class File:
 2     def __init__(self,x):
 3         self.x = x
 4     def __enter__(self):
 5         print("--->enter")
 6     def __exit__(self, exc_type, exc_val, exc_tb):
 7         print("--->exit")
 8 with File("test.txt") as f:
 9 print("对象兼容with语句")
10 运行结果:
11 --->enter

    对象兼容with语句

    --->exit

    从结果能够看出,文件操做流程中,首先触发__enter__方法,而后执行代码块中的内容,最后触发__exit__方法来释放文件内存。

    __exit__()中的三个参数分别表明异常类型,异常值和追溯信息,with语句中代码块出现异常,则with后的代码都没法执行。

    __call__:__call__方法的执行是经过对象名或类名后加()来执行的。

相关文章
相关标签/搜索