描述符是实现描述符协议方法的Python对象,当将其作为其他对象的属性进行访问时,该描述符使您能够创建具有特殊行为的对象。

通常,描述符是具有“绑定行为”的对象属性,其属性访问已被描述符协议中的方法所覆盖。这些方法是__get __(),__set __()和__delete __()。如果为对象定义了这些方法中的任何一种,则称其为描述符。属性访问的默认行为是从对象的字典中获取,设置或删除属性。例如,a.x具有一个查找链,查找链从a .__ dict __ ['x']开始,然后键入(a).__ dict __ ['x'],并继续遍历类型(a)的基类(不包括元类)。如果查找到的值是定义描述符方法之一的对象,则Python可能会覆盖默认行为并改为调用描述符方法。优先链在何处发生取决于定义了哪些描述符方法。描述符是功能强大的通用协议。它们是属性,方法,静态方法,类方法和super()背后的机制。在Python本身中使用它们来实现2.2版中引入的新样式类。

1
2
3
descr.__get__(self, obj, type=None) -> value
descr.__set__(self, obj, value) -> None
descr.__delete__(self, obj) -> None

定义这些方法中的任何一个,对象被视为描述符,并且在被视为属性时可以覆盖默认行为。

如果对象定义了__set __()或__delete __(),则将其视为数据描述符。仅定义__get __()的描述符称为非数据描述符(它们通常用于方法,但也可以用于其他用途)。数据和非数据描述符在实例字典中替代计算方式方面有所不同。如果实例的字典中的属性名称与数据描述符的名称相同,则以数据描述符为准。如果实例的字典中具有与非数据描述符同名的属性,则该字典属性优先。我们来看一下例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
class lazy(object):
    def __init__(self, func):
        self.func = func
 
    def __get__(self, instance, owner):
        val = self.func(instance)
        setattr(instance, self.func.__name__, val)
        return val
 
class Circle(object):
    def __init__(self, radius):
        self.radius = radius
         
    @lazy
    def area(self):
        print('evalute')
        return 3.14 * self.radius ** 2
 
    def __getattr__(self, item):
        return 1
 
 
c = Circle(4)
print(c.area)
print(c.area)

输出结果是

1
2
3
evalute
50.24
50.24

我们定义了一个描述符的类 lazy,它只实现了__get__方法,是一个非数据的描述符,我们用它定义了类Circle中的area方法,所以area方法成为了一个描述符的对象,可以看到,在第一次调用c.area的时候,执行了area的方法,打印了"evalute",在第二次的时间就直接输出了结果,没有指向area的方法,这是为什么呢?

那么重点来了,可以看到在lazy定义的__get__方法中,执行了被描述对象的方法,也就是这里的area函数,获取到结果之后,给当前的instance设置了一个同名的属性,并且设值为结果,这样下次在调用的时间,因为这是一个非数据的描述符,看上面的黑体字,实例的字典中的属性名称与数据描述符的名称相同,则以数据描述符为准。所以会取你刚刚设置的属性的值,不会再去取描述符的值。我们再来看看数据描述符的一个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
class lazy(object):
    def __init__(self, func):
        self.func = func
 
    def __get__(self, instance, owner):
        val = self.func(instance)
        setattr(instance, self.func.__name__, val)
        return val
 
    def __set__(self, instance, value):
        pass
 
 
class Circle(object):
    def __init__(self, radius):
        self.radius = radius
 
    @lazy
    def area(self):
        print('evalute')
        return 3.14 * self.radius ** 2
 
    def __getattr__(self, item):
        return 1
 
 
c = Circle(4)
print(c.area)
print(c.area)
 

我们看一下输出的结果:

1
2
3
4
evalute
50.24
evalute
50.24

  

 

同样的定义,只是在描述符中添加了__set__方法,就会执行调用描述符定义的属性,和非描述符的调用方式天壤之别。这就是这个高级特性的特别之处。我们可以使用非数据描述符做惰性加载,只计算一次,下次直接取值,我在工作中也是这样干的。

 

知其然,知其所以然,我们来看一下是为什么:

根据官方的解释,描述符可以通过其方法名称直接调用。例如,d .__ get __(obj)。另外,更常见的是在属性访问时自动调用描述符。例如,obj.d在obj的字典中查找d。如果d定义了方法__get __(),则根据下面列出的优先级规则调用d .__ get __(obj)。调用的细节取决于obj是对象还是类。

    对于对象,机制位于object .__ getattribute __()中,它将b.x转换为type(b).__ dict __ ['x'] .__ get __(b,type(b))。该实现通过优先级链进行工作,该优先级链赋予数据描述符优先于实例变量的优先级,实例变量优先于非数据描述符的优先级,并为__getattr __()分配最低优先级。完整的C实现可在Objects / object.c中的PyObject_GenericGetAttr()中找到。 

对于类,机制的类型为.__ getattribute __(),它将B.x转换为B .__ dict __ ['x'] .__ get __(无,B)。在纯Python中,它看起来像:

def __getattribute__(self, key):
    "Emulate type_getattro() in Objects/typeobject.c"
    v = object.__getattribute__(self, key)
    if hasattr(v, '__get__'):
        return v.__get__(None, self)
    return v

要记住的重要点是:

  • 描述符由__getattribute __()方法调用

  • 重写__getattribute __()防止自动描述符调用

  • object .__ getattribute __()和type .__ getattribute __()对__get __()进行不同的调用。

  • 数据描述符始终会覆盖实例字典。非数据描述符可以被实例字典覆盖。

 

具体的可以查看Python的c源码。

以上就是今天要和大家一起学习的内容。

代码地址

https://github.com/oldman1991/testdemo/blob/master/0028_python_descriptor.py

 

更多问题欢迎关注微信公众号

 

posted @ 2020-01-04 19:26 oldmanli 阅读(1102) 评论(0) 推荐(1) 编辑
摘要: TCP包头格式 首先是源端口号和目标端口号,如果没有这两个端口号,数据就不知道应该发给哪个应用 接下来是包的序号。包的序号是为了解决乱序的问题。 然后是确认序号。发送出去的包应该有确认,用来确定对方有没有收到。如果没有收到就应该重新发,直到送达。这个可以解决丢包问题。 接下来是一些位状态。例如SYN 阅读全文
posted @ 2019-10-21 10:10 oldmanli 阅读(323) 评论(0) 推荐(0) 编辑
摘要: git 阅读全文
posted @ 2019-08-05 13:05 oldmanli 阅读(241) 评论(0) 推荐(0) 编辑
摘要: 上节介绍了二叉树的一些基本概念,这篇文章开始,我们开始学习二叉树的一些算法问题,今天先看一些层次遍历的题目。 二叉树的层次遍历 题目 给定一个二叉树,返回其按层次遍历的节点值。 (即逐层地,从左到右访问所有节点)。 例如:给定二叉树: [3,9,20,null,null,15,7], 返回其层次遍历 阅读全文
posted @ 2018-05-31 14:32 oldmanli 阅读(265) 评论(0) 推荐(1) 编辑
摘要: 二叉树 定义: 来自于百度百科。 在计算机科学中,二叉树是每个节点最多有两个子树的树结构。通常子树被称作“左子树”(left subtree)和“右子树”(right subtree)。二叉树常被用于实现二叉查找树和二叉堆。 二叉树的每个结点至多只有二棵子树(不存在度大于2的结点),二叉树的子树有左 阅读全文
posted @ 2018-05-09 11:48 oldmanli 阅读(463) 评论(0) 推荐(0) 编辑
摘要: 回文链表 链接 请检查一个链表是否为回文链表。 进阶:你能在 O(n) 的时间和 O(1) 的额外空间中做到吗? 解题思路: 回文链表的特点就是对称。 把链表放到栈中去,利用栈的先进后出的规则,和原链表一一做比较。全部相等,则是回文链表。 代码实现如下: # Definition for singl 阅读全文
posted @ 2018-04-24 18:32 oldmanli 阅读(497) 评论(0) 推荐(1) 编辑
摘要: 反转一个单链表。 进阶:链表可以迭代或递归地反转。你能否两个都实现一遍? 示例 : 给定这个链表:1->2->3->4->5 返回结果: 5->4->3->2->1 题目链接 解题思路: 1. 迭代版本: 循环列表,定义两个指针,一个指针是已经迭代完的链表的最后一个节点称为last_node,一个指 阅读全文
posted @ 2018-04-23 19:20 oldmanli 阅读(331) 评论(0) 推荐(1) 编辑
摘要: 我们在上篇文章里面提到了链表的翻转,给定一个链表,对每两个相邻的节点作交换,并返回头节点,今天的这道题是它的升级版,如下: k个一组翻转链表 给出一个链表,每 k 个节点一组进行翻转,并返回翻转后的链表。 k 是一个正整数,它的值小于或等于链表的长度。如果节点总数不是 k 的整数倍,那么将最后剩余节 阅读全文
posted @ 2018-04-19 15:51 oldmanli 阅读(555) 评论(2) 推荐(3) 编辑
摘要: 继续关于linked list的算法题: 删除排序链表中的重复元素 给定一个排序链表,删除所有重复的元素使得每个元素只留下一个。 案例: 给定 1->1->2,返回 1->2 给定 1->1->2->3->3,返回 1->2->3 解题思路: 这道题很简单,只需要比较当前节点和下一个节点,相同,则当 阅读全文
posted @ 2018-04-10 17:49 oldmanli 阅读(533) 评论(0) 推荐(5) 编辑
摘要: 我们继续来看链表的第二道题,来自于leetcode: 两数相加 给定两个非空链表来代表两个非负整数,位数按照逆序方式存储,它们的每个节点只存储单个数字。将这两数相加会返回一个新的链表。 你可以假设除了数字 0 之外,这两个数字都不会以零开头。 示例: 分析: 因为是位数按照逆序方式存储,所以链表的前 阅读全文
posted @ 2018-04-05 09:49 oldmanli 阅读(595) 评论(0) 推荐(3) 编辑
点击右上角即可分享
微信分享提示