属性还是方法?
我正在写一个Python脚本,用来根据两个参数计算一些量,这两个参数分别是椭球的长半径和短半径。我想到可以写一个椭球类来完成这个任务。不过,我对面向对象设计还不太熟悉,想请教一下你们这些有经验的朋友。
这个类的实例化需要两个参数,分别是长半径a和短半径b,所以我设计了这个类,如下所示:
class Spheroid:
def __init__(self,a,b):
self.longax = a
self.shortax = b
我想计算的一个量是体积。椭球的体积公式是4*pi/3 * a * b * b。
我的问题是,我应该在这个类里定义一个方法来计算体积,还是定义一个属性来表示体积呢?
比如,我可以定义一个方法:
def Volume(self):
return 4*pi/3 * self.longax * self.shortax * self.shortax
或者我也可以直接用一个属性:
self.volume = 4*pi/3 * self.longax * self.shortax * self.shortax
我还可以把它放在初始化方法里:
class Spheroid:
def __init__(self,a,b):
self.longax = a
self.shortax = b
self.volume = 4*pi/3 * a * b * b.
哪种方式更好呢?一般来说,什么时候应该用方法,什么时候应该用属性?我通常不太在意这些,但我有很多类似的东西要实现,想对面向对象设计有个了解,以备将来参考。
谢谢!
编辑:
按照Martijn的建议,我实现了属性,结果变成了这样:
class Spheroid(object):
def __init__(self,a,b):
self.shortax = a
self.longax = b
self.alpha=self.longax/self.shortax
@property
def volume(self):
return (4*np.pi/3) * self.shortax * self.shortax * self.longax
@property
def epsilon(self):
return np.sqrt(1-self.alpha**(-2))
@property
def geometricaspect(self):
return 0.5 + np.arcsin(self.epsilon)*0.5*self.alpha/self.epsilon
@property
def surfacearea(self):
return 4*np.pi*self.shortax**2*self.geometricaspect
我实例化了一个对象s = Spheroid(),但是每当我尝试像s.volume或s.epsilon这样的操作时,都会出现一个属性错误:
AttributeError: 'Spheroid' object has no attribute 'volume'
我哪里做错了呢?
另外,在我的init方法里,我用self.alpha = self.longax/self.shortax,而不是用a/b,这样做有什么区别吗?哪种方式更好呢?
3 个回答
我会把体积实现为一个方法,原因如下:
- 体积可以通过其他属性计算出来,这样可以节省存储空间(当然,如果计算过程非常复杂,那就可以考虑把结果缓存起来)。
- 体积并不是对象的自然“特征”,比如对于圆形来说,半径是一个属性,但面积就不是(这并不是一种格式定义)。
- 像体积这样的东西有点像抽象方法,如果你想要一组对象,并以多态的方式计算每个对象的体积,这样做会更合适。
你会一直使用这些数据吗?
如果不会的话,你可以使用一个属性,然后在需要的时候再计算它...
class Spheroid(object):
def __init__(self,a,b):
self.longax = a
self.shortax = b
self._volume = None
@property
def volume(self):
if self._volume is None :
self._volume = 4*pi/3 * self.longax * self.shortax * self.shortax
return self._volume
你还有第三种选择:把它既当作属性又当作方法,使用一个叫做 property 的东西。
class Spheroid(object):
def __init__(self, a, b):
self.long = a
self.short = b
@property
def volume(self):
return 4 * pi / 3 * self.long * self.short * self.short
你可以像访问属性一样访问 .volume
:
>>> s = Spheroid(2, 3)
>>> s.volume
75.39822368615503
为了让 property
正常工作,在 Python 2 中,你需要确保你的类是从 object
继承的;在 Python 3 中,可以安全地省略这个基类。
在这个例子中,计算体积的过程很简单,但使用属性可以让你在真正需要体积的时候再去计算。
上面的例子创建了一个只读属性;只定义了一个获取器(getter):
>>> s.volume = None
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
AttributeError: can't set attribute
你可以很容易地缓存属性计算的结果:
class Spheroid(object):
_volume = None
def __init__(self, a, b):
self.long = a
self.short = b
@property
def volume(self):
if self._volume is None:
self._volume = 4 * pi / 3 * self.long * self.short * self.short
return self._volume
这样你每个 Spheroid
实例只需要计算一次。
你选择使用哪种方式取决于很多因素,比如你的API需要多容易使用,体积会被计算多少次,会创建多少个 Spheroid
实例等等。如果你在一个循环中创建了百万个这些实例,但只需要其中几个的体积,那么使用属性就比在 __init__
中设置体积更合理。
但是,如果你的类可以根据体积自动调整自己,比如自动调整某个半径,那么使用 @property
就更有意义了:
class Spheroid(object):
def __init__(self, a, b):
self.long = a
self.short = b
@property
def volume(self):
return 4 * pi / 3 * self.long * self.short * self.short
@volume.setter
def volume(self, newvolume):
# adjust the short radius
self.short = sqrt(newvolume / (4 * pi / 3 * self.long))
现在你有一个椭球体,它会随着你调整体积而自然调整它的短属性:
>>> s = Spheroid(2, 1)
>>> s.volume
8.377580409572781
>>> s.volume = 75.39822368615503
>>> s.long, s.short
(2, 3.0)
注意:从技术上讲,使用 .name
这种方式访问对象上的 任何东西 都是属性,包括方法。为了这个回答的目的,我把你的 attribute
理解为任何没有被调用(名字后面没有 ()
)的值。