Programming for Data Processing

Lectures: Nguyễn Tuấn Phong, Đào Việt Anh

Labs: Trịnh Ngọc Huỳnh, Phạm Tiến Du

Semester: 2025-2026-1

Institute for AI, VNU-UET

Lecture 4:
Error Handling
OOP

1. Debugging

Debugging

  • Challenging: debugging has a steep learning curve
  • Goal: produce reliable, bug-free programs
  • Tools & Strategies:
    • Built-in debuggers (IDLE, Anaconda)
    • print() statements — quick checks & tracing
    • Systematic thinking — reason through code logically
    • AI — leverage AI tools as debugging assistants

print() Statements

  • Good way to test hypotheses
  • When to print:
    • On function entry
    • Check parameters
    • Show function results
  • Bisection method:
    • Insert print() halfway in the code
    • Narrow down where the bug occurs

Debugging Steps

  • Study program code:
    • Don’t just ask what is wrong
    • Ask: how did I get this unexpected result?
    • Check if it’s part of a pattern/family
  • Scientific method:
    • Study available data
    • Form a hypothesis
    • Run repeatable experiments
    • Test with the simplest input

Two types of errors

  • Explicit errors: easily identified by error messages
  • Logic errors: harder to find, no error messages, but wrong results
    • Think before writing code
    • Test code with simple inputs
    • Use print() to trace code execution
    • Rubber duck debugging: explain code to a rubber duck (or a friend)

2. Handling Exceptions

2.1. Overview

Errors in Python

  • At least two distinguishable kinds of errors:
    • Syntax errors (parsing)
    • Exceptions (runtime)
  • Syntax errors must be fixed before execution
  • Exceptions occur at runtime and can be handled
    • Tools: try, except, else, finally, raise

Syntax Errors

  • Detected by the parser (before execution)
  • Point to where parsing failed
  • Must be fixed to run the program

>>> while True print('Hello world')
  File "<stdin>", line 1
    while True print('Hello world')
               ^^^^^
SyntaxError: invalid syntax

Exceptions

  • Raised during execution
  • Message indicates the error type, location, etc.

>>> 10 * (1/0)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
          ~^~
ZeroDivisionError: division by zero

>>> 4 + spam*3
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
        ^^^^
NameError: name 'spam' is not defined

2.2. Handling and Raising Exceptions

Handling Exceptions: try/except


while True:
    try:
        x = int(input("Please enter a number: "))
        break
    except ValueError:
        print("Oops! That was no valid number. Try again...")
  • try runs first
  • except runs if exception matches
  • If no match, the except clauses are skipped

Multiple Excepts in tuple

  • Use tuple to catch multiple exceptions in one clause

... except (RuntimeError, TypeError, NameError):
...     pass

Multiple Excepts: Order

  • Multiple except clauses allowed
  • Order matters: first match runs
  • Put subclasses before their base classes
    • Prevents base from catching all too early

class B(Exception): pass
class C(B): pass
class D(C): pass

for cls in [B, C, D]:
    try:
        raise cls()
    except D:  # subclass first
        print("D")
    except C:
        print("C")
    except B:  # base last
        print("B")
# Prints: B, C, D

Question: What if we put except B first?


class B(Exception): pass
class C(B): pass
class D(C): pass

for cls in [B, C, D]:
    try:
        raise cls()
    except B:
        print("B")
    except C:
        print("C")
    except D:
        print("D")

Exception Instance & .args

  • Exceptions may carry data
  • Accessible via except ... as ... and .args

>>> try:
...     raise Exception('spam', 'eggs')
... except Exception as inst:
...     print(type(inst))    # the exception type
...     print(inst.args)     # arguments stored in .args
...     print(inst)          # __str__ allows args to be printed directly,
...                          # but may be overridden in exception subclasses
...     x, y = inst.args     # unpack args
...     print('x =', x)
...     print('y =', y)
... 
<class 'Exception'>
('spam', 'eggs')
('spam', 'eggs')
x = spam
y = eggs

Exception Hierarchy

  • BaseException → root of all exception classes
  • Exception → base class for non-fatal errors
    • Can be used to catch most exceptions
    • Good practice: Be specific about exceptions you catch (see next slide)

Log & Re-raise Pattern


import sys

try:
    f = open('myfile.txt')
    s = f.readline()
    i = int(s.strip())
except OSError as err:
    print("OS error:", err)
except ValueError:
    print("Could not convert data to an integer.")
except Exception as err:
    print(f"Unexpected {err=}, {type(err)=}")
    raise

→ Errors not caught with OSError and ValueError will be caught by the generic Exception handler.

else Clause

  • Runs only if no exception under the try clause

for arg in sys.argv[1:]:
    try:
        f = open(arg, 'r')
    except OSError:
        print('cannot open', arg)
    else:
        print(arg, 'has', len(f.readlines()), 'lines')
        f.close()

Raising Exceptions

  • Use raise to signal an error
    • Can raise an exception instance or an exception class
    • If a class is raised, Python calls its __init__() with no args

>>> raise NameError('HiThere')
NameError: HiThere
>>> raise ValueError  # same as ValueError()
>>> def square_root(x):
...     if x < 0:
...         raise ValueError("Cannot compute square root of negative number")
...     return x ** 0.5
...
>>> square_root(-1)
ValueError: Cannot compute square root of negative number

User-defined Exceptions

  • Create custom exceptions by subclassing Exception
  • Use meaningful names (often ending with Error)
  • Provide context through attributes or messages

class NegativeNumberError(Exception):
    """Raised when a negative number is not allowed."""
    pass

def square_root(x):
    if x < 0:
        raise NegativeNumberError(f"Invalid input: {x}")
    return x ** 0.5

print(square_root(-9))
# Raises: NegativeNumberError: Invalid input: -9

Defining Cleanup: finally

  • Code in finally always runs
  • Used for cleanup (closing files, releasing resources, etc.)

>>> try:
...     raise KeyboardInterrupt
... finally:
...     print('Goodbye, world!')
...
Goodbye, world!
Traceback (most recent call last):
  File "<stdin>", line 2, in <module>
    raise KeyboardInterrupt
KeyboardInterrupt

Example: Combining all Clauses


>>> def divide(x, y):
...     try:
...         result = x / y
...     except ZeroDivisionError:
...         print("division by zero!")
...     else:
...         print("result is", result)
...     finally:
...         print("executing finally clause")
... 
>>> divide(2, 1)
result is 2.0
executing finally clause
>>> divide(2, 0)
division by zero!
executing finally clause
>>> divide("2", "1")
executing finally clause
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
    divide("2", "1")
    ~~~~~~^^^^^^^^^^
  File "<stdin>", line 3, in divide
    result = x / y
             ~~^~~
TypeError: unsupported operand type(s) for /: 'str' and 'str'
            

Raising Multiple Exceptions

  • Use ExceptionGroup to bundle multiple exceptions
  • Useful in concurrency / batch tasks where several operations fail
  • Is itself an exception; can be caught like others

ExceptionGroup Example


>>> def f():
...     excs = [OSError('error 1'), SystemError('error 2')]
...     raise ExceptionGroup('there were problems', excs)
... 
>>> f()
  + Exception Group Traceback (most recent call last):
  |   File "<stdin>", line 1, in <module>
  |     f()
  |     ~^^
  |   File "<stdin>", line 3, in f
  |     raise ExceptionGroup('there were problems', excs)
  | ExceptionGroup: there were problems (2 sub-exceptions)
  +-+---------------- 1 ----------------
    | OSError: error 1
    +---------------- 2 ----------------
    | SystemError: error 2
    +------------------------------------
>>> try:
...     f()
... except Exception as e:
...     print(f'caught {type(e)}: e')
... 
caught <class 'ExceptionGroup'>: e

Instances, Not Types

  • Groups hold exception instances, not classes
  • Typical pattern:
    • run tasks, collect failures, then raise one group

excs = []
for test in tests:
    try:
        test.run()
    except Exception as e:
        excs.append(e)

if excs:
    raise ExceptionGroup("Test Failures", excs)

Adding Notes to Exceptions

  • Use e.add_note() to attach extra context
  • Notes appear in the traceback after the exception
  • Helps debugging in multi-step processes

>>> try:
...   raise TypeError('bad type')
... except Exception as e:
...   e.add_note('Extra info 1')
...   e.add_note('Extra info 2')
...   raise
...
Traceback (most recent call last):
  File "<stdin>", line 2, in <module>
    raise TypeError('bad type')
TypeError: bad type
Add some information
Add some more information
>>>

Notes in ExceptionGroup

  • Each exception in a group can carry its own notes
  • Useful for reporting context per error

>>> def f():
...     raise OSError('operation failed')
... 
>>> excs = []
>>> for i in range(3):
...     try:
...         f()
...     except Exception as e:
...         e.add_note(f'Happened in Iteration {i+1}')
...         excs.append(e)
... 
>>> raise ExceptionGroup('We have some problems', excs)

  + Exception Group Traceback (most recent call last):
  |   File "<stdin>", line 1, in <module>
  |     raise ExceptionGroup('We have some problems', excs)
  | ExceptionGroup: We have some problems (3 sub-exceptions)
  +-+---------------- 1 ----------------
    | Traceback (most recent call last):
    |   File "<stdin>", line 3, in <module>
    |     f()
    |     ~^^
    |   File "<stdin>", line 2, in f
    |     raise OSError('operation failed')
    | OSError: operation failed
    | Happened in Iteration 1
    +---------------- 2 ----------------
    | Traceback (most recent call last):
    |   File "<stdin>", line 3, in <module>
    |     f()
    |     ~^^
    |   File "<stdin>", line 2, in f
    |     raise OSError('operation failed')
    | OSError: operation failed
    | Happened in Iteration 2
    +---------------- 3 ----------------
    | Traceback (most recent call last):
    |   File "<stdin>", line 3, in <module>
    |     f()
    |     ~^^
    |   File "<stdin>", line 2, in f
    |     raise OSError('operation failed')
    | OSError: operation failed
    | Happened in Iteration 3
    +------------------------------------

3. Assertions

Assertions in Python

  • Use assert to check conditions during runtime
    • If condition is True → program continues
    • If condition is False → raises AssertionError
  • Good for debugging and validating assumptions

>>> x = 5
>>> assert x > 0        # passes, no output
>>> assert x < 0        # AssertionError
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AssertionError
      

assert with Messages

  • Optional second argument adds a message
  • Helps explain the failed condition
  • Use for clearer debugging

>>> def divide(a, b):
...     assert b != 0, "Denominator must not be zero"
...     return a / b
... 
>>> print(divide(10, 2))
5.0
>>> print(divide(10, 0))
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 2, in divide
AssertionError: Denominator must not be zero
      

⚠️ When NOT to Use assert

  • Assertions can be disabled with python -O
  • Don’t use for data validation in production
  • Better to raise ValueError, TypeError for user input
  • Best for internal checks during development and debugging

# ❌ Bad: input validation
assert user_age > 0, "Age must be positive"

# ✅ Better
if user_age <= 0:
    raise ValueError("Age must be positive")

4. Object-Oriented Programming (Recap)

OOP Principles

  • Objects = data + behavior
  • Classes define blueprints; instances bring them to life
  • Abstraction: focus on what an object does, not how
  • Encapsulation: hide details, expose clean interfaces
  • Inheritance: reuse and extend existing classes
  • Polymorphism: same method name, different behavior
  • Goal: modularity, reuse, and clarity in design

Objects

  • In Python, everything is an object:
    • 42, 3.14, "hi", [1, 2, 3], {"CA": "California"}
  • Each object has:
    • Type — e.g., type([1,2]) == list
    • Data (internal state) — e.g., list holds items [1,2]
    • Methods — e.g., [1,2].append(3) → [1,2,3]
  • An object is an instance of its type — e.g., 42 is an int

OOP in Python

  • Everything in Python is an object
  • You can create, manipulate, and destroy objects
  • Python reclaims inaccessible objects automatically (garbage collection)

What are Objects?

  • Objects are abstractions that capture:
    • Data attributes (internal representation)
    • Methods (procedures for interaction)
      • Define behavior but hide implementation

Example: list

L = 1 -> 2 -> 3 -> 4 ->
  • list in Python is represented internally as linked list of cells
  • How to manipulate lists?
    • Index / slice / concat: L[i], L[i:j], +
    • Built-ins: len(L), min(L), max(L), del L[i]
    • Methods: L.append(), L.insert(), L.remove(), L.sort(), etc.
  • Internals should be private — use the public interface
  • Directly manipulating internals can break correct behavior

5. Python Classes & Inheritance

Classes vs Instances

  • Defining a class → specify name, attributes, behavior
  • Using a class → create instances, call operations

5.1. A First Look at Classes

Class Definition Syntax

Simplest form of a class:


class ClassName:
    <statement-1>
    .
    .
    .
    <statement-N>
  

Note: Definitions must be executed before use; can appear inside if or functions.

Class Body & Namespace

  • Class body mostly def methods; other statements allowed.
  • Entering creates a new namespace (local to the class body).
  • Exiting creates a class object bound to the class name.

class Greeter:
    msg = "Hello!"

# class object bound to Greeter
print(Greeter.msg)   # access attribute directly from the class
  

Class Objects

Support: attribute reference and instantiation.

Attributes are accessed with obj.name.

Class Attributes Example


>>> class MyClass:
...     """A simple example class"""
...     i = 12345
... 
...     def f(self):
...         return 'hello world'
... 
>>> MyClass.i
12345
>>> MyClass.f
<function MyClass.f at 0x7f9b8c2d1>
>>> MyClass.f()  # missing instance argument
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: f() missing 1 required positional argument: 'self'
  

Instantiation

Call the class like a function to create an instance:


x = MyClass()
  

Initializing with __init__

Customize initial state via __init__():


def __init__(self):
    self.data = []
  
  • Called automatically at instantiation: x = MyClass()
  • Use self to refer to current instance

__init__ with Arguments


>>> class Complex:
...     def __init__(self, realpart, imagpart):
...         self.r = realpart
...         self.i = imagpart
...
>>> x = Complex(3.0, -4.5)
>>> x.r, x.i
(3.0, -4.5)
  

Rule: Args passed to class are forwarded to __init__.

Instance Objects

Instances support attribute references only:

  • data attributes (instance variables)
  • methods (functions bound to instances)

Data Attributes Example

  • Data attributes need not be declared in advance
  • Method names are looked up in the instance's class

>>> class MyClass:
...     """A simple example class"""
...     i = 12345
... 
...     def f(self):
...         return 'hello world'
... 
>>> x = MyClass()
>>> x.counter = 1
>>> while x.counter < 10:
...     x.counter = x.counter * 2
... 
>>> print(x.counter)
16
>>> del x.counter
>>> x.f()
'hello world'
  

Methods vs Functions

  • If MyClass.f is a function, then x.f is a method object.
  • x.f is valid; x.i is not (since i isn’t a function).

Method Objects

Methods are like functions, but bound to an instance:


>>> x = MyClass()
>>> x.f()
'hello world'
  

Methods can be stored and called later:


>>> xf = x.f
>>> while True:
...     print(xf())
...
  

How Method Calls Work

  • Instance is passed as first arg (self).
  • x.f() is equivalent to MyClass.f(x)

Class & Instance Variables

  • Class vars: shared by all instances.
  • Instance vars: unique per instance.

Dog Example: Shared vs Unique


>>> class Dog:
...     kind = 'canine'         # class variable shared by all instances
...     def __init__(self, name):
...         self.name = name    # instance variable unique to each instance
... 
>>> d = Dog('Fido')
>>> e = Dog('Buddy')
>>> d.kind                  # shared by all dogs
'canine'
>>> e.kind                  # shared by all dogs
'canine'
>>> d.name                  # unique to d
'Fido'
>>> e.name                  # unique to e
'Buddy'
  

Mutable Class Vars Pitfall

Avoid mutable class variables (e.g., lists) unless truly shared:


>>> class Dog:
...     tricks = []               # mistaken use of a class variable
...     def __init__(self, name):
...         self.name = name
...     def add_trick(self, trick):
...         self.tricks.append(trick)
... 
>>> d = Dog('Fido')
>>> e = Dog('Buddy')
>>> d.add_trick('roll over')
>>> e.add_trick('play dead')
>>> d.tricks                      # unexpectedly shared by all dogs
['roll over', 'play dead']
  

Correct: Use Instance Variable


>>> class Dog:
...     def __init__(self, name):
...         self.name = name
...         self.tricks = [] # creates a new empty list for each dog
...     def add_trick(self, trick):
...         self.tricks.append(trick)
... 
>>> d = Dog('Fido')
>>> e = Dog('Buddy')
>>> d.add_trick('roll over')
>>> e.add_trick('play dead')
>>> d.tricks
['roll over']
>>> e.tricks
['play dead']
  

5.2. Random Remarks

Attribute Lookup Priority

Same name on instance & class → instance wins.


>>> class Warehouse:
...    purpose = 'storage'
...    region = 'west'
...
>>> w1 = Warehouse()
>>> print(w1.purpose, w1.region)
storage west
>>> w2 = Warehouse()
>>> w2.region = 'east'
>>> print(w2.purpose, w2.region)
storage east
  

Data Attributes & Conventions

  • No enforced data hiding in Python.
  • Use _name to mark "internal use".
  • Clients can add attributes, but risk breaking logic.

class Account:
    def __init__(self, balance):
        self._balance = balance   # convention: internal

a = Account(100)
a._balance = -50   # possible, but breaks invariant
a.owner = "Alice"  # allowed, adds new attribute
  

No Shorthand & self

  • Always use self.attr inside methods.
  • self is just a convention, not a keyword.
  • Conventions make code clear and consistent.

class Person:
    def __init__(self, name):
        self.name = name   # must use self

    def greet(self):
        return "Hi, I'm " + self.name

p = Person("Bob")
print(p.greet())   # Hi, I'm Bob
  

Functions as Methods

Any function stored on a class becomes a method.


# Function defined outside the class
def f1(self, x, y):
    return min(x, x+y)

class C:
    f = f1

    def g(self):
        return 'hello world'

    h = g
  
  • f, g, h are all methods; h ≡ g.
  • ⚠️ This is not a good practice.

Methods Calling Methods

Call via self.method(...).


class Bag:
    def __init__(self):
        self.data = []

    def add(self, x):
        self.data.append(x)

    def addtwice(self, x):
        self.add(x)
        self.add(x)
  

Global Scope for Methods

  • Methods use the module’s globals.
    • Classes are never used as globals.
  • Methods can access imports or top-level variables.

PI = 3.14   # module-level name

class Circle:
    def __init__(self, r):
        self.r = r

    def area(self):
        return PI * self.r * self.r   # uses global PI

c = Circle(2)
print(c.area())   # 12.56
  

Everything Has a Class

  • Each value is an object with a class (type).
  • Access via object.__class__.

>>> 3 .__class__
<class 'int'>
>>> 'hi'.__class__
<class 'str'>
>>> [].__class__
<class 'list'>
>>> {}.__class__
<class 'dict'>
>>> ().__class__
<class 'tuple'>
>>> (x for x in range(3)).__class__
<class 'generator'>
>>> def f(): pass
>>> f.__class__
<class 'function'>
  

5.3. Inheritance

Derived Class Syntax

Define a subclass by listing its base:


class DerivedClassName(BaseClassName):
    <statement-1>
    .
    .
    .
    <statement-N>
  

Note: Base must be accessible in scope (e.g., imported, or defined in the same module).


class DerivedClassName(modname.BaseClassName):
  

Lookup & Instantiation

  • Create instance with DerivedClass().
  • Looks for attributes in the class first, then its base(s).

>>> class Animal:
...     def sound(self):
...         return "generic sound"
...
>>> class Dog(Animal):
...     def bark(self):
...         return "woof"
...
>>> d = Dog()
>>> d.bark()      # from Dog
'woof'
>>> d.sound()     # inherited from Animal
'generic sound'
  

Overriding & Base Calls

  • Subclasses can override base methods.
  • Use base method directly if needed.

>>> class Animal:
...     def speak(self):
...         return "generic sound"
...
>>> class Dog(Animal):
...     def speak(self):
...         return "woof"
...
>>> d = Dog()
>>> d.speak()                    # override
'woof'
>>> Animal.speak(d)              # call base directly
'generic sound'
  

Type & Inheritance Checks

  • isinstance() → check if object is instance of a class (or subclass).
  • issubclass() → check if a class is derived from another.

>>> isinstance(5, int)
True
>>> isinstance(True, int)    # bool is a subclass of int
True
>>> issubclass(bool, int)
True
>>> issubclass(float, int)
False
  

Multiple Inheritance

Subclass from multiple bases:


class DerivedClassName(Base1, Base2, Base3):
    <statement-1>
    .
    .
    .
    <statement-N>
  

Use case: combine behaviors from several parents.

Multiple Inheritance Example


>>> class Flyer:
...     def fly(self):
...         return "I can fly"
...
>>> class Swimmer:
...     def swim(self):
...         return "I can swim"
...
>>> class Duck(Flyer, Swimmer):
...     pass
...
>>> d = Duck()
>>> d.fly()
'I can fly'
>>> d.swim()
'I can swim'
  

Method Resolution Order (MRO)

  • Defines the order Python searches classes for attributes.
  • Important when using multiple inheritance.

>>> class A: pass
>>> class B(A): pass
>>> class C(B): pass
>>> C.mro()
[<class '__main__.C'>, <class '__main__.B'>, 
  <class '__main__.A'>, <class 'object'>]
  

Diamond Problem

  • Occurs when a class is inherited through multiple paths.
  • Python ensures each parent is called once.

>>> class A:
...     def greet(self): return "Hello from A"
...
>>> class B(A): pass
>>> class C(A): pass
>>> class D(B, C): pass
>>> D.mro()
[<class '__main__.D'>, <class '__main__.B'>,
 <class '__main__.C'>, <class '__main__.A'>, <class 'object'>]
  

Using super()

  • super() follows MRO order.
  • Allows cooperative method calls across classes.

>>> class A:
...     def greet(self):
...         return "Hello from A"
...
>>> class B(A):
...     def greet(self):
...         return super().greet() + " & B"
...
>>> class C(A):
...     def greet(self):
...         return super().greet() + " & C"
...
>>> class D(B, C):
...     def greet(self):
...         return super().greet() + " & D"
...
>>> d = D()
>>> d.greet()
'Hello from A & C & B & D'
  

Why Dynamic MRO Matters

  • Each base is called once → avoids duplication.
  • Order is predictable (left-to-right).
  • Supports reliable, extensible class designs.

5.4. Private Variables

Private Variables: Overview

  • No true private variables in Python.
  • _name → convention: non-public API.
  • Use name mangling (__name) to avoid clashes in subclasses.

Name Mangling

  • __var becomes _ClassName__var.
  • Prevents accidental clashes in subclasses.

>>> class A:
...     def __init__(self):
...         self.__hidden = 42
...
>>> a = A()
>>> a.__hidden      # direct access fails
Traceback (most recent call last):
  File "<python-input-44>", line 1, in <module>
    a.__hidden
AttributeError: 'A' object has no attribute '__hidden'. Did you mean: '_A__hidden'?
>>> a._A__hidden    # can still access
42
  

Subclass Example


>>> class Base:
...     def __init__(self):
...         self.__secret = "base"
...
>>> class Child(Base):
...     def __init__(self):
...         super().__init__()
...         self.__secret = "child"
...
>>> c = Child()
>>> c._Base__secret     # from Base
'base'
>>> c._Child__secret    # from Child
'child'
  

Note: mangling keeps names separate.

Practical Use

  • Use _name for “internal” attributes.
  • Use __name to avoid subclass conflicts.
  • Still accessible if really needed.

5.5. Dataclasses

Dataclasses

  • Use dataclasses to group related data (like structs in C).
  • Saves boilerplate, auto-generates __init__, __repr__, etc.

from dataclasses import dataclass

@dataclass
class Employee:
    name: str
    dept: str
    salary: int
  

>>> john = Employee('john', 'computer lab', 1000)
>>> john.dept
'computer lab'
>>> john.salary
1000
  

6. Iterators & Generators

Iterators

Most container objects can be looped over using a for statement.


for element in [1, 2, 3]:
    print(element)
for element in (1, 2, 3):
    print(element)
for key in {'one':1, 'two':2}:
    print(key)
for char in "123":
    print(char)
for line in open("myfile.txt"):
    print(line, end='')

Iterator Mechanics

  • Iterators: Objects that implement the iterator protocol.
  • Protocol: methods __iter__() and __next__().

>>> s = 'abc'
>>> it = iter(s)
>>> next(it)
'a'
>>> next(it)
'b'
>>> next(it)
'c'
>>> next(it)
StopIteration
  

Custom Iterator


class Reverse:
    """Iterate over a sequence backwards."""
    def __init__(self, data):
        self.data = data
        self.index = len(data)

    def __iter__(self):
        return self

    def __next__(self):
        if self.index == 0:
            raise StopIteration
        self.index -= 1
        return self.data[self.index]
  

>>> for char in Reverse("spam"):
...     print(char)
... 
m
a
p
s
  

Generators

  • Functions with yield create iterators.
  • State is saved between calls.
  • Auto-raises StopIteration when done.

def reverse(data):
    for i in range(len(data)-1, -1, -1):
        yield data[i]
  

>>> for char in reverse("golf"):
...     print(char)
f
l
o
g
  

Generator Expressions

  • Like list comprehensions, but use ().
  • Compact and memory-friendly.

>>> sum(i*i for i in range(10))       # sum of squares
285
>>> xvec = [10, 20, 30]
>>> yvec = [7, 5, 3]
>>> sum(x*y for x, y in zip(xvec, yvec))  # dot product
260
>>> list(data[i] for i in range(len("golf")-1, -1, -1))
['f', 'l', 'o', 'g']