属性还是方法?

6 投票
3 回答
708 浏览
提问于 2025-04-17 19:47

我正在写一个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 个回答

1

我会把体积实现为一个方法,原因如下:

  1. 体积可以通过其他属性计算出来,这样可以节省存储空间(当然,如果计算过程非常复杂,那就可以考虑把结果缓存起来)。
  2. 体积并不是对象的自然“特征”,比如对于圆形来说,半径是一个属性,但面积就不是(这并不是一种格式定义)。
  3. 像体积这样的东西有点像抽象方法,如果你想要一组对象,并以多态的方式计算每个对象的体积,这样做会更合适。
2

你会一直使用这些数据吗?

如果不会的话,你可以使用一个属性,然后在需要的时候再计算它...

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
9

你还有第三种选择:把它既当作属性又当作方法,使用一个叫做 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 理解为任何没有被调用(名字后面没有 ())的值。

撰写回答