Derek
Derek

Reputation: 12378

Force one of two python class attributes to be always be assigned a value?

Is there a way in python such that given a python class:

class Foo(object):
    apple = None
    orange = None
    def __init__(self, apple=None, orange=None)
        super(Foo, self).__init__()
        self.apple = apple
        self.orange = orange

after a Foo object is init, one of the two attributes (apple, orange) must always be assigned to a value other than None, and that at no time should both attributes be assigned to something other than None.

In other words:

                      orange is None    |    orange is not None
                                        |
apple is None              NO           |         YES                                         
________________________________________|___________________________
                                        |
apple is not None          YES          |         NO

How would one do this in python?

Upvotes: 1

Views: 769

Answers (6)

jpmc26
jpmc26

Reputation: 29866

In the constructor, it's simple enough to raise a ValueError if they are both None or both set. The problem is later on in the code.

Following the principle of least surprise, I think you should mark the variables private and use setter methods (not property setter, plain old methods). This clearly suggests you're doing extra logic when the value is set, and it gives you an obvious place to add extra logic later if needed. Using getter property methods would be fine, though. So something like this:

class Foo(object):
    def __init__(self, apple=None, orange=None):
        super(Foo, self).__init__()
        if apple is None and orange is None:
            raise ValueError('apple and orange cannot both be None')
        if apple is not None and orange is not None:
            raise ValueError('apple and orange cannot both be set')

        self._apple = apple
        self._orange = orange

    @property
    def apple(self):
        return self._apple

    @property
    def orange(self):
        return self._orange

    def setAppleClearOrange(self, value):
        if value is None:
            raise ValueError('Cannot set both to None')
        self._orange = None
        self._apple = value

    def setOrangeClearApple(self, value):
        if value is None:
            raise ValueError('Cannot set both to None')
        self._apple = None
        self._orange = value

Yes, it's a bit verbose, but it's obvious what your intentions are, which is actually more important.

Upvotes: 4

John La Rooy
John La Rooy

Reputation: 304137

Here is a version that will set the other aspect to None if necessary. You'll have to define what should happen if they are both set to None

class Foo(object):
    def __init__(self, apple=None, orange=None):
        # make sure exactly 1 of apple/orange is not None
        if sum(x is None for x in (apple, orange)) != 1:
            raise ...
        self._apple = apple
        self._orange = orange

    @property
    def apple(self):
        return self._apple

    @apple.setter
    def apple(self, value):
        if value is not None:
            self._orange = None
        elif self._orange is None:
            raise ...
        self._apple = value

    @property
    def orange(self):
        return self._orange

    @orange.setter
    def orange(self, value):
        if value is not None:
            self._apple = None
        elif self._apple is None:
            raise ...
        self._orange = value

Upvotes: 0

user2134086
user2134086

Reputation:

assert (apple is None) != (orange is None)

If you put that code in the __init__ method, and your precondition is false, an AssertionError will be thrown.

Upvotes: 0

jpaugh
jpaugh

Reputation: 7035

You could override __setattr__() for that class, and have it handle that logic for you. See http://docs.python.org/2/reference/datamodel.html#object.setattr

Something like

class FooBar(object):
    def __setattr__(self, key, val):
        if key == 'apple' or key == 'orange':
            # Decide what to do about it
        self.__dict__[key] = val

Note that you need to access the key through the object's __dict__ to avoid an infinite loop, because __setattr__() replaces the normal assignment mechanism.

Also, note that this will check every assignment to these two variables, including in your own code. If that's not what you want, then put some test in __init__(), as others suggested.

Upvotes: 0

XORcist
XORcist

Reputation: 4367

objs = filter(None, [apple, orange, ...])
if len(objs) == 0:
   raise Exception("At least 1 object of [apple, orange] must be different from None.") 
elif len(objs) > 1:
    raise Exception("At most 1 object of [apple, orange] should be different from None.")

Upvotes: 0

Kyle G.
Kyle G.

Reputation: 880

Something like

if !((self.apple==None)^(self.orange==None))
    // some error

Might do the trick... ^ is the XOR operator, which returns true if one, but not both, of the operands are true.

Upvotes: 1

Related Questions