42

Possible Duplicate:
What's the best way to implement an 'enum' in Python?

I’m writing a function that, ideally, I’d like to return one of three states: “yes”, “no”, and “don’t know”.

  1. Do any programming languages have a type that has three, and only three states? Like a boolean, but with three states instead of two?

  2. In languages that don’t have such a type (like Python), what’s the best type to represent this?

    Currently I think I’ll go with an integer (0 for “no”, 1 for “don’t know” and 2 for “yes”), but maybe there’s a better way? Integers seem a bit “magic number”.

    I could return True, False or None, but as None would evaluate to False in most comparison contexts it seems a bit ripe for errors.

Community
  • 1
  • 1
Paul D. Waite
  • 89,393
  • 53
  • 186
  • 261
  • 3
    This may be relevant: http://stackoverflow.com/questions/36932/whats-the-best-way-to-implement-an-enum-in-python – Owen Feb 29 '12 at 14:46
  • 3
    Similar to this question, isn't it? http://stackoverflow.com/questions/36932/whats-the-best-way-to-implement-an-enum-in-python – Johannes Weiss Feb 29 '12 at 14:46
  • @JohannesWeiß: quite possibly — I don‘t have a C background, and I’m not familiar with the concept of enums. Looks good, cheers. – Paul D. Waite Feb 29 '12 at 14:48
  • 2
    1 and 2 are both True in boolean context anyway. Unless you plan to implement a class that raises a `ValueError` in `__bool__()` for the "maybe" value, you're a but stuck as far as using normal comparisons goes anyway. – Wooble Feb 29 '12 at 15:41
  • 6
    Three valued logic is much more involved than a simple enumeration. – Ethan Furman Mar 01 '12 at 12:09
  • 5
    This is exactly what the [Python Tribool](http://www.grantjenks.com/docs/tribool/) data type is used for. – GrantJ Jun 05 '15 at 18:10

9 Answers9

36

In Python I'd do that with a wrapper object that holds one of those three values; I'd use True, False, and None. Since the implicit truthiness value of a Boolean-like object with three possible values is problematic, we'll solve that by disallowing that entirely (raising an exception in __nonzero__(), or in Python 3, __bool__()), thus requiring that comparisons always be done explicitly, using in, ==, or !=. We'll implement equality as identity so that only the specific singleton values True, False, and None are matched.

class Tristate(object):

    def __init__(self, value=None):
       if any(value is v for v in (True, False, None)):
          self.value = value
       else:
           raise ValueError("Tristate value must be True, False, or None")

    def __eq__(self, other):
       return (self.value is other.value if isinstance(other, Tristate)
               else self.value is other)

    def __ne__(self, other):
       return not self == other

    def __nonzero__(self):   # Python 3: __bool__()
       raise TypeError("Tristate object may not be used as a Boolean")

    def __str__(self):
        return str(self.value)

    def __repr__(self):
        return "Tristate(%s)" % self.value

Usage:

t = Tristate(True)
t == True           # True
t != False          # True
t in (True, False)  # True
bool(t)             # Exception!
if t: print "woo"   # Exception!

When using Tristate objects, you must explicitly specify which values to match, i.e. foo == True or bar != None. You can also do foo in (False, None) to match multiple values (though of course in two values is the opposite of != with a single value). If there are other logic operations you wish to be able to perform with these objects, you could implement these as methods, or possibly by overriding certain operators (sadly, however, logical not, and, and or are not overrideable, though there's a proposal to add that).

Also note that you can't override id() in Python, so e.g. Tristate(None) is None is False; the two objects are in fact different. Since good Python style is to use is when comparing against singletons, this is unfortunate, but unavoidable.

Edit 4/27/16: Added support for comparing one Tristate object to another.

kindall
  • 158,047
  • 31
  • 244
  • 289
  • 1
    @PaulD.Waite I was going to ask why the unaccept, but it's pretty clear why. – Marcin Mar 01 '12 at 10:19
  • 1
    @Marcin: sure, sorry if I caused any offence — this answer is (if I understand correctly) essentially the same as yours, just with example code and discussion of boolean comparison. I figured the extra detail made it worth acceptance. – Paul D. Waite Mar 01 '12 at 10:57
  • @PaulD.Waite No, no offence taken - this answer is clearly much fuller, and addresses aspects of the issue that I hadn't even thought of. – Marcin Mar 01 '12 at 10:59
  • 2
    Updated with some Python 3 details. Thanks for accepting my answer, I sure wasn't expecting it! – kindall Mar 01 '12 at 17:15
  • Your implementation has at least one issue: what does t==t return? I would also override the cmp operator and throw an error (this is not necessary for python3). If I want to be maximal restrictive, I would only allow Tristate object as parameters for __bool__ respective __nonzero__ methods so that t==True would not be allowed. – Jakob Kroeker Nov 12 '13 at 19:05
  • 2
    If it only has one issue, well, that's not bad for some code I cranked out in 10 minutes and did only minimal testing on... :-) – kindall Nov 13 '13 at 18:05
  • 3
    For a more complete implementation that's also available on PyPI, check out the [Python tribool module](http://www.grantjenks.com/docs/tribool/). (I would've posted this as an answer but am not allowed.) – GrantJ Jun 05 '15 at 18:14
9

This is called Ternary logic or Three-valued Logic. As other answers suggest, you could either implement a class:

class Ternary:
    FALSE = 0
    TRUE = 1
    UNKNOWN = 2

Myself, I would probably go for your solution (True, False, None) but I understand your concern.

Linus Thiel
  • 36,497
  • 9
  • 102
  • 98
6

The parallel to the None problem exists with false = 0, true = 1, unknown = 2 (unknown is not actually true either, but will eval to True if you aren't careful).

I came up with a hackish way to get something that at least approximates what you want, I think. It will at least get you something that will evaluate in a trinary fashion in if/else and other boolean eval instances.

class Yes(object):
    def __nonzero__(self):
        return True

class No(object):
    def __nonzero__(self):
        return False

class Unknown(object):
    def __nonzero__(self):
        raise ValueError('Unknown typed values do not evaluate to True/False.  Try using Ternary.eval().')

class Ternary(object):
    def __init__(self, yes, no, unknown):
        setattr(self, yes, Yes())
        setattr(self, no, No())
        setattr(self, unknown, Unknown())
    @staticmethod
    def eval(value, unknown_eval):
        if isinstance(value, Unknown):
            return unknown_eval
        return bool(value)

Usage:

t = Ternary('yes', 'no', 'unknown')
# Do stuff to assign ternary value to x
if Ternary.eval(x, True):
    print 'x is yes or unknown'
if Ternary.eval(x, False):
    print 'x is yes only'

You could make Yes, No, and Unknown pseudo-singletons which would let you refine eval a little bit. You could still do simple if checks when you know that your value is going to be yes or no, but if you tried to do a straight bool() (ie if x) on Unknown you'd get a TypeError. This would make your code more explicit though, as every time you checked a value of the trinary type, you'd have to define in your code how you wanted unknown to be treated in the context of that conditional, so that would be a plus.

Edit: I thought of an alternative that would require less special handling but less flexible. Alter above thusly:

class Unknown(object):
    def __init__(self, eval):
        self._eval = eval
    def __nonzero__(self):
        return self._eval

class Ternary(object):
    def __init__(self, yes, no, unknown, unknown_eval):
        setattr(self, yes, Yes())
        setattr(self, no, No())
        setattr(self, unknown, Unknown(unknown_eval))

Usage:

t1 = Ternary('yes', 'no', 'unknown', True)
t2 = Ternary('yes', 'no', 'unknown', False)
# Do stuff to assign ternary values to x1 and x2
if x1:
    print 'x is yes or unknown'
if x2:
    print 'x is yes only'

This has the benefit of allowing nonzero to work as spec calls for in Unknown, but it has the downside of having the eval for Unknown set in stone from instantiation and of no longer allowing Unknown to be a pseudo-singleton.

Silas Ray
  • 24,298
  • 5
  • 44
  • 60
  • 1
    `__nonzero__` must return a `bool` or an `int` -- None is not allowed. – Ethan Furman Feb 29 '12 at 17:25
  • 1
    @Ethan Furman, and hence why I pointed out multiple times that this does not meet spec. And why I provided an alternative at the bottom that does. Do you have new information to add? – Silas Ray Feb 29 '12 at 17:29
  • 1
    Ah -- just read your post more thoroughly. If you are going to have an exception raised when the value is unknown it is better to define your own exception and raise that rather than having it look like a bug. For a complete implementation of ternary logic you can check the link in my answer. (If you want to go with a custom exception class in your example, or anything else that doesn't look like a bug, I'll happily change my vote.) – Ethan Furman Feb 29 '12 at 17:41
  • 1
    I looked at your comment below, but that just kicks the can down the road, so to speak. The problem is that the OP wants to be able to do a direct boolean evaluation of the value itself. `is Thruth`, `is Falsth`, and `is Unknown` are the sort of checks the OP is looking to avoid; Thruth, Falsth, and Unknown will still eval to True or False without a third value. I'll change my answer to throw a new exception though. – Silas Ray Feb 29 '12 at 17:52
  • 1
    Raising an exception is certainly a valid choice to make. For my use case the pattern is: Do something if the value is `Truth` (treat `False`/`Unknown` the same); do something if the value is `Falsth` (treat `True`/`Unknown` the same); or use `if/elif/else` if `Unknown` should be treated differently. – Ethan Furman Feb 29 '12 at 18:13
  • Thinking about it more though, raising an exception is definitionally the right thing to do here. The definition of Unknown is a value that does not evaluate to True or False, so to have a way, especially an implicit way, to allow it to evaluate to one of those values without any explicit special handling (like eval()) means the type is fundamentally flawed. – Silas Ray Feb 29 '12 at 18:21
  • My reasoning behind that choice was to make `Logical` a drop-in replacement for `bool` without having to either add `try/except` around all the relevant `if` statements and without having to add `== True` or `== False` -- however, your point is a good one, and correctness beats convenience. I'll change my code. Thank you. – Ethan Furman Feb 29 '12 at 18:45
3
  1. At least one language has this: C#: int? num = null;, now num is actually Nullable<Int32> (the question mark is a "syntactic sugar") see: http://msdn.microsoft.com/en-us/library/1t3y8s4s%28v=vs.80%29.aspx and http://en.wikipedia.org/wiki/Nullable_type
  2. Don't know Python, but it depends on your preference: usability (enum or class) vs. efficiency (4 bit fields)
Tar
  • 7,277
  • 7
  • 45
  • 101
2

It's generally known as an 'enum', after the C construct of that name.

You can easily create your own class, objects of which must be initialised with values from a set, and you can create the appropriate comparison, equality, and truth functions accordingly.

Marcin
  • 44,601
  • 17
  • 110
  • 191
1

I have a 3-valued Logical class in my dbf module.

It implements False/True/Unknown as singleton values Falsth/Truth/Unknown. It implements all the comparison operators, and also allows comparison with the Python singletons False/True/None (None taken to mean unknown). Any comparison with an Unknown value results in Unknown, and an attempt to implicitly use an Unknown value in an if statement (or other bool context) will raise a TypeError, although Truth and Falsth can be used in boolean contexts, and Unknown can be compared against directly.

Because it is not possible to override the and, or, and not behavior the type overrides the bitwise operators &, |, and ~.

It also implements __index__ with Unknown having the value of 2.

Example:

from dbf import Logical, Truth, Falsth, Unknown

middle_name = raw_input("What is the middle name? ['?' if unknown] ").strip()
if middle_name == '?':
    middle_name = ''
    middle_exists = Unknown
elif middle_name:
    middle_exists = Truth
else:
    middle_exists = Falsth
.
.
.
if middle_exists is Unknown:
    print "The middle name is unknown."
elif middle_exists:
    print "The middle name is %s." % middle_name
else:
    print "This person does not have a middle name."
Ethan Furman
  • 52,296
  • 16
  • 127
  • 201
0

There is no such built-in types. And enum are also not supported as a type. However, you can use constant as:

class Option:
    NO = 0
    DONT_KNOW = 1
    YES = 2

reply = Option.YES
Husain Basrawala
  • 1,609
  • 14
  • 20
0

I am not much of a Python programming but could you return None in the Don't Know case. The caller would have to check for "Yes", "No" or None but atleast they would know that you don't know.

http://boodebr.org/main/python/tourist/none-empty-nothing

Ninju Bohra
  • 460
  • 1
  • 5
  • 11
0

I never saw a type similar to boolean which had more than two values. After all "boolean" means that the function operates over "Boolean domain", which in turn has exactly 2 values.

That said, it is perfectly ok to use integers, enums, or even strings. In python you can create a module containing just variables YES, NO, MAYBE and import from there to every other place.

Ecir Hana
  • 9,122
  • 13
  • 58
  • 105