八叶一刀·无仞剑

万物流转,无中生有,有归于无

导航

Python描述符

Posted on 2019-11-22 20:15  闪之剑圣  阅读(396)  评论(0编辑  收藏  举报

描述符的定义

描述符是Python中一种特殊的语法,它是对多个属性利用相同存取逻辑的一种方式。描述符是实现了特定协议的类,这个协议包括实现__get__,__set__和__delete__方法,不过也可以只实现一部分协议。

描述符的简单例子

class Quantity(object):
	def __init__(self, name):
		self.name = name
	
	def __get__(self, instance, owner):
		return instance.__dict__[self.name]
	
	def __set__(self, instance, val):
		instance.__dict__[self.name] = val 
	

class Obj(object):
	weight = Quantity("weight")
	price = Quantity("price")
	
	def __init__(self, weight, price):
		self.weight = weight
		self.price = price
		
obj = Obj(1,2)
print obj.weight, obj.__dict__

输出为:
1 {'price': 2, 'weight': 1}
在以上的代码中,Quantity就是一个描述符,它实现了__get__、__set__方法,它其实是一个类属性。在访问类的对象时,会优先访问同名描述符的对象,并调用相应的__get__和__set__方法。
在__get__函数中,参数instance是类的对象,owner是类本身;__set__函数中,instance是类对象,val则是传入的值。描述符中存储了对象成员的名称,通过instance的__dict__来访问和存取相应的对象成员。

有人会觉得上图中给描述符命名的方法略显繁琐,毕竟名称可能会输入错误,那么有没有不用手动给描述符传入名称的初始化方法呢?当然有咯!直接上代码:

class Quantity(object):
	count=0
	def __init__(self):
		self.name = '{}'.format(Quantity.count)
		Quantity.count += 1
	
	def __get__(self, instance, owner):
		return getattr(instance, self.name)
	
	def __set__(self, instance, val):
		setattr(instance, self.name, val) 
	

class Obj(object):
	weight = Quantity()
	price = Quantity()
	
	def __init__(self, weight, price):
		self.weight = weight
		self.price = price
		
obj = Obj(1,2)
print obj.weight, obj.__dict__, Obj.__dict__

输出为:

1 {'1': 2, '0': 1} {'__module__': '__main__', 'weight': <__main__.Quantity object at 0x03C79450>, 'price': <__main__.Quantity object at 0x03C79470>, '__dict__': <attribute '__dict__' of 'Obj' objects>, '__weakref__': <attribute '__weakref__' of 'Obj' objects>, '__doc__': None, '__init__': <function __init__ at 0x03C7DD30>}

从中可以看到,我们能给描述符传入任意的名称,当我们访问obj的weight时,会触发Obj的weight描述符的__get__和__set__函数,实际操作的则是obj __dict__中的‘0’号对象。
另外,通过查看Obj的__dict__对象,我们也能了解其实描述符就是一种类属性。只不过它的函数能传入类的instance作为参数,以此来访问类对象的属性而已。

覆盖型描述符与非覆盖型描述符

描述符分为覆盖型和非覆盖型的,覆盖型与非覆盖型的区别在于是否实现了__set__函数。
实现了__set__函数的话,我们对类对象的属性赋值时,就会优先访问它相应描述符的__set__函数;没有实现__set__函数的话,对类对象成员进行赋值则会将描述符遮盖:

class Quantity(object):
	count=0
	def __init__(self):
		self.name = '{}'.format(Quantity.count)
		Quantity.count += 1
	
	def __get__(self, instance, owner):
		print "666"
		return 2
	
	def __set__(self, instance, val):
	      setattr(instance, self.name, val) 
	

class Obj(object):
	weight = Quantity()
	price = Quantity()
	
	def __init__(self, weight, price):
		pass
		
obj = Obj(1,2)
obj.weight = 3
print obj.weight, obj.__dict__

输出为:

666
2 {'0': 3}

可以看到,当存在覆盖型描述符时,对weight的赋值会直接调用weight描述符的__set__方法,实际更改的是__dict__中的"0"号对象。
让我们再看看非覆盖型的:

class Quantity(object):
	count=0
	def __init__(self):
		self.name = '{}'.format(Quantity.count)
		Quantity.count += 1
	
	def __get__(self, instance, owner):
		print "666"
		return 2
	

class Obj(object):
	weight = Quantity()
	price = Quantity()
	
	def __init__(self, weight, price):
		pass
		
obj = Obj(1,2)
print obj.weight
obj.weight = 3
print obj.weight, obj.__dict__

输出为:

666
2
3 {'weight': 3}

可以看到,当我们只是访问weight时,会调用描述符的__get__方法;而当我们为weight赋值时,程序会在对象的__dict__里创建一个属性,并覆盖描述符。

类的方法也是一种描述符

值得一提的是,类中定义的方法其实也是一种描述符。方法自己是function对象,定义了__get__函数(没有定义__set__,因此它们是非覆盖型描述符),当我们调用类对象的方法成员时,__get__函数调用,并返回一个绑定的方法对象(bound method),该对象的__func__成员即是真正引用的原始函数。通过调用绑定方法对象的__call__方法,来调用__func__,最终将函数执行起来。

最后感谢刘佳卉大佬建议我用Markdown来写博客。