关于@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

  

你开始在项目的其他地方使用这个类,但是之后你意识到:如果不小心给电影打了负分怎么办?你觉得这是错误的行为,希望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

但这行不通。因为其他部分的代码都是直接通过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")

这样在任何地方修改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")

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

描述符登场

什么是描述符?

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

描述符有什么作用?

The default behavior for attribute access is to get, set, or delete the attribute from an object’s dictionary. For instance, a.x has a lookup chain starting witha.__dict__['x'], then type(a).__dict__['x'], and continuing through the base classes of type(a) excluding metaclasses. If the looked-up value is an object defining one of the descriptor methods, then Python may override the default behavior and invoke the descriptor method instead. Where this occurs in the precedence chain depends on which descriptor methods were defined.-----摘自官方文档

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

先看如何用描述符来解决上面 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')

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

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

    a = Movie()
a.score = 1
a.ticket = 2
a.title = 'test'
a.descript = '...'

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

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

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

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

描述符如何工作

看官方的说明

If an object defines both __get__() and __set__(), it is considered a data descriptor. Descriptors that only define __get__() are called non-data descriptors (they are typically used for methods but other uses are possible).

Data and non-data descriptors differ in how overrides are calculated with respect to entries in an instance’s dictionary. If an instance’s dictionary has an entry with the same name as a data descriptor, the data descriptor takes precedence. If an instance’s dictionary has an entry with the same name as a non-data descriptor, the dictionary entry takes precedence.

The important points to remember are:

类调用__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

  

下面是摘自国外一篇博客上的内容。

Given a Class “C” and an Instance “c” where “c = C(…)”, calling “c.name” means looking up an Attribute “name” on the Instance “c” like this:

  • Get the Class from Instance
  • Call the Class’s special method __getattribute__. All objects have a default __getattribute__

Inside __getattribute__

  • Get the Class’s __mro__ as ClassParents
  • For each ClassParent in ClassParents
    • If the Attribute is in the ClassParent’s __dict__
    • If is a data descriptor
      • Return the result from calling the data descriptor’s special method __get__()
    • Break the for each (do not continue searching the same Attribute any further)
  • If the Attribute is in Instance’s __dict__
    • Return the value as it is (even if the value is a data descriptor)
  • For each ClassParent in ClassParents
    • If the Attribute is in the ClassParent’s __dict__
    • If is a non-data descriptor
      • Return the result from calling the non-data descriptor’s special method __get__()
    • If it is NOT a descriptor
      • Return the value
  • If Class has the special method __getattr__
    • Return the result from calling the Class’s special method__getattr__.

我对上面的理解是,访问一个实例的属性的时候是先遍历它和它的父类,寻找它们的__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

  

后记

只有当确实需要在访问属性的时候完成一些额外的处理任务时,才应该使用property。不然代码反而会变得更加啰嗦,而且这样会让程序变慢很多。

参考文章:

https://docs.python.org/3.5/howto/descriptor.html

http://www.betterprogramming.com/object-attribute-lookup-in-python.html

http://stackoverflow.com/questions/14787334/confused-with-getattribute-and-setattribute-in-python

http://www.jianshu.com/p/250f0d305c35

http://www.geekfan.net/7862/

如何正确地使用Python的属性和描述符的更多相关文章

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

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

  2. python高级编程之描述符与属性03

    # -*- coding: utf-8 -*- # python:2.x __author__ = 'Administrator' #属性Property #提供了一个内建描述符类型,它知道如何将一个 ...

  3. Python笔记(4)类__属性与描述符

    部分参考自:http://www.geekfan.net/7862/ 新式类与经典类 2和3不一样,3都是新式类. 新式类和经典类的区别: class A: #classic class " ...

  4. python高级编程之描述符与属性02

    # -*- coding: utf-8 -*- # python:2.x __author__ = 'Administrator' #元描述符 #特点是:使用宿主类的一个或者多个方法来执行一个任务,可 ...

  5. Python 为什么要使用描述符?

    学习 Python 这么久了,说起 Python 的优雅之处,能让我脱口而出的, Descriptor(描述符)特性可以排得上号. 描述符 是Python 语言独有的特性,它不仅在应用层使用,在语言的 ...

  6. 90% 的 Python 开发者不知道的描述符应用

    经过上面的讲解,我们已经知道如何定义描述符,且明白了描述符是如何工作的. 正常人所见过的描述符的用法就是上篇文章提到的那些,我想说的是那只是描述符协议最常见的应用之一,或许你还不知道,其实有很多 Py ...

  7. Python进阶——什么是描述符?

    本文的文字及图片来源于网络,仅供学习.交流使用,不具有任何商业用途,如有问题请及时联系我们以作处理 在 Python 开发中,你可能听说过「描述符」这个概念,由于我们很少直接使用它,所以大部分开发人员 ...

  8. python基础学习之描述符和装饰器

    描述符的了解: 描述符协议: python描述符是一个"绑定行为"的对象属性,在描述符协议中,它可以通过方法重写属性的访问.这些方法有: __get__, __set__, 和__ ...

  9. Python属性、方法和类管理系列之----描述符类

    什么是描述符类? 根据鸭子模型理论,只要具有__get__方法的类就是描述符类. 如果一个类中具有__get__和__set__两个方法,那么就是数据描述符,. 如果一个类中只有__get__方法,那 ...

随机推荐

  1. jquery事件切换hover/toggle

    1.hover([over,]out) 一个模仿悬停事件(鼠标移动到一个对象上面及移出这个对象)的方法.这是一个自定义的方法,它为频繁使用的任务提供了一种“保持在其中”的状态. 当鼠标移动到一个匹配的 ...

  2. Nginx 性能优化

    1.安全优化:隐藏Nginx版本号,server_tokens off; 2.安全优化:更改掉默认的用户  user nginx; 3.性能优化:  根据硬件配置,调整nginx worker 进程数 ...

  3. jsp中的内置对象(9个)、作用

    jsp内置对象 定义:可以不加声明就在JSP页面脚本(Java程序片和Java表达式)中使用的成员变量 JSP共有以下9种基本内置组件(可与ASP的6种内部组件相对应): 1.request对象 客户 ...

  4. scala中的=>符号的含义

    [声明]本帖的内容是copy来的,来源为stack overflow. It has several meanings in Scala, all related to its mathematica ...

  5. 【linux】日志管理

    1.日志文件内容的一般格式 (1)事件发生的日期与时间: (2)发生此事件的主机名: (3)启动此事件的服务名称或函数名称: (4)该信息的实际数据内容. 例如:Mar 14 15:38:00 www ...

  6. Visual Studio Professional 2015 (x86 and x64) - DVD (Chinese-Simplified)

    文件名cn_visual_studio_professional_2015_x86_x64_dvd_6846645.isoSHA1629E7154E2695F08A3C692C0B3F6CE19DF6 ...

  7. (转)mongodb分片

    本文转载自:http://www.cnblogs.com/huangxincheng/archive/2012/03/07/2383284.html 在mongodb里面存在另一种集群,就是分片技术, ...

  8. MySQL使用位运算

    通常 我们的数据表中 可能会包含各种状态属性, 例如 blog表中,我们需要有字段表示其是否公开,是否有设置密码,是否被管理员封锁,是否被置顶等等. 也会遇到在后期运维中,策划要求增加新的功能而造成你 ...

  9. nodejs小问题:express不是内部或外部命令

    来源:http://jingyan.baidu.com/article/922554468a3466851648f419.html 一时兴起想学习点东西,准备在heroku上部署一个Node.js的小 ...

  10. 5. redis管道, 发布订阅, 模拟队列

    一. 发布订阅 #订阅scribe 127.0.0.1:6379> SUBSCRIBE "channel_1" Reading messages... (press Ctrl ...