1. 数据描述符与非数据描述符
首先,描述符只能作用于类属性,如果将描述符作用于对象属性,则不会生效。
class D:
def __get__(self, instance, owner):
print("~get")
class C:
def __init__(self):
self.x = D()
应该将D对象赋值给类C的属性:
class C:
x = D()
所谓数据描述符,就是实现了__set__或者__delete__魔法方法,如果只实现了__get__魔法方法,就称为非数据描述符。通过给描述符分类,旨在说明数据访问的优先级:数据描述符 -> 对象的属性 -> 非数据描述符 -> 类属性。比如说上面的D的对象就是一个非数据描述符,它的优先级是低于对象属性的,所以如果给对象c的属性x赋新的值,然后查询c.x,不会打印“~get”:
说明没有经过__get__魔法方法拦截,但是如果访问类的属性x,则会被拦截到:
将D改为数据描述符:
class D:
def __get__(self, instance, owner):
print("~get")
def __set__(self, instance, value):
print("~set")
class C:
x = D()
此时数据描述符的访问属性最高,对对象属性的访问,全部会被__get__魔法方法拦截(当然对象属性赋值的操作也会被__set__魔法方法拦截):
即使修改c.__dict__字典,仍然无效:
但是对于对象属性的访问,最终是会走__getattribute__魔法方法,因此其优先级最高,默认的__getattribute__魔法方法中就实现了上述优先级,如果重写__getattribute__魔法方法,也就没描述符什么事了:
class C:
x = D()
def __getattribute__(self, name):
print("~aha")
在描述符里操作实例对象的__dict__属性,以修改实例对象的属性:
class D:
def __set_name__(self, owner, name):
self.name = name
def __get__(self, instance, owner):
print("~get")
return instance.__dict__[self.name]
def __set__(self, instance, value):
print("~set")
instance.__dict__[self.name] = value
class C:
x = D()
2. 类装饰器
上述代码相当于C = report(C),相当于一个oncall函数,创建C的对象,就会调用oncall函数:
类装饰器的作用就是类被实例化之前进行一些拦截和干预。
不仅可以使用函数来装饰类,也可以使用类来装饰函数:
class Counter:
def __init__(self, func):
self.count = 0
self.func = func
def __call__(self, *args, **kwargs):
self.count += 1
print(f'已经被调用了{self.count}次~')
return self.func(*args, **kwargs)
@Counter
def say_hi():
print("嗨~")
也可以用类装饰类:
class Check:
def __init__(self, cls):
self.cls = cls
def __call__(self, *args, **kwargs):
self.obj = cls(*args, **kwargs)
def __getattr__(self, name):
print(f'正在访问{name}')
return getattr(self.obj, name)
@Check
class C:
def __init__(self, name):
self.name = name
def say_hi(self):
print(f'嗨{self.name}~')
def say_hey(self):
print(f'嘿{self.name}~')
上述例子中,c1的name属性被c2给覆盖了。这是因为类装饰器装饰类时,相当于C = Check(C),即C不再是一个类了,而是一个Check对象; c1 = C("c1"),相当于调用check对象的__call__方法,__call__方法里是在创建一个C类的对象,赋给了check对象的属性,而这个check对象是只有一个,即用类装饰器装饰类的时候生成的那一个,因此再次执行c2 = C("c2")相当于覆盖了之前同一个check对象的obj属性;执行c1.name,相当于去寻找check对象的name属性,显然check对象没有name属性,因此去调用__getattr__方法,即去寻找其obj属性(也就是C类对象,这里就是c2)的name属性。本质原因是共用了一个Check对象。
为了解决这个问题,将Check类改为:
def report(cls):
class Check:
def __init__(self, *args, **kwargs):
self.obj = cls(*args, **kwargs)
def __getattr__(self, name):
print(f'正在访问{name}')
return getattr(self.obj, name)
return Check
@report
class C:
def __init__(self, name):
self.name = name
def say_hi(self):
print(f'嗨{self.name}~')
def say_hey(self):
print(f'嘿{self.name}~')
上述代码C不再是C类,而是C = report(C),是一个Check类,执行c1 = C("c1")相当于生成一个check对象,对象的obj属性是一个C类的对象,里面存了name属性。同理执行c2 = C("c2")相当于生成另一个check对象,同样obj属性保存了另一个C类的对象,里面存了name属性。此时Check类对象不再只只有一个,而是有两个,可以访问到各自的name属性,不会被覆盖。
3. type函数
type函数的常用的用法:
type函数的一些不常用的用法:
本质上就是因为type函数返回对象所属的类。那如果type函数传入的就是一个类,而不是一个对象,会发生什么?答案是会返回type类:
这是为什么?因为Python中万物皆对象,一个类也是对象,类是由type类衍生出来的对象。因type函数隐藏的一个更强大的功能就是创造类:
C = type("C", (), {})
上面的代码是用type函数创建了一个类,第一个参数是类名,第二个参数是父类,参数类型是元祖,第三个参数是类的属性和方法。以上代码与以下代码等同:
class C:
pass
使用type函数创建D类,继承C类
创建带有属性的类:
创建带有方法的类:
def funC(self, name):
print(f'Hello {name}')
F = type(F, (), dict(say_hi=funC))
type函数还有第四个参数,用于给__init_subclass__魔法方法传递参数。首先看__init__subclass__魔法方法的作用:用于覆盖子类的类属性
class C:
def __init_subclass__(cls, value):
print("父爱如山~")
cls.x = value
class D(C, value=250):
x = 520
一旦定义完类D,类C的__init_subclass__魔法方法就会被调用,打印“父爱如山~”,并且D的类属性x的值为520:
上述代码用type函数实现:
D = type("D", (C,), dict(x=250), value=520)
4. 元类
简单来说,继承type的类就是元类:
class MetaType(type):
pass
class C(metaclass=MetaType):
pass
可哟看出,类C的类型:type(C)是我们刚写的元类MetaType,不再是type了。而MetaType的类型是type:
元类相当于创造类的模板,在用元类创造普通类的过程中,一旦普通类定义完成,其元类的__new__和__init__魔法方法就会被调用,但是在创建普通类的对象的过程中不会被调用:
由此可见,普通类相当于人,元类相当于神,而type则是众神之神。元类中的__new__方法的各参数含义:mcls——元类,name——普通类名,bases——普通类继承的父类,attrs——普通类的属性,__init__方法的各参数含义:cls——普通类,name——普通类名,bases——普通类继承的父类,attrs——普通类的属性:
如果在元类中定义__call__魔法方法,那么在普通类实例化对象的过程中就会拦截:
class MetaType(type):
def __call__(self, *args, **kwargs):
print("__call() in MetaC~")
class C(metaclass=MetaType):
pass