Sign up ×
Code Review Stack Exchange is a question and answer site for peer programmer code reviews. It's 100% free, no registration required.

I recently have been working on a BinaryTree class, and looked into creating a proper __repr__ which when eval'ed would recreate the exact binary tree as the original.

The following code for __repr__ works as expected, but I'm wondering if there is a better way to handle the removal of values which already has a default parameter in the __init__ method of the class. That is, avoiding to use replace() to remove the None cases at the end of __repr__.

class BinaryTree(object):
    """Structure to hold a binary tree."""

    def __init__(self, value, left = None, right = None):
        self.value = value
        self.left = left
        self.right = right


    def __repr__(self):
       """Return a string which when eval'ed will rebuild tree"""

       return '{}({}, {}, {})'.format(
                 self.__class__.__name__,
                 repr(self.value),
                 repr(self.left) if self.left else None,
                 repr(self.right) if self.right else None) \
                      .replace(', None, None)', ')') \
                      .replace(', None)', ')')


    def __eq__(self, other):
        """Check if both trees exists, and have same value and subtrees."""

        # Return early if only one of the tree exists
        #   or if only one of them has a right tree
        #   or if only one of them has a left tree
        if (bool(self) ^ bool(other)
            or (bool(self.right) ^ bool(other.right))
            or (bool(self.left) ^ bool(other.left))):

            return False

        # Otherwise compare the values, and that sub trees are equal
        return (self.value == other.value
                and self.right == other.right
                and self.left == other.left)


    def __ne__(self, other):
        """Negated version of BinaryTree.__eq__"""
        return not self.__eq__(other)

Some test code verifying that it actually works:

trees = []
trees.append(('balanced manual', BinaryTree('D', BinaryTree('B', BinaryTree('A'), BinaryTree('C')),
                            BinaryTree('F', BinaryTree('E')))))

trees.append(('alphalevel manual', BinaryTree('A', BinaryTree('B', BinaryTree('D'), BinaryTree('E')), BinaryTree('C', BinaryTree('F')))))
trees.append(('integer_tree manual', BinaryTree(4, BinaryTree(2, BinaryTree(1), BinaryTree(3)), BinaryTree(6, BinaryTree(5)))))
trees.append(('strange_tree manual',
               BinaryTree('G',
                 BinaryTree('C',
                   BinaryTree('B',
                     BinaryTree('A')),
                   BinaryTree('F',
                     BinaryTree('D',
                       None,
                       BinaryTree('E')))),
                 BinaryTree('H',
                   None,
                   BinaryTree('I')))))


for tree_name, tree in trees:
    print('Treename: {}, equal to __repr__ version: {}'.format(tree_name,
                tree == eval(repr(tree))))

I do know that it doesn't hurt to explicitly specify the default parameters when eval'ing the repr version, but I'm contemplating on using some the repr-versions in tests and so on, and the version without None looks a little nicer than the one with:

  • BinaryTree('D', BinaryTree('B', BinaryTree('A'), BinaryTree('C')), BinaryTree('F', BinaryTree('E')))
  • BinaryTree('D', BinaryTree('B', BinaryTree('A', None, None), BinaryTree('C', None, None)), BinaryTree('F', BinaryTree('E', None, None), None))
share|improve this question
    
Why do you want to replace the None at all? – Barry 2 days ago
    
@Barry, Just to make it neater and cleaner, if copied out and used for test cases. :-) – holroy 2 days ago

6 Answers 6

One thing you can simplify is this subexpression:

repr(self.left) if self.left else None

to just:

repr(self.left)

since repr(None) == str(None) == 'None'. Just saves some typing.

Furthermore, when you are doing:

'({}, {}, {})'.format(repr(a), repr(b), repr(c))

that is equivalent to just repring the tuple:

repr((a, b, c))

Other than that, is it worth the added complexity of the replaces? It's not the end of the world that:

repr(BinaryTree(2)) == 'BinaryTree(2, None, None)'

I'd say just keep it simple:

return '{}{}'.format(
          self.__class__.__name__,
          repr((self.value, self.left, self.right)))
share|improve this answer
    
It's not the end of the world, but I just don't like: BinaryTree('D', BinaryTree('B', BinaryTree('A', None, None), BinaryTree('C', None, None)), BinaryTree('F', BinaryTree('E', None, None), None)), when it can be: BinaryTree('D', BinaryTree('B', BinaryTree('A'), BinaryTree('C')), BinaryTree('F', BinaryTree('E'))). Even though both actually work! – holroy 2 days ago

Sometimes it's best just to write simple code and not worry about a small amount of repetition:

def __repr__(self):
    if self.right is not None:
        fmt = '{}({value!r}, {left!r}, {right!r})'
    elif self.left is not None:
        fmt = '{}({value!r}, {left!r})'
    else:
        fmt = '{}({value!r})'
    return fmt.format(type(self).__name__, **vars(self))
share|improve this answer

I have to agree with you BinaryTree(1, None, None) is not very nice to look at.

First off in def __init__, left = None should be left=None to comply fully with PEP8.

Don't use spaces around the = sign when used to indicate a keyword argument or a default parameter value.

And using self.__class__ can on the very rare occasion return the wrong result, so it is better to use type(x). Python2 docs as the Python3 docs don't state it as clearly. Also type(x) looks nicer than x.__class__.

type(x) is typically the same as x.__class__ (although this is not guaranteed – a new-style class instance is permitted to override the value returned for x.__class__).

So aside from nit-pics, I would: (With help from @holroy)

lst = [self.value, self.left, self.right]
if self.left or not self.right:
    lst = filter(lambda x: x is not None, lst)
return '{}({})'.format(type(self).__name__, ', '.join(map(repr, lst)))
share|improve this answer
    
That if is a mouthful... – holroy 2 days ago
    
@holroy I agree, it's not the nicest one. I changed it to something easier, as I tested it and it worked. I'm still sceptical, but I know it should work. – Joe Wallis 2 days ago
    
Is using if self.left or not self.right considered ugly compared to checking against if self.left is not None or self.right is None? – holroy 2 days ago
1  
Not a separate function but exchange the "if/filter" combo with something like while not lst[-1]: lst.pop()... Hmm... I might need to answer my own post with that... – holroy 2 days ago
1  
@holroy I thought of that when you posted your answer, but I'll let you do that. If you want you could do something like reversed(list(itertools.dropwhile(lambda x: x is None, reversed(lst)))), but that is a bit... horrible – Joe Wallis 2 days ago

The answer by Joe Wallis got me thinking about an option of just popping the last values of a list which would make for something like:

# Make a list matching the parameters of the __init__ method
lst = [self.value, self.left, self.right]

# Pop values from lst as long as they match the default value of None (or falsy)
while lst and not lst[-1]:
    lst.pop()

# Regenerate the call to the constructor
return '{}({})'.format(type(self).__name__, ', '.join(map(repr, lst)))

Which would pop values from the end as long as they're not returning a falsy value, which would work in my current case. However as I tend to want generic solutions (not directly asked for in the question), I found this alternate solution which would allow for the default values to be real default values, and not only None.

# Make a list matching the parameters of the __init__ method
lst = [self.value, self.left, self.right]

# Get all default values using introspection, and reverse list
default_values_reversed = reversed(getargspec(type(self).__init__).defaults)

# Pop values from lst as long as they match the default value
for default_value in default_values_reversed:
    if lst[-1] == default_value:
        lst.pop()
    else:
        break

# Regenerate the call to the constructor
return '{}{}'.format(type(self).__name__, repr(tuple(lst)))

##old: return '{}({})'.format(type(self).__name__, ', '.join(map(repr, lst)))

This version does indeed work, also with different types and values of default parameters. I do however not like that you'll manually have to specify the default parameters at the beginning, which I guess could be built using introspection as well (with some extra magic). Nor do I like the regeneration at the end. This applies to both these version, as well as the version suggested by Joe Wallis.

But it does work, and can absolutely be used in the generic case as well as in my specific case.

Added: I did however not really like the regeneration in Joe Wallis' version (or my first version in this answer), so after thinking a little on that, combined with the idea of calling repr() only once from Barry's solution, I changed the last line of the previous code block. Now I would only like to simplify the for-loop somehow... (and possibly autogenerate the argument list)

Addendum: Taking it to the extreme

Thanks to more hints from Joe Wallis, here is the extreme variant where the instance values, and default values are gathered through introspection, and through a bit of magic removed if the tailing end is equal using multiple reversed lists and itertools.dropwhile. Resulting in the following code:

from inspect import getargspec
from itertools import dropwhile

def __repr__(self):
    """Return a string which when eval'ed returns equal instance."""

    init_arg_spec = getargspec(type(self).__init__)

    # Make a list matching the parameters of the __init__ method
    instance_values_reversed = [self.__dict__[i]
                                for i in init_arg_spec.args[:0:-1]]

    # Make a reversed generator going through default values from inspection 
    default_values_reversed = reversed(init_arg_spec.defaults)

    init_args = tuple(list(dropwhile(
                             lambda val: val == next(default_values_reversed),
                             instance_values_reversed
                          ))[::-1])

    # Regenerate the call to the constructor
    return '{}{}'.format(type(self).__name__, repr(init_args))

This code works under the assumption that the instance variables have corresponding names to the constructor parameters, and that given an equal set of constructor parameters the instances are equal in value. If these requirements are met, this is a generic __repr__ method applicable to any given class.

share|improve this answer
    
I really like this solution. For the 'extra magic' you could try using self.__dict__ with the assumption that the passed name is the same in the dict. lst = [self.__dict__[i] for i in inspect.getargspec(type(self).__init__).args[1:]] – Joe Wallis yesterday
    
@JoeWallis, That seems to work nicely, the assumption is true for me at least. Do you know how to avoid or simplify the for-loop also? – holroy yesterday
    
I kinda tired to think straight atm, so the best I could come up with is lst = reversed(list(itertools.dropwhile(lambda x: x == next(defaults), reversed(lst)))), but it doesn't work, I think it runs out of defaults and stops working. IMO the for loop you have is way nicer, than dropwhile. – Joe Wallis yesterday
    
@JoeWallis, you're missing a list( ... ) around the whole expression... But it does look a little nicer with the for-loop, still... – holroy yesterday

Let's talk about this:

if (bool(self) ^ bool(other)
    or (bool(self.right) ^ bool(other.right))
    or (bool(self.left) ^ bool(other.left))):

The ^ operator is a bitwise operator. It only works in this case because True == 1 and False == 0, which could be confusing if the reader is thinking logically instead of numerically (i.e. "Wait, why are we doing bit fiddling here?"). There is no logical XOR operator because logical XOR is just !=. So you can rewrite this as follows:

if (bool(self) != bool(other)
    or (bool(self.right) != bool(other.right))
    or (bool(self.left) != bool(other.left))):

We still need the bool() calls because otherwise we'd be recursing here, which is not what we want to do.

bool(self) is True. If self were None, we would not have gotten this far, and we haven't overridden any magic methods which could make instances falsey. So we can change the first clause to True != bool(other) which is just a fancy way of writing not other (or other is None, if you prefer explicit None checks):

if (not other
    or (bool(self.right) != bool(other.right))
    or (bool(self.left) != bool(other.left))):
share|improve this answer

A variant over this answer form Gareth Rees, without repetition, but slightly more advanced if conditions:

def __repr__(self):
   fmt = '{}({value!r}'

   if self.left or self.right:
       fmt += ', {left!r}'

   if self.right:
       fmt += ', {right!r}'

   fmt += ')'

   return fmt.format(type(self).__name__, **vars(self))

Note that using !r calls repr() on the value (and !s, if used, calls str() on the value), according to Format string syntax. But Gareth is right that sometimes, simplicity is better than avoiding repetition.

A more compact version of this could be formed using join:

def __repr__(self):
    fmt = ''.join(('{}({value!r}',
                   ', {left!r}' if self.left or self.right else '',
                   ', {right!r}' if self.right else '',
                   ')'  ))

    return fmt.format(type(self).__name__, **vars(self))
share|improve this answer

Your Answer

 
discard

By posting your answer, you agree to the privacy policy and terms of service.

Not the answer you're looking for? Browse other questions tagged or ask your own question.