What is the best way to allow only one of a pair of arguments in a Python class constructor?

Viewed 647

I have a class which essentially functions like the example below. The constructor takes two arguments, which are both defaulted to None if no value is given. The goal of the class is that, given a value for foo, the user can call the calculateBar() function to calculate the value of bar. Conversely, the user should be able to provide a value for bar and then call the calculateFoo() function to calculate a value for foo. Notably, these functions are essentially the inverse of one another.

class ExampleClass:
    def __init__(self, foo=None, bar=None):
        self.foo = foo
        self.bar = bar

        # Essentially an XNOR. Raises an exception if:
        # 1. Both foo and bar are 'None' (no value given for either)
        # 2. Neither foo, nor bar are 'None' (value given for both)
        if bool(foo == None) == bool(bar == None):
            raise ValueError("Please give a value for Foo or Bar (but not both)")

    def calculateFoo(self):
        return self.bar / 2.0

    def calculateBar(self):
        return self.foo * 2.0

The issue I have is that I want to restrict the class to only allow the user to give a value for foo OR a value for bar, but not both, i.e. exactly one of them should hold a value, the other should be None. My current method is to simply raise an exception in the case where either foo=None and bar=None, or foo!=None and bar!=None. This, however, seems like an inelegant solution. Is there a better way to restrict the user to entering just one of the two arguments, but still allow the correct use of the calculateFoo() and calculateBar() functions? Is there a better way to organise the class to allow what I need?

2 Answers

As suggested in the comments, you could split your class ExampleClass into two separate classes. At the moment your class is trying to do foo things and bar things but if one is not defined you leave the potential for an exception being raised because one member is not defined when calling calculateFoo() or calculateBar().

What you could do is have an abstract base class with a function calculate() and derive two separate classes Foo and Bar from this. Your derived classes will be forced to override the function. The abstract base class is purely a template - you will not be able to create an instance of it but it acts as a template for derived classes.

Example:

from abc import ABC, abstractmethod

class BaseClass(ABC):
    @abstractmethod
    def calculate(self):
        pass

class Foo(BaseClass):
    def __init__(self, foo):
        self.foo = foo

    def calculate(self):
        return self.foo / 2.0

class Bar(BaseClass):
    def __init__(self, bar):
        self.bar = bar

    def calculate(self):
        return self.bar * 2.0


foo = Foo(10)
print(foo.calculate())

bar = Bar(10)
print(bar.calculate())

Output:

5.0
20.0

I think your __init__ is mostly fine, but make the check for both arguments first.

Once you've determined only one value was passed, set the other based on it.

class ExampleClass:
    def __init__(self, foo=None, bar=None):
        if foo is not None and bar is not None:
            raise ValueError("Please give a value for Foo or Bar (but not both)")

        if foo is None:
            self.bar = bar
            self.foo = bar / 2.0
        else:
            self.foo = foo
            self.bar = foo * 2.0

An improvement would be to provide specific class methods for creating an instance for a single value, and discourage use of ExampleClass.__new__ itself.

class ExampleClass:

    def __init__(self, foo, bar):
        self.foo = foo
        self.bar = bar

    @classmethod
    def from_foo(cls, value):
        return cls(foo=value, bar=value/2)

    @classmethod
    def from_bar(cls, value):
        return cls(bar=value, foo=value*2)
Related