python属性和property_python property解读和对比

简介

Python的property属性从表面上来看是一个比较简单的特性,实际上它的实现和一些在工程上的应用里和结合了descriptor等东西。我们这里从一个简单的属性赋值和访问开始一步步的推导。  同时,这里也和一些对应于java里的用法做了一个比较。通过这些比较我们可以看到一些python的典型用法能够带来一定的灵活性。

初始代码

有的时候,我们写一些python的类里,需要定义一些属性,比如如下的代码:

class Person:

def __init__(self, first_name):

self.first_name = first_name

这里的代码再简单不过了,就是设置一个对象里first_name属性。

仅仅是以上的这么一个简单的代码,我们可以在如下的代码里来使用Person:

>>> person = Person("firstname")

>>> person.first_name

'firstname'

>>> person.first_name = "another name"

>>> person.first_name

'another name'

因为在python里,所有的属性默认都是public的,所以这里就相当于java里将属性设置成public一样的效果。我们也可以得到一个类似的java类:

public class Person {

public String firstName;

public Person(String firstName) {

this.firstName = firstName;

}

}

我们从一般的常识里会有这么一种感觉,就是代码这样写有点不妥。因为将对象的属性都暴露在外面了。而且容易被多个地方修改导致错误。这是一个方面的问题,另外一方面,我们希望代码更加defensive,可能会在里面加入很多限制和检查,比如说传入的对象不要为空了。甚至在有的时候我们需要对传入的属性做一些其他的限制,比如邮件地址的格式,传入数据的长度或者 数字的范围等等。

这个时候,如果只是一个暴露出来的这么个属性确实不合适了。

property和属性封装

在python里,如果我们要封装一个属性,那么我们会考虑使用property。假设在前面的代码里,我们需要在设置属性的时候检查它的类型,然后对于它的删除操作不支持,我们可以使用如下的代码:

class Person:

def __init__(self, first_name):

self.first_name = first_name

@property

def first_name(self):

return self._first_name

@first_name.setter

def first_name(self, value):

if not isinstance(value, str):

raise TypeError('Expected a string')

self._first_name = value

@first_name.deleter

def first_name(self):

raise AttributeError("Can't delete attribute")

前面的这部分代码,我们定义了一个first_name的属性。这样以后我们每次访问它们的时候,可以通过person.first_name的方式来访问,和前面的使用方法是一样的。唯一不同的就是我们在实现里增加了类型检查。这里的实现也比较有意思,我们需要考虑的几个属性就是读,写和删除。这几个属性都用同样的方法名,唯一不同的就是对属性的读我们是在first_name方法上增加了@property修饰,而写是@first_name.setter,删除则是@first_name.deleter。这些就是python里设置property的套路。有了这些设置,我们使用一些方式来访问属性的时候会产生如下的结果:

>>> a = Person("first_name")

>>> a.first_name

'first_name'

>>> a.first_name = "new name"

>>> a.first_name = 43

Traceback (most recent call last):

File "", line 1, in

File "/home/frank/programcode/python/person.py", line 12, in first_name

raise TypeError('Expected a string')

TypeError: Expected a string

>>> del a.first_name

Traceback (most recent call last):

File "", line 1, in

File "/home/frank/programcode/python/person.py", line 17, in first_name

raise AttributeError("Can't delete attribute")

AttributeError: Can't delete attribute

这里正好对应我们定义的各种行为,因为a.first_name对应了设置property的方法,而那里我们设置了参数类型的检查,所以会有这么个异常。而del a.first_name对应我们删除属性的方法,所以才会出现AttributeError。

当然,我们并不是一定要定义这所有的方法,有时候如果我们只是需要这个property只读的,设置那个@property读方法就可以了。

针对这个问题,我们虽然修改了python类里面的代码,但是从使用者的角度来说,基本上没有变化。完全看不出来我们是使用了它的property还是我们最开始设置的public attribute。而对于java代码来说呢?这个时候不可避免的,我们就需要设置属性访问方法了,我们要在方法里进行参数检查。那么一个大致对应的代码实现如下:

public class Person {

private String firstName;

public getFirstName() {

return firstName;

}

public void setFirstName(String firstName) {

if(firstName == null)

throw new IllegalArgumentException();

this.firstName = firstName;

}

public Person(String firstName) {

this.firstName = firstName;

}

}

而这里因为有了参数检查,所以我们使用它们的代码如果原来是直接访问属性的则需要修改为getFirstName和setFirstName了。这也是为什么java里推荐使用get, set方法来访问属性。因为有了这些方法我们可以更加方便的去检查属性的合法性。

减少重复

前面关于property的使用确实比较合理。可是当我们有多个属性的时候呢?比如说,我们类里有first_name, last_name等等几个同样类型的属性。如果我们需要访问他们的话,都采用同样的property来做吗?

从前面的代码里已经看到,光定义一个property就要扯上3个方法,如果我们有3, 4个这样的属性要设置...其实就算用property还是满无聊的。那么有没有办法来达到这方面的代码重用呢?我们这里需要使用若干个同样的参数,而且对它们的访问以及参数检查都是一样的,每个property里都这么去检查显得太傻。

在python这里,还有一个办法可以解决,那就是descriptor类。python里descriptor类是做什么的呢?通常来说,当我们访问一个对象里的属性时,可以通过它来做一些定制化的工作。它是怎么来定制的呢?我们先来看对应的定制代码实现:

class String:

def __init__(self, name):

self.name = name

def __get__(self, instance, cls):

if instance is None:

return self

else:

return instance.__dict__[self.name]

def __set__(self, instance, value):

if not isinstance(value, str):

raise TypeError('Expect a string')

if len(value) < 4:

raise AttributeError('Invalid length')

instance.__dict__[self.name] = value

def __del__(self, instance):

del instance.__dict__[self.name]

上面实现的代码类似于一个类,它定义了__get__, __set__, __del__这几个方法。从官方的文档定义来说,任何一个对象只要实现其中的任何一个方法,它就可以称为descriptor。这里对__get__, __set__, __del__里定义的方法似乎有点难以理解,我们一个个的讨论过来。在python里,如果我们访问一个对象的属性,其默认的访问方式其实就是对这个对象的字典进行get, set和delete操作。比如说我们访问一个对象的属性a.x,在其内部的实现是通过去查看对象a的字典a.__dict__['x'],如果这里找不到对应的属性,则查找type(a).__dict__['x'],这里type(a)相当于a对象的类,如果这里也找不到的话则去查找type(a)的父类。

所以有了前面这么个过程,我们就可以看到在get里,如果instance为空的话,我们就返回。这里是因为这个方法尝试返回instance,也就是对象实例或者类所对应的__dict__元素。__set__方法里除了做了一个类型检查以外,我还额外的增加了一个对参数长度的检查。

使用它们的代码如下:

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值