self.__dict__.update(**kwargs) 是好的编程风格还是不好的编程风格?

57

在Python中,假设我有一个继承自Shape类的Circle类。Shape类需要x和y坐标,并且Circle类还需要半径。我希望能够通过以下方式初始化Circle类:

c = Circle(x=1., y=5., r=3.)

Circle继承自Shape类,因此我需要使用命名参数来调用__init__方法,因为不同的类需要不同的构造函数。我可以手动设置x、y和r。

class Shape(object):
    def __init__(self, **kwargs):
        self.x = kwargs['x']
        self.y = kwargs['y']

class Circle(Shape):
    def __init__(self, **kwargs):
        super(Circle, self).__init__(**kwargs)
        self.r = kwargs['r']

或者,我可以使用self.__dict__.update(kwargs)自动设置我的Circle的属性。

class Shape(object):
    def __init__(self, **kwargs):
        self.__dict__.update(**kwargs)

class Circle(Shape):
    def __init__(self, **kwargs):
        super(Circle, self).__init__(**kwargs)
这样做的优点是代码量减少了,而且不需要维护类似于self.foo = kwargs ['foo']这样的样板文件。缺点是不明确哪些参数是圆形所需的。这算作弊吗?或者这是一个好的风格(只要向圆形接口进行了良好记录)?
感谢大家的周到回复。对于我在组织我的代码时使用的self.__dict__.update(**kwargs)技巧很有用,但我会确保用显式传递参数和在生产代码中进行明确的错误检查来替换它。

我认为这将允许调用者重载方法......但如果你是唯一使用代码的人,并且不关心恶意行为,那么这可能是可以的。 - Joran Beasley
@JoranBeasley - 是的,但如果你这样做了,实际上就是违反规则,这是你自己的责任。 - Chris Lutz
4个回答

33
class Shape(object):
    def __init__(self, x=None, y=None):
        self.x = x
        self.y = y

class Circle(Shape):
    def __init__(self, r=None, **kwargs):
        super(Circle, self).__init__(**kwargs)
        self.r = r

就这样了。当你不真正需要它们时,请不要使用**kwargs

这算作弊吗?还是说这是好的风格(只要Circle的接口有很好的文档)?

当你在简单易懂的代码和头疼的代码+美好的文档字符串之间做出选择时,实际上你没有任何选择,你只需编写简单自我记录的代码:)


有很多完全合理的情况可以使用 **whatever,特别是当您正在重载其他类时。 - Joran Beasley
@StevenRumbalski 是的,我有一种感觉似乎漏掉了什么 :) - Roman Bodnarchuk
2
对于这个解决方案,请注意,继承链中的所有类都无法访问从**kwargs中剥离的关键字值(在子类中)。例如,Shape不能定义一个r参数并期望获得它,而不修改Circle的__init__方法。 - Casey Kuball
我喜欢这种解决方案,因为它使我不必在每个 __init__() 中明确地提及x,同时确保正确的参数出现在正确的层级。 - Alex Szatmary

19

如果您想要自动分配,我建议采用以下方法:

def __init__(self, **kwargs):
    for key, value in kwargs.iteritems():
        setattr(self, key, value)

从样式上看,它介于明确编写代码和使用 self.__dict__ 进行自行操作之间。


1
我更喜欢看到 self.__dict__.update(kwargs) 而不是这种(可能不太高效且)更长的方式来实现相同的结果。 - Chris Lutz
6
这样做的好处是可以与描述符一起使用。仅仅直接使用 __dict__ 就因此成了一个坏主意。 - Chris Morgan
3
你能帮我理解为什么使用描述符会更好吗? - Eli Rose
3
阅读了 https://docs.python.org/3/howto/descriptor.html 后,我的理解是 Chris Morgan (~2) 推荐 Simeon 的答案,因为它可以更好地适用于基于您的类的类,这些类确实重写了默认的 get/set 方法并成为数据描述符。使用 setattr 与其覆盖兼容,而直接使用 dict 则会绕过任何此类覆盖。 - Michael J. Evans

19

我认为第一种方法绝对是更可取的,因为显式优于隐式

想象一下,如果您在初始化圆形时出现了拼写错误,比如Circle(x=1., y=5., rr=3.)。 您希望立即看到此错误,而这不会发生在__dict__.update(kwargs)中。


4
如果你想要更加明显,你可以让 Circle.__init__ 对参数进行一些检查。假设你会检查所有的参数是否都存在,并可能针对无意义的参数提出错误。
你甚至可以在 Shape 中制作一个装饰器或帮助函数来为您完成此操作。类似于这样的东西:
class Circle(Shape):
    def __init__(self, **kwargs):
        self.check(kwargs, 'x', 'y', 'r')
        super(Circle, self).__init__(**kwargs)

.check将在Shape中实现,其主要验证所有参数是否在kwargs中,并且可能没有额外的参数(对不起,这个没有代码-您可以自己解决)。甚至可以让子类重载它以检查可选参数,您可能希望将它们与其他参数(即为其分配的默认值)处理方式不同(例如,在Shape.__init__中赋予它们默认值)。

否则,如果您记录了接口并且它按照记录的方式工作,那么它总是可以的。除此之外,您所做的任何使其按我们“期望”的方式工作的事情(为错误参数抛出异常)都是额外的奖励。


你怎么知道“.check是在Shape中实现的”?你是OP的老师吗? - Steven Rumbalski
2
如果我是的话,我将成为最年轻的计算机科学老师。我的意思是 OP 可以实现 Shape.check 然后在所有子类中使用它。我会澄清这一点。 - Chris Lutz

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接