普遍接受的最佳实践来创建其实例将具有许多(非默认)变量的类?
例如,通过显式参数:
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
显然,更好的方法是定义Engine
、Tire
和Paint
类(或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
取决于情况。这里有一个很好的经验法则:
- 如果你有很多参数,
**kwargs
是一个很好的解决方案,因为它避免了这样的代码:
def __init__(first, second, third, fourth, fifth, sixth, seventh,
ninth, tenth, eleventh, twelfth, thirteenth, fourteenth,
...
)
- 如果您大量使用继承。
**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)
但所有已经提到的缺点仍然存在......