3

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?

mypetlion
  • 2,415
  • 5
  • 18
  • 22
Thomas M
  • 131
  • 1
  • 1
  • 7
  • 4
    Why is this one class? If one type can only do foo things and the other bar things (foo can't calculate foo), then 2 classes may be a better solution. If they share some common functionality, then perhaps a shared base class for both? – tdelaney Mar 12 '20 at 17:16
  • If there's common functionality, you should just use parent and child classes though. So if both foo and bar can do baz things then create class `Baz` to do baz and then classes `Foo(Baz)` and `Bar(Baz)`. – Preston Hager Mar 12 '20 at 17:24

2 Answers2

4

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
jignatius
  • 6,304
  • 2
  • 15
  • 30
  • As suggested in your comment, there will be some common functionality here. I tried to make this as minimal as example as possible just to show off the problem. In reality my class takes a number of other arguments in its constructor and they are used (alongside some additional class member functions) in the respective calculations of `foo` and `bar`. My hope was that there was some straightforward way to have these both calculated using just the single class, but I think having a shared (abstract) base class may indeed be the way forward here. Thanks! – Thomas M Mar 12 '20 at 21:08
2

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)
chepner
  • 497,756
  • 71
  • 530
  • 681