【问题标题】:Python class design: explicit keyword arguments vs. **kwargs vs. @propertyPython 类设计:显式关键字参数 vs. **kwargs vs. @property
【发布时间】:2015-09-02 03:23:45
【问题描述】:

对于创建实例将具有许多(不可默认)变量的类,是否存在普遍接受的最佳实践?

例如,通过显式参数:

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

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

【问题讨论】:

  • 您似乎混合了两件事:传递给类的参数和存储在实例上的属性。您的第三个示例与其他示例完全不同,因为在创建实例时没有传递任何参数。您是否关心参数或属性,或者特别是指定属性值的参数?
  • 在这种情况下,我关心实例的属性。我意识到第三个示例的不同之处在于创建实例时没有传入任何参数,但是我可以使用我提供的三个类中的任何一个来有效地创建同一个实例,不是吗?

标签: python class oop


【解决方案1】:

将参数传递给__init__ 通常是最佳实践,就像在任何面向对象编程语言中一样。在您的示例中,setter/getter 将允许对象处于这种还没有任何属性的奇怪状态。

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

  1. 如果您有很多参数,**kwargs 是一个很好的解决方案,因为它避免了这样的代码:
def __init__(first, second, third, fourth, fifth, sixth, seventh,
             ninth, tenth, eleventh, twelfth, thirteenth, fourteenth,
             ...
             )
  1. 如果您大量使用继承。 **kwargs 是最好的解决方案:
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 并使用它。

【讨论】:

    【解决方案2】:

    在不了解具体情况的情况下,经典答案是:如果您的类初始化程序需要一大堆参数,那么 可能 做得太多,应该将其考虑在内几个类。

    采用这样定义的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 类(或namedtuples)并将这些的实例传递给Car()

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

    如果需要创建一个类的实例,例如,radius 在您的Circle 类中,它应该是__init__ 的必需参数(或分解为较小的类,它被传递到__init__,或由替代构造函数设置)。原因是这样的:IDE、自动文档生成器、代码自动完成器、linter 等可以读取方法的参数列表。如果只是**kwargs,那里没有信息。但是,如果它具有您期望的参数名称,那么这些工具就可以发挥作用。

    现在,属性非常酷,但我会犹豫在必要时使用它们(你会知道什么时候需要它们)。保持您的属性不变,并允许人们直接访问它们。如果不应设置或更改它们,请记录下来。

    最后,如果你真的必须有一大堆参数,但又不想在你的 __init__ 中写一堆作业,你可能会对 Alex Martelli 的 answer 感兴趣到一个相关的问题。

    【讨论】:

    • 我不太同意这个答案的第一部分。根据具体情况,有一个有很多论点的类是有意义的。但是,我绝对同意第二部分关于使用命名 args 而不是 kwargs,因为它们是自我记录的。
    • @BrenBarn 我也不一定总是同意它。但根据我的经验,每当我开始为类的初始化器添加大量参数时,10 次中有 9 次后退并重构类都会带来好处。我只是建议有很多参数可能是一种代码味道——尽管我同意有时它是有意义的。
    【解决方案3】:

    我相信对此有很多不同的思想流派,以下是我通常的想法:

    显式关键字参数

    优点

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

    缺点

    • 当你有很多东西要传递时,可能会变得非常笨拙

    预后

    这通常应该是您的第一次攻击方法。但是,如果您发现您传入的内容列表太长,则可能更多地指向代码的结构性问题。你传递的这些东西有什么共同点吗?你能把它封装在一个单独的对象中吗?有时我为此使用了配置对象,然后你从传递大量 args 到传递 1 或 2

    使用 **kwargs

    优点

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

    例如

    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__'s
    • 当您想要呈现一个简单的属性界面时非常有用,例如对于一个 api,但在后台正在做更复杂的更酷的事情,比如维护缓存或其他有趣的事情

    缺点

    • 更冗长,需要维护的代码更多
    • 与上述相反,当某些属性尚未完全初始化的无效对象不应被允许存在时,可能会产生危险

    预后

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

    TL;DR

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

    【讨论】:

      【解决方案4】:

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

      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)
      

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

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2022-12-01
        • 2023-04-06
        • 1970-01-01
        • 2016-11-12
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多