Unit 3: Data Structures, Classes, and Inheritance

CSR101 — Python Programming 11 min read

I. Orientation

Python programming combines built-in collection types with object-oriented mechanisms for organizing data and behavior. Data structures store values, while classes define reusable models whose instances possess attributes and methods. Python’s dynamic typing, object references, and emphasis on iteration connect these topics closely.

  • Objects and references: Every value is an object; a variable such as x = [1, 2] refers to a list object rather than containing an independent copy.
  • Mutability: Mutable objects can change in place; immutable objects require creation of a new object for a changed value.
  • Iteration: for loops and tools such as enumerate(), zip(), and itertools process iterable objects systematically.
  • Abstraction: Classes package data and operations, while inheritance reuses or specializes existing class definitions.
  • Python conventions: Indentation defines blocks, self refers to the current instance, and names beginning with _ conventionally indicate internal use.

II. Lists — Ordered, mutable collections

A list is an ordered, zero-indexed collection that can contain duplicate and mixed-type values.

A. Lists

Lists support creation, indexing, slicing, insertion, deletion, and in-place updates.

  • Creation: numbers = [10, 20, 30]; list("cat") creates ['c', 'a', 't'].
  • Access: numbers[0] is 10; numbers[-1] is 30; numbers[1:] produces [20, 30].
  • Manipulation: append(40) adds one item, extend([50, 60]) adds several, and insert(1, 15) inserts at index 1.
  • Removal: remove(20) deletes the first matching value, while pop() removes and returns the last item.
  • Mutability: numbers[0] = 5 changes the existing list object; list methods such as sort() and reverse() usually return None.

B. List comprehension

A list comprehension creates a list through a compact expression containing iteration and optional filtering.

  • Basic form: [expression for item in iterable]; [x * x for x in range(4)] gives [0, 1, 4, 9].
  • Condition: [x for x in range(10) if x % 2 == 0] retains only even values.
  • Nested processing: [a + b for a in [1, 2] for b in [10, 20]] creates four sums.
  • Readability: Comprehensions are best for simple transformations; several nested conditions may be clearer as ordinary loops.

III. Tuples — Immutable sequences

A tuple is an ordered sequence whose structure cannot be changed after creation.

A. Tuples

Tuples support indexing, slicing, unpacking, and repeated or mixed values, but not item assignment.

  • Creation: point = (3, 4); a one-item tuple requires a comma: single = (7,).
  • Access: point[0] returns 3, and len(point) returns 2.
  • Unpacking: x, y = point assigns x = 3 and y = 4; starred unpacking can collect remaining values.
  • Immutability: point[0] = 8 raises TypeError, making tuples suitable for fixed records and dictionary keys when their contents are hashable.
  • Nested mutability: A tuple may contain a mutable list, such as t = ([1], 2); t[0].append(3) is allowed because the list changes, not the tuple structure.

IV. Dictionaries — Key–value mappings

A dictionary maps unique, hashable keys to values and preserves insertion order in modern Python.

A. Dictionaries

Dictionaries provide efficient lookup by key and can be created, updated, traversed, and deleted.

  • Creation: student = {"name": "Mira", "mark": 88}; dict(city="Pune") creates {"city": "Pune"}.
  • Access: student["mark"] returns 88; student.get("age", 0) avoids an exception when "age" is absent.
  • Manipulation: student["mark"] = 91 updates a value; student["passed"] = True adds a key.
  • Traversal: student.items() yields key–value pairs, while for key, value in student.items(): processes both.
  • Mutability: Dictionaries change in place, but keys must be hashable; lists cannot be keys, whereas strings and tuples of immutable values can be.

V. Sets — Unique unordered collections

Sets store unique hashable elements and are useful for membership testing and mathematical operations.

A. Sets: creation, manipulation, and mutability

Sets can be changed in place, while frozenset provides an immutable alternative.

  • Creation: a = {1, 2, 3}; set([1, 1, 2]) produces {1, 2}; {} creates an empty dictionary, so use set() for an empty set.
  • Manipulation: add(4) inserts an item, discard(2) removes it without failure if absent, and remove(2) raises KeyError if absent.
  • Operations: For sets A and B, A | B is union, A & B intersection, A - B difference, and A ^ B symmetric difference.
  • Mutability: A.update(B) changes A; frozenset(A) cannot be modified and may be used as a dictionary key.
  • Ordering: Set iteration order should not be relied upon; use sorted(A) when ordered output is required.

VI. Functional and Iteration Tools

Python supplies functions that transform, select, combine, and lazily traverse iterable data.

A. Map

map() applies a function to every item and returns a lazy iterator.

  • Form: map(function, iterable); list(map(str.upper, ["a", "b"])) produces ['A', 'B'].
  • Multiple inputs: map(pow, [2, 3], [3, 2]) computes 2**3 and 3**2; iteration stops at the shortest input.
  • Efficiency: Results are generated when consumed, avoiding an immediate result list.

B. Filter

filter() retains items for which a function returns a truthy value.

  • Form: filter(predicate, iterable); list(filter(lambda n: n > 5, [3, 6, 8])) gives [6, 8].
  • Predicate: A predicate returns True or False; filter(None, values) removes false-like values such as 0 and "".
  • Comparison: A comprehension such as [n for n in values if n > 5] is often more readable.

C. Reduce

reduce() repeatedly combines items into one result and is imported from functools.

  • Form: reduce(function, iterable, initializer); reduce(lambda a, b: a + b, [1, 2, 3], 0) returns 6.
  • Process: The function first combines 0 with 1, then the result with 2, and finally with 3.
  • Limitation: Built-ins such as sum() are clearer for common operations; use reduce() when a genuine cumulative combination is needed.

D. Cartesian product

A Cartesian product contains every ordered combination formed by selecting one item from each input.

  • Definition: For A = [1, 2] and B = ['x', 'y'], the product is (1, 'x), (1, 'y'), (2, 'x'), and (2, 'y').
  • Python tool: itertools.product(A, B) generates these pairs lazily.
  • Size: If input lengths are m and n, the result has m × n combinations.

E. Itertools

The itertools module provides efficient iterator building blocks.

  • Infinite tools: count(10, 2) generates 10, 12, 14, ...; combine it with islice() to impose a limit.
  • Selection tools: chain([1, 2], [3]) joins iterables, while islice(range(10), 2, 5) selects indices 2 through 4.
  • Grouping: groupby() groups consecutive equal-key items, so data generally must be sorted by the same key first.
  • Laziness: Most tools produce values only when requested, reducing memory use for large streams.

VII. Built-in Evaluation and Pairing Tools

These functions interpret expressions or organize iterable positions and relationships.

A. eval

eval() evaluates a string containing a Python expression and returns its value.

  • Example: eval("2 + 3 * 4") returns 14; expressions may include literals, operators, and function calls.
  • Scope: Optional globals and locals dictionaries control available names.
  • Safety: Never evaluate untrusted input because eval() can access dangerous operations through Python expressions.

B. exec

exec() executes Python statements from a string and normally returns None.

  • Example: exec("x = 5\nprint(x)") assigns and prints 5.
  • Capability: Unlike eval(), it can execute assignments, loops, imports, and function definitions.
  • Safety: Untrusted strings must never be passed to exec(); prefer explicit parsing or predefined operations.

C. enumerate

enumerate() produces index–value pairs while iterating.

  • Example: for i, item in enumerate(["a", "b"], start=1): produces (1, "a") and (2, "b").
  • Advantage: It avoids manually maintaining and incrementing a counter.

D. zip

zip() combines corresponding elements from multiple iterables.

  • Example: dict(zip(["a", "b"], [1, 2])) creates {"a": 1, "b": 2}.
  • Length rule: Normal zip() stops at the shortest input; zip(..., strict=True) can report mismatched lengths in supported Python versions.

E. copy

Copying determines whether nested objects remain shared.

  • Assignment: b = a creates another reference to the same object; changing b changes a.
  • Shallow copy: b = a.copy() duplicates the outer list, but nested lists remain shared.
  • Deep copy: copy.deepcopy(a) recursively copies nested objects; it is imported from copy and may be expensive.

VIII. Classes and Object Construction

Classes define the blueprint and behavior of related objects.

A. Introduction to classes

A class combines state and behavior through attributes and methods.

  • Definition: class Car: begins a class block; an instance is created with Car().
  • Purpose: A Car class can represent data such as color and actions such as start().
  • Encapsulation: Methods provide controlled operations instead of exposing every implementation detail.

B. Constructors

The constructor initializes a newly created instance, conventionally through __init__().

  • Example: def __init__(self, name): self.name = name stores the supplied name.
  • Invocation: p = Person("Asha") automatically calls Person.__init__(p, "Asha").
  • Special method: __init__() initializes an object; object allocation is handled separately by __new__().

C. Methods

Methods are functions defined inside a class and usually receive the instance as self.

  • Instance method: def area(self): return self.width * self.height reads instance data.
  • Class method: @classmethod receives cls and can construct or modify class-level state.
  • Static method: @staticmethod receives neither automatic self nor cls and is suitable for utility logic related to the class.

D. Attributes

Attributes store object or class state.

  • Instance attributes: self.balance = 100 gives each account its own balance.
  • Class attributes: species = "Canis familiaris" is shared unless shadowed by an instance attribute.
  • Access control: Python uses conventions such as _name; __name triggers name mangling rather than strict private access.

E. Instances

An instance is a concrete object created from a class.

  • Identity: a = Account(); b = Account() creates two distinct objects, tested with a is not b.
  • State: a.balance and b.balance may hold different values because each has separate instance attributes.
  • Behavior: a.deposit(50) invokes the class method with a automatically supplied as self.

IX. Inheritance — Reusing and specializing classes

Inheritance creates a derived class from a base class, allowing shared behavior and controlled specialization.

A. Inheritance concepts

Inheritance models an “is-a” relationship and supports reuse, extension, and polymorphism.

  • Relationship: If Dog inherits from Animal, every Dog is an Animal, but not every Animal is a Dog.
  • Lookup: Python searches the instance’s class and then its method resolution order, or MRO.
  • Polymorphism: Code can call animal.speak() without knowing whether the object is a Dog or another subclass.

B. Base and derived classes

A base class supplies common features, while a derived class adds or changes features.

  • Syntax: class Dog(Animal): declares Animal as the base and Dog as the derived class.
  • Initialization: super().__init__(name) invokes the base initializer without naming the base directly.
  • Reuse: Common validation belongs in Animal; dog-specific behavior belongs in Dog.

C. Method overriding

Overriding occurs when a derived class defines a method with the same name as one in its base class.

  • Example: Dog.speak() may return "Woof" instead of the base Animal.speak() result.
  • Extension: An overriding method can call super().speak() and then add subclass-specific output.
  • Polymorphic effect: Calling obj.speak() selects the implementation appropriate to obj’s actual type.

D. Multiple inheritance

Multiple inheritance allows a class to inherit from more than one base class.

  • Syntax: class SmartPhone(Camera, Phone): combines capabilities from both parents.
  • MRO: Python’s C3 linearization determines lookup order; SmartPhone.__mro__ displays that sequence.
  • Cooperative design: Each class should use super() and compatible signatures to ensure every initializer participates once.
  • Risk: Conflicting methods and diamond-shaped hierarchies can make behavior difficult to reason about.

E. Practical OOP examples

A practical class should model a coherent entity with validated state and meaningful operations.

  • Example: A BankAccount may define owner, _balance, deposit(amount), and withdraw(amount).
  • Validation: withdraw() should reject an amount greater than the balance rather than allowing invalid state.
  • Design principle: Keep data and operations together; a LibraryBook can expose checkout() and return_book() instead of permitting arbitrary status changes.

X. Use of decorators and generators

Decorators modify callable behavior, while generators produce sequences lazily.

A. Use of decorators and generators

These mechanisms support reusable control logic and memory-efficient iteration.

  • Decorator: A function receiving another function can wrap it; @log_call above f is equivalent to f = log_call(f).
  • Preservation: functools.wraps() copies metadata such as the original function’s name and documentation.
  • Generator: A function containing yield returns an iterator; yield n pauses execution and resumes on the next next() call.
  • Memory benefit: def squares(n): yield i*i produces values one at a time rather than storing all n results in a list.
  • Combination: A decorator can measure a generator function, but it should preserve lazy behavior instead of eagerly converting the generator to a list.