Pytest testing for class value - python

Doing a basic implementation of a binary tree in python and testing the implementation using pytest. One of the unit test is to check if the new node is inserted properly to the left (or the right) of the parent node. When I do the assert statement is shows the correct comparison on the left and on the right of the == but tails me the test failed. Snippets of code is attached.
Relevant part of the main code:
class Node:
# Contains data variables.
def __init__(self, val: Optional[int]=None) -> None:
self.value: Optional[int] = val
self.left: Optional[Node] = None
self.right: Optional[Node] = None
def __repr__(self):
return f"{self.value}"
def insert(self, data: int) -> bool:
# Insert a node to the right or left of a parent node.
# Return False only if trying to insert duplicate.
if self.value == data: # Duplicate found
return False
if self.value > data: # New node to the left of parent node
if self.left: # If left exist, then recurse
return self.left.insert(data)
self.left = Node(data) # Default, create a new left node
return True
else:
if self.right: # If right exist, then recurse
return self.right.insert(data)
self.right = Node(data) # Default, create a new right node
return True
Specific pytest unit test
def test_node_insert_left():
node = Node(4)
node.insert(3)
#print(f"\n{node.left}-{node}-{node.right}")
assert node.value == 4
assert node.left == Node(3) # I've also tried node.left == 3
assert type(node.left) == type(Node())
assert node.right == None
The error message
> assert node.left == Node(3)
E assert 3 == 3
E -3
E +3

Two nodes will not equal unless you implement __eq__ in your node class.
You could write an __eq__ method in Node:
def __eq__(self, other):
if not isinstance(other, Node):
return NotImplemented
return self.value==other.value
if you want to regard any two nodes holding the same value as equal.
Then assert node.left == Node(3) would work.
However, sinces nodes contain more than just a value, that might not be ideal.
More simply:
For your assertion, what you actually want to test is that the node holds the value 3. For that, you could simply change your assertion to
assert node.left.value == 3

Related

Removing Root Node in Binary Search Tree

I'm currently unable to delete the root node of my binary search tree in these situations
the root node doesn't have a left attribute or doesn't have a right attribute
the root has neither attribute (leaf)
from __future__ import annotations
from collections.abc import Iterator
from typing import Any, Optional
class TreeNode:
def __init__(self, value: Any, left: BST, right: BST):
self.value = value
self.left = left
self.right = right
def __repr__(self):
return f"TreeNode({self.value}, {self.left}, {self.right})"
def __eq__(self, other):
return self.value == other.value and \
self.right == other.right and self.left == other.left
BST = Optional[TreeNode]
def is_empty(tree: BST) -> bool:
"""Return True if the tree is empty, False otherwise."""
return tree is None # check if BST input is none (empty)
def search(tree: BST, value: Any) -> bool:
"""Return True if value is in tree, False otherwise."""
if tree is None:
return False
else:
return (
tree.value == value
or search(tree.left, value)
or search(tree.right, value)
)
def insert(tree: BST, value: Any) -> BST:
"""Insert the value into the tree in the proper location."""
if tree is None:
return TreeNode(value, None, None)
elif tree.value is None:
tree.value = value
elif value > tree.value:
if tree.right is None:
tree.right = TreeNode(value, None, None)
return insert(tree.right, value)
elif value < tree.value:
if tree.left is None:
tree.left = TreeNode(value, None, None)
return insert(tree.left, value)
def delete(tree: BST, value: Any) -> BST:
"""Remove the value from the tree (if present).
If the value is not present, this function does nothing.
"""
try:
root = (tree.value,)
print(root[0])
finally:
pass
if tree is None:
return tree
elif value < tree.value:
tree.left = delete(tree.left, value)
elif value > tree.value:
tree.right = delete(tree.right, value)
else:
if tree.left is None and tree.right is None: # No children
if tree.value == root[0]:
tree.value = None
return tree
elif tree.left is not None and tree.right is not None: # Two children
replacement = tree.right
while replacement.left is not None: # Find smallest "big" value
replacement = replacement.left
tree.value = replacement.value # Set value to smallest "big" value
tree.right = delete(tree.right, replacement.value) # Delete value
else: # One child
if tree.left is not None: # Promote left child
return tree.left
else:
return tree.left
return tree
I'm struggling to delete the root of my BST when one of its left/right components is empty or if both components are empty. Did you guys keep track of your root value throughout your function? I tried implementing a tuple to save keep my root Node, however, my implementation is still running into errors. 
First of all, you have some code that gives a special meaning to the value None in the root node. This is not how it should be done. Your is_empty function is right: a tree is empty only when the tree reference itself is None, not when the root's value happens to be None.
So remove this logic (concerning tree.value is None) from your insert and delete functions. And in your main program, initialise your tree as None, not as TreeNode(None).
Insert
There are some bugs in insert:
In the value > tree.value block, when tree.right is None, your code creates a new node, and then still makes a recursive call to insert a node. This is not right. In fact, you should not create a node here at all, but completely rely on the recursive call, which will do that when it gets a None as argument.
In this same block, you should not return what insert() returns, which is right subtree. Instead you should assign that return value to tree.right and then return tree
In case none of the if blocks is entered (which happens when the value is already in the tree), you should still return tree.
Here is a correction for insert:
def insert(tree: BST, value: Any) -> BST:
"""Insert the value into the tree in the proper location."""
if tree is None:
return TreeNode(value, None, None)
if value > tree.value:
tree.right = insert(tree.right, value) # correction
elif value < tree.value:
tree.left = insert(tree.left, value) # correction
return tree # missing: must always return BST type
Delete
There are these issues in the delete function:
There is a trivial bug near the end of the function where you do return tree.left in both cases. One of those should be return tree.right.
In the case the node is a leaf you should not return tree, but None as that will serve for the caller to actually detach that node.
Not a problem, but the case where there are no children does not need a separate treatment. The code that deals with the 1-child case can also deal with the 0-children case.
Here is a correction for delete:
def delete(tree: BST, value: Any) -> BST:
"""Remove the value from the tree (if present).
If the value is not present, this function does nothing.
"""
if tree is None:
return tree
elif value < tree.value:
tree.left = delete(tree.left, value)
elif value > tree.value:
tree.right = delete(tree.right, value)
elif tree.left and tree.right: # Has two children
replacement = tree.right
while replacement.left is not None: # Find smallest "big" value
replacement = replacement.left
tree.value = replacement.value # Set value to smallest "big" value
tree.right = delete(tree.right, replacement.value) # Delete value
else: # Zero or one child (same treatment)
return tree.left or tree.right # shorter notation to promote a child
return tree
Main program
There was no main program included in your code, so I will provide one here. It includes:
A utility function to display a tree in a 90° rotated way (with root pictured at the left side), using indentation to indicate depth.
Code that populates a tree.
Note that the tree starts as None (empty) and that the return value of insert is always assigned back to tree.
Code that deletes some nodes from this tree.
Note again that the return value of delete is always assigned back to tree.
def display(tree: BST, tab: str=""):
if tree:
display(tree.right, tab + " ")
print(tab, tree.value)
display(tree.left, tab + " ")
tree: BST = None # This is an empty tree
for value in (4, 2, 6, 1, 3, 5, 7):
tree = insert(tree, value)
print("populated BST:")
display(tree)
for value in (6,9,1,4):
print("delete", value)
tree = delete(tree, value)
display(tree)
This demo program will produce this output:
populated BST:
7
6
5
4
3
2
1
delete 6
7
5
4
3
2
1
delete 9
7
5
4
3
2
1
delete 1
7
5
4
3
2
delete 4
7
5
3
2

Deletion in BST (python) | unexpected additional deletions?

def delete(node, key):
if not node: return None
# Wrong node, search correct child
if key < node.data:
delete(node.left, key)
elif key > node.data:
delete(node.right, key)
# Correct node found
else:
#1. node has no children
if not (node.left and node.right): return None
#2. node has only left child
if node.left and not node.right: return node.left
#3. node has only right child
if not node.left and node.right: return node.right
#4. node has both left & right children
## Need to replace current value with next biggest value
## So go right once then all left to end
## Once this value is found, assign to appropriate position
## Then remove this val from its previous position
temp = node.right
while temp.left: temp = temp.left
node.data = temp.data
node.right = delete(node.right, temp.data)
t = BinaryTree([100, 50, 200, 25, 75, 350])
delete(t.root, 100)
I think that this BST deletion code mostly works, but it's a little buggy. If I delete the root node, 100, then 350 will be missing, following, given the BST, t = BinaryTree([100, 50, 200, 25, 75, 350]).
What is going on here? I'm not sure why 350 has been deleted in the process. I'm wondering if it's related to how I replace the node value upon successful deletion.
Optional but possibly helpful context
class BinaryTreeNode:
def __init__(self, data):
self.data = data
self.left = None
self.right = None
class BinaryTree:
def __init__(self, *args):
if len(args) < 1:
self.root = None
elif isinstance(args[0], int):
self.root = BinaryTreeNode(args[0])
else:
self.root = None
for x in args[0]:
self.insert(x)
def insert(self, node_data):
new_node = BinaryTreeNode(node_data)
if not self.root:
self.root = new_node
else:
# root has no parent, so assign none for 1st iteration
parent = None
temp_pointer = self.root
while temp_pointer:
# update parent
parent = temp_pointer
#update temp_pointer to left or right child
if node_data <= temp_pointer.data:
temp_pointer = temp_pointer.left
else:
temp_pointer = temp_pointer.right
# eventually, temp_pointer will point to None, exiting while loop
# assign to left or right child as appropriate
if node_data <= parent.data:
parent.left = new_node
else:
parent.right = new_node
There are a few issues:
As delete is designed to return a node reference or None, you should make sure not to ignore that returned reference. You did it right near the end of your function (node.right = delete(node.right, temp.data)), but elsewhere delete is called without regards of the returned reference. So:
The initial call in the main program should look like this:
t.root = delete(t.root, 100)
This will ensure that the root attribute is set to None when the last node has been deleted from the tree.
The recursive call in the first if block should be:
node.left = delete(node.left, key)
And similarly in the second block:
node.right = delete(node.right, key)
The function delete should always return a node reference after a recursive call has been made, yet this is missing in many of your cases, so add at the very bottom of your function a kind of "catch all" and return the current reference you have:
return node
The condition for identifying a leaf node is wrong. The and should be a or:
if not (node.left or node.right): return None
The corrected code -- comments indicate changes:
def delete(node, key):
if not node: return None
if key < node.data:
node.left = delete(node.left, key) # assign back!
elif key > node.data:
node.right = delete(node.right, key) # assign back!
else:
if not (node.left or node.right): return None # condition corrected
if node.left and not node.right: return node.left
if not node.left and node.right: return node.right
temp = node.right
while temp.left: temp = temp.left
node.data = temp.data
node.right = delete(node.right, temp.data)
return node # always return a node when a recursive call was made
t = BinaryTree([100, 50, 200, 150, 175, 25, 75, 350])
t.root = delete(t.root, 350) # assign back!
Considerations
Not a problem in the algorithm, but it is a good habit to put the body of an if or while statement on the next line, indented
This function would better be a method on the BinaryTree class -- then the main program should not have to worry about getting/setting the root attribute -- and most of the function's (recursive) logic could be implemented as a method on the BinaryTreeNode class.

adding a node in Binary Search Tree

I am currently working on implementing a Binary search tree but I have a problem with creating the add_node() function. I tried to make it in a recursive way, but the result was weird.
For example- If I run add_node(1), add_node(5), and add_node(0), my tree has only 1, no 5 and 0.
I would be happy if you tell me what the problem is.
def add_node(self, value: int) -> None:
if self.root == None:
self.root = Node(value)
return
else:
return self.add_recursion(self.root, value)
def add_recursion(self, node: Node, value: int) -> None:
if node == None:
node = Node(value)
return
elif value < node.value:
return self.add_recursion(node.left, value)
else:
return self.add_recursion(node.right, value)
When a None value is passed into a function, it is passed by value, not by reference, since... there is no reference.
elif value < node.value:
return self.add_recursion(node.left, value)
else:
return self.add_recursion(node.right, value)
When node.left or node.right is None, a Node ends up being created but not attached to node.
So what you could do is handle the cases where they are None separately.
def add_recursion(self, node: Node, value: int) -> None:
if node == None:
node = Node(value)
elif value < node.value:
if node.left == None:
node.left = Node(value)
else:
self.add_recursion(node.left, value)
else:
if node.right == None:
node.right = Node(value)
else:
self.add_recursion(node.right, value)
While this is workable, it becomes quite ugly. Look at the answer by Locke to see a better way to structure your code.
Also, an additional tip for readability, avoid using return where they aren't necessary such as at the end of a flow.
The issue is how you handle if node.left or node.right does not exist. Since the arguments are copied for each call, setting the value of node in add_recursion has no effect on the tree.
def foo(val):
print("foo start:", val)
val = 5
print("foo end:", val)
bar = 3
foo(bar) # Value of bar is copied for call
print("after foo:", bar) # Prints bar is still 3
I think you might also be getting confused due to how the root node is handled. Here is some starter code on how can handle the initial call to add_recursive.
class Node:
def __init__(self, value: int):
self.value = value
self.left = None
self.right = None
def add_recursive(self, value: int):
# TODO: recursively add to left or right
# For example, here is how you could recursively make a linked list
if self.right is None:
self.right = Node(value)
else:
self.right.add_recursive(value)
class BinarySearchTree:
def __init__(self):
self.root = None
def add_node(self, value: int):
if self.root is None:
self.root = Node(value)
else:
# Call add_recursive on root instead of with root.
self.root.add_recursive(value)
tree = BinarySearchTree()
tree.add_node(1)
tree.add_node(5)
tree.add_node(0)

Sum of binary tree leaves' values

I wrote this code and when I use print I see that I get the leaves. However, the final return from the function is None and not the sum of the leaves, which is supposed to be 7 in this example. I'd be happy to know whats wrong here. Thank you !
class Node:
def __init__(self, val=None):
self.left = None
self.right = None
self.val = val
def sum_leafs(tree):
if tree is None:
return 0
if tree.right and tree.left:
sum_leafs(tree.right)
sum_leafs(tree.left)
elif tree.right or tree.left:
if tree.right:
sum_leafs(tree.right)
elif tree.left:
sum_leafs(tree.left)
elif tree.right is None and tree.left is None:
return sum_leafs(tree.left) + 1
node = Node(10)
node.right = Node(2)
node.left = Node(11)
node.left.right = Node(5)
print(sum_leafs(node))
You forgot to add + when you sum the branches (left/right) and also you forgot to access val which is the most crucial thing for the whole thing to work.
Further, the logic can be simplified:
def sum_leafs(tree):
if tree is None:
return 0
if not tree.right and not tree.left:
return tree.val
return sum_leafs(tree.right) + sum_leafs(tree.left)
You are not adding the sums together or returning them. This can also be done with a method in the class:
class Node:
def __init__(self, val=None):
self.left = None
self.right = None
self.val = val
def sum(self):
s = 0
if self.left is not None:
s += self.left.sum()
if self.right is not None:
s += self.right.sum()
return self.val + s
node = Node(10)
node.right = Node(2)
node.left = Node(11)
node.left.right = Node(5)
print(node.sum())
returns:
28
You are not properly returning the calculated leaf sums. Try this:
class Node:
def __init__(self, val=None):
self.left = None
self.right = None
self.val = val
def sum_leafs(tree):
if tree is None:
return 0
elif tree.right and tree.left:
return sum_leafs(tree.right) + sum_leafs(tree.left)
elif tree.right or tree.left:
if tree.right:
return sum_leafs(tree.right)
elif tree.left:
return sum_leafs(tree.left)
elif tree.right is None and tree.left is None:
return tree.val
node = Node(10)
node.right = Node(2)
node.left = Node(11)
node.left.right = Node(5)
print(sum_leafs(node))
7
node
First I'm going to update your Node interface so that it's possible to set left and right branches when creating nodes -
class Node:
def __init__(self, val=None, left=None, right=None):
self.left = left
self.right = right
self.val = val
This allows us to create tress more ergonomically, such as -
t = Node(10, Node(11, None, Node(5)), Node(2))
traverse
Now we write a generic traverse procedure. This allows us to separate 1) the traversal of our tree from 2) the intended operation we want to perform on each tree element -
def traverse(tree):
if tree is None:
return
else:
yield tree.val
yield from traverse(tree.left)
yield from traverse(tree.right)
Now the need for sum_leafs disappears. We have decoupled traversal logic from summing logic. We can calculate the sum of leafs with a simple combination of sum and traverse -
print(sum(traverse(t)))
# 28
don't repeat yourself
Or, instead of summing the values, we could write a search function to find the first value that passes a predicate -
def search(test, tree):
for val in traverse(tree):
if test(val):
return val
print(search(lambda x: x < 10, t))
# 5
print(search(lambda x: x > 99, t))
# None
Or, we could simply collect each value into a list -
print(list(traverse(t)))
# [ 10, 11, 5, 2 ]
As you can see, removing the traversal logic from each function that depends on our tree can be a huge help.
without generators
If you don't like generators, you can write the eager version of traverse which always returns a list. The difference now is there is no way to partially traverse the tree. Note the similarities this program shares with the generator version -
def traverse(t):
if t is None:
return [] # <-- empty
else:
return \
[ t.val
, *traverse(t.left) # <-- yield from
, *traverse(t.right) # <-- yield from
]
print(traverse(t))
# [ 10, 11, 5, 2 ]

Python Binary Search Tree Delete Function

class Node:
def __init__(self, val):
self.val = val
self.left = None
self.right = None
class BST:
def __init__(self, root=None):
self.root = root
def remove(self, val):
if self.root == None:
return root
else:
self._remove(val, self.root)
def _remove(self, val, node):
if node == None:
return node # Item not found
if val < node.val:
self._remove(val, node.left)
elif val > node.val:
self._remove(val, node.right)
else:
# FOUND NODE TO REMOVE
if node.left != None and node.right != None: # IF TWO CHILDREN
node.val = self._find_min(node.right)
node.right = self._remove(node.val, node.right)
else: # ZERO OR ONE CHILD
if node.left == None: # COVERS ZERO CHILD CASE
node = node.right
elif node.right == None:
node = node.left
return node
Cannot figure out why this function will not delete some values. I debugged with print statements and can see that if I try to remove a value, the function will enter the else block and successfully remove a node with two children. However, when attempting to remove a node with one or zero children, the codes executes with no errors, but when I print the tree to view its contents the node is still there.
The node to be removed will have at least one None child, and it seems straightforward to set the node equal to its right (or left) child, which I assume sets the node to None.
I have some experience with Java, but fairly new to Python, and I sometimes run into trouble with the "self" protocol, but I don't think that is the case here.

Categories