`
frank-liu
  • 浏览: 1666930 次
  • 性别: Icon_minigender_1
  • 来自: 北京
社区版块
存档分类
最新评论

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 "<stdin>", line 1, in <module>
  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 "<stdin>", line 1, in <module>
  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__方法里除了做了一个类型检查以外,我还额外的增加了一个对参数长度的检查。

    使用它们的代码如下:
from string import String

class Person:
    first_name = String('first_name')
    last_name = String('last_name')

    def __init__(self, first_name, last_name):
        self.first_name = first_name
        self.last_name = last_name
   这里定义了两个属性,first_name, last_name。定义的Person类反而清爽了许多。使用Person类的代码如下:
>>> p = Person('first_name', 'last_name')
>>> p.first_name = 'new first'
>>> p.first_name
'new first'
>>> p.last_name = 'new last'
>>> p.last_name
'new last'
>>> p.first_name = 'abc'
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "/home/frank/programcode/python/string.py", line 15, in __set__
    raise AttributeError('Invalid length')
AttributeError: Invalid length
>>> p.first_name = 1
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "/home/frank/programcode/python/string.py", line 13, in __set__
    raise TypeError('Expect a string')
TypeError: Expect a string
     这种descriptor的方式相当于一个切面重用一样,可以对应于一个类型的参数进行检查。我们可以实现类型参数设置和检查的重用。
    这个时候,如果我们去考虑对应java的实现,则有点麻烦了。我们可能需要将参数合法性检查之类的东西提取出来作为一个方法,然后所有set访问方法来引用它。还有一个潜在的问题就是如果我们是在构造函数里检查呢?那这方法能管用吗?这个时候要达到这个效果就比较麻烦了。

实现细节

    既然在前面的方法里我们使用了@property这个东西,而在前面的一些文章里我们提到过,@xxx修饰到某些方法的时候,它实际上是一个decorator。所以,前面那个设置属性的代码,其实质上是一个类似于如下的代码:

class Person:
    def __init__(self, first_name):
        self.set_first_name(first_name)

    def get_fist_name(self):
        return self._first_name

    def set_first_name(self, value):
        if not isinstance(value, str):
            raise TypeError('Expected a string')
        self._first_name = value

    def del_first_name(self):
        raise AttributeError("Can't delete attribute")

    name = property(get_first_name, set_first_name, del_first_name)

    这个时候,如果我们访问person.name,就和前面访问属性的效果是一样的。

    另外,这里property的构造参数里对应的是几个方法,分别是get, set, 和delete的。所以,我们可以通过如下的方式看到它对应的原始方法:

>>> Person.first_name.fget
<function Person.first_name at 0x7f493ab8d0d0>
>>> Person.first_name.fset
<function Person.first_name at 0x7f493ab8d158>
>>> Person.first_name.fdel
<function Person.first_name at 0x7f493ab8d1e0>

    从这个角度来说,property修饰就相当于是对3个主要的属性访问方法的封装。有了这个封装,我们每次只要把它套到对应的方法上就可以让它们给分别对上get, set和delete的号了。

 

总结

     property这个看似简单的概念里其实牵涉到了decorator,descriptor等东西以及我们对python对象属性访问的理解。可以说,python里对对象属性的组织和使用的灵活性有时候有点让人无所适从。所以要理解清楚这个东西还是要仔细啊。而且,采用不同的语言,确实对应着不同的使用和思考方式,要转换也不太容易。

 

参考材料

http://stackoverflow.com/questions/6618002/python-property-versus-getters-and-setters
http://tomayko.com/writings/getters-setters-fuxors

http://stackoverflow.com/questions/17330160/python-how-does-the-property-decorator-work

http://blaag.haard.se/What-s-the-point-of-properties-in-Python/

http://www.programiz.com/python-programming/property

http://www.ibm.com/developerworks/library/os-pythondescriptors/

http://martyalchin.com/2007/nov/23/python-descriptors-part-1-of-2/

http://martyalchin.com/2007/nov/24/python-descriptors-part-2-of-2/

分享到:
评论

相关推荐

Global site tag (gtag.js) - Google Analytics