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

Posted 风-fmgao

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了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 = ‘testa.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而已。

 

以上是关于Python中属性和描述符的简单使用的主要内容,如果未能解决你的问题,请参考以下文章

python-类与对象 详解属性描述符(图文并茂)

python-类与对象 详解属性描述符(图文并茂)

即学即用的 30 段 Python 实用代码

检查是不是支持占位符的简单方法?

Java中各种(类方法属性)访问修饰符与修饰符的简单说明

在 Python 中,为啥属性优先于实例属性?