Python类设计:显式关键字参数与** kwargs vs. @property

时间:2015-06-17 04:24:32

标签: python class oop

是否有一种普遍接受的最佳实践来创建一个实例将具有许多(不可违约)变量的类?

例如,通过显式参数:

class Circle(object):
    def __init__(self,x,y,radius):
        self.x = x
        self.y = y
        self.radius = radius

使用** kwargs:

class Circle(object):
    def __init__(self, **kwargs):
        if 'x' in kwargs:
            self.x = kwargs['x']
        if 'y' in kwargs:
            self.y = kwargs['y']
        if 'radius' in kwargs:
            self.radius = kwargs['radius']

或使用属性:

class Circle(object):
    def __init__(self):
        pass

    @property
    def x(self):
        return self._x

    @x.setter
    def x(self, value):
        self._x = value

    @property
    def y(self):
        return self._y

    @y.setter
    def y(self, value):
        self._y = value

    @property
    def radius(self):
        return self._radius

    @radius.setter
    def radius(self, value):
        self._radius = value

对于实现少量实例变量的类(如上例所示),似乎自然的解决方案是使用显式参数,但随着变量数量的增加,这种方法很快变得不可靠。当实例变量的数量增长很长时,是否有一种首选方法?

4 个答案:

答案 0 :(得分:9)

我确信在这方面有许多不同的思想流派,在这里我通常会想到它:

显式关键字参数

赞成

  • 简单,代码少
  • 非常清楚,明确您可以传递给班级的属性

缺点

  • 当你提到很多东西要提及
  • 时,会变得非常笨拙

预后

这应该是你的第一次攻击方法。但是,如果您发现您传递的内容列表过长,则可能表明代码存在更多结构性问题。你传递的这些东西中的一些是否存在共同点?你能把它封装在一个单独的对象中吗?有时候我已经使用了配置对象,然后你会从一个非常多的args传递到传递1或2

使用** kwargs

赞成

  • 在将参数传递给包装系统之前无缝修改或转换参数
  • 很好,当你想让可变数量的参数看起来像是api的一部分时,例如如果你有一个列表或字典
  • 避免长时间且难以维持直通定义到较低级别的系统,

e.g。

def do_it(a, b, thing=None, zip=2, zap=100, zimmer='okay', zammer=True):
    # do some stuff with a and b
    # ...
    get_er_done(abcombo, thing=thing, zip=zip, zap=zap, zimmer=zimmer, zammer=zammer)

取而代之的是:

def do_it(a, b, **kwargs):
    # do some stuff with a and b
    # ...
    get_er_done(abcombo, **kwargs)

在这种情况下更加清晰,并且可以看到get_er_done的完整签名,虽然好的文档字符串也可以列出所有参数,好像它们是do_it接受的真实参数

缺点

  • 使其不太可读和明确,如果它不是一个或多或少简单的直通,那么参数是什么
  • 如果你不小心,可以很容易地隐藏错误并为维护者添加混淆

预后

* args和** kwargs语法非常有用,但也可能超级危险且难以维护,因为你失去了可传入的参数的显式特性。我通常喜欢在我拥有的情况下使用它们一个基本上只是另一个方法或系统的包装器的方法,你想要在不重新定义所有内容的情况下传递信息,或者在需要预先过滤或更加动态化的参数的有趣情况下等等。如果你只是使用它来隐藏你有大量参数和关键字参数这一事实,** kwargs可能会让你的代码变得更加笨拙和神秘,从而加剧了这个问题。

使用属性

赞成

  • 非常明确
  • 提供一种很好的方式来创建对象,当它们仍然是有效的"当你不知道所有参数并通过管道传递半成形物体以慢慢填充args时。此外,对于不需要设置的属性,但有时也可以提供一种简洁的方式来配对__init__
  • 当您想要提供简单的属性界面时很好,例如对于一个api,但在引擎盖下正在做更复杂的更酷的事情,比如维护缓存或其他有趣的东西

缺点

  • 更详细,更多维护代码
  • 与上述相反,可能会引入危险,允许在永远不允许存在的情况下生成尚未完全初始化的某些属性的无效对象

预后

我实际上非常喜欢利用getter和setter属性,特别是当我使用那些我不想公开的属性的私有版本做一些棘手的事情时。它也可以用于配置对象和其他东西,并且很好,很明确,我喜欢。但是,如果我正在初始化一个对象,我不想让半成形的对象四处走动并且它们没有任何用处,那么最好只使用显式的参数和关键字参数。

TL; DR

** kwargs和属性具有很好的特定用例,但只要在实际/可能的情况下坚持使用显式关键字参数。如果实例变量太多,请考虑将您的类拆分为分层容器对象。

答案 1 :(得分:5)

如果没有真正了解你的具体情况,那么经典的答案是这样的:如果你的类初始化程序需要一大堆参数,那么可能做得太多了,它应该被考虑在内几节课。

定义一个Car类:

class Car:
    def __init__(self, tire_size, tire_tread, tire_age, paint_color, 
                 paint_condition, engine_size, engine_horsepower):
        self.tire_size = tire_size
        self.tire_tread = tire_tread
        # ...
        self.engine_horsepower = engine_horsepower

显然,更好的方法是定义EngineTirePaint 类(或namedtuple s)并将这些的实例传递给Car()

class Car:
    def __init__(self, tire, paint, engine):
        self.tire = tire
        self.paint = paint
        self.engine = engine

如果需要来创建类的实例,例如,radius类中的Circle,它应该是{{1}的必需参数(或者被分解为一个较小的类,它传递给__init__,或者由另一个构造函数设置)。原因是:IDE,自动文档生成器,代码自动完成器,短接器等可以读取方法的参数列表。如果它只是__init__,则那里没有任何信息。但如果它具有您期望的参数的名称,那么这些工具可以完成它们的工作。

现在,属性非常酷,但在必要时我会毫不犹豫地使用它们(并且您知道何时需要它们)。保留您的属性,让人们直接访问它们。如果不应该设置或更改它们,请记录下来。

最后,如果确实必须有一大堆参数,但又不想在**kwargs中写一堆作业,那么您可能会对Alex感兴趣马尔泰利answer提到相关问题。

答案 2 :(得分:2)

将参数传递给__init__通常是最好的做法,就像任何面向对象的编程语言一样。在你的例子中,setter / getters允许对象处于这种奇怪的状态,它还没有任何属性。

指定参数或使用**kwargs取决于具体情况。这是一个很好的经验法则:

  1. 如果您有很多参数,**kwargs是一个很好的解决方案,因为它避免了这样的代码:
  2. def __init__(first, second, third, fourth, fifth, sixth, seventh,
                 ninth, tenth, eleventh, twelfth, thirteenth, fourteenth,
                 ...
                 )
    
    1. 如果你大量使用继承。 **kwargs是最佳解决方案:
    2. class Parent:
          def __init__(self, many, arguments, here):
              self.many = many
              self.arguments = arguments
              self.here = here
      
      class Child(Parent):
          def __init__(self, **kwargs):
              self.extra = kwargs.pop('extra')
              super().__init__(**kwargs)
      

      避免写作:

      class Child:
          def __init__(self, many, arguments, here, extra):
              self.extra = extra
              super().__init__(many, arguments, here)
      

      对于所有其他情况,指定参数更好,因为它允许开发人员使用位置参数和命名参数,如下所示:

      class Point:
          def __init__(self, x, y):
             self.x = x
             self.y = y
      

      可以Point(1, 2)Point(x=1, y=2)实例化。

      一般知识,you can see how namedtuple does it并使用它。

答案 3 :(得分:0)

您的第二种方法可以用更优雅的方式编写:

class A:
    def __init__(self, **kwargs):
        self.__dict__ = {**self.__dict__, **kwargs}

a = A(x=1, y=2, verbose=False)
b = A(x=5, y=6, z=7, comment='bar')

print(a.x + b.x)

但是所有已经提到的缺点仍然存在...