• Python中属性和描述符的简单使用


    Python的描述符和属性是接触到Python核心编程中一个比较难以理解的内容,自己在学习的过程中也遇到过很多的疑惑,通过google和阅读源码,现将自己的理解和心得记录下来,也为正在为了该问题苦恼的朋友提供一个思考问题的参考。

    关于@property装饰器

    在Python中我们使用@property装饰器来把对函数的调用伪装成对属性的访问。

    那么为什么要这样做呢?因为@property让我们将自定义的代码同变量的访问/设定联系在了一起,同时为你的类保持一个简单的访问属性的接口。

    举个栗子,假如我们有一个需要表示电影的类:

    class Movie(object):
     def __init__(self, title, description, score, ticket):
     self.title = title
     self.description = description
     self.score = scroe
     self.ticket = ticket
    View Code

    你开始在项目的其他地方使用这个类,但是之后你意识到:如果不小心给电影打了负分怎么办?你觉得这是错误的行为,希望Movie类可以阻止这个错误。 你首先想到的办法是将Movie类修改为这样:

    class Movie(object):
     def __init__(self, title, description, score, ticket):
     self.title = title
     self.description = description
         self.ticket = ticket
     if score < 0:
      raise ValueError("Negative value not allowed:{}".format(score))
     self.score = scroe
    View Code

    但这行不通。因为其他部分的代码都是直接通过Movie.score来赋值的。这个新修改的类只会在__init__方法中捕获错误的数据,但对于已经存在的类实例就无能为力了。如果有人试着运行m.scrore= -100,那么谁也没法阻止。那该怎么办?

    Python的property解决了这个问题。

    我们可以这样做

    class Movie(object):
     def __init__(self, title, description, score):
     self.title = title
     self.description = description
     self.score = score
         self.ticket = ticket
      
     @property
     def score(self):
     return self.__score
      
      
     @score.setter
     def score(self, score):
     if score < 0:
      raise ValueError("Negative value not allowed:{}".format(score))
     self.__score = score
      
     @score.deleter
     def score(self):
     raise AttributeError("Can not delete score")
    View Code

    这样在任何地方修改score都会检测它是否小于0。

    property的不足

    对property来说,最大的缺点就是它们不能重复使用。举个例子,假设你想为ticket字段也添加非负检查。

    下面是修改过的新类:

    class Movie(object):
     def __init__(self, title, description, score, ticket):
     self.title = title
     self.description = description
     self.score = score
     self.ticket = ticket
      
     @property
     def score(self):
     return self.__score
      
      
     @score.setter
     def score(self, score):
     if score < 0:
      raise ValueError("Negative value not allowed:{}".format(score))
     self.__score = score
      
     @score.deleter
     def score(self):
     raise AttributeError("Can not delete score")
      
      
     @property
     def ticket(self):
     return self.__ticket
      
     @ticket.setter
     def ticket(self, ticket):
     if ticket < 0:
      raise ValueError("Negative value not allowed:{}".format(ticket))
     self.__ticket = ticket
      
      
     @ticket.deleter
     def ticket(self):
     raise AttributeError("Can not delete ticket")
    View Code

    可以看到代码增加了不少,但重复的逻辑也出现了不少。虽然property可以让类从外部看起来接口整洁漂亮,但是却做不到内部同样整洁漂亮。

    描述符登场

    什么是描述符?

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

    描述符有什么作用?

    简单的说描述符会改变一个属性的基本的获取、设置和删除方式

    先看如何用描述符来解决上面 property逻辑重复的问题。

    class Integer(object):
     def __init__(self, name):
     self.name = name
      
     def __get__(self, instance, owner):
     return instance.__dict__[self.name]
      
     def __set__(self, instance, value):
     if value < 0:
      raise ValueError("Negative value not allowed")
     instance.__dict__[self.name] = value
      
    class Movie(object):
     score = Integer('score')
     ticket = Integer('ticket')
    View Code

    因为描述符优先级高并且会改变默认的getset行为,这样一来,当我们访问或者设置Movie().score的时候都会受到描述符Integer的限制。

    不过我们也总不能用下面这样的方式来创建实例

    a = Movie()
    a.score = 1
    a.ticket = 2
    a.title = ‘test'
    a.descript = ‘…'
    View Code

    这样太生硬了,所以我们还缺一个构造函数。

    class Integer(object):
     def __init__(self, name):
     self.name = name
      
     def __get__(self, instance, owner):
     if instance is None:
      return self
     return instance.__dict__[self.name]
      
     def __set__(self, instance, value):
     if value < 0:
      raise ValueError('Negative value not allowed')
     instance.__dict__[self.name] = value
      
      
    class Movie(object):
     score = Integer('score')
     ticket = Integer('ticket')
      
     def __init__(self, title, description, score, ticket):
     self.title = title
     self.description = description
     self.score = score
     self.ticket = ticket
    View Code

    这样在获取、设置和删除scoreticket的时候都会进入Integer__get__ __set__ ,从而减少了重复的逻辑。

    现在虽然问题得到了解决,但是你可能会好奇这个描述符到底是如何工作的。具体来说,在__init__函数里访问的是自己的self.scoreself.ticket,怎么和类属性scoreticket关联起来的?

    描述符如何工作

    类调用__getattribute__()的时候大概是下面这样子:

    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
    View Code

    我对上面的理解是,访问一个实例的属性的时候是先遍历它和它的父类,寻找它们的__dict__里是否有同名的data descriptor如果有,就用这个data descriptor代理该属性,如果没有再寻找该实例自身的__dict__ ,如果有就返回。任然没有再查找它和它父类里的non-data descriptor,最后查找是否有__getattr__

    描述符的应用场景

    python的property、classmethod修饰器本身也是一个描述符,甚至普通的函数也是描述符(non-data discriptor)

    django model和SQLAlchemy里也有描述符的应用

    class User(db.Model):
     id = db.Column(db.Integer, primary_key=True)
     username = db.Column(db.String(80), unique=True)
     email = db.Column(db.String(120), unique=True)
      
     def __init__(self, username, email):
     self.username = username
     self.email = email
      
     def __repr__(self):
     return '<User %r>' % self.username
    View Code

     __get__,__getattr__,__getattribute__

    __get__,__getattr__和__getattribute__都是访问属性的方法,但不太相同。
    object.__getattr__(self, name)
    当一般位置找不到attribute的时候,会调用getattr,返回一个值或AttributeError异常。

    object.__getattribute__(self, name)
    无条件被调用,通过实例访问属性。如果class中定义了__getattr__(),则__getattr__()不会被调用(除非显示调用或引发AttributeError异常)

    object.__get__(self, instance, owner)
    如果class定义了它,则这个class就可以称为descriptor。owner是所有者的类,instance是访问descriptor的实例,如果不是通过实例访问,而是通过类访问的话,instance则为None。(descriptor的实例自己访问自己是不会触发__get__,而会触发call,只有descriptor作为其它类的属性才有意义。)(所以下文的d是作为C2的一个属性被调用)

    class C(object):
        a = 'abc'
        def __getattribute__(self, *args, **kwargs):
            print("__getattribute__() is called")
            return object.__getattribute__(self, *args, **kwargs)
    #        return "haha"
        def __getattr__(self, name):
            print("__getattr__() is called ")
            return name + " from getattr"
    
        def __get__(self, instance, owner):
            print("__get__() is called", instance, owner)
            return self
    
        def foo(self, x):
            print(x)
    
    class C2(object):
        d = C()
    if __name__ == '__main__':
        c = C()
        c2 = C2()
        print(c.a)
        print(c.zzzzzzzz)
        c2.d
        print(c2.d.a)
    
    结果:
    __getattribute__() is called
    abc
    __getattribute__() is called
    __getattr__() is called 
    zzzzzzzz from getattr
    __get__() is called <__main__.C2 object at 0x16d2310> <class '__main__.C2'>
    __get__() is called <__main__.C2 object at 0x16d2310> <class '__main__.C2'>
    __getattribute__() is called
    abc
    View Code

    小结:可以看出,每次通过实例访问属性,都会经过__getattribute__函数。而当属性不存在时,仍然需要访问__getattribute__,不过接着要访问__getattr__。这就好像是一个异常处理函数。
    每次访问descriptor(即实现了__get__的类),都会先经过__get__函数。

    需要注意的是,当使用类访问不存在的变量是,不会经过__getattr__函数。而descriptor不存在此问题,只是把instance标识为none而已。

    Descriptor Protocol(协议)

    参考链接:http://www.cnblogs.com/btchenguang/archive/2012/09/18/2690802.html

    代码示例:

    class RevealAccess(object):
        """创建一个Descriptor类,用来打印出访问它的操作信息
        """
    
        def __init__(self, initval=None, name='var'):
            self.val = initval
            self.name = name
    
        def __get__(self, obj, objtype):
            print('Retrieving', self.name)
    
            return self.val
    
        def __set__(self, obj, val):
            print('Updating', self.name)
    
            self.val = val
            # 使用Descriptor
    
    
    class MyClass(object):
        # 生成一个Descriptor实例,赋值给类MyClass的x属性
        x = RevealAccess(10, 'var "x"')
        y = 5  # 普通类属性
    
    
    if __name__ == "__main__":
        a = MyClass()
        print(a.x)
        print()
        a.x = 1
        print(a.x)
        print()
        print(a.y)
        print()
        print(MyClass.x)
    结果:
    Retrieving var "x"
    10
    
    Updating var "x"
    Retrieving var "x"
    1
    
    5
    
    Retrieving var "x"
    1
    View Code

    有下面这三个方法

    get__(self, obj, type=None) --> value
    set__(self, obj, value) --> None
    delete__(self, obj) --> None
     
    只要对象重写任何上面的一个方法,对象就被看作是descriptor,就可以不去采用默认的查找属性的顺序。
     
     
    如果一个对象同时定义了__get__,__set__方法,被看作是data descriptor;只定义了__get__,被称
    为non-data descriptor。如果实例字典中有一个key和data descriptor同名,那么查找时优先采用
    data descriptor;如果实例字典中有一个key和non-data descriptor同名,那么优先采用实例字典的
    方法。
     
     
    创建一个只读data descriptor,只需要在同时定义__get__,__set__方法的同时,让__set__方法抛出异常
    AttributeError。
    需要注意的几点:
    • descriptor是被__getattribute__方法调用的。
    • 重写__getattribute__方法,会阻止自动的descriptor调用,必要时需要你自己加上去。
    • __getattribute__方法只在新式类和新式实例中有用。
    • object.__getattribute__和class.__getattribute__会用不一样的方式调用__get__
    • data descriptors总是覆盖instance dictionary
    • non-data descriptors有可能被instance dictionary覆盖
  • 相关阅读:
    开发报表的先进工具
    强大的报表前端展现功能
    管理驾驶舱监控大屏展现
    换乘算法【转】
    提交中文数据乱码问题总结
    重定向
    容易遗漏的
    jsp基础语法【转】
    说说Java NIO【转】
    Java读取大文件的操作【转】
  • 原文地址:https://www.cnblogs.com/fmgao-technology/p/9413117.html
Copyright © 2020-2023  润新知