Most pythonic way to provide defaults for class constructor

Viewed 549

I am trying to stick to Google's styleguide to strive for consistency from the beginning.

I am currently creating a module and within this module I have a class. I want to provide some sensible default values for different standard use cases. However, I want to give the user the flexibility to override any of the defaults. What I am currently doing is I provide a module scoped "constant" dictionary with the default values (for the different use cases) and in my class I give the parameters in the constructor precedence over the defaults.

Finally, I want to make sure that we end with valid values for the parameters.

That's what I have done:

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)

Questions

  • While technically working, I was wondering whether this is the most pythonic way of doing it?
  • With more and more default values for my class the code becomes very repetitive, what could I do to simplify that?
  • assert seems also for me not the best option at it is rather a debugging statement than a validation check. I was toying with the @property decorator, where I would raise an Exception in case there are invalid parameters, but with the current pattern I want to allow x and y for a short moment to be not truthy to implement the precedence properly (that is I only want to check the truthiness at the end of the constructor. Any hints on that?
3 Answers

In general if there is more than one way to reasonably construct your object type, you can provide classmethods for alternate construction (dict.fromkeys is an excellent example of this). Note that this approach is more applicable if your use cases are finite and well defined statically.

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)  

If the only variation in the use cases is default arguments then re-writing the list of positional arguments each time is a lot of overhead. Instead we can write one classmethod to take the use case and the optional overrides as keyword only.

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)

If you wanted the arguments to be passed positionally that would be more difficult but I imagine this would suit your needs.

Python is a very flexible language. If your code runs, there is no technically wrong way of doing things. However, if you want to be "Pythonic", here are a few tips for you. First of all, you should never use AssertionErrors for verifying the presence or value of a parameter. If a parameter is not passed and it should be there, you should raise a TypeError. If the value passed is not acceptable, you should raise a ValueError. Assertions are mainly used for testing.

When you want to verify the presence of a value in the parameter a, it is best to do a is not None, rather than not a. You can do not a when None and 0 or other Falsy values are equally invalid for you. However, when the purpose is to check the presence of a value, 0 and None are not the same.

Regarding your class, I believe that a nicer way of doing this is unwrapping the values of the dictionary upon the class initalization. If you remove use_case from the function signature, and call your class like this:

MyClass(**MY_DEFAULTS["use_case_1"])

Python will unwrap the values of the nested dictionary and pass them as keyword arguments to your __init__ method. If you do not want the values to be optional, remove the default value and Python will raise a TypeError for you if the parameters provided do not match the function signature.

If you still want your parameters to not be Falsy, perhaps you should want to provide a more concrete scope for the possible values of the parameters. If the type of x is int, and you don't want 0 values, then you should compare x with 0:

def __init__(x, y):
    if x == 0 or y == 0:
        raise ValueError("x or y cannot be 0")

keeping your original interface, you could use kwargs to read parameters. If some are missing, set the defaults, only if the use case matches.

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())

executing this:

(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'}

With more and more default values for my class the code becomes very repetitive, what could I do to simplify that?

This solution allows to have many parameters.

Related