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

and*_*kee 7 python oop class

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

例如,通过显式参数:

class Circle(object):
    def __init__(self,x,y,radius):
        self.x = x
        self.y = y
        self.radius = radius
Run Code Online (Sandbox Code Playgroud)

使用**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']
Run Code Online (Sandbox Code Playgroud)

或使用属性:

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
Run Code Online (Sandbox Code Playgroud)

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

lem*_*ead 10

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

显式关键字参数

优点

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

缺点

  • 当你提到很多事情时,你可能会变得非常笨拙

预测

这应该是你的第一次攻击方法.但是,如果您发现您传递的内容列表过长,则可能表明代码存在更多结构性问题.你传递的这些东西中的一些是否存在共同点?你能把它封装在一个单独的对象中吗?有时我会使用配置对象,然后你会从一个巨大的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)
Run Code Online (Sandbox Code Playgroud)

取而代之的是

def do_it(a, b, **kwargs):
    # do some stuff with a and b
    # ...
    get_er_done(abcombo, **kwargs)
Run Code Online (Sandbox Code Playgroud)

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

缺点

  • 如果它不是一个或多或少简单的直通,那么它的可读性和显式性就会降低
  • 如果你不小心的话,可以很容易地为维护者隐藏bug和混淆的东西

预测

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

使用属性

优点

  • 很明确
  • 当你不知道所有参数并通过管道传递半成形对象来缓慢填充args时,提供一种创建对象的好方法,当它们以某种方式仍然"有效"时.同时对于不需要进行设置,但可能是属性,它有时会提供配对了你的清洁方式__init__
  • 当你想要提供一个简单的属性接口时很好,例如对于api,但是在引擎盖下做更复杂的更酷的事情,比如维护缓存或其他有趣的东西

缺点

  • 更详细,更多代码需要维护
  • 与上述相反,如果永远不允许存在允许具有尚未完全初始化的某些属性的无效对象,则会引入危险

预测

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

TL; DR

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


Ant*_*ton 5

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

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

  1. 如果您有很多参数,**kwargs这是一个很好的解决方案,因为它避免了如下代码:
def __init__(first, second, third, fourth, fifth, sixth, seventh,
             ninth, tenth, eleventh, twelfth, thirteenth, fourteenth,
             ...
             )
Run Code Online (Sandbox Code Playgroud)
  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)
Run Code Online (Sandbox Code Playgroud)

避免写:

class Child:
    def __init__(self, many, arguments, here, extra):
        self.extra = extra
        super().__init__(many, arguments, here)
Run Code Online (Sandbox Code Playgroud)

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

class Point:
    def __init__(self, x, y):
       self.x = x
       self.y = y
Run Code Online (Sandbox Code Playgroud)

可以通过Point(1, 2)或实例化Point(x=1, y=2)

对于一般知识,您可以查看namedtuple它是如何使用的。


jme*_*jme 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
Run Code Online (Sandbox Code Playgroud)

显然,更好的方法是定义Engine,TirePaint 类(或namedtuples)并将这些实例传递到Car():

class Car:
    def __init__(self, tire, paint, engine):
        self.tire = tire
        self.paint = paint
        self.engine = engine
Run Code Online (Sandbox Code Playgroud)

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

现在,属性很酷,但我会毫不犹豫地使用它们直到必要时(你会知道它们何时是必要的).保留您的属性,让人们直接访问它们.如果不应该设置或更改它们,请记录它.

最后,如果你真的必须拥有一大堆论点,但又不想写一堆作业__init__,你可能会对Alex Martelli 对相关问题的回答感兴趣.