属性描述符

迈不过友情╰ 2022-04-06 08:28 373阅读 0赞

属性描述符

文章目录

  • 属性描述符
    • 描述符
    • 描述符协议
    • 示例
    • 资料描述符与非资料描述符
    • 描述符的陷阱
      • 第一点:描述符必须在类的层次上(类属性)
      • 第二点:确保实例属性属于实例本身
    • 感谢

018.12.4

描述符

在Python中,描述符作为一个用语言描述起来会有些抽象的概念。其定义有如下说法:

一般来说,描述符是一个具有绑定行为的对象属性,其属性的访问被描述符协议方法覆写。这些方法是__get__()、 set()和__delete__(),一个对象中只要包含了这三个方法(译者注:包含至少一个),就称它为描述符。

而我认为,如果可以通俗地去解释 “为什么需要描述符”,会让初学者更易接受这个概念。


如果你知道Python中的property——也可能不曾听过,但你可以看我的@property——就会晓得我们可以对实例属性操作时做一些限制,比方说陌生人的姓名必须得字符串类型(这里当然会有些不严谨,因为Python中如"512"也是字符串类型),如果不是,就抛异常TypeError。代码可以如下:

  1. class Stranger(object):
  2. def __init__(self, name):
  3. self._name = name
  4. @property
  5. def name(self):
  6. return self._name
  7. @name.setter
  8. def name(self, value):
  9. if not isinstance(value, str):
  10. raise TypeError("我期待一个字符串")
  11. else:
  12. self._name = value
  13. if __name__ == "__main__":
  14. stger = Stranger("Ying")
  15. stger.name = 512

运行结果如下:
在这里插入图片描述
乍一看,上述代码似乎满足了我们的需求。其实不然,存在两个缺点。

其一,当我们初始化一个对象,就给name传int类型的参数,程序会默然允许:

  1. if __name__ == "__main__":
  2. stger = Stranger(512)
  3. print(type(stger.name))
  4. # 输出:
  5. # <class 'int'>

其二,当我们需要限制的属性较多时:

  1. class Stranger(object):
  2. def __init__(self, name, age, hobbies, addr, school):
  3. self._name = name
  4. self._age = age
  5. self._hobbies = hobbies
  6. ...

为了一一限制,便不得不对应的去定义 @name.setter@age.setter … 代码怎么不优雅了?


Python的代码需要优雅,这就是引进描述符的原因。你也可以认为描述符是property的升级版。

描述符协议

描述符协议包含:

  • __get__ 用于访问属性的值。当请求的属性不存在时,抛出AttributeError
  • __set__ 设置操作
  • __delete__ 删除操作

一个对象中只要包含了这三个方法中的一个,就称它为描述符。

示例

  1. import numbers
  2. class InterField(object):
  3. def __get__(self, instance, owner):
  4. return self.value
  5. def __set__(self, instance, value):
  6. if not isinstance(value, numbers.Integral):
  7. raise ValueError("期待一个整数")
  8. else:
  9. self.value = value
  10. class CharField(object):
  11. def __get__(self, instance, owner):
  12. return self.value
  13. def __set__(self, instance, value):
  14. if not isinstance(value, str):
  15. raise ValueError("期待一个字符串")
  16. else:
  17. self.value = value
  18. class Stranger(object):
  19. name = CharField()
  20. age = InterField()
  21. hobbies = CharField()
  22. addr = CharField()
  23. school = CharField()
  24. def __init__(self, name, age, hobbies, addr, school):
  25. self.name = name
  26. self.age = age
  27. self.hobbies = hobbies
  28. self.addr = addr
  29. self.school = school
  30. if __name__ == "__main__":
  31. stger = Stranger("Ying", 22, "music", "chengdu", "XHU")
  32. print(stger.name, stger.age, stger.hobbies)
  33. # 输出:
  34. # Ying 22 music

此时

  1. stger = Stranger("Ying", "22", "music", "chengdu", "XHU")

在这里插入图片描述
或者

  1. stger = Stranger("Ying", 22, "music", "chengdu", "XHU")
  2. stger.name = 512

在这里插入图片描述

资料描述符与非资料描述符

认为,如果一个描述符只定义了__get__方法,则为非资料描述符;如果同时定义了__get____set__,为资料描述符

  • 资料描述符:当类属性与实例属性同名时,优先访问类属性

    class CharField(object):

    1. def __init__(self, value):
    2. self.value = value
    3. def __get__(self, instance, owner):
    4. return self.value
    5. # 此时,我甚至不需要对__set__添加任何逻辑
    6. def __set__(self, instance, value):
    7. pass
  1. class Stranger(object):
  2. # 这是一个资料描述符
  3. name = CharField("Guan")
  4. def __init__(self):
  5. self.name = "Ying"
  6. if __name__ == "__main__":
  7. stger = Stranger()
  8. print(stger.name)
  9. # 输出:
  10. # Guan
  • 非资料描述符:当类属性与实例属性同名时,优先访问实例属性

    class CharField(object):

    1. def __init__(self, value):
    2. self.value = value
    3. def __get__(self, instance, owner):
    4. return self.value
  1. class Stranger(object):
  2. # 这是一个非资料描述符
  3. name = CharField("Guan")
  4. def __init__(self):
  5. self.name = "Ying"
  6. if __name__ == "__main__":
  7. stger = Stranger()
  8. print(stger.name)
  9. # 输出:
  10. # Ying

描述符的陷阱

第一点:描述符必须在类的层次上(类属性)

  1. ...
  2. class Stranger(object):
  3. def __init__(self):
  4. self.name = CharField("Guan")
  5. if __name__ == "__main__":
  6. stger = Stranger()
  7. print(stger.name)
  8. # 输出:
  9. # <__main__.CharField object at 0x7ff3178f1cc0>

这是因为:只有类层次上的描述符才会默认调用__get__

第二点:确保实例属性属于实例本身

事实上类属性是该类实例对象共有的,可参看我的类属性与实例属性。所以很可能出现 “一荣俱荣,一损俱损” 的现象:

  1. class CharField(object):
  2. def __get__(self, instance, owner):
  3. return self.value
  4. def __set__(self, instance, value):
  5. self.value = value
  6. class Stranger(object):
  7. name = CharField()
  8. if __name__ == "__main__":
  9. g = Stranger()
  10. g.name = "Guan"
  11. print(g.name) # 输出: Guan
  12. y = Stranger()
  13. y.name = "Ying"
  14. print(g.name) # 输出: Ying
  15. print(y.name) # 输出: Ying

解决方案:
在描述符类中维护一个字典,将每个实例对象作为字典的key,而类属性对应的值作为字典的value:

  1. class CharField(object):
  2. def __init__(self):
  3. # 一些实例使用的WeakKeyDictionary()类型字典
  4. # 暂时我并不能明白为什么
  5. self.data = dict()
  6. def __get__(self, instance, owner):
  7. return self.data.get(instance)
  8. # 当y.name时,instance == y
  9. # 当g.name时,instance == g
  10. def __set__(self, instance, value):
  11. self.data[instance] = value

这样做的缺点是,也许会碰到不可以被hash的实例,那就不能作为字典的key。常用的解决方案是:为描述符增加标签,并通过这个标签,把本来应该访问类属性这一过程,“偷偷地”转化成访问实例属性的过程

  1. class CharField(object):
  2. def __init__(self, label):
  3. self.label = label
  4. def __get__(self, instance, owner):
  5. return instance.__dict__.get(self.label)
  6. def __set__(self, instance, value):
  7. instance.__dict__[self.label] = value
  8. class MyList(list):
  9. name = CharField("NAME")

这种做的缺点是,你可能会在没有察觉的情况下修改了name值

  1. mylist = MyList()
  2. mylist.name = "Guan" # 这里是name
  3. print(mylist.name) # 输出: Guan
  4. mylist.NAME = "Ying" # 这里是NAME
  5. print(mylist.name) # 输出:Ying

建议标签名最好与描述符对象的名字相同


最后可以利用元类来简化这个过程:

  1. class LabelType(type):
  2. def __new__(cls, name, bases, attrs):
  3. for k, v in attrs.items():
  4. if isinstance(v, CharField):
  5. v.label = k # 为描述符增加label
  6. return super().__new__(cls, name, bases, attrs)
  7. class CharField(object):
  8. def __get__(self, instance, owner):
  9. return instance.__dict__.get(self.label)
  10. def __set__(self, instance, value):
  11. instance.__dict__[self.label] = value
  12. class MyList(list, metaclass=LabelType):
  13. name = CharField()

如果你对元类不熟悉,不妨参看我的元类。

感谢

  • 参考 Python描述符简介
  • 参考 【译】Python描述符指南
  • 参考 解密 Python 的描述符(descriptor)

发表评论

表情:
评论列表 (有 0 条评论,373人围观)

还没有评论,来说两句吧...

相关阅读

    相关 JS对象属性描述符对象

    JS中Object对象的静态方法getOwnPropertyDescriptor可以返回指定对象的指定属性的描述,该描述是一个对象,称为属性描述符对象。属性描述符是 ECMAS