风-fmgao

导航

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覆盖

posted on 2018-08-03 11:26  风-fmgao  阅读(389)  评论(0编辑  收藏  举报