PAPER KHOJ
Practice Examination — Set 1
For BITM · Semester 4
Based on the prescribed TU Faculty of Management syllabus and examination structure
Candidates are required to give their answers in their own words as far as practicable. The figures in the margin indicate full marks.
Group 'A'
Brief Answer Questions. Attempt all questions.
[5 × 2 = 10]- [2]
Distinguish between mutable and immutable data types in Python with two concrete examples of each.
View model answerHide model answer
Mutable vs. Immutable Types in Python:
- Mutable Objects: In-place modifications to the memory contents are permitted without changing the object’s identity (
id()).- Examples:
list(e.g.[1, 2, 3]),dict(e.g.{"key": "val"}), andset.
- Examples:
- Immutable Objects: Once instantiated in heap memory, the internal value cannot be altered. Any operation that modifies the value allocates a new object with a distinct memory address.
- Examples:
int/float,str(e.g."Kathmandu"), andtuple(e.g.(10, 20)).
- Examples:
- Mutable Objects: In-place modifications to the memory contents are permitted without changing the object’s identity (
- [2]
Explain the LEGB rule for scope resolution in Python.
View model answerHide model answer
The LEGB Scope Resolution Rule:
Python resolves variable references by searching hierarchical namespaces in sequential priority order:
- L - Local: Variables declared inside the currently executing function/lambda.
- E - Enclosing: Namespaces of enclosing or nested outer functions (closures).
- G - Global: Variables defined at the top level of the module or explicitly bound with the
globalkeyword. - B - Built-in: Predefined core built-ins (e.g.,
len,range,ValueError,print).
- [2]
What is the difference between
isand==operators in Python?View model answerHide model answer
isvs.==Operators:==(Equality Operator): Evaluates whether the values/contents of two objects are equivalent by calling__eq__().- Example:
[1, 2] == [1, 2]evaluates toTrue.
- Example:
is(Identity Operator): Evaluates whether two variables reference the exact same memory address (id(a) == id(b)).- Example:
[1, 2] is [1, 2]evaluates toFalse(separate heap allocations).
- Example:
- [2]
How do list comprehensions differ from generator expressions in memory consumption?
View model answerHide model answer
List Comprehension vs. Generator Expression:
- List Comprehension (
[x for x in data]): Eagerly evaluates and allocates memory for the entire list in RAM at once (space complexity). - Generator Expression (
(x for x in data)): Lazily computes and yields one element on-demand per iteration using the iterator protocol (__next__()), consuming constantauxiliary memory regardless of dataset size.
- List Comprehension (
- [2]
What is the purpose of
*argsand**kwargsin Python function parameters?View model answerHide model answer
*argsand**kwargs:*args(Variable Positional Arguments): Packs an arbitrary number of non-keyword arguments into an immutabletuple.**kwargs(Variable Keyword Arguments): Packs an arbitrary number of named keyword arguments into a standard key-valuedict.- They provide dynamic variadic function signatures, widely utilized in wrapper decorators and subclass constructor overrides.
Group 'B'
Descriptive Answer Questions. Attempt any SIX questions.
[6 × 5 = 30]- [5]
Write a Python program that reads a text file, counts the frequency of each distinct word (ignoring case and punctuation), and writes the top 5 most frequent words to a JSON file.
View model answerHide model answer
Word Frequency Counter & JSON Exporter:
import json import re from collections import Counter def analyze_word_frequencies(input_filepath: str, output_filepath: str) -> None: try: with open(input_filepath, 'r', encoding='utf-8') as f: content = f.read() # Normalize text: convert to lowercase and extract alphabetic words words = re.findall(r'\b[a-z]+\b', content.lower()) # Calculate frequencies word_counts = Counter(words) top_5 = dict(word_counts.most_common(5)) # Output to JSON with open(output_filepath, 'w', encoding='utf-8') as out_f: json.dump({ "total_distinct_words": len(word_counts), "top_5_frequent_words": top_5 }, out_f, indent=4) print(f"Analysis complete. Top 5 words saved to {output_filepath}: {top_5}") except FileNotFoundError: print(f"Error: Input file '{input_filepath}' not found.") except Exception as e: print(f"An unexpected error occurred: {e}") if __name__ == "__main__": analyze_word_frequencies("sample.txt", "top_words.json")Key Engineering Details:
- Context Manager (
with open): Guarantees deterministic file descriptor closure even upon runtime exceptions. - Regular Expression (
\b[a-z]+\b): Strips out commas, periods, numbers, and quotation marks seamlessly. collections.Counter: Executes an optimalhash-map counting algorithm.
- Context Manager (
- [5]
Explain Python decorators. Write a custom decorator
@execution_timerthat calculates and prints the elapsed execution time of any decorated function in milliseconds.View model answerHide model answer
Python Decorators & Timing Implementation:
A decorator is a higher-order function that takes another function as an argument, extends its runtime behavior without modifying its source code, and returns a callable object.
import time from functools import wraps from typing import Callable, Any def execution_timer(func: Callable) -> Callable: @wraps(func) # Preserves func name, docstring, and annotations def wrapper(*args: Any, **kwargs: Any) -> Any: start_time = time.perf_counter() result = func(*args, **kwargs) end_time = time.perf_counter() elapsed_ms = (end_time - start_time) * 1000.0 print(f"[BENCHMARK] Function '{func.__name__}' executed in {elapsed_ms:.3f} ms") return result return wrapper # Demonstration @execution_timer def compute_heavy_sum(limit: int) -> int: return sum(x * x for x in range(limit)) if __name__ == "__main__": res = compute_heavy_sum(1_000_000) print(f"Result: {res}")Architectural Highlights:
@wraps(func): Essential for preventing introspection loss (wrapper.__name__stays"compute_heavy_sum").time.perf_counter(): Provides the highest available fractional resolution clock for elapsed wall-time measurement.
- [5]
Demonstrate Object-Oriented Programming in Python by implementing an abstract class
Employeeand subclassesFullTimeEmployeeandContractEmployeedemonstrating encapsulation and polymorphism.View model answerHide model answer
Abstract Classes and Polymorphism in Python:
from abc import ABC, abstractmethod class Employee(ABC): def __init__(self, emp_id: str, name: str, base_salary: float): self._emp_id = emp_id # Protected attribute self._name = name self._base_salary = base_salary @property def name(self) -> str: return self._name @abstractmethod def calculate_monthly_pay(self) -> float: """Abstract method enforced on all concrete subclasses.""" pass class FullTimeEmployee(Employee): def __init__(self, emp_id: str, name: str, base_salary: float, allowance: float): super().__init__(emp_id, name, base_salary) self.__allowance = allowance # Private attribute def calculate_monthly_pay(self) -> float: return self._base_salary + self.__allowance class ContractEmployee(Employee): def __init__(self, emp_id: str, name: str, hourly_rate: float, hours_worked: float): super().__init__(emp_id, name, 0.0) self.hourly_rate = hourly_rate self.hours_worked = hours_worked def calculate_monthly_pay(self) -> float: return self.hourly_rate * self.hours_worked # Polymorphic processing demo if __name__ == "__main__": staff: list[Employee] = [ FullTimeEmployee("EMP01", "Sujan Karki", 65000.0, 15000.0), ContractEmployee("CNT02", "Pooja Shrestha", 850.0, 120.0) ] for member in staff: print(f"Staff: {member.name:15} | Gross Pay: NPR {member.calculate_monthly_pay():,.2f}") - [5]
Explain the difference between shallow copy and deep copy in Python. Demonstrate with code modifying a nested list.
View model answerHide model answer
Shallow Copy vs. Deep Copy:
- Shallow Copy (
copy.copy()): Constructs a new compound object and populates it with references to the original object’s child elements. Mutating a nested collection mutates both objects. - Deep Copy (
copy.deepcopy()): Recursively copies all referenced compound objects, allocating independent memory for all child elements.
import copy original = [[10, 20], [30, 40]] shallow = copy.copy(original) deep = copy.deepcopy(original) # Mutate nested sub-list in original original[0][0] = 999 print(f"Original: {original}") # [[999, 20], [30, 40]] print(f"Shallow: {shallow}") # [[999, 20], [30, 40]] -> Reflects mutation! print(f"Deep: {deep}") # [[10, 20], [30, 40]] -> Completely isolated! - Shallow Copy (
- [5]
What are Python generator functions? How does the
yieldstatement operate, and why are generators memory efficient?View model answerHide model answer
Python Generators and the
yieldMechanism:A generator function contains one or more
yieldstatements. When invoked, it returns an iterator object without executing the body immediately.- State Preservation: When execution encounters
yield, the generator suspends execution, saves its local stack frame (variables, instruction pointer), and sends the evaluated value to the caller. - Resumption: When
next()is called again, execution resumes immediately after the suspendedyieldstatement. - Memory Efficiency: Instead of assembling a collection of millions of elements in RAM (
space), generators stream items lazily on demand ( space).
def fibonacci_stream(limit: int): a, b = 0, 1 count = 0 while count < limit: yield a a, b = b, a + b count += 1 # Consuming the stream for num in fibonacci_stream(6): print(num, end=" ") # Outputs: 0 1 1 2 3 5 - State Preservation: When execution encounters
- [5]
Explain how custom exception classes are created in Python. Write a program that defines
InsufficientFundsErrorand raises it in an ATM withdrawal class.View model answerHide model answer
Custom Exception Implementation:
class BankingException(Exception): """Base exception for domain banking errors.""" pass class InsufficientFundsError(BankingException): """Raised when an account balance is insufficient for a withdrawal.""" def __init__(self, current_balance: float, requested_amount: float): self.current_balance = current_balance self.requested_amount = requested_amount self.shortage = requested_amount - current_balance super().__init__(f"Withdrawal denied: Shortage of NPR {self.shortage:,.2f} " f"(Current Balance: NPR {current_balance:,.2f}, Attempted: NPR {requested_amount:,.2f})") class BankAccount: def __init__(self, account_no: str, balance: float): self.account_no = account_no self.balance = balance def withdraw(self, amount: float) -> float: if amount <= 0: raise ValueError("Withdrawal amount must be strictly positive.") if amount > self.balance: raise InsufficientFundsError(self.balance, amount) self.balance -= amount return self.balance # Execution & verification if __name__ == "__main__": acc = BankAccount("NABIL-0129", 5000.0) try: acc.withdraw(7500.0) except InsufficientFundsError as err: print(f"[REJECTED] {err}") except Exception as err: print(f"System Error: {err}")
Group 'C'
Comprehensive / Analytical Questions. Attempt all questions.
[2 × 10 = 20]- [10]
Design and implement a complete student grade management system in Python adhering to object-oriented principles. The system must support loading student records from a CSV file, computing individual GPAs, calculating class statistics (highest, lowest, average GPA), and exporting a formatted analytical report to a text file.
View model answerHide model answer
Complete Student Grade Management System:
import csv import statistics from typing import List, Dict class Student: def __init__(self, student_id: str, name: str, scores: Dict[str, float]): self.student_id = student_id self.name = name self.scores = scores def compute_gpa(self) -> float: """Converts percentage marks to a standard 4.0 GPA scale.""" avg_score = statistics.mean(self.scores.values()) if avg_score >= 90: return 4.0 elif avg_score >= 80: return 3.6 elif avg_score >= 70: return 3.2 elif avg_score >= 60: return 2.8 elif avg_score >= 50: return 2.4 else: return 0.0 class GradeBook: def __init__(self, course_name: str): self.course_name = course_name self.students: List[Student] = [] def load_from_csv(self, filepath: str) -> None: with open(filepath, mode='r', encoding='utf-8') as f: reader = csv.DictReader(f) for row in reader: s_id = row['id'] name = row['name'] # Assume subject headers start with 'sub_' scores = {col: float(val) for col, val in row.items() if col not in ('id', 'name')} self.students.append(Student(s_id, name, scores)) def generate_analytics(self) -> Dict[str, float]: if not self.students: return {} gpas = [s.compute_gpa() for s in self.students] return { "count": len(gpas), "highest_gpa": max(gpas), "lowest_gpa": min(gpas), "mean_gpa": statistics.mean(gpas), "median_gpa": statistics.median(gpas) } def export_report(self, output_path: str) -> None: stats = self.generate_analytics() with open(output_path, 'w', encoding='utf-8') as f: f.write(f"=== TU FoM Grade Report: {self.course_name} ===\n\n") f.write(f"{'ID':<10} {'Name':<20} {'GPA':<6}\n") f.write("-" * 40 + "\n") for s in self.students: f.write(f"{s.student_id:<10} {s.name:<20} {s.compute_gpa():<6.2f}\n") f.write("-" * 40 + "\n") f.write(f"Total Enrolled: {int(stats['count'])}\n") f.write(f"Class Average GPA: {stats['mean_gpa']:.2f}\n") f.write(f"Highest GPA: {stats['highest_gpa']:.2f}\n") f.write(f"Lowest GPA: {stats['lowest_gpa']:.2f}\n")Architectural Merits:
- Separation of Concerns:
Studenthandles individual domain logic;GradeBookorchestrates collection lifecycle and file serialization. - Robust CSV Ingestion: Leverages
csv.DictReaderto dynamically absorb arbitrary subject column names.
- Separation of Concerns:
- [10]
Explain the Global Interpreter Lock (GIL) in CPython. Compare Multithreading and Multiprocessing in Python. Write two complete benchmark programs: one executing a CPU-bound task using
multiprocessing.Pooland another executing an I/O-bound task usingconcurrent.futures.ThreadPoolExecutor.View model answerHide model answer
The CPython Global Interpreter Lock (GIL):
The GIL is a mutual-exclusion lock implemented in CPython that restricts execution of native bytecode to exactly one operating system thread at a time. It ensures memory safety in CPython’s reference-counting garbage collection.
- Impact on CPU-Bound Tasks: Pure multithreading cannot leverage multi-core CPU architectures. The threads contend for the single GIL lock, resulting in slower execution due to context-switching overhead.
- Impact on I/O-Bound Tasks: Highly effective because threads release the GIL while waiting for network socket responses, disk I/O, or database queries.
Comparison: Multithreading vs. Multiprocessing
Dimension Multithreading ( threading/ThreadPoolExecutor)Multiprocessing ( multiprocessing/ProcessPoolExecutor)Address Space Shared heap/memory between threads Isolated, distinct memory space per process GIL Bypass No (Bound by single GIL) Yes (Each process has its own independent Python interpreter and GIL) Overhead Low memory footprint; lightweight Higher memory overhead; IPC requires serialization (pickling) Ideal Use Case Network requests, file I/O, GUI responsiveness High-performance numerical computing, image processing, cryptography
Implementation 1: CPU-Bound Processing via
multiprocessing.Poolfrom multiprocessing import Pool, cpu_count import time def count_primes_in_range(range_tuple: tuple[int, int]) -> int: start, end = range_tuple primes_count = 0 for n in range(start, end): if n > 1: is_prime = True for i in range(2, int(n**0.5) + 1): if n % i == 0: is_prime = False break if is_prime: primes_count += 1 return primes_count def run_cpu_benchmark(): chunks = [(1, 500000), (500001, 1000000), (1000001, 1500000), (1500001, 2000000)] t0 = time.perf_counter() with Pool(processes=cpu_count()) as pool: results = pool.map(count_primes_in_range, chunks) t1 = time.perf_counter() print(f"[MULTIPROCESSING CPU] Total Primes: {sum(results)} in {t1 - t0:.3f} s")
Implementation 2: I/O-Bound Processing via
ThreadPoolExecutorfrom concurrent.futures import ThreadPoolExecutor, as_completed import time import urllib.request def fetch_url_status(url: str) -> tuple[str, int]: req = urllib.request.Request(url, headers={'User-Agent': 'BITM-Bot/1.0'}) try: with urllib.request.urlopen(req, timeout=5) as response: return url, response.status except Exception as e: return url, 500 def run_io_benchmark(): urls = [ "https://www.google.com", "https://www.github.com", "https://www.python.org", "https://www.wikipedia.org" ] t0 = time.perf_counter() with ThreadPoolExecutor(max_workers=4) as executor: futures = [executor.submit(fetch_url_status, u) for u in urls] for f in as_completed(futures): url, status = f.result() print(f"[THREAD I/O] {url} -> HTTP {status}") t1 = time.perf_counter() print(f"[THREADING I/O] Total Elapsed: {t1 - t0:.3f} s")