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


是否有

普遍接受的最佳实践来创建其实例将具有许多(非默认)变量的类?

例如,通过显式参数:

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

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

我相信对此

有许多不同的思想流派,以下是我通常的想法:

显式关键字参数

优点

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

缺点

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

预后

这通常应该是您的第一次攻击方法。 但是,如果您发现要传递的内容列表太长,则可能指向代码的更多结构问题。 你通过的这些事情中是否有任何共同点? 你能把它封装在一个单独的对象中吗?有时我为此使用了配置对象,然后您从传入大量参数到传入 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 可能会让你的代码更加笨拙和晦涩难懂,从而加剧问题。

使用属性

优点

  • 非常明确
  • 当并非所有参数都已知时,当对象以某种方式仍然
  • "有效"时,提供了一种创建对象的好方法,并通过管道传递半成型的对象以缓慢填充参数。同样对于不需要设置但可以设置的属性,它有时提供了一种干净的方式来配对您的__init__
  • 当您想要呈现一个简单的属性界面时,例如对于 api,但在引擎盖下正在做更复杂的更酷的事情,例如维护缓存或其他有趣的东西时,这很棒

缺点

  • 更冗长,需要维护的代码更多
  • 与上述相反,可能会引入危险,允许生成具有某些尚未完全初始化的属性的无效对象,而这些对象永远不应该被允许存在

预后

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

博士

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

在不真正了解您的情况细节的情况下,经典的答案是:如果您的类初始值设定项需要一大堆参数,那么它可能做得太多了,应该将其分解为几个类。

取一个定义如下的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)并将这些实例传递到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对相关问题的回答感兴趣。

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

指定参数或使用**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)实例化。

对于一般知识,您可以看到namedtuple如何使用它并使用它。

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

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)

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

最新更新