@property python知乎_Python3基础之:property
0、访问类的成员变量的几种方式
由于某些原因(将在最后一小节详细讨论),当我们外部访问(读写)一个类的成员变量时,一般会通过一组Getter和Setter(获取子和设置子)函数来达到目的,而不是直接访问谇变量。而为了避免使用者直接访问这种类型的成员变量,这些成员变量往往会被声明为私有的。
关于私有变量的讨论,请阅读我往期的文章:酸痛鱼:Python3基础之:私有成员zhuanlan.zhihu.com
在Python3中,Getter和Setter一般有两种方式,一种是直接声明get和set函数,一直则是通过property装饰器(属性)。由于property有两种语法,所以通过property修饰器又有两种不同的实现方式。
在本文中,当我们提到“成员变量”时,就是指直接访问数据成员;当提到“属性”时,就是通过property修饰器访问“成员变量”。“使用getter和setter”及“使用属性”是有区别的,Python中,属性是getter和setter的一种实现方式。本文会严格区分这两种的叫法。
出于介绍property使用的需要,下面的例子中除了实现getter和setter之外,也会实现deleter。
第一种,get和set函数
class Foo:
def __init__(self, v):
self.__val = v
def GetV(self):
'''Member variable __val'''
return self.__val
def SetV(self, v):
self.__val = v
def DelV(self):
del self.__val
第二种,通过property修饰器使用get和set函数
class Foo:
def __init__(self, v):
self.__val = v
def GetV(self):
return self.__val
def SetV(self, v):
self.__val = v
def DelV(self):
del self.__val
v = property(GetV, SetV, DelV, 'Member variable __val')
这种方式是在上一种的基础上进的改造,用户仍然可以使用GetV、SetV、DelV,可以直接使用属性v来达到同样的效果:
f = Foo("bar")
# 如下两行代码是等价的
print(f.GetV())
print(f.v)
# 如下两行代码是等价的
f.SetV("pass gas")
f.v = "pass gas"
# 如下两行代码是等价的
f.DelV()
del f.v
当然,既然使用了属性v,那么原则上GetV、SetV、DelV应该声明为私有的,我们没有充分理由再直接使用GetV、SetV、DelV这三个方法。
第三种,使用get函数修饰器的setter和deleter
class Foo:
def __init__(self, v):
self.__val = v
@property
def v(self):
return self.__val
@v.setter
def v(self, _v):
self.__val = _v
@v.deleter
def v(self):
del self.__val
@v.setter和@v.deleter是可选的。例如,如果我们只想声明只读的属性v,可以这么实现:
class Foo:
def __init__(self, v):
self.__val = v
@property
def v(self):
return self.__val
1、一个完整的实例
在这个实现中,我们实现一个Human类。Human有四个成员变量,即姓名、年龄、体重和性别。我们用上一节介绍的三种方式,分别实现了姓名、年龄、体重的setter、getter、deleter;但因为一个人一旦出生,性别就是不可以改变的(No offence to GLBTQ),所以我们把它声明为了只读属性(readonly property)。
本文是出于教学型的目的编写的,所以混合了各种实现方式。在实际开发中,要保持风格和实现方式的统一,只选其中一种方式就好;另外,property修饰器既然做为一个便利而存在,我个人更推荐第三种实现方式(只读属性是这种方式的特例之一)。
class Gender:
Female = "Female"
Male = "Male"
class Human:
def __init__(self, name, age, weight, gender):
self.__name = name # 名字
self.__age = age # 年龄
self.__weight = weight # 体重
self.__gender = gender # 性别
def GetName(self):
"""Retrive A Human's Name"""
return self.__name
def SetName(self, name):
self.__name = name
def DelName(self):
del self.__name
def GetAge(self):
return self.__age
def SetAge(self, age):
self.__age = age
def DelAge(self):
del self.__age
age = property(GetAge, SetAge, DelAge, \
"We are talking about AGE...")
@property
def weight(self):
return self.__weight
@weight.setter
def weight(self, weight):
self.__weight = weight
@weight.deleter
def weight(self):
del self.__weight
@property
def gender(self):
'''Gender Cannot bo motified'''
return self.__gender
def __str__(self):
fmt = '''Basic Infos:
Name: {}
Age: {}
Weight:{}
Gender:{}
'''
return fmt.format(self.__name, self.__age, self.__weight, self.__gender)
if __name__ == "__main__":
girl = Human("Emily", 17, 45, Gender.Female)
print("Initiated Info:")
print(girl)
girl.SetName("Emilie")
girl.age = girl.age + 1
girl.weight = girl.weight - 1
# gender是只读属性,不可修改
# girl.gender = Gender.Male
print("Modified Info:")
print(girl)
执行结果:
Initiated Info:
Basic Infos:
Name: Emily
Age: 17
Weight:45
Gender:Female
Modified Info:
Basic Infos:
Name: Emilie
Age: 18
Weight:44
Gender:Female
在上面的例子中,我们通过propery修饰器声明了weight属性,虽然它们都是函数,但在实际使用的过程中,它看起来更像一个成员变量,所以要符合成员变更的命名规范。
2、为什么要使用getter和setter
如果使用getter和setter只是为了单纯地访问一个成员变量,那么完全没有必要。
在我们上面的例子中,只要把目标变量变成公有的(严格意义上来说,python没有所谓私有变量),然后直接访问该成员变量就好。
那么,为什么要使用getter和setter呢?
其实这个是基于很多层面上的考虑。在这里,我们举几个常见的场景来说明。在这里列举的不是全部原因,但足以说明getter和setter的重要性。deleter也是出于getter和setter类似的考虑,为了简化表述,下面中将不提及deleter。
A、访问限制控制
通过getter和setter,我们可以灵活地控制一个成员变量的访问限制。如果只有getter,就是只读的;如果只有setter,就是只写的;如果同时有getter和setter,就是可读可写的。
B、值的合法范围限制
如果我们我们知道一个人的生日,那么我们也知道他的年龄;然而年龄每年都是增长一岁;我们可以通过getter动态获取年龄。
我们也知道,女孩子的年龄是不可以超过18岁的,如果我们算出来的年龄超过18,应该返回18。
一个人的体重不可能为0的,它肯定是大于零的。一个新生儿的体重一般不低于2500g。
反正就是,一个值,它总会有它的合理范围的。在某些必要的条件下,当某个值被访问时,我们应该返回一个合理的值;当某个值被修改是,我们应该拒绝不合理的修改。
class Gender:
Female = "Female"
Male = "Male"
class Human:
def __init__(self, name, gender, birth_year, weight):
self.__name = name
self.__gender = gender
self.__by = birth_year
self.__weight = weight
def IsGirl(self):
return (self.__gender == Gender.Female)
@property
def name(self):
'''只读属性'''
return self.__name
@property
def birth_year(self):
'''只读属性'''
return self.__by
@property
def age(self):
'''只读属性'''
this_year = GetYear() # Pseudo function
age = this_year - self.birth_year
if self.IsGirl():
# Girls never age!!!
if age > 18: age = 18
return age
@property
def weight(self):
if self.__weight <= 2500:
return 2500
return self.__weight
@weight.setter
def weight(self, v):
'''体重的合理范围应该在2.5kg和100kg之间'''
if v < 2500 or v > 100000:
### return
raise InvalidWeightException
self.__weight = v
C、线程同步(并发、数据竞争)
在多线程环境中(Python使用GIL,实际上同时只会有一个线程在运行,为了讨论需要,我们假设了一个多线程环境),如果多个线程对同一个变量进行修改,会出现一些意想不到的问题。往往这个时候我们需要进行线程同步。(对线程同步不了解的读者,应该先了解一下线程同步问题。)
如果我们直接使用成员变量,在每个访问这个变量的地方进程同步处理,这将是一个冗余而又不安全的做法。这个时候,通过setter就可以很好的解决问题。
class Foo:
def __init__(self, v):
self.__v = v
self._lock = SomeKindOfLock()
@property
def v(self):
return self.__v
@v.setter(self, _v)
self._lock.lock() # 获取锁
self.__v = _v # 修改数据
self._lock.release() # 释放锁
D、重构与可维护性
由于某种原因,我们需要对原有的某些功能进行重构。这在项目开发中,是非常常见的。
在一开始,我们通过如下的方式实现了Human类。
class Human:
def __init__(self, name, gender, age):
self.__name = name
self.__gender = gender
self.__age = age
@property
def name(self):
return self.__name
@property
def gender(self):
return self.__gender
@property
def age(self):
return self.__age
@age.setter
def age(self, v):
self.__age = v
到这里,一切看起来没什么问题。
在功能(系统)设计的初期,我们往往会对某些问题欠缺考虑,而导致设计上的一些问题。看到这里,相信各位读者已经知道问题在哪了。事实上,人的年龄是会变化的。通过目前的这种实现方式,我们必须每一年都去同步一个每个Human对象的年龄。
于是,我们进行了简单的重构,为Human类设置出生年份字段,而非年龄字段。代码改起来也很简单。我们只需要将构造函数的age参数改为birth_year,并重写一下age属性的getter方法就好了,我们还可以顺便把setter去掉。
class Human:
def __init__(self, name, gender, birth_year):
self.__name = name
self.__gender = gender
self.__by = birth_year
@property
def name(self):
return self.__name
@property
def gender(self):
return self.__gender
@property
def age(self):
age = GetYear() - self.__by
return age
因为我们使用了属性(即使用了getter和setter),这个重构看起来简单而又高效。
哪怕是我们一开始没有使用属性,借助property将age转为属性,代码重构起来也会很简单。
class Human:
def __init__(self, name, gender, age):
self.name = name
self.gender = gender
self.age = age
# 无处不在地使用Human类
jim = Human("Jim", Gender.Male, 20)
print(jim.age)
# 经过一年
jim.age += 1
print(jim.age)
# Lucy想知道jim多少岁
print("Hei lucy, Jim is {} year-old".format(jim.age))
上面我们多次直接使用age成员变量(注意,没有通过getter和setter函数)。如果我们想重构成通过出生年份来动态推算实现年龄,也不会有太多的工作量。
但对于许多编程语言来说,事件并没有那么简单。例如C++和JAVA,如果一开始就直接使用age成员变量,而且age被多次直接使用(读和写),那么这个重构将会是一场灾难。为所有用到age的地方,都必须换成一个函数调用(如GetAge()什么的)。