Python中属性和描述符的正确使用_python

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

因为描述符优先级高并且会改变默认的getset行为,这样一来,当我们访问或者设置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

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

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

描述符如何工作

看官方的说明

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:

descriptors are invoked by the __getattribute__() method
overriding __getattribute__() prevents automatic descriptor calls
object.__getattribute__() and type.__getattribute__() make different calls to __get__().
data descriptors always override instance dictionaries.
non-data descriptors may be overridden by instance dictionaries.

类调用__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。不然代码反而会变得更加啰嗦,而且这样会让程序变慢很多。以上就是本文的全部内容,由于个人能力有限,文中如有笔误、逻辑错误甚至概念性错误,还请提出并指正。

以上是小编为您精心准备的的内容,在的博客、问答、公众号、人物、课程等栏目也有的相关内容,欢迎继续使用右上角搜索按钮进行搜索python
, 描述符
, 属性描述符
描述器
python 描述符、python 描述器、python 描述性统计、python 文件描述符、python 描述直方图,以便于您获取更多的相关知识。

时间: 2024-08-30 18:21:53

Python中属性和描述符的正确使用_python的相关文章

mingw-MinGW编译环境中由文件描述符(fd)获得windows文件句柄有什么方法

问题描述 MinGW编译环境中由文件描述符(fd)获得windows文件句柄有什么方法 我在MinGW中打开了一个文件后把描述出输出来是3,推测MinGW中不是用的文件句柄表示open打开的文件,现在想从这个描述符中获取windows句柄,有什么函数可以实现吗

Linux内核中的文件描述符

Kernel version:2.6.14 CPU architecture:ARM920T 作为文件的使用者,进程理所当然的要将所使用的文件记录于自己的控制块中,也就是task_struct.另外,由于进程所对应的程序也是一个文件,因此进程控制块还必须记录这个文件的相关信息.由于OS要对所有进程提供服务,因此OS还要维护一个记录所有进程打开的文件的总表. 1.文件对象 当进程通过open系统调用打开一个文件时,该系统调用找到这个文件后,会把文件封装到一个file结构的实例中提供给进程,这个实例

详解Python中内置的NotImplemented类型的用法_python

它是什么?  >>> type(NotImplemented) <type 'NotImplementedType'> NotImplemented 是Python在内置命名空间中的六个常数之一.其他有False.True.None.Ellipsis 和 __debug__.和 Ellipsis很像,NotImplemented 能被重新赋值(覆盖).对它赋值,甚至改变属性名称, 不会产生 SyntaxError.所以它不是一个真正的"真"常数.当然,我们

Python中__init__和__new__的区别详解_python

__init__ 方法是什么? 使用Python写过面向对象的代码的同学,可能对 __init__ 方法已经非常熟悉了,__init__ 方法通常用在初始化一个类实例的时候.例如: # -*- coding: utf-8 -*- class Person(object): """Silly Person""" def __init__(self, name, age): self.name = name self.age = age def __

Python中的二叉树查找算法模块使用指南_python

python中的二叉树模块内容: BinaryTree:非平衡二叉树  AVLTree:平衡的AVL树  RBTree:平衡的红黑树 以上是用python写的,相面的模块是用c写的,并且可以做为Cython的包. FastBinaryTree  FastAVLTree  FastRBTree 特别需要说明的是:树往往要比python内置的dict类慢一些,但是它中的所有数据都是按照某个关键词进行排序的,故在某些情况下是必须使用的. 安装和使用 安装方法 安装环境: ubuntu12.04, py

在Python中使用SimpleParse模块进行解析的教程_python

与大多数程序员一样,我经常需要标识存在于文本文档中的部件和结构,这些文档包括:日志文件.配置文件.分隔的数据以及格式更自由的(但还是半结构化的)报表格式.所有这些文档都拥有它们自己的"小语言",用于规定什么能够出现在文档内. 我编写处理这些非正式解析任务的程序的方法总是有点象大杂烩,其中包括定制状态机.正则表达式以及上下文驱动的字符串测试.这些程序中的模式大概总是这样:"读一些文本,弄清是否可以用它来做些什么,然后可能再多读一些文本,一直尝试下去." 各种形式的解析

Python中optionParser模块的使用方法实例教程_python

本文以实例形式较为详尽的讲述了Python中optionParser模块的使用方法,对于深入学习Python有很好的借鉴价值.分享给大家供大家参考之用.具体分析如下: 一般来说,Python中有两个内建的模块用于处理命令行参数: 一个是 getopt,<Deep in python>一书中也有提到,只能简单处理 命令行参数: 另一个是 optparse,它功能强大,而且易于使用,可以方便地生成标准的.符合Unix/Posix 规范的命令行说明. 示例如下: from optparse impo

在Python中使用dict和set方法的教程_python

dict Python内置了字典:dict的支持,dict全称dictionary,在其他语言中也称为map,使用键-值(key-value)存储,具有极快的查找速度. 举个例子,假设要根据同学的名字查找对应的成绩,如果用list实现,需要两个list: names = ['Michael', 'Bob', 'Tracy'] scores = [95, 75, 85] 给定一个名字,要查找对应的成绩,就先要在names中找到对应的位置,再从scores取出对应的成绩,list越长,耗时越长. 如

Python中__init__.py文件的作用详解_python

__init__.py 文件的作用是将文件夹变为一个Python模块,Python 中的每个模块的包中,都有__init__.py 文件. 通常__init__.py 文件为空,但是我们还可以为它增加其他的功能.我们在导入一个包时,实际上是导入了它的__init__.py文件.这样我们可以在__init__.py文件中批量导入我们所需要的模块,而不再需要一个一个的导入. # package # __init__.py import re import urllib import sys impo