AISE501_CLASS/AST Files/ex02_class_methods_attributes.py
2026-05-03 20:27:09 +02:00

224 lines
8.0 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""
Exercise 2 Analyse Class Methods and Attributes
==================================================
AISE501 · AST Exercises · Spring Semester 2026
Learning goals
--------------
* Use ``ast.NodeVisitor`` to build a targeted traversal.
* Extract method signatures (parameters, return annotations).
* Identify instance attributes assigned in ``__init__``.
* Distinguish between regular methods and static methods.
Tasks
-----
Part A Use a NodeVisitor to collect methods per class (TODOs 1-2).
Part B Extract the parameter list for each method (TODOs 3-4).
Part C Find instance attributes set in __init__ (TODOs 5-6).
Part D Detect @staticmethod decorators (TODOs 7-8).
"""
import ast
from pathlib import Path
SOURCE_FILE = Path(__file__).parent / "sample_stats.py"
source_code = SOURCE_FILE.read_text()
tree = ast.parse(source_code)
# ── Part A: Collect Methods Per Class with NodeVisitor ──────────────────────
print("=" * 60)
print("Part A Methods per class (NodeVisitor)")
print("=" * 60)
# TODO 1: Complete the ClassMethodVisitor.
# In visit_ClassDef, iterate over node.body and collect every
# FunctionDef into a list. Store the result in self.classes
# as a dict mapping class_name -> list of method names.
#
# Hint: Don't forget to call self.generic_visit(node) at the end
# so that nested classes (if any) are also visited.
class ClassMethodVisitor(ast.NodeVisitor):
def __init__(self):
self.classes: dict[str, list[ast.FunctionDef]] = {}
def visit_ClassDef(self, node: ast.ClassDef):
# TODO: collect method names and store in self.classes
functions = [func for func in node.body if isinstance(func, ast.FunctionDef)]
self.classes[node.name] = functions
self.generic_visit(node)
# TODO 2: Instantiate the visitor, call visitor.visit(tree),
# and print each class with its methods.
visitor = ClassMethodVisitor()
visitor.visit(tree)
for cls_name, methods in visitor.classes.items():
print(f"\n class {cls_name}:")
for m in methods:
print(f" - {m.name}()")
# ── Part B: Extract Method Signatures ───────────────────────────────────────
# For each method, extract its parameter names (excluding 'self')
# and any type annotations.
print("\n" + "=" * 60)
print("Part B Method signatures")
print("=" * 60)
# TODO 3: Write a function `get_signature(func_node)` that returns a string
# representation of the function's parameters.
#
# For each parameter in func_node.args.args:
# - Skip 'self'
# - Get the parameter name from arg.arg
# - If arg.annotation exists, unparse it with ast.unparse()
# - Format as "name: type" or just "name"
#
# Also check func_node.returns for a return annotation.
#
# Example output: "(data: np.ndarray, z_threshold: float) -> np.ndarray"
def get_signature(func_node: ast.FunctionDef) -> str:
"""Return a string like '(param1: Type, param2) -> ReturnType'."""
params = []
for arg in func_node.args.args:
# Skip 'self'
if arg.arg == "self":
continue
# Get parameter name
name = arg.arg
# Check for type annotation
if arg.annotation:
type_hint = ast.unparse(arg.annotation)
params.append(f"{name}: {type_hint}")
else:
params.append(name)
return_type = ast.unparse(func_node.returns) if func_node.returns else None
return f"({", ".join(params)}) -> {return_type}"
# TODO 4: For each class and method, print the full signature.
# Reuse the visitor results from Part A.
for cls_name, methods in visitor.classes.items():
print(f"\n class {cls_name}:")
# You need the actual FunctionDef nodes, not just names.
# Hint: walk the tree again or modify the visitor to store nodes.
for m in methods:
print(f" - {get_signature(m)}")
# ── Part C: Find Instance Attributes ───────────────────────────────────────
# Instance attributes are typically assigned in __init__ as self.xxx = ...
print("\n" + "=" * 60)
print("Part C Instance attributes (self.xxx in __init__)")
print("=" * 60)
# TODO 5: Write a function `find_instance_attributes(class_node)` that
# returns a list of attribute names assigned via self.xxx = ...
# in the __init__ method.
#
# Approach:
# 1. Find the __init__ method in class_node.body.
# 2. Walk through the __init__ body looking for ast.Assign or
# ast.AnnAssign nodes.
# 3. For Assign: check if any target is an ast.Attribute where
# target.value is ast.Name(id='self').
# 4. Collect the attribute names (target.attr).
def find_instance_attributes(class_node: ast.ClassDef) -> list[str]:
"""Return attribute names assigned as self.xxx in __init__."""
attributes = []
# 1. Find __init__
init_method = None
for node in class_node.body:
if isinstance(node, ast.FunctionDef) and node.name == "__init__":
init_method = node
break
if not init_method:
return attributes
# 2. Walk __init__ body
for node in ast.walk(init_method):
# 3a. Handle regular assignment: self.x = ...
if isinstance(node, ast.Assign):
for target in node.targets:
if (isinstance(target, ast.Attribute) and
isinstance(target.value, ast.Name) and
target.value.id == "self"):
attributes.append(target.attr)
# 3b. Handle annotated assignment: self.x: int = ...
elif isinstance(node, ast.AnnAssign):
target = node.target
if (isinstance(target, ast.Attribute) and
isinstance(target.value, ast.Name) and
target.value.id == "self"):
attributes.append(target.attr)
return attributes
# TODO 6: For each class, print its instance attributes.
for node in ast.walk(tree):
if isinstance(node, ast.ClassDef):
attrs = find_instance_attributes(node)
print(f"\n class {node.name}:")
for attr in attrs:
print(f" self.{attr}")
# ── Part D: Detect Static Methods ──────────────────────────────────────────
print("\n" + "=" * 60)
print("Part D Static methods")
print("=" * 60)
# TODO 7: Write a function `is_static_method(func_node)` that returns True
# if the function has a @staticmethod decorator.
#
# Hint: Decorators are in func_node.decorator_list.
# Each decorator is an ast.Name node (for simple decorators).
# Check if any decorator has .id == "staticmethod".
def is_static_method(func_node: ast.FunctionDef) -> bool:
"""Return True if *func_node* is decorated with @staticmethod."""
return any([ast.unparse(dec) == "staticmethod" for dec in func_node.decorator_list])
# TODO 8: For each class, list its static methods separately.
for node in ast.walk(tree):
if isinstance(node, ast.ClassDef):
statics = [m.name for m in node.body
if isinstance(m, ast.FunctionDef) and is_static_method(m)]
regulars = [m.name for m in node.body
if isinstance(m, ast.FunctionDef) and not is_static_method(m)]
print(f"\n class {node.name}:")
print(f" Regular methods : {regulars}")
print(f" Static methods : {statics}")
# ── Expected Output (abbreviated) ──────────────────────────────────────────
# Part A: Each class with its method list.
# Part B: Full signatures, e.g. "remove_outliers(z_threshold: float) -> np.ndarray"
# Part C: DataCleaner -> self.raw_data, self.cleaned
# DescriptiveStats -> self.data
# etc.
# Part D: CurveFitter has static methods: linear_model, quadratic_model, exponential_model