ISSUES CLOSED: #2
20 KiB
Class Hierarchy Navigation
The class hierarchy is the core feature of CleverRDFLib, providing object-oriented navigation of ontology class structures without requiring SPARQL queries.
Class Hierarchy Structure
The class hierarchy is built on a foundation of node types that represent different ontology elements:
-
Node: Abstract base class for all hierarchy nodes. Cannot be instantiated directly.ClassNode: Represents OWL classes in the hierarchyDataTypeNode: Represents datatypes (e.g., xsd:string, xsd:integer)
-
PropertyNode: Abstract base class for all properties. Cannot be instantiated directly.ObjectPropertyNode: Represents OWL object properties that link classes to other classesDatatypePropertyNode: Represents OWL datatype properties that link classes to datatypes
All nodes implement the OntologyElement interface and support the Visitor pattern through the accept() method.
Building the Hierarchy
The class hierarchy is automatically built when you access it:
from cleverrdf_lib import OntologyLoader
loader = OntologyLoader()
result = loader.load_ontology("ontology.ttl")
# Access the hierarchy (automatically builds if not already built)
hierarchy = result.class_hierarchy
Manual Building
You can also build the hierarchy manually from a graph:
from cleverrdf_lib.core.class_hierarchy import ClassHierarchy
from rdflib import Graph
graph = Graph()
# ... populate graph ...
hierarchy = ClassHierarchy(graph)
# Build with read-only nodes (default behavior)
hierarchy.build()
# Or explicitly build with mutable nodes if you need to modify them
hierarchy.build(read_only=False)
Navigating Classes
Getting All Classes
# Get all class IRIs
all_iris = hierarchy.get_all_classes_iris()
print(f"Total classes: {len(all_iris)}")
# Get all ClassNode instances as a dictionary (IRI -> ClassNode)
all_classes = hierarchy.get_all_classes()
for class_iri, class_node in all_classes.items():
print(f"Class: {class_node.iri}")
# Or iterate over just the ClassNode instances
for class_node in hierarchy.get_all_classes().values():
print(f"Class: {class_node.iri}")
# Or use get_all_classes_as_list() for a list
all_classes_list = hierarchy.get_all_classes_as_list()
for class_node in all_classes_list:
print(f"Class: {class_node.iri}")
Finding a Specific Class
# Get a class by IRI from the dictionary
class_iri = "http://example.org/ontology#Person"
all_classes = hierarchy.get_all_classes()
class_node = all_classes.get(class_iri) # Returns None if not found
if class_node:
print(f"Found class: {class_node.iri}")
print(f"Label: {class_node.label}")
print(f"Comment: {class_node.comment}")
else:
print(f"Class not found: {class_iri}")
Root and Leaf Classes
# Get root classes (classes with no parents)
root_classes = hierarchy.get_all_root_classes()
for root in root_classes:
print(f"Root class: {root.iri}")
# Get leaf classes (classes with no children)
leaf_classes = hierarchy.get_all_leaf_classes()
for leaf in leaf_classes:
print(f"Leaf class: {leaf.iri}")
ClassNode Properties
Each ClassNode provides rich navigation capabilities:
Basic Properties
# Get a class by IRI
class_iri = "http://example.org/ontology#Person"
class_node = hierarchy.get_all_classes().get(class_iri)
if class_node:
# IRI
print(f"IRI: {class_node.iri}")
# Label and comment
print(f"Label: {class_node.label}")
print(f"Comment: {class_node.comment}")
# Check if root or leaf
print(f"Is root: {class_node.is_root()}")
print(f"Is leaf: {class_node.is_leaf()}")
print(f"Is datatype: {class_node.is_datatype()}")
Parent-Child Relationships
# Get parents
parents = class_node.parents # Returns frozenset[ClassNode]
print(f"Parent classes:")
for parent in parents:
print(f" - {parent.iri}")
# Get children
children = class_node.children # Returns frozenset[ClassNode]
print(f"Child classes:")
for child in children:
print(f" - {child.iri}")
Object Properties
Object properties link classes to other classes:
# Properties where this class is the domain
domain_properties = class_node.object_properties_as_domain
print(f"Properties with this class as domain:")
for prop_iri, prop_node in domain_properties.items():
print(f" - {prop_iri}")
print(f" Range: {prop_node.range.iri if prop_node.range else 'None'}")
# Properties where this class is the range
range_properties = class_node.object_properties_as_range
print(f"Properties with this class as range:")
for prop_iri, prop_node in range_properties.items():
print(f" - {prop_iri}")
print(f" Domain: {prop_node.domain.iri if prop_node.domain else 'None'}")
Datatype Properties
Datatype properties link classes to datatypes:
# Datatype properties where this class is the domain
datatype_props = class_node.datatype_properties_as_domain
print(f"Datatype properties:")
for prop_iri, prop_node in datatype_props.items():
print(f" - {prop_iri}")
print(f" Range: {prop_node.range.iri if prop_node.range else 'None'}")
PropertyNode Properties
Properties themselves can be navigated. CleverRDFLib distinguishes between object properties and datatype properties:
Object Properties
# Get all object properties from the hierarchy
object_properties = hierarchy.get_all_object_properties() # Returns frozendict[str, ObjectPropertyNode]
object_properties_list = hierarchy.get_all_object_properties_as_list() # Returns list[ObjectPropertyNode]
# Iterate over dictionary values
for prop in object_properties.values():
print(f"Property: {prop.iri}")
print(f" Type: {prop.ptype}") # PropertyType.Object
print(f" Type IRI: {prop.type_iri}")
print(f" Label: {prop.label}")
print(f" Comment: {prop.comment}")
if prop.domain:
print(f" Domain: {prop.domain.iri}")
if prop.range:
print(f" Range: {prop.range.iri}") # Range is a ClassNode for object properties
Datatype Properties
# Get all datatype properties from the hierarchy
datatype_properties = hierarchy.get_all_datatype_properties() # Returns frozendict[str, DatatypePropertyNode]
datatype_properties_list = hierarchy.get_all_datatype_properties_as_list() # Returns list[DatatypePropertyNode]
# Iterate over dictionary values
for prop in datatype_properties.values():
print(f"Property: {prop.iri}")
print(f" Type: {prop.ptype}") # PropertyType.DataType
print(f" Type IRI: {prop.type_iri}")
print(f" Label: {prop.label}")
print(f" Comment: {prop.comment}")
if prop.domain:
print(f" Domain: {prop.domain.iri}")
if prop.range:
print(f" Range: {prop.range.iri}") # Range is a DataTypeNode for datatype properties
Getting All Properties
If you need both object and datatype properties together:
# Get both types separately and combine
object_props = hierarchy.get_all_object_properties()
datatype_props = hierarchy.get_all_datatype_properties()
# Combine if needed
all_properties = {**object_props, **datatype_props}
Property Node Types
CleverRDFLib provides two concrete property node types:
ObjectPropertyNode: Represents OWL object properties that link classes to other classesDatatypePropertyNode: Represents OWL datatype properties that link classes to datatypes
Both inherit from the abstract PropertyNode base class. The PropertyNode class cannot be instantiated directly - use the concrete subclasses instead.
Traversing the Hierarchy
Depth-First Traversal
def traverse_depth_first(class_node, visited=None):
"""Traverse the hierarchy depth-first."""
if visited is None:
visited = set()
if class_node.iri in visited:
return
visited.add(class_node.iri)
print(f"Visiting: {class_node.iri}")
# Traverse children
for child in class_node.children:
traverse_depth_first(child, visited)
# Start from root classes
for root in hierarchy.get_all_root_classes():
traverse_depth_first(root)
Breadth-First Traversal
from collections import deque
def traverse_breadth_first(root_class):
"""Traverse the hierarchy breadth-first."""
queue = deque([root_class])
visited = set()
while queue:
current = queue.popleft()
if current.iri in visited:
continue
visited.add(current.iri)
print(f"Visiting: {current.iri}")
# Add children to queue
for child in current.children:
queue.append(child)
# Start from root classes
for root in hierarchy.get_all_root_classes():
traverse_breadth_first(root)
Finding Ancestors
def get_ancestors(class_node):
"""Get all ancestor classes (parents, grandparents, etc.)."""
ancestors = set()
def collect_ancestors(node):
for parent in node.parents:
if parent.iri not in ancestors:
ancestors.add(parent.iri)
collect_ancestors(parent)
collect_ancestors(class_node)
return ancestors
# Get a class by IRI
class_iri = "http://example.org/ontology#Employee"
class_node = hierarchy.get_all_classes().get(class_iri)
if class_node:
ancestors = get_ancestors(class_node)
print(f"Ancestors: {ancestors}")
Finding Descendants
def get_descendants(class_node):
"""Get all descendant classes (children, grandchildren, etc.)."""
descendants = set()
def collect_descendants(node):
for child in node.children:
if child.iri not in descendants:
descendants.add(child.iri)
collect_descendants(child)
collect_descendants(class_node)
return descendants
# Get a class by IRI
class_iri = "http://example.org/ontology#Person"
class_node = hierarchy.get_all_classes().get(class_iri)
if class_node:
descendants = get_descendants(class_node)
print(f"Descendants: {descendants}")
Visitor Pattern
The class hierarchy supports the Visitor pattern for traversing and processing ontology elements. All elements (ClassNode, ObjectPropertyNode, DatatypePropertyNode, DataTypeNode) implement the OntologyElement interface with an accept method.
Visitor Interface
The ClassHierarchyVisitor interface defines the visitor methods:
from cleverrdf_lib.core.interfaces.interfaces import ClassHierarchyVisitor, OntologyElement
class MyVisitor(ClassHierarchyVisitor):
"""Custom visitor for processing ontology elements."""
def visit_class(self, cls: OntologyElement) -> None:
"""Called when visiting a ClassNode."""
print(f"Visiting class: {cls.iri}")
# Process the class node
def visit_datatype(self, dt: OntologyElement) -> None:
"""Called when visiting a DataTypeNode."""
print(f"Visiting datatype: {dt.iri}")
# Process the datatype node
def visit_object_property(self, prop: OntologyElement) -> None:
"""Called when visiting an ObjectPropertyNode."""
print(f"Visiting object property: {prop.iri}")
# Process the object property node
def visit_datatype_property(self, prop: OntologyElement) -> None:
"""Called when visiting a DatatypePropertyNode."""
print(f"Visiting datatype property: {prop.iri}")
# Process the datatype property node
Using the Visitor
from cleverrdf_lib.core.interfaces.interfaces import ClassHierarchyVisitor
class StatisticsVisitor(ClassHierarchyVisitor):
"""Visitor that collects statistics about the hierarchy."""
def __init__(self):
self.class_count = 0
self.object_property_count = 0
self.datatype_property_count = 0
self.datatype_count = 0
def visit_class(self, cls: OntologyElement) -> None:
self.class_count += 1
def visit_datatype(self, dt: OntologyElement) -> None:
self.datatype_count += 1
def visit_object_property(self, prop: OntologyElement) -> None:
self.object_property_count += 1
def visit_datatype_property(self, prop: OntologyElement) -> None:
self.datatype_property_count += 1
# Create visitor
visitor = StatisticsVisitor()
# Visit all classes (iterate over dictionary values)
for class_node in hierarchy.get_all_classes().values():
class_node.accept(visitor)
# Visit all object properties
for prop in hierarchy.get_all_object_properties().values():
prop.accept(visitor)
# Visit all datatype properties
for prop in hierarchy.get_all_datatype_properties().values():
prop.accept(visitor)
# Access collected statistics
print(f"Classes: {visitor.class_count}")
print(f"Object properties: {visitor.object_property_count}")
print(f"Datatype properties: {visitor.datatype_property_count}")
print(f"Datatypes: {visitor.datatype_count}")
Traversing with Visitor
You can combine the visitor pattern with hierarchy traversal:
from cleverrdf_lib.core.interfaces.interfaces import ClassHierarchyVisitor, OntologyElement
class HierarchyTraversingVisitor(ClassHierarchyVisitor):
"""Visitor that traverses the entire hierarchy."""
def __init__(self):
self.visited = set()
def visit_class(self, cls: OntologyElement) -> None:
if cls.iri in self.visited:
return
self.visited.add(cls.iri)
print(f"Visiting class: {cls.iri}")
# Visit children recursively
for child in cls.children:
child.accept(self)
def visit_datatype(self, dt: OntologyElement) -> None:
print(f"Visiting datatype: {dt.iri}")
def visit_object_property(self, prop: OntologyElement) -> None:
print(f"Visiting object property: {prop.iri}")
def visit_datatype_property(self, prop: OntologyElement) -> None:
print(f"Visiting datatype property: {prop.iri}")
# Start traversal from root classes
visitor = HierarchyTraversingVisitor()
for root in hierarchy.get_all_root_classes():
root.accept(visitor)
The Visitor pattern provides a clean way to separate traversal logic from element processing, making it easy to implement different operations on the hierarchy without modifying the element classes.
Hierarchy Statistics
from cleverrdf_lib.core.class_hierarchy import PropertyType
# Get hierarchy statistics
all_classes = hierarchy.get_all_classes() # Returns frozendict[str, ClassNode]
root_classes = hierarchy.get_all_root_classes() # Returns list[ClassNode]
leaf_classes = hierarchy.get_all_leaf_classes() # Returns list[ClassNode]
object_properties = hierarchy.get_all_object_properties() # Returns frozendict[str, ObjectPropertyNode]
datatype_properties = hierarchy.get_all_datatype_properties() # Returns frozendict[str, DatatypePropertyNode]
all_datatypes = hierarchy.get_all_datatypes_as_list() # Returns list[DataTypeNode]
print(f"Total classes: {len(all_classes)}")
print(f"Root classes: {len(root_classes)}")
print(f"Leaf classes: {len(leaf_classes)}")
print(f"Object properties: {len(object_properties)}")
print(f"Datatype properties: {len(datatype_properties)}")
print(f"Total datatypes: {len(all_datatypes)}")
Read-Only Mode
Default Read-Only Behavior
By default, the hierarchy is built with all nodes as read-only to prevent accidental modifications:
from cleverrdf_lib.core.class_hierarchy import ClassHierarchy
from rdflib import Graph
graph = Graph()
# ... populate graph ...
hierarchy = ClassHierarchy(graph)
# Build with read-only=True by default
hierarchy.build()
# All class nodes and property nodes are automatically read-only
# Attempting to modify will raise ReadOnlyPropertyException
class_node = hierarchy.get_all_classes().values().__iter__().__next__()
# class_node.add_parent(other_class) # Raises ReadOnlyPropertyException
# class_node.set_label("new label") # Raises ReadOnlyPropertyException
object_prop_node = hierarchy.get_all_object_properties().values().__iter__().__next__()
# object_prop_node.set_label("new label") # Raises ReadOnlyPropertyException
datatype_prop_node = hierarchy.get_all_datatype_properties().values().__iter__().__next__()
# datatype_prop_node.set_label("new label") # Raises ReadOnlyPropertyException
Building with Mutable Nodes
If you need to modify nodes after building, you can explicitly build with mutable nodes:
from cleverrdf_lib.core.class_hierarchy import ClassHierarchy
from rdflib import Graph
graph = Graph()
# ... populate graph ...
hierarchy = ClassHierarchy(graph)
# Build with mutable nodes
hierarchy.build(read_only=False)
# Now you can modify nodes
class_node = hierarchy.get_all_classes().values().__iter__().__next__()
class_node.set_label("Modified Label") # Works fine
# You can later make individual nodes read-only if needed
class_node.make_read_only()
# class_node.set_label("Another Label") # Now raises ReadOnlyPropertyException
Manually Making Nodes Read-Only
Even when building with mutable nodes, you can selectively make individual nodes read-only:
from cleverrdf_lib.core.class_hierarchy import ClassHierarchy, PropertyType
from rdflib import Graph
graph = Graph()
# ... populate graph ...
hierarchy = ClassHierarchy(graph)
hierarchy.build(read_only=False)
# Make specific class nodes read-only
for class_node in hierarchy.get_all_classes().values():
if class_node.iri.startswith("http://example.org/protected/"):
class_node.make_read_only()
# Make specific property nodes read-only
for prop_node in hierarchy.get_all_object_properties().values():
prop_node.make_read_only()
for prop_node in hierarchy.get_all_datatype_properties().values():
prop_node.make_read_only()
Read-Only Operations
When a node is read-only, the following operations will raise ReadOnlyPropertyException:
For ClassNode:
add_parent(parent)- Cannot add parent relationshipsadd_child(child)- Cannot add child relationshipsset_label(label)- Cannot modify labelset_comment(comment)- Cannot modify commentadd_object_property_as_domain(property_node)- Cannot add object propertiesadd_object_property_as_range(property_node)- Cannot add object propertiesadd_datatype_property_as_domain(property_node)- Cannot add datatype properties
For ObjectPropertyNode:
set_domain(domain_class)- Cannot modify domainset_range(range_class)- Cannot modify range (ClassNode)set_type_iri(type_iri)- Cannot modify type IRIset_label(label)- Cannot modify labelset_comment(comment)- Cannot modify comment
For DatatypePropertyNode:
set_domain(domain_class)- Cannot modify domainset_range(range_datatype)- Cannot modify range (DataTypeNode)set_type_iri(type_iri)- Cannot modify type IRIset_label(label)- Cannot modify labelset_comment(comment)- Cannot modify comment
Note: Read-only mode only prevents modifications to the node structure. You can still navigate and read all properties of read-only nodes.
Memory Management
To free memory, you can forget the underlying graph:
# After building, you can forget the graph to save memory
hierarchy.forget_graph()
# The hierarchy remains usable, but the graph is no longer accessible
Best Practices
- Lazy building: The hierarchy builds automatically when accessed
- Use frozensets: Parent and child sets are immutable for safety
- Check for None: Always check if
.get(iri)returns None when looking up classes by IRI - Use the dictionary API: Access classes efficiently using
hierarchy.get_all_classes().get(iri) - Traverse efficiently: Use appropriate traversal algorithms for your use case
- Read-only by default: Hierarchies are built as read-only by default to prevent accidental modifications. Use
build(read_only=False)only when you specifically need to modify the hierarchy structure - Selective mutability: If you need to modify only specific nodes, build with
read_only=Falseand then selectively callmake_read_only()on nodes that should be protected
Next Steps
- Learn about Class Hierarchy Conversion
- Explore Loading Result API
- Understand Observers API