Python3描述器(Descriptor)

Python中的属性

访问类和实例中的属性

Python3中的类及其实例都各自有一个__dict__属性,该属性用来保存类或者对象的全部属性。

def get_class_attribute(cls):
    common_dict_keys = dict(__module__=None,
                            __dict__=None,
                            __weakref__=None,
                            __doc__=None,
                            __init__=None).keys()

    return {k: v for k, v in cls.__dict__.items() if k not in common_dict_keys}


class Book:
    title = "BOOK"
    author = "AUTHOR"
    
book1 = Book()
print(get_class_attribute(Book))
print(vars(book1))
print(Book.title, book1.title)
print(Book.author, book1.author)

"""output
{'title': 'BOOK', 'author': 'AUTHOR'}
{}
BOOK BOOK
AUTHOR AUTHOR
"""

上例中,我们新建了一个类Book,并实例化它赋值给book1作为Book的实例化对象。如果我们调用Book.dict,会返回Book类的全部属性,这里我们通过调用get_class_attribute函数来过滤掉Book类的一些魔法方法后看到,第一个print输出了我们定义在Book类中的属性title和author。
第二个print调用了vars方法,并将book1实例作为参数,vars调用book1.__dict__属性,该方法返回了book1实例的全部属性,我们可以看到此时返回了一个空的字典,说明book1还没有实例对象。
第三和第四个print我们通过"."的方式分别通过调用类的属性和实例的属性观察结果,发现book1没有属性title和author,因此返回了类的属性title和author。
下面的例子描述了调用实例对象的过程

class Book:
    title = "BOOK"
    author = "AUTHOR"

    def __init__(self):
        pass

    def __getattribute__(self, item):
        attr = super().__getattribute__(item)
        print("__getattribute__{}".format(item), end=': ')
        return attr

    def __getattr__(self, item):
        return 2

book1 = Book()
print(book1.title)
print(book1.author)
print(book1.time)
"""output
__getattribute__BOOK: BOOK
__getattribute__AUTHOR: AUTHOR
2
"""

当调用实例对象book1的属性时,Python解释器会调用该对象的魔方方法__getattribute__,我们自定义实现了该魔方方法方便调试观察。当调用book1.title时,book1的__getattribute__方法会拦截到这次调用,再通过父类object的__getattribute__进行处理返回属性,object中的__getattribute__会先查询book1是否有属性title,如果没有则返回Book类的属性title。
我们最后调用了一个book1和Book中都不存在的属性time,此时__getattribute__依然会拦截这次调用,但当执行attr = super().getattribute(item)时会抛出异常,因此后面的print(“getattribute …”)将不会执行,如果不定义__getattr__魔法方法,则object中的__getattr__属性将会执行,并抛出AttributeError: ‘Book’ object has no attribute ‘time’,我们重载这个方法可以拦截到这次调用,最后book1.time获取到值“2”。因此__getattr__方法,会在__getattribute__产生异常时触发,如果我们用在__getattribute__里做了try/except的拦截时,__getattr__则永远不会被触发。
此时如果调用以下代码,会发现Book类多了比原来多了两个函数的属性,因为类中定义的函数也是类的属性,下面的代码将__getattribute__和__getattr__两个函数独立出来,并用类属性定义的方式来描述,这种方法和在类中定义函数的方式实现是一样的,直观的表明了类中的函数也是类的属性。

def __getattribute__(self, item):
    attr = object.__getattribute__(self, item)
    print("__getattribute__{}".format(item), end=': ')
    return attr

def __getattr__(self, item):
    return 2

class Book:
    title = "BOOK"
    author = "AUTHOR"
    __getattribute__ = __getattribute__
    __getattr__ = __getattr__

    def __init__(self):
        pass

book1 = Book()
print(list(get_class_attribute(Book).keys()))
print(book1.title)
print(book1.author)
print(book1.time)

"""output
['title', 'author', '__getattribute__', '__getattr__']
__getattribute__BOOK: BOOK
__getattribute__AUTHOR: AUTHOR
2
"""

设置类和实例的属性

在上一节中,我们在Book类中定义了类属性title, author以及同样作为属性的两个函数,我们也可以在Book类定义外设置Book类的属性。

book1 = Book()
print(book1.title)
print(book1.time)
Book.time = "2018"
print(book1.time)
"""output
__getattribute__title: BOOK
2
__getattribute__time: 2018
"""

通过class.attr的方式定义类属性,回顾上文,当定义Book类属性time后__getattr__将不会触发,book1.time返回了类属性time。
下面我们单独为book1实例类设置属性。

book1 = Book()
book1.page = "300"
print(book1.page)
print(vars(book1))
"""output
__getattribute__page: 300
__getattribute____dict__: {'page': '300'}
"""

为book1设置属性page后,book1中的__dict__存储了属性page, 如果调用Book.page将抛出Attribute异常,因为Book类作为一种对象并没有重写__getattr__方法,如要像上文所述增加的__getattr__将需要为Book提供一个实现该方法的元类(metaclass),下面为修改代码,元类的具体细节不在本篇阐述。

class Book(metaclass=type("meta", 
                          (type,), 
                          dict(__getattr__ = __getattr__))):
    title = "BOOK"
    author = "AUTHOR"
    __getattribute__ = __getattribute__
    __getattr__ = __getattr__

    def __init__(self):
        pass

book1 = Book()
book1.page = 300
print(book1.page)
print(vars(book1))
print(Book.page)
"""output
__getattribute__page: 300
__getattribute____dict__: {'page': '300'}
2
"""

类描述器

在上文中,对实例对象属性的访问或是设置,都是从instance.__dict__中获取,并没有对属性进行控制,例如若在设置book1.page = 300时进行控制,若大于300时则抛出异常,上文中对实例对象的赋值并没有实现这个控制,下文将要讲述的类描述器可以实现对对象属性的访问和赋值控制。

描述器

描述器是一个类,它可以实现__get__, __set__, __delete__方法分别控制属性的访问、赋值和删除操作,当某个类定义了一个描述器对象的类属性,调用这个类对象的此属性,会触发相应描述器的__get__, __set__, __delete__方法。例如,对Book类中的属性page进行赋值控制,定义了一个描述器类Unsigned并实现__set__方法,并在Book类中定义属性page为该描述器对象。

class Unsigned:
    def __init__(self, name):
        self.name = name

    def __set__(self, instance, value):
        if value > 0:
            instance.__dict__[self.name] = value
        else:
            raise ValueError('size must > 0')

class Book:
    title = "BOOK"
    author = "AUTHOR"
    page = Unsigned("page")

book1 = Book()
book1.page = 300
## raise ValueError when execute 'book1.page = -20'.

若想实现访问控制需要实现描述器中的__get__方法,例如想在访问book1.page时增加5。

class Unsigned:
    def __init__(self, name):
        self.name = name

    def __set__(self, instance, value):
        if value > 0:
            instance.__dict__[self.name] = value
        else:
            raise ValueError('size must > 0')

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

class Book:
    title = "BOOK"
    author = "AUTHOR"
    page = Unsigned("page")

book1 = Book()
book1.page = 300
print(book1.page)
print(vars(book1))

"""output
305
{'page': 300}
"""

我们发现,在__set__方法中,instance指的是对象book1,value则为300,我们通过instance.dict[self.name] = value,为实例book1增加了属性’page’。目前为止,Book类拥有了属性page(值为Unsigned描述器类的对象),以及book1拥有了属性page(值为300), 因此按照描述器章节前面所述,调用book1.page时,book1的__dict__属性有值为300的属性page,应当返回300,可这里却触发了描述器的__get__返回了305。这里需要理解Python编译器处理点访问对象属性的过程,下面通过流程图和伪代码进行描述。
在这里插入图片描述

def get_attr_pesud(obj, attr):
    obj_cls = type(obj)
    if hasattr(obj_cls, attr):
        descipter = getattr(obj_cls, attr)
        descipter_cls = type(descipter)

        if hasattr(descipter_cls, '__set__'):
            if hasattr(descipter_cls, '__get__'):
                return descipter.__get__(obj, obj_cls)

        if hasattr(obj, attr):
            return obj.__dict__[attr]

        if hasattr(descipter_cls, '__get__'):
            return descipter.__get__(obj, obj_cls)

    if hasattr(obj, attr):
        return obj.__dict__[attr]

    raise AttributeError('')

同样,book1.page = 300是为book1实例对象属性赋值,我们也同样看看Python编译器的处理细节。

在这里插入图片描述

def set_obj_attr_pesud(obj, attr, value):
    obj_cls = type(obj)
    if hasattr(obj_cls, attr):
        descipter = getattr(obj_cls, attr)
        descipter_cls = type(descipter)

        if hasattr(descipter_cls, '__set__'):
            descipter.__set__(obj, value)
            return

    obj.__dict__[attr] = value

下面看看几种实现描述器的例子

实现了__get__和__set__方法的描述器

当使用实现了这两个方法的描述器控制实例属性,根据上面的原则,在访问实例属性时,实际上触发了__get__函数从中获取属性值,而当为实例属性赋值时则会触发__set__函数,一个经典的用法是为实例创建一个只读属性。

class ReadOnly:
    def __init__(self, init_value):
        self.init_value = init_value

    def __set__(self, instance, value):
        error = "Can't set {} attribute.".format(self.init_value)
        raise AttributeError(error)

    def __get__(self, instance, owner):
        if instance is None:
            return self
        else:
            return self.init_value


class Book:
    title = "BOOK"
    author = "AUTHOR"
    page = Unsigned("page")
    publish = ReadOnly("xinhua")

book1 = Book()
print(book1.publish)
book1.__dict__['publish'] = 3
print(vars(book1))
print(book1.publish)
del Book.publish
print(book1.publish)

"""output
xinhua
{'publish': 3}
xinhua
3
"""

按照上文中介绍的实例属性的访问原理,即使我们用__dict__的方式强制为book1添加了属性publish,但因为ReadOnly实现了__set__和__get__,因此调用book1.publish依然会直接触发ReadOnly中的__get__而非获取到book1.__dict__中的属性,当然如果用del删除了Book类中的publish属性,则book1.publish则返回book1.__dict__的属性。
因此若想实现一个只读属性,需要通过一个实现了__get__和__set__方法的描述器,若只实现__set__,则可以通过book1.__dict__的方式修改book1的属性。

只实现__set__方法的描述器

这类描述器通常用来验证属性的赋值操作。

class Unsigned:
    def __init__(self, name):
        self.name = name

    def __set__(self, instance, value):
        if value > 0:
            instance.__dict__[self.name] = value
        else:
            raise ValueError('size must > 0')

class Book:
    title = "BOOK"
    author = "AUTHOR"
    page = Unsigned("page")

所有的book1.page = something的赋值操作都会直接触发Unsigned描述器的__set__方法。

只实现__get__方法的描述器

这类描述器通常可以用来处理耗时的工作并进行缓存,可以理解为延迟加载,下面用例子进行说明。

class Cache:
    def __init__(self, name):
        self.name = name

    def __get__(self, instance, owner):
        if instance is None:
            return self
        else:
            ### do expensive computation then the result is a list
            print('Cache __get__.')
            result = [1, 2, 3, 4]
            instance.__dict__[self.name] = result
            return result

class Book:
    title = "BOOK"
    author = "AUTHOR"
    data = Cache('data')

book1 = Book()
print(book1.data)
print(vars(book1))
print(book1.data)

"""output
Cache __get__.
[1, 2, 3, 4]
{'data': [1, 2, 3, 4]}
[1, 2, 3, 4]
"""

按照上文介绍的属性访问原理,当第一调用book1.data访问属性时,由于data的描述器Cache没有__set__方法,因此会先从book1.__dict__中找data属性,此时没有找到,接着就会触发Cache描述器的__get__方法,在这里可以完成一些耗时的工作,并将结果保存到book1.__dict__的data属性中,最后返回。因此再次调用book1.data时,由于book1.__dict__此时拥有了data属性,直接返回了结果,并没有触发Cache的__get__方法。

函数是只实现了__get__的描述器对象

class Book:
    title = "BOOK"
    author = "AUTHOR"

    def read(self):
        print('read')

book1 = Book()
print(Book.read)
print(book1.read)
book1.read()
Book.read(book1)
Book.read.__get__(book1, Book)()

"""output
<function Book.read at 0x10cc727b8>
<bound method Book.read of <__main__.Book object at 0x10b9456a0>>
read
read
"""

当在类Book中定义了函数read时,Book拥有了属性read,它是一个只实现了__get__方法的描述器对象。通过Book.read返回的是一个普通函数function(这其实是该描述器的自身对象),通过book1.read返回的是个绑定方法,这些都是通过描述器中的__get__方法实现。这种绑定可以理解为下面代码:

class read:
    def __call__(self, instance=None, *args, **kwargs):
        print('read:', instance.title)

    def __get__(self, instance, owner):
        if instance is None:
            return self
        else:
            return lambda *args, **kwargs: self.__call__(instance, *args, **kwargs)

class Book:
    title = "BOOK"
    author = "AUTHOR"

    read = read()

book1 = Book()
print(Book.read)
print(book1.read)
book1.read()
Book.read(book1)
Book.read.__get__(book1, Book)()

"""output
<__main__.read object at 0x10eab46a0>
<function read.__get__.<locals>.<lambda> at 0x10fde0510>
read: BOOK
read: BOOK
read: BOOK
"""

因为函数是个描述器对象,需要实现__call__方法使之可调用,instance可以理解为在类中定义函数的’self’, __get__方法通过绑定instance的方式使self生效。

property

property是Python中内置的一种描述器。

class Book:
    title = "BOOK"
    author = "AUTHOR"

    @property
    def page(self):
        return self._page

    @page.setter
    def page(self, value):
        if value < 500:
            raise ValueError('value is not valid')
        self._page = value


book1 = Book()
book1.page = 300

当调用book1.page会抛出异常,property实现了描述器的三种方法,当不设置page.setter时,则page为只读属性,当调用为page赋值时,property中的__set__会抛出异常。property的__init__函数接收四个参数

property(fget=None, fset=None, fdel=None, doc=None)

我们也可以不用装饰器,显示的调用property创建属性。

class Book:
    title = "BOOK"
    author = "AUTHOR"

    def get_page(self):
        return self._page

    def set_page(self, value):
        if value < 500:
            raise ValueError('value is not valid')
        self._page = value

    page = property(get_page, set_page)


book1 = Book()
book1.page = 300

总结

类和实例对象的属性
实例和类的属性访问和赋值原理
用描述器控制属性
函数是描述器(只实现了__get__)
property是描述器,实现__get__, __set__, __delete__

猜你喜欢

转载自blog.csdn.net/miuric/article/details/83054504