Python 中的属性函数

前言

Python中的属性函数可以实现一些有用的功能,例如将类方法转换为只读属性、重新实现一个属性的setter和getter方法。下面进入正题。

将类方法转为只读属性

下面的例子参考这篇文章
对于下面的一个类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class Person(object):
""""""

#----------------------------------------------------------------------
def __init__(self, first_name, last_name):
"""Constructor"""
self.first_name = first_name
self.last_name = last_name

#----------------------------------------------------------------------
@property
def full_name(self):
"""
Return the full name
"""

return "%s %s" % (self.first_name, self.last_name)

通过将full_name设置为只读属性,外接可以像调用普通的属性一样使用full_name,例如下面这样

1
2
3
4
5
6
7
8
9
>>> person = Person("Mike", "Driscoll")
>>> person.full_name
'Mike Driscoll'
>>> person.first_name
'Mike'
>>> person.full_name = "Jackalope"
Traceback (most recent call last):
File "<string>", line 1, in <fragment>
AttributeError: can't set attribute

可以正常地访问它,但是无法设置属性,因为没有相应的setter方法,想要改变,只能通过修改first_name属性或者last_name属性的值来实现。下面看另外一个property的用处,即取代getter和setter方法。

实现属性的getter和setter方法

熟悉Java编程的人肯定对getter和setter并不陌生,对于类中的每一个属性,对外要提供getter和setter方法,如果用这种方式写Python代码,显得优点不专业,不光代码冗长,而且影响美观。采用property的方式可以改善这一问题。下面先看第一个版本的property改写示例。
假设现在有一个类Fees,它有一个属性_fee,如果模仿其他语言的方式提供getter和setter方法的话就是这样的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
from decimal import Decimal

class Fees(object):

def __init__(self):
"""Constructor"""
self._fee = None

def get_fee(self):
"""
The getter
"""

return self._fee

def set_fee(self, value):
"""
The setter
"""

if isinstance(value, str):
self._fee = Decimal(value)
elif isinstance(value, Decimal):
self._fee = value

#----------------------------------------------------------------------
if __name__ == "__main__":
f = Fees()
f.set_fee("100")
f.get_fee()

这样表面看起来挺不错的。上面本意是检查输入数据是不是数值类型或者是可以转换为数值的字符串,如果是则允许,否则不允许设置。但是如果用直接访问属性的方式来设定属性的值,那么对_fee的验证就不起作用了。看下面的使用过程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
>>> f.set_fee("200")
>>> f.get_fee()
Decimal('200')

#看起来还不错
>>> f.set_fee("haha")
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "<stdin>", line 24, in set_fee
File "C:\Users\wenqiang\Anaconda\lib\decimal.py", line 547, in __new__
"Invalid literal for Decimal: %r" % value)
File "C:\Users\wenqiang\Anaconda\lib\decimal.py", line 3873, in _raise_error
raise error(explanation)
decimal.InvalidOperation: Invalid literal for Decimal: 'haha'

#但是下面这样就不行了
>>> f._fee="haha"
>>> f.get_fee()
'haha'

下面通过property来实现getter和setter的功能。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
from decimal import Decimal

class Fees(object):

def __init__(self):
"""Constructor"""
self._fee = None

@property
def fee(self):
"""
The fee property - the getter
"""

return self._fee

@fee.setter
def fee(self, value):
"""
The setter of the fee property
"""

if isinstance(value, str):
self._fee = Decimal(value)
elif isinstance(value, Decimal):
self._fee = value

#----------------------------------------------------------------------
if __name__ == "__main__":
f = Fees()

这回看一下使用起来如何

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
>>> f=Fees()
>>> f.fee="1"
>>> f.fee
Decimal('1')
>>> f.fee="haha"
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "<stdin>", line 11, in fee
File "C:\Users\wenqiang\Anaconda\lib\decimal.py", line 547, in __new__
"Invalid literal for Decimal: %r" % value)
File "C:\Users\wenqiang\Anaconda\lib\decimal.py", line 3873, in _raise_error
raise error(explanation)
decimal.InvalidOperation: Invalid literal for Decimal: 'haha'
#哈哈,还是被异常修改了
>>> f._fee="haha"
>>> f.fee
'haha'
>>>

通过以上对比,发现这种使用方式相对于getter和setter来说并没有多大改进,别急,看一下版本2中的改进。
版本二,专门定义一个类,将这个类应用到多个属性上。下面举个例子说明一下,例子来自于这篇文章

看一下这个例子使用第一个版本的效果

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
class Movie(object):
def __init__(self, title, rating, runtime, budget, gross):
self._rating = None
self._runtime = None
self._budget = None
self._gross = None

self.title = title
self.rating = rating
self.runtime = runtime
self.gross = gross
self.budget = budget

#nice
@property
def budget(self):
return self._budget

@budget.setter
def budget(self, value):
if value < 0:
raise ValueError("Negative value not allowed: %s" % value)
self._budget = value

#ok
@property
def rating(self):
return self._rating

@rating.setter
def rating(self, value):
if value < 0:
raise ValueError("Negative value not allowed: %s" % value)
self._rating = value

#uhh...
@property
def runtime(self):
return self._runtime

@runtime.setter
def runtime(self, value):
if value < 0:
raise ValueError("Negative value not allowed: %s" % value)
self._runtime = value

#is this forever?
@property
def gross(self):
return self._gross

@gross.setter
def gross(self, value):
if value < 0:
raise ValueError("Negative value not allowed: %s" % value)
self._gross = value

def profit(self):
return self.gross - self.budget

可以看到,对于多个属性这样设置仍然很麻烦,下面看一下版本2的写法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
from weakref import WeakKeyDictionary

class NonNegative(object):
"""A descriptor that forbids negative values"""
def __init__(self, default):
self.default = default
self.data = WeakKeyDictionary()

def __get__(self, instance, owner):
# we get here when someone calls x.d, and d is a NonNegative instance
# instance = x
# owner = type(x)
return self.data.get(instance, self.default)

def __set__(self, instance, value):
# we get here when someone calls x.d = val, and d is a NonNegative instance
# instance = x
# value = val
if value < 0:
raise ValueError("Negative value not allowed: %s" % value)
self.data[instance] = value

class Movie(object):

#always put descriptors at the class-level
rating = NonNegative(0)
runtime = NonNegative(0)
budget = NonNegative(0)
gross = NonNegative(0)

def __init__(self, title, rating, runtime, budget, gross):
self.title = title
self.rating = rating
self.runtime = runtime
self.budget = budget
self.gross = gross

def profit(self):
return self.gross - self.budget

这里结合了访问描述符,当解释器遇到对属性的访问时,如果是输出,就会将该属性看作一个带有get方法的描述符;如果是赋值操作,Python就会识别出该属性是一个带有set方法的描述符,从而执行Nonnegative类中的set方法。关于描述符的更多介绍,推荐看一下这篇文章
下面我们通过这种方式再来试试可不可以随意修改属性

1
2
3
4
5
6
7
8
9
10
11
12
>>> m = Movie('Casablanca', 97, 102, 964000, 1300000)
>>> print m.budget # calls Movie.budget.__get__(m, Movie)
964000
>>> m.budget=300
>>> m.budget
300
>>> m.budget=-24
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "<stdin>", line 16, in __set__
ValueError: Negative value not allowed: -24
>>>

这里面在Nonegative类中对数值是否为负做了限定,通过直接访问属性的方法无法再修改属性的值了,原理上面说过了。不过对于多个属性执行不同逻辑的getter和setter功能时,一个访问描述符就不足以解决问题了,就要引入多个,所以要看问题的具体情况合理使用。

文章目录
  1. 1. 前言
  2. 2. 将类方法转为只读属性
  3. 3. 实现属性的getter和setter方法
,