python 描述器 详解_Python 黑魔法:描述器(descriptor)

问: 什么是描述器?

答: 描述器是一个Python对象。

问: 这个Python对象和普通的Python对象有什么区别?

答: 只要具有__get__(), __set__(), __delete__()方法中任意一个方法的对象就叫做描述器。

问: 描述器在功能上有什么特殊之处吗?

答: 一个对象在访问描述器时,如果该属性是一个描述器,则默认属性回调规则会被__set__, __get__,__delete__方法所覆盖。

问: 描述器有什么用?

答: Python内部自带的staticmethod, classmethod, property,super等都是描述器,在很多Python库中也都有描述器的身影(例如SQLAlchemy),使用描述器能让你有更高的概率写出优美的代码、更简洁的API,并会加深对Python理解。

通过上面假象式的QA,希望你对描述器是什么能有初步的了解,接下来正式介绍描述器。

1. 描述器的定义

具有下面三个方法中任意一个方法的Python对象就叫做描述器。

__set__(**)

__get__(**)

__delete__(**)图 1. 描述器协议相关方法

描述器又可分为数据描述器(data descriptor)和非数据描述器(non-data descriptor),只有__get__方法的对象被称为非数据描述器(non-data desriptor),其他的都称为数据描述器(data descriptor)。以上便是描述器的定义,虽然定义简单但用起不一定那么好把握,接下来通过代码片段来分析描述器的特点。

2. 使用描述器

描述器怎么定义?请下面代码(code#1):

# code#1

class PositiveNumber(object):

def __init__(self, name, value):

self.name = name

self._value = value

def __get__(self, obj, dtype):

return self._value

def __set__(self, obj, value):

if value < 0:

raise ValueError('Cannot be negative.')

self._value = value

if __name__ == '__main__':

number = PositiveNumber('number', 10)

上面代码中的number便是一个描述器。直接将描述器作为一个独立对象,并不能展现出描述器的魔力,只有在描述器作为另一个对象的属性的时候,描述器的魔力才能真正展现出来。例如下面代码(code#2):

# code#2

class PositiveNumber(object):

def __init__(self, name, value):

self.name = name

self._value = value

def __get__(self, obj, dtype):

print("call `__get__` method")

print("obj : {}".format(obj))

print("dtype: {}".format(dtype))

return self._value

def __set__(self, obj, value):

if value < 0:

raise ValueError('Cannot be negative.')

print("call `__set__` method")

print("obj : {}".format(obj))

print("set price to {:d}".format(value))

self._value = value

def __delete__(self, obj):

print("call `__delete__` method")

print("obj : {}".format(obj))

print("delete object :{}".format(obj))

class Apple(object):

price = PositiveNumber('price', 0)

def __init__(self):

self.price = 10

上面代码中我将描述器price作为Apple的一个类对象,然后在__init__对其进行赋值操作。在这里停一下,想一下创建一个Apple对象时,会发生什么?

从上面结果可以看出,当我们创建Apple对象的时候,内部自动调用了price描述器的__set__方法。

当我们通过apple对象访问price时,又会发生什么?

从上面结果可以看出,当我们通过apple对象访问price时,内部自动调用了__get__方法。

当我们通过apple对象对price进行赋值操作时,又会发生什么?

从上面结果可知,当通过apple向price赋一个负值的时候,price的__set__方法被调用了,并抛出了ValueError(因为定义中指定了price不能小于零)。

当我们通过del删除apple.price时,又会发生什么?

从上面结果可知,通过del删除apple.price时,内部并没有真正删除price,而是直接调用了price的__delete__方法。

通过上面几行代码,我想你对描述器的工作原理已经有了大致的理解,总结如下:当通过其他对象A访问描述器时,不会直接返回描述器本身,而是调用描述器的__get__方法,并把对象A和A的类(即type(A))作为__get__的输入参数,最后返回__get__方法的返回值。如果没有定义__get__方法,则返回描述器本身。需要注意的是,当对象存在一个与描述器同名的属性时(即同时存在两个名字相同的属性,一个为描述器,另一个为其他普通属性),对象访问该属性的回调顺序会根据描述器的不同(数据描述器和非数据描述器)而不同,如下图所示:Python 对象属性访问回调顺序,图来源[1],侵删

当描述器为数据描述器时,数据描述器会先于对象的普通属性被访问(图蓝色框部分);如果描述器为非数据描述器时,对象的普通属性会先于非数据描述器被访问(图红色框部分)。不信?来飚一波代码?算了,空间有限,放图太占空间,代码给留在这里,有心可以在线运行一下,有相应注释。当通过其他对象A对描述器赋值时,不会把值直接赋给描述器本身,而是调用描述器的__set__方法,并把对象A和赋的值作为__set__的输入参数。如果描述器没有__set__方法(即为非资料描述器),强行对其赋值,此时会抛出AttributeError。如果你想让你的描述器为只读属性,可以先实现__set__方法,然后在其内部进行错误处理,并抛出对应的Error,这样会显得更友好一点。

和属性访问相同的,在给对象属性赋值的时候,也要遵从一定赋值顺序,不过相比于对象属性访问要简单的多,如下图所示。对象属性幅值顺序

简单说,如果赋值属性是数据描述器,则调用数据描述器的__set__进行赋值操作,如果不是,则直接在当前对象的__dict__创建对应的属性,并赋值。当通过del删除描述器时,不会删除描述器本身,而是调用描述器的__delete__方法,并把当前对象作为__delete__的输入参数。

3. 描述器的使用

文章开头提到过Python内部的property,classmethod, staticmethod, super都是描述器,这一节就使用前面讲到描述器简单实现一下property,classmethod和staticmethod。

3.1 实现property

代码(code#3),在线访问点击这里。

class mproperty(object):

def __init__(self, fget, fset=None, fdel=None):

self._fget = fget

self._fset = fset

self._fdel = fdel

def __get__(self, obj, klass):

return self._fget(obj)

def __set__(self, obj, val):

if not hasattr(self._fset, '__call__'):

raise AttributeError("Readonly attribute!")

self._fset(obj, val)

def __delete__(self, obj):

if not hasattr(self._fdel, '__call__'):

raise AttributeError("Can't delete the attribute!")

self._fdel(obj)

def setter(self, fset):

self._fset = fset

return self

def deleter(self, fdel):

self._fdel = fdel

return self

class Apple(object):

def __init__(self, price=0):

self._price = price

@mproperty

def price(self):

return self._price

@price.setter

def price(self, value):

if value < 0:

raise ValueError("Price must be greater than 0")

self._price = value

@price.deleter

def price(self):

print("delete price")

测试:

访问测试访问测试

赋值测试赋值测试

删除测试删除测试

3.2 实现staticmethod

代码(code#4)

class mstaticmethod(object):

def __init__(self, func):

self._func = func

def __get__(self, obj, klass=None):

return self._func

@property

def __func__(self):

return self._func

class Apple(object):

def __init__(self, price=0):

self._price = price

@mstaticmethod

def kg2jin(weight):

# 将千克转换为斤

return weight * 2

测试:staticmethod 测试

3.3 实现classmethod

代码(code#5)

class mclassmethod(object):

def __init__(self, func):

self._func = func

def __get__(self, obj, klass=None):

if klass is None:

klass = type(obj)

def wapper(*args):

return self._func(klass, *args)

return wapper

@property

def __func__(self):

return self._func

class Apple(object):

lowest_price = 10

def __init__(self, price=15):

self._price = price

@mclassmethod

def calc_lowest_total_price(cls, weight):

return cls.lowest_price * weight

测试:classmethod测试

3.4 实现super

想一想,算了,这个时间是大家的表演时间,我就不参合了。

此处我想说,,,,。其实我没什么想说的,该回去睡觉了→_→。

参考与荐读:

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值