简介
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的方式相当于一个切面重用一样,可以对应于一个类型的参数进行检查。我们可以实现类型参数设置和检查的重用。
实现细节
既然在前面的方法里我们使用了@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/