有效的python属性管理:描述符的使用

简介:

前言

最近使用描述符对自己的催化动力学模拟程序进行了改进,在Python描述符的帮助下实现了更加灵活而且强大有效的属性管理,使得程序各个组件的数据封装更加完善管理也更加有条理。

本文就以自己程序中运用描述符来进行有效的python属性管理为例子,介绍python中描述符的概念以及如何更好的使用描述符这个强有力的工具帮助我们有效管理python程序中的数据访问控制。

正文

在其他语言中我们经常会在类中实现gettersetter等工具方法,这样有助于定义类的接口,也使得开发者能够方便的封装功能,例如验证用法或者进行取值范围的检测。但是在Python中我们一般都是直接从public属性写起,但是当我们对属性有特殊需求,例如进行类型验证(Python是动态类型),数值范围检测,返回深复制(而不是引用)的时候,我们一般会考虑使用:

  1. 内建的@property装饰器

  2. 使用描述符

@property装饰器使用起来方便快捷,例如

class KineticModel(obejct):

    # ...

    @property

    def kB(self):

        return self.__kB



    @property.setter

    def kB(self, kB):

        #...

但是@property的缺点就在于他无法被复用,同一套逻辑不能在不同的属性之间重复使用,这样除了对波尔兹曼常数进行处理外如果还有普朗克常数需要做同样的处理,难道要重复写一次settergetter函数?这显然已经"Repeat yourself"了。

这时候就要召唤Python的描述符机制了,他的存在是python开发者能够复用与属性相关的逻辑。

描述符协议

Python描述符协议是一种再模型中引用属性时将要发生事件的方法。Python会对属性的访问操作进行一定的转译,这种转译的方式就是由描述符协议确定的。借助Python提供给我们的描述符协议,我们就可以用来以Python的方式实现与私有变量类似的功能。

描述符协议包括几个方法: 

- descr.__get__(self, obj, type=None) --> value, 用于访问属性 
- descr.__set__(self, obj, value) --> None, 用于设置属性的值 
- descr.__delete__(self, obj) --> None, 控制属性的删除操作

任何对象如果定义了上面的任何一个方法便实现了描述符协议,也就变成了一个描述符了。我们通过将之前的gettersetter方法中的逻辑重写到__get____set__方法中,便可以把同一套逻辑运用在不同类中不同的属性上面了。

创建描述符

这里只介绍使用类方法创建描述符。

我的动力学模型中的KineticModel需要很多类属性,例如基本的基元反应式rxn_expression(这里我用了一个包含string的list来表示)、模型反应发生的温度temperature(用一个float类型表示)。

为了能够在对属性进行赋值的时候进行相应的类型检测,我就定义了几个基本类型的描述符,提供了检测数据类型的相应逻辑,下面是个简单的整型描述符(当然这不是最后的使用的版本):

class Float(object):

    def __init__(self, name):

        self.name = name



    def __get__(self):

        private_name = "_{}__{}".format(instance.__class__.__name__, self.name)

        if private_name not in instance.__dict__:

            instance.__dict__[private_name] = self.default



    def __set__(self, instance, value):

        # 检测类型

        if type(value) is not float:

            msg = "{} ({}) is not a float number".format(self.name, value)

            raise ValueError(msg)

        # 将对象的相应属性进行赋值,注意这里我使用了`mangled name`用来进行私有化处理

        private_name = "_{}__{}".format(instance.__class__.__name__, self.name)

        instance.__dict__[private_name] = value

这样我们就可以在我们类中相应的类属性定义成相应的描述符对象,后面我们就可以像使用正常属性一样使用他,但是他却拥有了类型检测功能:

...



class KineticModel(obejct):



    # 设置temperature为类属性

    temperature = Float("temperature")



...

当我试图向其赋值一个字符串时,便会抛出异常:

640?wx_fmt=png&tp=webp&wxfrom=5&wx_lazy=

描述符的原理

上面进行了基本的描述符创建和使用效果,那么描述符是如何工作的才能让我们以这种方式操作属性呢?

一句话总结就是通过将属性访问进行了转译

描述符触发

当我们进行属性访问时便会触发描述符(如果这个属性具有描述符定义的时候),当我们对对象obj的属性d进行访问时候,obj.d,描述符的触发过程大致:先在对象obj的字典中寻找d,如果d是个含有__get__()的对象,则直接调用d.__get__(obj).

官方文档中对具体的触发细节进行了更详细的描述,具体的触发又分为我们访问的是类属性还是实例属性: 
1. 如果是对实例属性进行访问,则属性访问转译的关键就在于基类object__getattribute__方法,我们知道这个内置方法是在进行属性访问的时候无条件调用的,因此这个方法中将obj.d转译成了type(obj).__dict__['d'].__get__(obj, type(obj)) 
其实现的C代码参见:https://docs.python.org/3/c-api/object.html#c.PyObject_GenericGetAttr

  1. 如果是对类对象的属性进行访问,则属性的访问转译关键在于元类type__getattribute__方法,它将cls.d转译成cls.__dict__['d'].__get__(None, cls),这里__get__()instance没有也就是相应的None了。 
    其实现的C代码参见:https://hg.python.org/cpython/file/3.5/Objects/typeobject.c#l2936

描述符优先级

首先,描述符和描述符之间也是有区别的
1. 如果一个对象同时定义了__get__()__set__()方法,则这个描述符被称为data descriptor 
2. 如果一个对象只定义了__get__()方法,则这个描述符被称为non-data descriptor

我们对属性进行访问的时候需要几行打交道的基本上包含这几个对象:
1. data descriptor 
2. non-data descriptor 
3. 实例的字典 
4. 内置的__getattr__()函数

他们几个的优先级顺序是:

data descriptor >> instance's dict >> non-data descriptor >> __getattr__()

也就是说如果实例obj重现了同名的data descriptor d 和 实例属性d, 当我们访问d的时候,由于data descriptor具有更高的优先级,python便会调用type(obj).__dict__['d'].__get__(obj, type(obj))而不是返回obj.__dict__['d']

但是如果描述符是个non-data descriptor,则正好相反,python会返回obj.__dict__['d'] 

描述符实现惰性访问(按需访问)

很多时候一个类的属性,我们并不需要在这个类初始化的时候就进行初始化,我们可以在第一次使用这个属性的时候顺便将这个属性初始化,这样在后面重复使用这个属性的时候便直接返回结果就可以了,这样既可以减少计算的次数,也在一定程度上减少了内存的需求。

因此我在定义自己的描述符__get__()的时候进行了判断是否该相应的实例属性已经初始化,若未初始化则进行初始化,若已初始化直接返回,达到了惰性访问的目的:

def __get__(self, instance, owner):

    private_name = "_{}__{}".format(instance.__class__.__name__, self.name)

    # 是否实例属性已存在

    if private_name not in instance.__dict__:

        instance.__dict__[private_name] = self.default

    return instance.__dict__[private_name]

创建只读描述符

当我们想让一个属性(描述符)禁止调用者进行修改的时候,可以通过在__set__()方法中抛出AttributeError异常来实现,例如:

def __set__(self, instance, value):

    private_name = "_{}__{}".format(instance.__class__.__name__, self.name)

    # 在第一次赋值后便无法修改属性的值

    if private_name not in instance.__dict__:

        instance.__dict__[private_name] = value

    else:

        msg ="Changing value of {}.{} is not allowed".format(instance.__class__.__name__,

                                                             self.name)

        raise AttributeError(msg)

这样便实现了私有变量的效果,可以将数据更安全的进行封装,防止在外部调用的时候意外修改了对象的数据造成不想要的结果。

对于mutable的变量可以使用深复制

如果实例属性是字典或者列表这类的变量,python都会返回对象的引用,因此在获取其值以后也是有可能修改其内部数据的,因此如果真的想要是这个属性不被做任何的修改,可以使用deepcopy直接返回对象的深复制,这样在外部无论怎么蹂躏这个对象,都跟返回他的对象本身没有关系了。

def __get__(self, instance, owner):

    private_name = "_{}__{}".format(instance.__class__.__name__, self.name)

    if private_name not in instance.__dict__:

        instance.__dict__[private_name] = self.default

    if self.deepcopy:

        return copy.deepcopy(instance.__dict__[private_name])

    else:

        return instance.__dict__[private_name]

需要注意的点

描述符都是针对类属性,因此如果把数据存放在描述符对象中的时候,会出现意想不到的结果。 
例如我想针对每个学生类创建对应的身高描述符,而且把身高数据放在描述符中,我可以这样定义描述符: 

640?wx_fmt=png&tp=webp&wxfrom=5&wx_lazy=

我们创建了两个学生实例,但是身高属性却是同一个对象,这是因为描述符是类属性,因此每个实例中进行访问的时候都是访问的类属性的

引用

这个时候我们可以不把数据放在描述符中,而是在相应的实例对象中创建私有变量,这样不同的对象的私有变量是不同的变量,便不会出想上图的问题。

class Height(object):

    def __init__(self, name):

        self.name = name



    def __get__(self, instance, cls):

        return getattr(instance, self.name)



    def __set__(self, instance, value):

        setattr(instance, self.name, value)

同时也可以将相应的对象和值以字典的键值对存到描述符的字典中,但是这样会造成引用计数无法为0导致无法进行垃圾回收从而导致内存泄漏的风险,因此这个方法就不详细描述了。

总结

本文总结了Python中的描述符相关的概念和使用,描述符可以帮助我们实现强大而灵活的属性管理,通过结合使用描述符可以实现优雅的编程,但是同时也应该保持谨慎的态度,避免由于覆盖普通对象行为而产生不必要的代码复杂性。



原文发布时间为:2016-12-11

本文作者:Pytlab

本文来自云栖社区合作伙伴“Python中文社区”,了解相关信息可以关注“Python中文社区”微信公众号

相关文章
|
2月前
|
Unix Shell Linux
赞!优雅的Python多环境管理神器!易上手易操作!
赞!优雅的Python多环境管理神器!易上手易操作!
|
1月前
|
存储 JSON 数据管理
文件操作与管理:用Python读写数据
【4月更文挑战第8天】本文介绍了Python中进行高效文件操作与数据管理的方法。通过`os`模块管理目录,使用`open`函数读写文件,`pickle`模块保存和加载复杂数据结构。示例代码展示了如何读取和写入文本文件,处理CSV和JSON数据,以及利用`pickle`序列化和反序列化Python对象。掌握这些技能对于处理现代社会中的数据至关重要。
|
4天前
|
Python
在Python中,类的属性可以分为公有属性和私有属性
Python类包含公有和私有属性。公有属性可直接外部访问,如`person.name`,而私有属性如`_name`需通过getter/setter方法访问,如`person.getName()`和`person.setName()`。私有属性用于封装数据、隐藏实现细节,增强代码可维护性和安全性。封装能灵活修改内部实现,不影响外部;安全性防止外部非法修改数据;一致性确保所有数据操作在类内完成,简化代码并减少错误。
23 10
|
4天前
|
Python
python属性错误(AttributeError)
【5月更文挑战第2天】python属性错误(AttributeError)
7 1
|
13天前
|
Python
Python常见的魔术方法和魔术属性(二)
Python常见的魔术方法和魔术属性(二)
|
13天前
|
数据库 Python
Python常见的魔术方法和魔术属性(一)
Python常见的魔术方法和魔术属性(一)
|
13天前
|
存储 网络安全 数据安全/隐私保护
【专栏】Python 网络设备管理中,`ConnectHandler`(Paramiko库)和`telnetlib`模块常用于设备交互。
【4月更文挑战第28天】Python 网络设备管理中,`ConnectHandler`(Paramiko库)和`telnetlib`模块常用于设备交互。`ConnectHandler`简化SSH连接,便于与网络设备交互,而`telnetlib`是Python内置模块,支持Telnet协议的远程登录操作。两者都提供命令执行和响应接收功能。示例代码展示了如何使用它们获取防火墙设备的版本信息,降低了代码复杂度,提高了可读性和维护性。
|
17天前
|
运维 Shell 网络安全
第十八章 Python批量管理主机(paramiko、fabric与pexpect)
第十八章 Python批量管理主机(paramiko、fabric与pexpect)
|
23天前
|
SQL 安全 Go
如何在 Python 中进行 Web 应用程序的安全性管理,例如防止 SQL 注入?
在Python Web开发中,确保应用安全至关重要,主要防范SQL注入、XSS和CSRF攻击。措施包括:使用参数化查询或ORM防止SQL注入;过滤与转义用户输入抵御XSS;添加CSRF令牌抵挡CSRF;启用HTTPS保障数据传输安全;实现强身份验证和授权系统;智能处理错误信息;定期更新及审计以修复漏洞;严格输入验证;并培训开发者提升安全意识。持续关注和改进是保证安全的关键。
20 0
|
24天前
|
API Python
Python邮件与日历管理
【4月更文挑战第13天】Python 通过 `smtplib` 和 `email` 发送邮件,`imaplib` 接收邮件。`google-api-python-client` 库用于管理 Google Calendar,示例代码展示了列出日历事件的功能。要使用 Google Calendar API,需设置服务帐户凭据和范围。
20 1