提供类构造函数默认值的最Pythonic方式

6
我试图遵循Google的样式指南,从一开始就追求一致性。
我正在创建一个模块,在这个模块中我有一个类。我想为不同的标准用例提供一些合理的默认值。但是,我希望用户能够灵活地覆盖任何默认值。我目前所做的是提供一个模块范围的“常量”字典,其中包含默认值(用于不同的用例),并在我的类中,将构造函数中的参数优先于默认值。
最后,我想确保我们以有效的参数值结束。
这就是我所做的:
MY_DEFAULTS = {"use_case_1": {"x": 1, "y": 2},
               "use_case_2": {"x": 4, "y": 3}}

class MyClass:
   def __init__(self, use_case = None, x = None, y = None):
      self.x = x
      self.y = y
      if use_case:
         if not self.x:
            self.x = MY_DEFAULTS[use_case]["x"]
         if not self.y:
            self.y = MY_DEFAULTS[use_case]["y"]
      assert self.x, "no valid values for 'x' provided"
      assert self.y, "no valid values for 'y' provided"
   def __str__(self):
      return "(%s, %s)" % (self.x, self.y)  

print(MyClass()) # AssertionError: no valid values for 'x' provided
print(MyClass("use_case_1")) # (1, 2)
print(MyClass("use_case_2", y = 10) # (4, 10)

问题

  • 虽然从技术上讲它可以工作,但我想知道这是否是最符合Python风格的方式?
  • 随着我的类有越来越多的默认值,代码变得非常重复,我该怎么简化它?
  • assert 对我来说似乎也不是最好的选择,因为它更像是一个调试语句而不是验证检查。我尝试使用 @property 装饰器,如果存在无效参数,则会引发异常,但在当前模式下,我希望允许 xy 在构造函数的短暂时刻内不是 truthy,以正确实现优先级(也就是说,我只想在构造函数的最后检查truthiness)。有什么提示吗?

3
我会将"if not self.x"替换为"if self.x is not None",因为如果x == 0,则会出问题。这样做不会改变原意,同时让语言更加通俗易懂。 - Jean-François Fabre
您也希望将您的问题简化为一个问题。多个问题通常会受到反对并被关闭。 - Jean-François Fabre
也许 Code Review 最适合这种类型的问题。 - MrBean Bremen
公正的观点:然而,在我的实际例子中,我确实认为所有“falsy”值都是不好的。我还会将我的问题重新表述为一个问题。 - thothal
@MrBeanBremen 这样的问题在CR上不会是主题,因为代码过于假设性 - Sᴀᴍ Onᴇᴌᴀ
显示剩余2条评论
3个回答

8
通常情况下,如果有多种合理的构造对象类型的方法,您可以提供classmethod以进行替代构造(dict.fromkeys是一个很好的例子)。请注意,如果您的用例是有限且静态定义的,则此方法更适用。
class MyClass:
   def __init__(self, x, y):
      self.x = x
      self.y = y
   @classmethod
   def make_use_case1(cls, x=1, y=2):
       return cls(x,y)
   @classmethod
   def make_use_case2(cls, x=4, y=3):
       return cls(x,y)

   def __str__(self):
      return "(%s, %s)" % (self.x, self.y)  

如果使用情况的唯一变化是默认参数,那么每次重写位置参数列表都会增加很多开销。相反,我们可以编写一个 classmethod 来接受使用情况以及可选的关键字覆盖。
class MyClass:
    DEFAULTS_PER_USE_CASE = {
        "use_case_1": {"x": 1, "y": 2},
        "use_case_2": {"x": 4, "y": 3}
    }
    @classmethod
    def make_from_use_case(cls, usecase, **overrides):
        args = {**cls.DEFAULTS_PER_USE_CASE[usecase], **overrides}
        return cls(**args)

    def __init__(self, x,y):
        self.x = x
        self.y = y

    def __str__(self):
        return "(%s, %s)" % (self.x, self.y)

x = MyClass.make_from_use_case("use_case_1", x=5)
print(x)

如果您想按位置传递参数,这将更加困难,但我想这会满足您的需求。

有趣的方法。然而,这需要将不同的用例附加到类定义上。如果这些用例与函数的抽象定义无关,则可能并不总是最佳实践。 - user4396006
谢谢,我需要查找classmethod装饰器(所以我学到了东西)+1。现在,这对我来说似乎是很多重复的工作。每当我需要添加一个新的用例时,我都需要添加一个新的classmethod,这似乎是一种过度设计。对字典添加新的用例似乎更加轻量级。 - thothal
1
@thothal 对于静态使用情况,我的原始答案通常是最好的,我添加了另一种情况,适用于更动态的情况,可能更适合您。 - Tadhg McDonald-Jensen
听起来非常有趣,但是 make_from_use_case(cls, usecase, **overrides) 这种方式不是更好吗?因为我不需要将 overrides 指定为 dict,而是可以作为普通参数传递。 - thothal
1
任何一种都可以,@thothal。这是一个风格选择。取决于覆盖数量和您想要代码的外观。 - user4396006
1
只要覆盖不使用与位置参数(clsusecase)相同的名称,并且在覆盖后从未传递第三个参数是,**overrides 就可以正常工作,而且可能更好看。虽然如果以后需要,您可以为备用调用签名添加另一个 classmethod。 ;) - Tadhg McDonald-Jensen

3

Python是一种非常灵活的语言。如果你的代码能运行,那么没有技术上的错误方法。然而,如果你想要更符合“Pythonic”的风格,这里有一些建议。首先,你不应该使用AssertionError来验证参数的存在或值。如果一个参数没有被传递,但是它应该在那里,你应该引发一个TypeError。如果传递的值不可接受,你应该引发一个ValueError。断言主要用于测试。

当你想要验证参数a中是否存在一个值时,最好使用a is not None,而不是not a。当None0或其他Falsy值对你来说同样无效时,你可以使用not a。然而,当目的是检查值的存在时,0None并不相同。

关于你的类,我认为更好的方式是在类初始化时解包字典的值。如果你从函数签名中删除use_case,并像这样调用你的类:

MyClass(**MY_DEFAULTS["use_case_1"])

Python会解包嵌套字典的值,并将它们作为关键字参数传递给您的__init__方法。如果您不希望这些值是可选的,请删除默认值,如果提供的参数与函数签名不匹配,Python会为您引发TypeError异常。
如果您仍然不希望参数为空,则可能需要为参数的可能值提供更具体的范围。如果x的类型是整数,并且您不想要0值,则应将x与0进行比较。
def __init__(x, y):
    if x == 0 or y == 0:
        raise ValueError("x or y cannot be 0")

如何让用户在您的展开示例中覆盖一些默认设置? - thothal
1
只需像这样包装默认值 {**MY_DEFAULTS[usecase], **overrides},然后在函数调用中取消包装该字典。这将替换值。 - user4396006

2
保持原始接口,您可以使用kwargs来读取参数。如果有些参数缺失,则只有在使用情况匹配时才设置默认值。
MY_DEFAULTS = {"use_case_1": {"x": 1, "y": 2},
               "use_case_2": {"x": 4, "y": 3}}

class MyClass:
   def __init__(self, use_case = None, **kwargs):
      for k,v in kwargs.items():
        setattr(self,k,v)
      if use_case:
        for k,v in MY_DEFAULTS[use_case].items():
            if k not in kwargs:
                setattr(self,k,v)
      unassigned = {'x','y'}
      unassigned.difference_update(self.__dict__)
      if unassigned:
        raise TypeError("missing params: {}".format(unassigned))

   def __str__(self):
      return "(%s, %s)" % (self.x, self.y)

print(MyClass("use_case_1")) # (1, 2)
print(MyClass("use_case_2", y = 10)) # (4, 10)
print(MyClass())

执行这个命令:
(1, 2)
(4, 10)
Traceback (most recent call last):
  File "<string>", line 566, in run_nodebug
  File "C:\Users\T0024260\Documents\module1.py", line 22, in <module>
    print(MyClass())
  File "C:\Users\T0024260\Documents\module1.py", line 15, in __init__
    raise TypeError("missing params: {}".format(unassigned))
TypeError: missing params: {'y', 'x'}

随着我的类越来越多的默认值,代码变得非常重复,我该怎么做才能简化它?
这个解决方案允许有很多参数。

1
我认为对于如此简单的任务,您使用 setattr(self, k, v) 的方式是过度设计。字典解包或默认类方法是更简单的方法。此外,引发 Exception 是被强烈反对的。 - user4396006
你建议使用什么?ValueError? - Jean-François Fabre
OP要求避免过多的复制粘贴。我同意这对于只有2个参数来说有些过度。 - Jean-François Fabre
2
@J.C.Rocamonde 如果只有两个参数,那么是有点过度设计了,但显然 OP 有更多的参数需要处理,参见“随着我的类有越来越多的默认值,代码变得非常重复”。 - bruno desthuilliers
好的。如果有很多参数,那么你所做的事情就有很多意义了。然而,对于大多数类来说,以编程方式设置许多属性通常不是一个好的实践方法(除非你是一位经验丰富的程序员正在构建某个库),因为它会显著地混淆代码。我认为使用defaultdict可能是更好的解决方案。 - user4396006
显示剩余3条评论

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