面向对象相关概念与在python中的面向对象知识(魔法方法+反射+元类+鸭子类型)
面向对象知识
封装
封装的原理是,其成员变量代表对象的属性,方法代表这个对象的动作
真正的封装是,经过深入的思考,做出良好的抽象(设计属性时用到),给出“完整且最小”的接口,并使得内部细节可以对外透明
(注意:对外透明的意思是,外部调用者可以顺利的得到自己想要的任何功能,完全意识不到内部细节的存在)
优点:
1.控制对数据的操作
将数据隐藏起来这不是目的。隐藏起来然后对外提供操作该数据的接口,然后我们可以在接口附加上对该数据操作的限制,以此完成对数据属性操作的严格控制
2.隐藏实现方法实现代码
例如:#取款是功能,而这个功能有很多功能组成:插卡、密码认证、输入金额、打印账单、取钱
对使用者来说,只需要知道取款这个功能即可,其余功能我们都可以隐藏起来,很明显这么做隔离了复杂度,同时也提升了安全性
3.具有很强都代码复用性
将复用性更高的代码封装成一个方法,配合继承会使得代码复用比普通函数更好
4.便于扩展,更改
只暴露出接口,隐藏了内部实现,使得类实现者可以修改封装内的东西而不影响外部调用者的代码
外部使用用者只知道一个接口(函数),只要接口(函数)名、参数不变,使用者的代码永远无需改变。
这就提供一个良好的合作基础——或者说,只要接口这个基础约定不变,则代码改变不足为虑。
继承
继承描述的是子类与父类之间的关系,是一种什么是什么的关系。要找出这种关系,必须先抽象再继承
对比组合:
软件重用的重要方式除了继承之外还有另外一种方式,即:组合,组合指的是,在一个类中以另外一个类的对象作为数据属性,称为类的组合
继承,可以建立 派生类与基类之间的关系,它是一种'是'的关系,
当类之间有很多相同的功能,提取这些共同的功能做成基类,用继承比较好,比如老师是人,学生是人
实践中,继承的这种用途意义并不很大,甚至常常是有害的。因为它使得子类与基类出现强耦合。
组合,可以建立类与组合的类之间的关系,它是一种‘有’的关系,
当类之间有显著不同,并且较小的类是较大的类所需要的组件时,用组合比较好
比如教授有生日,教授教python和linux课程,教授有学生s1、s2、s3
接口
什么是接口?
首先区分接口概念,一般的接口指的是:自己提供给使用者来调用自己功能的方式\方法\入口(一般为函数名)。而面向对象的接口指的是是一组功能的集合,而不是一个功能。
接口有什么意义?
实现归一化,只要是基于同一个接口实现的类,那么所有的这些类产生的对象在使用时,从用法上来说都一样。
归一化的好处在于:
1) 归一化让使用者无需关心对象的类是什么,只需要的知道这些对象都具备某些功能就可以了,这极大地降低了使用者的使用难度。
2)针对某一接口功能时,可以使得使用者可以不加区分地处理
2.1)就好象linux的泛文件概念一样,所有东西都可以当文件处理,不必关心它是内存、磁盘、网络还是屏幕
(当然,对底层设计者,当然也可以区分出“字符设备”和“块设备”,然后做出针对性的设计:细致到什么程度,视需求而定)。
2.2)再比如:我们有一个汽车接口,里面定义了汽车所有的功能,然后由本田汽车的类,奥迪汽车的类,大众汽车的类,他们都实现了汽车接口,
这样就好办了,大家只需要学会了怎么开汽车,那么无论是本田,还是奥迪,还是大众我们都会开了,
开的时候根本无需关心我开的是哪一类车,操作手法(函数调用)都一样
抽象类
抽象类是一个特殊的类,它的特殊之处在于只能被继承,不能被实例化
从定义上说,如果说类是从一堆对象中抽取相同的内容而来的,那么抽象类就是从一堆类中抽取相同的内容而来的,内容包括数据属性和函数属性。
比如我们有香蕉的类,有苹果的类,有桃子的类,从这些类抽取相同的内容就是水果这个抽象的类,
你吃水果时,要么是吃一个具体的香蕉,要么是吃一个具体的桃子。。。。。。你永远无法吃到一个叫做水果的东西。
抽象类与普通类有什么不同之处?
抽象类中只能有抽象方法(没有实现功能),该类不能被实例化,只能被继承,且子类必须实现抽象方法。这一点与接口有点类似,但其实是不同的
抽象类和接口在设计上有什么区别?
抽象类的本质还是类,指的是一组类的相似性,包括数据属性(如all_type)和函数属性(如read、write),而接口只强调函数的相似性。
多态
同一类别的不同对象可以用自己的方式去响应相同的消息,以产生不同的效果。
比如:老师.下课铃响了(),学生.下课铃响了(),老师执行的是下班操作,学生执行的是放学操作,虽然二者消息一样,但是执行的效果不同
使用多态有什么好处?
1.增加了程序的灵活性
以不变应万变,不论对象千变万化,使用者都是同一种形式去调用,如func(animal)
2.增加了程序额可扩展性
通过继承animal类创建了一个新的类,使用者无需更改自己的代码,还是用func(animal)去调用
在python中,面向对象相关知识
面向对象在python中的实现和机制
对象
对象的创建过程
例如:class Foo
第一步:遇到 class Foo,执行type的__init__方法(type的init方法里面做什么呢?不知道,可能源码里面做些复杂操作)
第二步:然后执行type的__call__方法,
第三步:执行Foo的__new__方法。注意:该方法是在type的__call__里面调用的
第四步:执行Foo的__init__方法。注意:__call__执行完__new__方法后才会调用
总结:type.__init__ ----> type.__call__ ----> Foo.__new__ ----> Foo.__init__
对象:私有与公有属性
在python中用双下划线开头的方式将属性设置成私有的
这种操作并不是严格意义上的限制外部访问,仅仅只是一种语法意义上的变形,主要用来限制外部的直接访问。
知道了类名和属性名就可以拼出名字:_类名__属性,然后就可以访问了,如a._A__n
扩展:模块的私有变量
python并不会真的阻止你访问私有的属性,模块也遵循这种约定,如果模块名以单下划线开头,
那么from module import *时不能被导入,但是你from module import _private_module依然是可以导入的。
例如:以单下划线开头的模块名也是, 例如socket._socket,sys._home,sys._clear_type_cache,
这些都是私有的,原则上是供内部调用的,作为外部的你,一意孤行也是可以用的,只不过显得稍微傻逼一点点
属性查找
对象属性一开始在自己的属性字典__dict__查找,没有就按照mro列表查找父类的__dict__字典
方法
对类绑定函数时,若由对象调用,则函数的第一个参数为对象
如果使用像绑定变量一样的方法,将函数绑定于一个对象,对象不会作为被绑定函数的第一个参数,这时候还是要“显式”将对象作为第一个参数
john = Student() def set_age(student,age): pass john.set_age(john,17)
若要使得绑定后,将对象自己作为第一参数传入,只能使用MethodType绑定:
from types import MethodType s = Student() s.set_age = MethodType(set_age, s) # 给实例绑定一个方法 >>> s.set_age(25) # 调用实例方法
绑定到类的方法:用classmethod装饰器装饰的方法。
类名.方法,自动将类当作第一个参数传入 。其实对象也可调用,但仍将类当作第一个参数传入,这个行为可以利用于构造新的对象
用途:由于python不支持构造函数重载,所以经常要使用类方法来定义仿重载构造函数
@classmethod def from_conf(cls): print(cls) return cls(settings.HOST,settings.PORT)
staticmethod装饰的函数即非绑定方法,就是普通函数
statimethod不与类或对象绑定,谁都可以调用,没有自动传值效果
注意:无论是类方法还是静态方法,都可以被其子类调用
继承
1.当子类一旦重新定义了自己的属性且与父类重名,就以自己为准了
2.在子类覆写父类函数时,会清掉原有父类功能,若需要原来父类功能,则应该super().__init__(XXXX)
MRO列表
为了实现继承,python会在MRO列表上从左到右开始查找基类,直到找到第一个匹配这个属性的类为止。
注:python会计算出一个方法解析顺序=>MRO列表,这个MRO列表就是一个简单的所有基类的线性顺序列表
所以,千万不要通过看代码去找继承关系,一定要看MRO列表.因为对于你定义的每一个类,所有父类的MRO列表并遵循如下三条准则:
1.子类会先于父类被检查
2.多个父类会根据它们在列表中的顺序被检查
3.如果对下一个类存在两个合法的选择,选择第一个父类
super解析
不要一说到 super 就想到父类!super 指的是 MRO 中的下一个类!
例如:super(cls, instance)
这条代码包含了以下步骤:
1.super会先获取instance的__mro__列表,__mor__返回继承关系解析顺序列表
2.找到列表中cls的下一个类,返回
3.super()省略参数的时候,cls默认为自身的类。
所以,super()和super(cls,instance)的区别是,super(cls,instance)可解决钻石继承问题
额外:新式类与旧式类的区别,当类是新式类时,多继承情况下,在查找属性不存在时,会按照广度优先的方式查找下去,后者为深度
多重继承
多重继承中,写在前面的父类优先级更高, 表现为若有同名的父类函数,优先执行。
常用于定义一个继承object类定义相同的方法名,实现类似装饰器的效果
# 注意的是:super传入根据self的对象是谁,根据其mro列表的方法进行依次查找, # 所以出现了继承object类仍然调用super(xx,self).func的代码,毫无疑问,这是用于多继承的其中一个父类 class MyBaseView(object): def dispatch(self, request, *args, **kwargs): print('begin....') # 注意:面向对象要注意在子类中调用super(FatherCls,self).dispath 这种类型的调用父类方法,self还是代表子类对象 # 这种方式时常结合反射 ret = super(StudentView, self).dispatch(request, *args, **kwargs) print('after....') return ret class StudentView(MyBaseView, View): def get(self, request, *args, **kwargs): return HttpResponse('get') def post(self, request, *args, **kwargs): return HttpResponse('post')
注意:super()调用父类方法时注意:一个类拥有两个父类,若父类函数中也有super()调用,
即使两个父类没有继承关系 ,super仍然会按照mro继续往后查找,即会出现在mro列表前的父类调用mro列表靠后的父类方法
自省/反射
什么是反射?
自省也称作反射,这个性质展示了某对象是如何在运行期取得自身信息的。
并且在python里,反射可以使得程序运行时对象拥有增删改查它本身属性或行为的一种能力
如果Python不支持某种形式的自省功能,dir和type内建函数,将很难正常工作。还有那些特殊属性,像__dict__,__name__及__doc__
反射的使用场景?
即插即用,即可以事先定义好接口,接口只有在被完成后才会真正执行
比如:如果和别人共同合作开发项目,但是需要用到对方的类的方法,对方还没完成
f1=FtpClient('192.168.1.1') if hasattr(f1,'get'): func_get=getattr(f1,'get') func_get() else: print('---->不存在此方法') print('处理其他的逻辑')
四个可以实现自省的函数
python通过字符串的形式操作对象相关的属性。python中的一切事物都是对象(都可以使用反射)
getattr(x, 'y') #x.y setattr(x, 'y', v) #x.y = v可以设置属性setattr(b1,'show_name',lambda self:self.name+'sb') delattr(x, 'y') #del x.y hasattr(b1,'name')
魔法方法
自省相关的三个魔法方法
__setattr__,__delattr__,__getattr__ #看名字就知道是自省函数在对象内部的实现形式
覆写以上函数非常容易出现无限递归,因为无论是操纵全局自省函数,还是对象.属性都是去调用对应的魔法方法。所以只能操纵魔法字典这个属性
def __getattr__(self, item): return self.__dict__[item]
覆写以上函数的格式和对应的全局自省函数参数相同,因为全局自省函数就是调用对应的它们
__getattribute__
__getattr__对比__getattribute__有什么区别?
1)当使用 对象.属性 找不到的时候,会调用getattr,返回一个值或AttributeError异常,但是若属性存在,则不调用。
但是__getattribute__无论属性存在与否都会调用
2)当__getattribute__与__getattr__同时存在,只会执行__getattrbute__,除非__getattribute__在执行过程中抛出异常AttributeError
描述符协议
__get__,__set__,__delete__ # 描述符
什么是描述符?
描述符的功能就是描述其他类的类属性
描述符本质就是一个新式类,在这个新式类中,至少实现了__get__(),__set__(),__delete__()中的一个,这也被称为描述符协议
__get__():调用一个属性时,触发
__set__():为一个属性赋值时,触发
__delete__():采用del删除属性时,触发
描述符的原理是将描述符定义为指定类的类属性,产生指定操作时,便去属性字典里面找,如果是描述符对象,便执行
class Int: def __get__(self, instance, owner): #owner是instance的class print('Int调用') def __set__(self, instance, value): print('Int设置...') def __delete__(self, instance): print('Int删除...') class People: age=Int() def __init__(self,name,age): self.age=age #执行Int.__get__
描述符的分类
描述符分两种,数据描述符(至少实现类__get__()和__set__()), 非数据描述符(没有实现__set__()),
考虑要定义哪一种,主要是考虑需不需要给实例定义自己相同的属性(该属性名与类描述符名相同)
并且数据描述符的优先级只有类属性可以覆盖。
1.__set__设置类.描述符时,新设的类属性值会覆盖描述符,但是通过实例.描述符却不会覆盖。在类的__dict__可以观察到。
2.描述符只能通过 类.描述符名=XX 改变,实例是无法改变,并且实例可以设置描述符同名的属性(所以数据描述符,和非数据描述符就是有区别,区别就是有__set__方法,就应该去执行,没有的话就直接绑定了)
描述符的用处
干预被描述的属性的操作
当操纵指定的类属性时,会跳到相应的描述符函数并执行返回。我们可以在描述符函数通过操作指定对象的__dict__模仿正常的属性操作。所以说这中间多了个干预工程,常用于制作类构造函数的类型限制。
给类加上类属性的三种方法:(注意,函数也属于类属性)
1)直接在类中声明,例如a=Des()
2)定义一个函数’装饰器‘,装饰于类中。在函数里面为类添加属性
3 ) 定义一个类’装饰器‘,然后装饰于另外一个类的方法里,
然后被装饰器的函数名就指向这个类’装饰器‘产生的对象(obj=cls(func)),若此时类装饰器又实现了__get__方法,
换句话说,函数是类属性,类属性指向描述符对象。专门应用于函数。
扩展:类装饰器
类装饰器,原理和函数装饰器一样,前者将函数当作对象传入,后者将类当作对象传入,通过类比可知。
当构造函数的参数过多时,可以额外写一个函数作装饰器(参数为 属性名=期望类型..的可变参数**kwargs,函数里面,为类加入描述符属性(类.属性名=描述符)
例如:自制proprety就是类装饰器配合描述符的好例子
class Pro(): def __init__(self,ab): self.ab=ab def __get__(self,instance,owner): return self.ab(instance) class Apple(): def __init__(self,a,b): self.a=a self.b=b @Pro def ab(self): return self.a*self.b def __str__(self): return ("是我a=%s,b=%s"%(self.a,self.b)) if __name__=='__main__': aa=Apple(2,3) print(aa.ab)
重载运算符相关
__setitem__,__getitem,__delitem__ #等于c++的运算符[]重载
__call__ #变为函数对象
对象描述相关
__str__,__repr__,__format__
1)改变对象的字符串显示__str__,__repr__,自定制格式化字符串__format__
2)在类中__str__和__repr__没什么区别。如果__str__没有被定义,那么就会使用__repr__来代替输出
注意:这俩方法的返回值必须是字符串,否则抛出异常
特殊的__slots__
__slots__ #内存优化工具,顺便可以限制属性
用法:
可以选择性定义的一个类属性,类型是列表,列表元素是定义的实例属性名(字符串)
作用:
1)__slots__机制借由数组存储属性来节约内存。因为类的字典是共享的,而每个实例的是独立的,字典占用内存很大,
2)在类中定义__slots__属性列表后,实例通过一个很小的固定大小的数组来构建,在__slots__中列出的属 性名在内部被映射到这个数组的指定小标上。而不是为每个实例定义一个字典,节约内存。
3)使用__slots__的代价就是不能再给实例添加新的属性,只能使用在__slots__中定义的那些属性名
__next__和__iter__ #实现迭代器协议
__doc__ #类中最开头处的字符串 class Foo: '我是描述信息' pass print(Foo.__doc__)
模块相关
__module__ #表示当前操作的对象在那个模块
类相关
class Fo: pass Fo.__class__ #表示当前操作的对象的类是什么 Fo.__del__ #析构方法,当对象在内存中被释放时,自动触发执行。 Fo.__name__# 类的名字(字符串) Fo.__base__# 类的第一个父类(在讲继承时会讲) Fo.__bases__# 类所有父类构成的元组(在讲继承时会讲) Fo.__dict__# 类的字典属性 Fo.__class__# 实例对应的类(仅新式类中) #注:类属性使用:类.类属性,例如person.son
1)创建数据库类,用该类实例化出数据库链接对象,对象本身是存放于用户空间内存中,而链接则是由操作系统管理的,存放于内核空间内存中
当程序结束时,python只会回收自己的内存空间,即用户态内存,而操作系统的资源则没有被回收,这就需要我们定制__del__,在对象被删除前向操作系统发起关闭数据库链接的系统调用,回收资源
2)f=open('a.txt') #做了两件事,在用户空间拿到一个f变量,在操作系统内核空间打开一个文件
del f #只回收用户空间的f,操作系统的文件还处于打开状态
#所以我们应该在del f之前保证f.close()执行,即便是没有del,程序执行完毕也会自动del清理资源。
上下文管理器
__enter__和__exit__ #上下文管理器
注意的是:__exit__的函数参数有点多。还有一般这上下文管理器中的异常会有点多。
上下文中间的代码发生异常时,若__exit__()返回值为True,那么异常就会被吞掉,就好像啥都没发生一样,
例如:
class Open: def __init__(self,name): self.name=name def __enter__(self): print('出现with语句,对象的__enter__被触发,有返回值则赋值给as声明的变量') def __exit__(self, exc_type, exc_val, exc_tb): print('with中代码块执行完毕时执行我啊') print(exc_type) print(exc_val) print(exc_tb) #return True with Open('a.txt') as f: print('=====>执行代码块') raise AttributeError('***着火啦,救火啊***') print('0'*100) #------------------------------->不会执行
元类
type()函数产生类
Foo=type(class_name,class_bases,class_dic)
第 1 个参数是字符串 ‘Foo’,表示类名
第 2 个参数是元组 (object, ),表示所有的父类
第 3 个参数是字典,这里是一个空字典,表示没有定义属性和方法
使用自己定义的元类(class A(metaclass=MyType))
当定义自己的元类时,首先要继承type(注意一下,元类中self意味着实例化的cls)
1)如果想干涉‘元类-->类’,就改写__new__。(def__new__(cls,class_name,class_bases,class_dict))
因为类创建对象的流程是__new__创建对象后将对象地址传给__init__,__init__加工上自己所需的属性,
换句话说,前者负责创建返回,后者负责加工。
2)如果想干涉’对象的创建‘,就改写元类的__call__,其实元类的__call__才是大佬,
由它决定要不要调用类的__new__函数和__init__函数。默认会调
额外:鸭子类型
Python崇尚鸭子类型,即‘如果看起来像、叫声像而且走起路来像鸭子,那么它就是鸭子’,python程序员通常根据这种行为来编写程序
如果想编写现有对象的自定义版本,可以继承该对象
也可以创建一个外观和行为像,但与它无任何关系的全新对象,后者通常用于保存程序组件的松耦合度。
例如:利用标准库中定义的各种‘与文件类似’的对象,尽管这些对象的工作方式像文件,但他们没有继承内置文件对象的方法
#二者都像鸭子,二者看起来都像文件,因而就可以当文件一样去用
class TxtFile: def read(self): pass def write(self): pass class DiskFile: def read(self): pass def write(self): pass