文章目录
- 元类
- 类工厂
- 初始元类
- 元类属性
- 元类作用
- 面向方面和元类
- 小结
- 新型类
- 新型类VS传统类
- 静态方法和类方法
- 特定方法
- 特定属性
- super()方法
- 小结
元类
既然对象是以类为模板生成的,那么类又是以什么为模板生成的?
事实上绝大部分情况下都都不是必须使用元类才能完成开发,但是元类动态地生成类的能力能更方便地解决下面情景的难题:
- 类在设计时不是所有细节都能确定,有些细节需要程序运行时得到的信息才能决定。
- 类比实例更重要的情况,如用声明性语言在类声明中直接表示了它的程序逻辑,使用元类来影响类的创建过程就相当有用。
类工厂
在Python老版本中,可以使用类工厂函数来创建类,返回在函数体内动态创建的类。 类工厂的方法是通过一个函数来生产不同的类。类工厂可以是类,就像它们可以是函数一样容易。 例如:
代码语言:javascript复制def class_with_method(func):
class klass: pass
setattr(klass, func.__name__, func)
return klass
def say_tip(self):
print('记得一键三连~')
Tip = class_with_method(say_tip)
tip = Tip()
tip.say_tip()
函数class_with_method
是一个类工厂函数,通过setattr()
方法来设置类的成员函数,并且返回该类,这个类的成员方法可以通过class_with_method
的func
参数来指定。
初始元类
在Python2.2之后,type特殊类就是这样的类工厂,即所谓的元类,元类是类的类,类是元类的实例,对象是类的实例。 元类type使用方法:
代码语言:javascript复制def say_tip(self):
print('记得一键三连~')
Tip = type('Tip',(),{'say_tip':say_tip})
tip = Tip()
tip.say_tip()
元类type首先是一个类,所以比类工厂的方法梗灵活多变,可以自由的创建子类来继承扩展元类的能力。例如:
代码语言:javascript复制class ChattyTypr(type):
def __new__(cls, name, bases, dct):
print("分配内存空间给类",name)
return type.__new__(cls, name, bases, dct)
def __init__(cls, name, bases, dct):
print("初始化类", name)
super(ChattyTypr, cls).__init__(name, bases, dct)
a = ChattyTypr('Test',(),{})
其中,__new__
分配创建类和__init__
方法配置类是类type内置的基本方法,需要注意的是,第一个蚕食是cls
(特指类本身)而非self
(类的实例)。
元类实例化一个类时,类将会获得元类所拥有方法,就像类实例化对象时对象获得类所拥有方法一样,但是注意多次实例化和多次继承的区别:
元类属性
Python中每一个类都是经过元类实例化而来,只不过这个实例化过程在很多情况下都是由Python解释器自动完成的。那么怎么设置元类的属性?
每个类都有一个属性__metaclass__
用来说明该类的元类,该属性一般由解释器自动设置,不过用户也可以更改该属性来更改类的元类。可以在类的内部直接设置__metaclass__
属性,也可以设置全局变量,那么该命名空间下定义所有类的元类都将是全局变量__metaclass__
所指定的元类。
class ChattyTypr(type):
def __new__(cls, name, bases, dct):
print("分配内存空间给类",name)
return type.__new__(cls, name, bases, dct)
def __init__(cls, name, bases, dct):
print("初始化类", name)
super(ChattyTypr, cls).__init__(name, bases, dct)
class example(metaclass=ChattyTypr):
def __init__(self):
print('初始化')
元类作用
改变全局变量__metaclass
就能改变类的元类,而类又是元类的实例化结果,所以元类可以改变类的定义过程。换句话说,只要改变全局变量__metaclass__
就能改变类的定义,这就是元类的作用了。
class example:
def __init__(self):
print('类example初始化')
def say_tip(self):
print('记得一键三连')
a = example()
a.say_tip()
class change(type):
def __new__(cls, name, bases, dict):
def say_tip(self):
print('记得点赞关注收藏~')
dict['say_tip']=say_tip
return type.__new__(cls, name ,bases, dict)
class example(metaclass=change):
def __init__(self):
print('类example初始化')
def say_tip(self):
print('记得一键三连')
a = example()
a.say_tip()
面向方面和元类
元类的作用能带来什么实用价值吗? 实际用途确实有的,接近于面向方面编程(Aspect Oriented Programming,AOP)的核心内容,即所谓的“横切关注点”。
使用面向对象方法构建软件系统,我们可以利用OO的特性很好地解决纵向问题,因为OO的核心概念(如继承等)都是纵向结构的。 但是软件系统中往往很多模块/类共享某个行为,或者说某个行为存在于软件的各个部分中,看作是横向 存在于软件之中,它所关注的是软件个部分共有的一些行为,而且很多情况下这种行为不属于业务逻辑的一部分。
一个软件系统的业务逻辑很大一部分代码都是AOP里所说的横切关注点。例如日志处理、安全检测、事务处理、权限检测等占比很大,几乎每个地方都要调用。AOP的思想就是把这些横切关注点代码都抽取出来,不再在各个软件模块中显示使用。
以日志处理为例,一般习惯在做一些操作前写上开始模块处理的每个步骤都需要由正常日志和异常日志,那么这个软件光是写日志的代码就要成千上万行了,维护起来相当困难。
如果部分代码不需要手工写到各个业务逻辑处理的地方,而是把这部分代码独立出来,那么在各个业务逻辑处理的地方,会在运行的时候自动调用这些横切关注点功能,这样代码量就少很多,这就是AOP的核心思想。
要实现AOP所说的自动调用,有的语言使用AspectJ编译器,Python则使用元类。
小结
元类具有动态改变类的能力,给编程带来了更方便的动态性和能力。 实际使用过程中,需要防止过度使用元类来改变类,过于复杂的元类通常会带来代码难以和可读性差的问题,所以一定要在确实需要使用是再使用元类。
新型类
Python在2.2版本后,新引入了两种不同的类:新型类和传统类/经典类。Python的对象世界相比也发生了重大变化。
新型类VS传统类
老版本的Python中不是所有的元素都是对象,内置的数值类型都不能被继承,而在版本2.2后,任何内建类型也都是继承自object类的类,凡是继承自类object或者object子类的类都是新型类,而不是继承自object或object子类的都成为传统类。
新的对象模型于传统模型相比有小但是很重要的优势,Python版本对传统类的支持主要是为了兼容性,所以使用类的时候推荐从现在开始直接使用新型类。在Python3版本将放弃兼容性,即Python3.X版本中只存在新型类。
新型类继承自object或object子类,实际上所有的内建类型都是从object继承而来,可以用issubclass()
函数验证,当存在子类和父类关系时返回True,否则返回False。
(
插播反爬信息)博主CSDN地址:https://wzlodq.blog.csdn.net/
静态方法和类方法
新的对象模型提供了两种类的方法:静态方法和类方法。
静态方法可以直接被类或类的实例调用,没有常规方法的那样限制(绑定、非绑定、默认第一个参数规则等),即静态函数的第一个参数不需要指定为self
,也不需要只有对象(类的实例)才能调用。使用关键字@staticmethod
定义。
如下定义静态方法、常规方法(第一个参为self
和不带self
两种)
class Test(object):
@staticmethod
def static_tip(str):
print(str)
def normal_tip(str):
print(str)
def normal_tip2(self,str):
print(str)
- 使用类调用 直接使用类调用时,不需要传入self表示具体的类的实例,即报错只传了一个参数。
- 使用对象(类的实例)调用 使用对象调用时,自动将类实例对象作为第一个参数传给该方法,即报错给了两个参数。
类方法不管是使用类来调用还是使用对象(类的实例)来调用,都是将类作为第一个参数传入。使用关键字@classmethod
定义。
特定方法
__new__
方法 当一个类C调用C(*args,**kwds)
创建一个C类实例时,Python内部实际上调用的是C.__new__(C,*args,**kwds)
。new方法的返回值x就是该类的实例对象,new即用来分配内存生成类的实例。 注意第一个参数是cls
(即这里写的类C),用来接受一个类参数,然后才能返回该类的实例。
使用new方法可以实现一些传统类无法做到的功能,例如让类只能实例化一次:
__init__
方法 当调用new方法分配内存创建一个类C对象后,Python判断该实例是该类的实例,然后会调用C.__init__(x,*args,**kwds)
来初始化这个实例,x就是new方法的返回值,init即对类实例对象做初始化操作。 注意第一个参数是self
(即这里写的x)表示接受类的实例对象。
上述实例化对象代码c = C()
就等价于:
__getattribute__
方法__getattribute__
负责实现对象属性引用的全部细节。新型类在调用它自身的类或方法是,实际上都是先通过该方法来调用。
因为新型类调用自身属性和方法时都会先调用__getattribute__
方法,所以可以实现一些新功能,如隐藏父类的方法:
特定属性
内建property
类用来绑定类实例的方法,并将其返回值绑定为一个类属性,语法:
attrib = property(fget=None, fset=None, fdel=None, doc=None)
设类C通过property
创建了属性attrib
,x是类C的一个实例。
- 当引用
x.attrib
时,会调用fget()
方法取值; - 当为
x.attrib
赋值时,会调用fset()
方法; - 当执行删除
del x.attrib
时,会调用fdel()
方法; doc
参数为该属性的文档字符串。
如果不定义fset()
和fdel()
方法,那么该属性将是一个只读属性。
property
可以方便地将一个函数的返回值转换为属性,这下操作就很灵活方便了。
比如定义一个长方形类,如果要将它的面积也作为一个属性,就可以用property
将计算面积的方法绑定为一个属性:
class Rectangle(object):
def __init__(self,width,height):
self.width=width
self.height=height
def getArea(self):
return self.width*self.height
area = property(getArea(),doc='长方形的面积')
上述代码中,getArea()
是计算面积的方法,使用property
将该方法的返回值转换为属性area
,这样引用Rectangle的area
是,Python会自动使用getArea()
计算出面积。同时由于该例中只定义了fget()
方法,所以area
是一个只读属性。
super()方法
新型类提供了一个特殊的方法super()
。super(aclass,obj)
返回对象obj是一个特殊的超对象(superobject)。当我们调用该超对象的一个属性或方法时,就保证了每个父类的实现均被调用且仅仅调用了一次。
以下时直接调用父类的同名方法,无法避免类A的方法被重复调用:
代码语言:javascript复制class A(object):
def test(self):
print('A')
class B(A):
def test(self):
print('B')
A.test(self)
class C(A):
def test(self):
print('C')
A.test(self)
class D(B,C):
def test(self):
print('D')
B.test(self)
C.test(self)
d = D()
d.test()
以下时使用super()
方法,保证父类方法均调用一次:
class A(object):
def test(self):
print('A')
class B(A):
def test(self):
print('B')
super(B, self).test()
class C(A):
def test(self):
print('C')
super(C, self).test()
class D(B,C):
def test(self):
print('D')
super(D, self).test()
d = D()
d.test()
小结
新型类相比于传统类,支持更多特性和机制,有更多的弹性。例如可以定制实例化的过程,尤其时在多重继承的情况下能避免传统类存在的缺陷。而事实上Python3.X版本中已经不存在传统类了,目前传统类存在的意义主要是为了保持之前的兼容性。