这可能是将Python描述器(descriptor)介绍得最透彻的文章!(持续更新)

文章目录

  • 一、引入描述器概念
    • 1. 使用`property`的可能陷阱
    • 2. 如何规避使用`property`的陷阱
    • 3. 描述器是实现特定方法的类
  • 二、理解描述器必备
    • 1. 描述器方法参数的含义
    • 2. 类和对象的`__dict__`属性
    • 3. 设置/删除属性的优先级
    • 4. 获取属性的优先级
  • 三、参考资料

一、引入描述器概念

1. 使用property的可能陷阱

在文章如何使用property掌控属性访问?中,我们学习了如何使用property来实现在获取或设置“实例属性”的同时进行额外自定义操作,如:类型检查或验证等功能,但在仅了解这些的情况下使用property可能会产生如下述代码一样的问题:

class Person:
    def __init__(self, first_name, last_name):
        self.first_name = first_name
        self.last_name = last_name

    @property
    def first_name(self):
        return self._first_name

    @first_name.setter
    def first_name(self, value):
        if not isinstance(value, str):
            raise TypeError('姓名必须是字符串格式!')
        self._first_name = value

    # 以下为重复代码,仅仅是换了一个名字而已
    @property
    def last_name(self):
        return self._last_name

    @last_name.setter
    def last_name(self, value):
        if not isinstance(value, str):
            raise TypeError('姓名必须是字符串格式!')
        self._last_name = value


def main():
    python_guru = Person('Raymond', 'Hettinger')
    print(python_guru.first_name)
    print(python_guru.last_name)
    print('-' * 50)
    python_creator = Person(1, 2)


if __name__ == '__main__':
    main()

上述代码的运行结果为:

Raymond
Hettinger
--------------------------------------------------
Traceback (most recent call last):

TypeError: 姓名必须是字符串格式!

上述# 1# 2# 3# 4处代码都可以实现类型检查等功能(因为使用int数值尝试创建Person类的对象时会报错),但代码除了变量名称不一样外,其余完全一致,即代码重复了,这样的代码不仅无美感可言,而且修改比较麻烦。这就是标题所说的陷阱所指。

2. 如何规避使用property的陷阱

为了避免上述问题,下面代码使用本文主角描述器(在本文稍后会立马给出描述器的定义,这里仅给读者对其做一个感性认识)来实现同样功能:

class Name:
    def __init__(self, storage_name):
        self.storage_name = storage_name

    def __get__(self, instance, owner):  # 1
        print('Name类中的__get__方法正在被调用...')
        if instance is None:
            return self
        else:
            return instance.__dict__[self.storage_name]  # 3

    def __set__(self, instance, value): # 2
        print('Name类中的__set__方法正在被调用...')
        if not isinstance(value, str):
            raise TypeError('姓名必须是字符串格式!')
        instance.__dict__[self.storage_name] = value  # 4


class Person:
    first_name = Name('first_name')  # 5
    last_name = Name('last_name')  # 6

    def __init__(self, first_name, last_name):
        self.first_name = first_name
        self.last_name = last_name


def main():
    python_guru = Person('Raymond', 'Hettinger')
    print('-' * 50)
    print(python_guru.first_name)
    print(python_guru.last_name)
    print('-' * 50)
    python_creator = Person(1, 2)


if __name__ == '__main__':
    main()

上述代码的运行结果为:

Name类中的__set__方法正在被调用…
Name类中的__set__方法正在被调用…
--------------------------------------------------
Name类中的__get__方法正在被调用…
Raymond
Name类中的__get__方法正在被调用…
Hettinger
--------------------------------------------------
Name类中的__set__方法正在被调用…
Traceback (most recent call last):

TypeError: 姓名必须是字符串格式!

由上述代码及其运行结果可知,该代码也可以实现和使用property相同的功能。实际上,在上述代码中,类Name其实就是一个描述器,而此时Person类的实例将设置和获取实例属性的功能代理给了描述器中的__set____get__方法,进而在这两个方法中通过操作Person类实例的__dict__属性来设置或获取属性的值。

3. 描述器是实现特定方法的类

定义:描述器是一个类,该类实现了__get____set____delete__三个方法中的至少一个方法1,描述器可以为对象访问多个属性时提供一种相同的逻辑和操作。

仅从上述定义并不能看出描述器究竟有多么强大的功能,但实际上描述器可以说是Python实现面向对象编程范式的重要基础之一,因为Python中的函数、方法、属性、类方法装饰器@classmethod、静态方法装饰器@staticmethod等都基于描述器实现。对此,这个系列的文章将为大家一一道来。

二、理解描述器必备

在上述代码中,你可能对于这几个位置有疑问:

  • # 1# 2方法处的参数instanceowner的作用;
  • # 3# 4__dict__属性的功能;
  • # 5# 6处存在和实例对象同名类属性的必要性。

为了更好地深入理解Python的描述器,下面先对这三点进行详细分析:

1. 描述器方法参数的含义

在描述器实现的几个方法中,各个方法的参数含义为:

  • instance:表示Person类的一个实例;
  • owner:表示实例化得到上述instance的类,即Person类。

验证代码如下:

class Name:
    def __init__(self, storage_name):
        self.storage_name = storage_name

    def __get__(self, instance, owner):
    	print('描述器Name的__get__方法正在被调用...')
        print('instance = ', instance, 'owner = ', owner)
        return instance.__dict__[self.storage_name]

    def __set__(self, instance, value):
        print('描述器Name的__set__方法正在被调用...')
        print('instance = ', instance, 'value = ', value)
        if not isinstance(value, str):
            raise TypeError('姓名必须是字符串格式!')
        instance.__dict__[self.storage_name] = value


class Person:
    """描述一个人的类"""
    first_name = Name('first_name')
    last_name = Name('last_name')

    def __init__(self, first_name, last_name):
        self.first_name = first_name
        self.last_name = last_name


def main():
    python_guru = Person('Guido', 'Rossum')
    print(python_guru.first_name)


if __name__ == '__main__':
    main()

上述代码的运行结果为:

描述器Name的__set__方法正在被调用…
instance = <__main__.Person object at 0x7f6d01576630> value = Guido
描述器Name的__set__方法正在被调用…
instance = <__main__.Person object at 0x7f6d01576630> value = Rossum
描述器Name的__get__方法正在被调用…
instance = <__main__.Person object at 0x7f6d01576630> owner =
Guido

2. 类和对象的__dict__属性

在Python中,不管是类还是用类创建的实例都是对象(关于类也是一种对象,以及类这种对象是由什么实例化而来,请见文章为什么说元类是你最常使用却也最陌生的Python语言特性之一?),这两类对象都有一个名为__dict__的属性,该属性都是一个字典,其中:

  • 类对象的__dict__属性保存了该类的命名空间,其中以键值对形式包含:类属性、实例方法、帮助文档信息等;
  • 用类所创建实例对象的__dict__属性以键值对形式包含实例属性。

关于上述说明,可由下列代码来运行确认:

class Name:
    def __init__(self, storage_name):
        self.storage_name = storage_name

    def __get__(self, instance, owner):
        if instance is None:
            return self
        else:
            return instance.__dict__[self.storage_name]

    def __set__(self, instance, value):
        if not isinstance(value, str):
            raise TypeError('姓名必须是字符串格式!')
        instance.__dict__[self.storage_name] = value


class Person:
    """描述一个人的类"""
    first_name = Name('first_name')
    last_name = Name('last_name')

    def __init__(self, first_name, last_name):
        self.first_name = first_name
        self.last_name = last_name


def main():
    print('Person.__dict__ = ', Person.__dict__)
    python_guru = Person('Raymond', 'Hettinger')
    print('python_guru.__dict__ = ', python_guru.__dict__)


if __name__ == '__main__':
    main()

上述代码的运行结果为:

Person.__dict__ = {…, ‘__doc__’: ‘描述一个人的类’, ‘first_name’: <__main__.Name object at 0x7f0ae2eb99e8>, ‘last_name’: <__main__.Name object at 0x7f0ae11a0550>, ‘__init__’: , ‘__dict__’: , …}
python_guru.__dict__ = {‘first_name’: ‘Raymond’, ‘last_name’: ‘Hettinger’}

3. 设置/删除属性的优先级

在Python中,当使用obj.attr(请记住,实例方法的参数self也引用了实例对象,这对理解本节有较大帮助)的语法设置/删除某指定名称的属性时,根据以下几点:

  • 类中是否有通过描述器创建的对象作为类属性;
  • 描述器实现了__get____set____delete__中的哪些方法(其中:仅定义__get__方法的类叫做非数据型描述器,仅定义__set____delete__方法的类叫做数据型描述器);
  • 实例对象是否包含指定名称属性。

程序的属性查找顺序不同,具体来说可用下图来表示:
这可能是将Python描述器(descriptor)介绍得最透彻的文章!(持续更新)_第1张图片

结论1:在使用obj.attr的语法设置/删除指定名称的属性时,数据型描述器具有最高优先级,其次为实例属性。

下面对上述结论做依次验证:

验证1.1:当满足下列情形(即对应上图中的# 1 --> # 2 --> # 3 --> # 4),则描述器对应方法被优先调用:

  • 创建对象的类中有指定名称的类属性
  • 该类属性为数据类描述器
  • 设置属性时数据类描述器实现了__set__方法;
  • 删除属性时数据描述其实现了__delete__方法。

验证代码如下图所示:

class Name:
    def __init__(self, storage_name):
        self.storage_name = storage_name

    def __get__(self, instance, owner):
        if instance is None:
            return self
        else:
            return instance.__dict__[self.storage_name]

    def __set__(self, instance, value):
        print('描述器Name的__set__方法正在被调用...')
        if not isinstance(value, str):
            raise TypeError('姓名必须是字符串格式!')
        instance.__dict__[self.storage_name] = value

    def __delete__(self, instance):
        print('描述器Name的__delete__方法正在被调用...')
        del instance.__dict__[self.storage_name]


class Person:
    """描述一个人的类"""
    first_name = Name('first_name')
    middle_name = Name('middle_name')
    last_name = Name('last_name')

    def __init__(self, first_name, last_name):
        self.first_name = first_name
        self.last_name = last_name


def main():
    print('Person.__dict__ = ', Person.__dict__)
    python_guru = Person('Guido', 'Rossum')
    python_guru.middle_name = 'van'
    print('python_guru.__dict__ = ', python_guru.__dict__)
    del python_guru.first_name
    del python_guru.middle_name
    print('python_guru.__dict__ = ', python_guru.__dict__)


if __name__ == '__main__':
    main()

上述代码的运行结果为:

Person.__dict__ = {…, ‘first_name’: <__main__.Name object at 0x7f1b6d0bd5f8>, ‘middle_name’: <__main__.Name object at 0x7f1b6d0bd630>, ‘last_name’: <__main__.Name object at 0x7f1b6d0bd668>, …}
描述器Name的__set__方法正在被调用…
描述器Name的__set__方法正在被调用…
描述器Name的__set__方法正在被调用…
python_guru.__dict__ = {‘first_name’: ‘Guido’, ‘last_name’: ‘Rossum’, ‘middle_name’: ‘van’}
描述器Name的__delete__方法正在被调用…
描述器Name的__delete__方法正在被调用…
python_guru.__dict__ = {‘last_name’: ‘Rossum’}

由上述代码还可以知道:

使用obj.attr的语法设置属性时,并不要求实例对象中有和类属性同名的实例属性

验证1.2:在使用obj.attr的语法设置/删除指定名称的属性时,当满足下列情形(即对应上图中的# 1 --> # 2 --> # 3 --> # 5流程)时程序会优先尝试调用对应的描述器方法,调用失败则抛出AttributeError异常:

  • 创建对象的类中使用描述器创建的指定名称类属性
  • 设置属性时描述器实现了__delete__方法;
  • 删除属性时描述器实现了__set__方法。
class Name:
    def __init__(self, storage_name):
        self.storage_name = storage_name

    def __delete__(self, instance):
        print('描述器Name的__delete__方法正在被调用...')
        del instance.__dict__[self.storage_name]


class Person:
    """描述一个人的类"""
    first_name = Name('first_name')

    def __init__(self, first_name):
        self.first_name = first_name

def main():
    python_guru = Person('Guido')


if __name__ == '__main__':
    main()

上述代码的运行结果为:

AttributeError: __set__

验证1.3:在使用obj.attr的语法设置/删除指定名称的属性时,即使Person类中定义了由描述器创建的对象作为类属性,只要描述器仅实现了__get__方法(即仅为数据型描述器),程序都会直接对实例对象的__dict__属性进行相应操作。

4. 获取属性的优先级

当使用obj.attr的语法获取属性时,属性查找的优先级要比设置/删除属性时要复杂得多,因为解释器将根据下列不同情况进行属性查找:

  • 类中是否有由描述器创建的对象作为类属性;
  • 描述器是数据描述器还是非数据描述器。

具体流程可以用下图来表示:

这可能是将Python描述器(descriptor)介绍得最透彻的文章!(持续更新)_第2张图片

结论2:在使用obj.attr获取属性时,属性查找的优先级为:数据类描述器的优先级最高,其次为实例属性,再次为非数据型描述器,然后是普通类属性。

下面对上述结论做依次验证:

验证2.1:当类中数据型描述器创建的类属性,且描述器该描述器同时为非数据型描述器(即实现了__get__方法),则优先调用描述器中的__get__获取属性(对应流程# 1 --> # 2 --> # 3 --> # 4);如果描述器仅实现__delete__方法而未实现__set__方法,则__get__方法也不会被调用。

class Name:
    def __init__(self, storage_name):
        self.storage_name = storage_name

    def __get__(self, instance, owner):
        print('描述器Name的__get__方法正在被调用...')
        if instance is None:
            return self
        else:
            return instance.__dict__[self.storage_name]

    def __set__(self, instance, value):
        print('描述器Name的__set__方法正在被调用...')
        if not isinstance(value, str):
            raise TypeError('姓名必须是字符串格式!')
        instance.__dict__[self.storage_name] = value


class Person:
    """描述一个人的类"""
    first_name = Name('first_name')
    last_name = Name('last_name')

    def __init__(self, first_name, last_name):
        self.first_name = first_name
        self.last_name = last_name


def main():
    python_guru = Person('Guido', 'Rossum')
    print(python_guru.first_name)
    print(type(python_guru).__dict__['first_name'].__get__(python_guru, type(python_guru)))


if __name__ == '__main__':
    main()

上述代码的运行结果为:

描述器Name的__set__方法正在被调用…
描述器Name的__set__方法正在被调用…
描述器Name的__get__方法正在被调用…
Guido
描述器Name的__get__方法正在被调用…
Guido

即此时通过obj.attr的方式获取属性相当于type(obj).__dict__['attr'].__get__(obj, type(obj))

验证2.2:当类中没有数据型描述器创建的类属性,而实例属性中待获取属性,则返回实例属性(对应流程# 1 --> # 2 --> # 6 --> # 7–> # 8)。

class Name:
    def __init__(self, storage_name):
        self.storage_name = storage_name


class Person:
    """描述一个人的类"""
    first_name = Name('first_name')
    last_name = Name('last_name')

    def __init__(self, first_name, last_name):
        self.first_name = first_name
        self.last_name = last_name


def main():
    python_guru = Person('Guido', 'Rossum')
    print(python_guru.first_name)


if __name__ == '__main__':
    main()

上述代码的运行结果为:

Guido

验证2.3:当实例中不具有待获取属性,而有同名类属性,但类属性是一个非数据型描述器创建的对象,此时调用__get__方法(对应流程# 1 --> # 2 --> # 6 --> # 7–> # 8–> # 9 --> # 10–> # 11)。(注意此时虽然调用了__get__方法,但是程序会报错!)

class Name:
    def __init__(self, storage_name):
        self.storage_name = storage_name

    def __get__(self, instance, owner):
    	print('描述器Name的__get__方法正在被调用...')
		if instance is None:
			return self
		else:
			return instance.__dict__[self.storage_name]


class Person:
    """描述一个人的类"""
    first_name = Name('first_name')
    last_name = Name('last_name')

    def __init__(self, first_name, last_name):
        self.last_name = last_name


def main():
    python_guru = Person('Guido', 'Rossum')
    print(python_guru.first_name)


if __name__ == '__main__':
    main()

上述代码的运行结果为:

描述器Name的__get__方法正在被调用…
Traceback (most recent call last):

KeyError: ‘first_name’

验证2.4:当实例对象中没有待获取属性,且类属性是由任意未实现任何描述器方法的类创建,则直接返回类属性((对应流程# 1 --> # 2 --> # 6 --> # 7–> # 8–> # 9 --> # 10–> # 12))。

class Name:
    def __init__(self, storage_name):
        self.storage_name = storage_name


class Person:
    """描述一个人的类"""
    first_name = Name('first_name')
    last_name = Name('last_name')

    def __init__(self, first_name, last_name):
        self.last_name = last_name


def main():
    python_guru = Person('Guido', 'Rossum')
    print(python_guru.first_name)


if __name__ == '__main__':
    main()

上述代码的运行结果为:

<__main__.Name object at 0x7ff9e759e748>

三、参考资料

  • [1] Descriptor HowTo Guide

  1. 这就是描述器协议,即只要一个类实现了__get____set____delete__三个方法其中之一,则其就是一个描述器,而不管该类是否还实现了其他任何方法,这也是鸭子类型的一种体现,关于协议鸭子类型这两个名词,更深入了解请见文章浅谈Python中的注解和类型提示。 ↩︎

你可能感兴趣的:(#,高级语法)