property提供了一个内置的描述符类型,它知道如何将一个属性链接到一组方法上。property接受4个可选参数:fget, fset, fdel, doc。

我们来看一个例子:

class Rectangle:
def __init__(self,x1,y1,x2,y2):
self.x1,self.y1=x1,y1
self.x2,self.y2=x2,y2

def _width_get(self):
return self.x2-self.x1

def _width_set(self,value):
self.x2=self.x1+value

def _height_get(self):
return self.y2-self.y1

def _height_set(self,value):
self.y2=self.y1+value

width=property(
_width_get,_width_set,
doc="calc width from left")

height=property(
_height_get,_height_set,
doc="calc height from top")

#__repr__函数的作用是返回更加易于查看的对象,假如我们实例化一个对象
# rectangle=Rectangle(0,0,100,100)
# 然后执行 rectangle
# 如果编写了__repr__函数,输出就是你定义的return内容
# 否则返回的是rectangle实例在内存中的信息
def __repr__(self):
return "{}({},{},{},{})".format(
self.__class__.__name__,
self.x1,self.y1,self.x2,self.y2)

可以使用property函数来封装关于一个对象的get和set函数,并且返回一个对象,使用返回的对象(width,height)时,property会自动根据代码语境选择对应的函数,我们来运行一下这个程序,并且在运行程序的ipython中执行以下命令:

#自定义__repr__函数的情况
>>> rectangle=Rectangle(10,10,25,34)
>>> rectangle
Out: Rectangle(10,10,25,34)
#不自定义__repr__的情况,在注释中已经有说明
>>> rectangle=Rectangle(10,10,35,34)
>>> rectangle
Out: <__main__.Rectangle at 0x7f64b80625f8>

>>> rectangle=Rectangle(10,10,25,34)
>>> rectangle
Out: Rectangle(10,10,25,34)

>>> rectangle.width
Out: 15

>>> rectangle.width,rectangle.height
Out: (15, 24)

>>> rectangle.width=100
>>> rectangle.width,rectangle.height
Out: (100, 24)

>>> rectangle
Out: Rectangle(10,10,110,34)

>>> help(rectangle)
Out: Help on Rectangle in module __main__ object:

class Rectangle(builtins.object)
| Methods defined here:
|
| __init__(self, x1, y1, x2, y2)
| Initialize self. See help(type(self)) for accurate signature.
|
| __repr__(self)
| Return repr(self).
|
| ----------------------------------------------------------------------
| Data descriptors defined here:
|
| __dict__
| dictionary for instance variables (if defined)
|
| __weakref__
| list of weak references to the object (if defined)
|
| height
| calc height from top
|
| width
| calc width from

从测试程序中可以看出,直接使用property()函数也是能正常如期的工作的,但是考虑类的继承的情况:

class MetricRectangle(Rectangle):
def _width_get(self):
return "{} meters".format(self.x2-self.x1)

我们执行一个测试样例:

>>> rectangle=Rectangle(0,0,100,100)
>>> rectangle.width
Out: 100

可以看到虽然子类修改了_width_get()的实现,但是无法覆盖父类中由property()函数修饰的fget方法。

对于这个问题,只需要在子类中重写整个property,

class MetricRectangle(Rectangle):
def _width_get(self):
return "{} meters".format(self.x2-self.x1)
width=property(_width_get,Rectangle.width.fset)

我们再运行测试样例:

>>> MetricRectangle(0,0,100,100).width
Out: '100 meters'

这时候子类就覆盖了父类中的fget方法。

但是这样写的缺点是可维护性不好,如果开发者要修改父类,而忘记修改property调用的话,就会出现问题。

使用property的最佳实践是使用property作为装饰器,这样就会避免可维护性的问题,而且减少类内部方法签名的数量。

最佳实践

class Rectangle:
def __init__(self,x1,y1,x2,y2):
self.x1,self.y1=x1,y1
self.x2,self.y2=x2,y2

@property
def width(self):
"""rectangle width measured from left"""
return self.x2-self.x1

@width.setter
def width(self,value):
self.x2=self.x1+value

@property
def height(self):
"""rectangle height measured from top"""
return self.y2-self.y1

@height.setter
def height(self,value):
self.y2=self.y1+value

def __repr__(self):
return "{}({},{},{},{})".format(
self.__class__.__name__,
self.x1,self.y1,self.x2,self.y2)