2 Programming
2 Programming
Daniel Arrieta
[email protected]
IE University
Global Variables
Object-Oriented Programming
Introduction
This unit is based on chapters 6 to 10 of “Introduction to
Computation and Programming Using Python” by Guttag (2021).
First sections are addressed to introducing global variables, modules
and how to handle files.
Next, testing and debugging will be introduced, these are two
important activities in software engineering.
Testing involves verifying that a software system meets its
requirements, while debugging focuses on identifying and fixing
defects or issues in the software.
Following testing and debugging, exceptions and assertions, which are
mechanisms that help handle and manage errors in code, will be
addressed.
Lastly, classes in the context of object-oriented programming will be
presented. They allow you to define blueprints for objects, encapsulate
data and behavior, and create reusable and modular code.
1/85
Next
Global Variables
Object-Oriented Programming
Global Variables
A global variable in Python is a variable that is defined outside of any
function or class and can be accessed and modified from anywhere in
the code.
It has a global scope, meaning it can be used in different functions
and modules within the same program.
Global variables are useful when you have data that needs to be
shared and accessed by multiple functions or modules.
However, it’s generally recommended to use global variables sparingly,
as they can make code harder to understand and maintain.
It’s often better to pass variables as function arguments or use return
values to pass data between functions.
The most common use of global variables is probably to define a
global constant that will be used in many places.
Another possible use of global variables is in the implementation of
so-called recursive definitions.
2/85
Next
Global Variables
Recursive Definitions
Financial Example
Object-Oriented Programming
Recursive definitions
In general, a recursive definition is made up of two parts.
Base case: there is at least one a special case, usually the first one,
that directly specifies the result of the definition.
Recursive or inductive case: there is at least one recursive case that
defines the output of the definition on some other input, typically a
simpler version.
It is the presence of a base case that keeps a recursive definition from
being a circular definition or and endless loop.
A simple recursive definition is the factorial function, denoted in
mathematics using the symbol “!”, on natural numbers N.
For each n ∈ N, an inductive definition of the factorial is given by
(
1 if n = 1,
n! :=
n × (n − 1)! if n > 1.
3/85
Implementing the factorial of a natural number
The next code snippet
def fact_iter ( n ) :
''' Assumes n an int > 0. Returns n ! '''
result = 1
for i in range (1 , n +1) :
result *= i
return result
If fib is called with a large number it will took a very long time to run.
A sensible guess is that is caused by the huge number of recursive
calls that are made.
We could do a careful analysis of the code and figure out why this is
happening.
Another approach is to add some code that counts the number of
calls, one way to do that is by using global variables.
6/85
Fibonacci numbers with a global variable
To define a global variable in Python, simply declare the variable
outside of any function or class.
Consider the code,
def fib ( n ) :
''' Assumes n int >= 0. Returns Fibonacci of n . '''
global num_fib_calls
num_fib_calls += 1
if n == 0 or n == 1:
return 1
else :
return fib (n -1) + fib (n -2)
def test_fib ( n ) :
for i in range ( n +1) :
global num_fib_calls
num_fib_calls = 0
print ( ' fib of ' , i , '= ' , fib ( i ) )
print ( ' fib called ' , num_fib_calls , ' times . ')
7/85
Fibonacci numbers with a global variable (ii)
By declaring a variable as global within a function, you can access and
modify the global variable from within that function.
Within the fib definition, the line of code global num_fib_calls
implies that the name num_fib_calls is a global variable and it should
be defined outside of the function.
If the global keyword is omitted, Python will create a new local
variable with the same name instead of modifying the global variable.
That is why if we had not included the code global num_fib_calls,
the name num_fib_calls would have been local to each of the
functions fib and test_fib.
The functions fib and test_fib both have unfettered access to the
object referenced by the variable num_fib_calls.
The function test_fib binds num_fib_calls to 0 each time it calls
fib, and fib increments the value of num_fib_calls each time fib is
entered.
8/85
Next
Global Variables
Recursive Definitions
Financial Example
Object-Oriented Programming
Financial example
As we have just seen, recursion is a programming technique where a
function calls itself to solve a problem.
Furthermore, in some cases, it is useful to use global variables within
a recursive function to keep track of certain information, e.g., for
computing the total number of recursive calls that are made when
executing the function.
Let’s consider a financial example to illustrate recursion with a global
variable in Python.
Suppose you have a portfolio of investments, and you want to
calculate the total value of the portfolio.
Each investment has a quantity and a price, and, consequently, a
portfolio can be represented as a list of tuples, where each tuple
contains the quantity and price of an investment.
Next slides will detail the code to calculate the total value of a
portfolio using recursion and a global variable.
9/85
Portfolio value example
Let’s consider the next code snippet
total_value = 0 # Global variable to store the total
value
def c a l c u l a t e _ p o r t f o l i o _ va l u e ( index ) :
global total_value # Declare the global variable
inside the function
c a l c u l a t e _ p o r t f o l i o _ v a l u e ( index + 1) # Recursive
call to process the next investment
10/85
Portfolio value example (ii)
In this example, the calculate_portfolio_value function takes an
index parameter that represents the current position in the portfolio
list.
The function uses the global keyword to indicate that the
total_value variable is a global variable.
The base case of the recursion occurs when the index is greater than
or equal to the length of the portfolio, indicating that all investments
have been processed.
At each step, the function calculates the value of the current
investment and adds it to the total_value variable.
Now running the below code lines
portfolio = [(10 , 100) , (5 , 50) , (8 , 75) ]
c a l c u l a t e _ p o r t f o l i o _ v a l u e (0)
print ( " Total portfolio value : " , total_value )
Global Variables
Object-Oriented Programming
Modules and files
So far, we have operated under the assumptions that:
i. the entire program is stored in one file;
ii. programs do not depend upon previously written code (other
than the code implementing Python); and
iii. programs do not access previously gathered data nor do they
store their results in a way that allows them to be accessed after
the program is finished running.
The first assumption is perfectly reasonable as long as programs are
small.
The second and third assumptions are reasonable for exercises
designed to help people learn to program, but rarely reasonable when
writing programs designed to accomplish something useful.
As programs get larger, however, it is typically more convenient to
store different parts of them in different files.
12/85
Modules and files (ii)
Imagine, for example, that multiple people are working on the same
program.
It would be a nightmare if they were all trying to update the same file.
We will discuss a mechanism, i.e., Python modules, that allow us to
easily construct a program from code in multiple files.
A Python module is a .py file containing Python definitions and
statements.
In addition, we will show how to take advantage of library modules
that are part of the standard Python distribution.
We will use a couple of these modules in this subsection of the unit,
and many others later in other parts of the course.
Lastly, it will be provided a brief introduction to reading from and
writing data to files.
13/85
Next
Global Variables
Object-Oriented Programming
Creating a module
A module is a .py file containing Python definitions and statements.
We could create, for example, a file named as U2_1_circle.py
containing the following code
pi = 3.14159
14/85
import statement
As aforementioned, a module is accesed through an import statement.
For instance, the code
import U2_1_circle
pi = 3
print ( pi )
print ( U2_1_circle . pi )
print ( U2_1_circle . area (3) )
print ( U2_1_circle . circumference (3) )
print ( U2_1_circle . sphere_surface (3) )
will print
3
3.14159
28.27431
18.84953 99 99 99 99 98
113.09724
15/85
Importing context
Modules are typically stored in individual files. Each module has its
own private symbol table.
Executing import M creates a binding for module M in the scope in
which the import appears.
Therefore, in the importing context we use dot notation to indicate
that we are referring to a name defined in the imported module.
Consequently, within U2_1_circle.py we access objects (e.g., pi and
area) in the usual way.
Outside of U2_1_circle.py, the references pi and U2_1_circle.pi
can (and in this case do) refer to different objects.
That is why, once the module has been imported, the statements
print ( pi )
print ( U2_1_circle . pi )
20/85
Renaming an imported module
Using the character “*” when importing is said to be a “wild card”
import.
Many Python programmers believe that this kind of import makes
code more difficult to read because it is no longer obvious where a
name is defined.
A commonly used variant of the import statement is to rename the
imported module, i.e., using the keyword as. For example,
import module_name as new_name
Global Variables
Object-Oriented Programming
Python Standard Library
The Python Standard Library is a collection of modules that provide a
wide range of functionalities for various tasks.
It includes modules for file I/O, networking, string manipulation, data
structures, mathematical operations, and more.
These modules are built-in and readily available when you install
Python.
Below are summarized some key modules in the Python Standard
Library.
os: Provides functions for interacting with the operating system, such
as file and directory operations.
calendar: functionalities related to calendars, such as determining
leap years, calculating week numbers, and generating calendars.
datetime: Allows you to work with dates, times, and timezones.
math: Provides mathematical functions and constants.
22/85
Python Standard Library (ii)
csv: Provides functionality for reading and writing CSV files.
json: Allows you to work with JSON data.
urllib: Provides tools for working with URLs and making HTTP
requests.
re: Provides regular expression matching operations.
collections: Offers additional data structures like named tuples,
deque, Counter, and defaultdict.
sys: Provides access to system-specific parameters and functions.
logging: Enables logging functionality for debugging and error
handling.
These are just a few examples of the modules available in the Python
Standard Library.
Each module has its own set of functions, classes, and methods that
can be used to accomplish specific tasks.
23/85
math module examples
For example, to print the log of x base 2, using the math module, can
be done with the code
import math
print ( math . log (x , 2) )
24/85
math module financial example
In this example, we calculate the final amount and interest earned on
an investment using compound interest formula.
We use the math.pow() function to raise the base (1 + rate) to the
power of time.
The result is then multiplied by the principal to get the final amount.
The interest earned is the difference between the final amount and the
principal.
This is just a simple example to demonstrate the usage of the math
module for a very simple financial calculation.
In real-world financial modeling, more advanced libraries, like pandas
or numpy, are tipically used. We will see the most useful in further
units.
In addition to containing approximately 50 useful mathematical
functions, the math module contains several useful floating-point
constants, e.g., math.pi or math.inf for positive infinity.
25/85
calendar module example
Let’s consider the following code
import calendar
26/85
Inflation example
Last code provides a macroeconomic example using the calendar
module. This code computes the average monthly inflation rate for a
given year.
In this example, we have a def calculate_average_inflation using
two inputs: one inflation_data which is a list that represents the
monthly inflation rates for a given year; and two year to which said
inflation_data corresponds.
We use the calendar.month_abbr attribute to get the abbreviated
month names.
We calculate the total inflation by summing up all the inflation rates
and then calculate the average inflation by dividing the total by the
number of months.
The example prints the average monthly inflation for each month and
the total inflation for the year, along with the average inflation for the
year.
27/85
Next
Global Variables
Object-Oriented Programming
Opening a file
Every computer system uses files to save things from one computation
to the next.
Python provides many facilities for creating and accessing files.
Each operating system, e.g., Windows and macOS, comes with its
own file system for creating and accessing files.
Python achieves operating-system independence by accessing files
through something called a file handle.
The code
name_handle = open ( ' kids ' , 'w ')
instructs the operating system to create a file with the name kids and
return a file handle for that file.
The argument 'w' to open indicates that the file is to be opened for
writing.
28/85
Writing to a file
In a Python string, the escape character “\” is used to indicate that
the next character should be treated in a special way.
For instance, the string '\n' denotes a newline character.
The following code
name_handle = open ( ' students . txt ' , 'w ')
for i in range (2) :
name = input ( ' Enter name : ')
name_handle . write ( name + '\ n ')
name_handle . close ()
opens a .txt file, and uses the write method to write two lines ending
each line with a newline character insertion. Finally, the code closes
the file.
Remember to close a file when the program is finished using it.
Otherwise there is a risk that some or all of the writes may not be
saved.
29/85
with statement
File closing can be ensured if it is open using a with statement. The
syntax is of the form
with open ( file_name ) as name_handle :
code_block
opens a file for reading, using the argument 'r', and prints its
contents.
Since Python treats a file as a sequence of lines, we can use a for
statement to iterate over the file’s contents.
30/85
Appending
If a file is opened for writing, then the previous contents of the file
overwritten.
A file can be opened for appending, instead of writing, by using the
argument 'a'.
For example, if the next code is executed
name_handle = open ( ' students . txt ' , 'a ')
name_handle . write ( ' Maria ' + '\ n ')
name_handle . write ( ' Catalina ')
name_handle . close ()
name_handle = open ( ' students . txt ' , 'r ')
31/85
Common operations on files
Given a string representing a file name fn and a file handle fh, then
open(fn,'w'): creates a file for writing and returns a file handle.
open(fn,'r'): opens a file for reading and returns a file handle.
open(fn,'a'): opens a file for appending and returns a file handle.
fh.read(): returns a string containing the contents of the file
associated with the file handle fh.
fh.readline(): returns the next line in the file associated with fh.
fh.readlines(): returns a list, each element of which is one line of
the file associated with he file handle fh.
fh.write(s): writes the string s to the end of the file associated with
the file handle fh.
fh.writelines(S): writes each element of the sequence of strings S
as a separate line to the file associated with the file handle fh.
fh.close(): closes the file associated with the file handle fh.
32/85
Next
Global Variables
Object-Oriented Programming
Testing and Debugging
The first step in getting a program to work is eliminating syntax and
static semantic errors that can be detected without running the code.
Nevertheless programs don’t always function properly the first time
they are run.
This section of the unit provides a highly summarized discussion of
about how to deal with this problem.
Obviously, all the programming examples are in Python, however,
general principles apply to running any complex system.
Testing is the process of running a program to try to determine if it
works as intended.
Debugging is the process of trying to fix a program that is not
working as expected.
The key to doing this is breaking the program into separate
components that can be implemented, tested, and debugged
independently of other components.
33/85
Next
Global Variables
Object-Oriented Programming
Partitions and test suites
The purpose of testing is to show that bugs exist, not to show that a
program is bug-free.
Nevertheless, even the simplest of the programs has billions of
possible inputs.
The key to testing is finding a collection of inputs, called a test suite,
that has a high likelihood of revealing bugs, yet does not take too
long to run.
A partition of a set divides that set into a collection of subsets such
that each element of the original set belongs to exactly one of the
subsets.
The key to finding a test suite is partitioning the space of all possible
inputs into subsets that provide equivalent information about the
correctness of the program, and then constructing a test suite that
contains at least one input from each partition.
Usually, constructing such a test suite is not actually possible.
34/85
Black-box testing
Black-box tests are constructed without looking at the code to be
tested.
This kind of testing is based on exploring paths through the code
specification.
Black-box testing allows testers and implementers to be drawn from
separate populations.
A good way to generate black-box test data is to explore paths
through a specification.
Boundary conditions should also be tested.
Looking at an argument of type list often means looking at the empty
list, a list with exactly one element, a list with immutable elements, a
list with mutable elements, and a list containing lists.
When dealing with numbers, it typically means looking at very small
and very large values as well as “typical” values.
35/85
Black-box test example
An important boundary condition to think about is aliasing.
Consider the code
def copy ( L1 , L2 ) :
""" Assumes L1 , L2 are lists .
Mutates L2 to be a copy of L1 . """
It will work most of the time, but not when L1 and L2 refer to the
same list.
Any test suite that did not include a call of the form copy(L,L),
would not reveal the bug.
36/85
Glass-box testing
Glass-box tests are based on exploring paths through the internal
structure of the code.
Consider the following code snippet
def is_prime ( x ) :
""" Assumes x is a nonnegative int .
Returns True if x is prime ; False otherwise . """
if x <= 2:
return False
for i in range (2 , x ) :
if x % i == 0:
return False
return True
38/85
Glass-box testing best practices
Below are summarized a few rules that are usually worth following
when conducting glass-box tests.
i. Exercise both branches of all if statements.
ii. For each for loop, have test cases in which:
a) The loop is not entered, e.g., if the loop is iterating over the
elements of a list, make sure that it is tested on the empty list.
b) The body of the loop is executed exactly once.
c) The body of the loop is executed more than once.
iii. For each while loop:
a) Look at the same kinds of cases as when dealing with for loops.
b) Include test cases for all possible ways of exiting the loop.
iv. Make sure that each except clause, as we will see in the next
section of the current unit, is executed.
v. For recursive functions, include test cases that cause the function
to return with no recursive calls, exactly one recursive call, and
more than one recursive call.
39/85
Testing phases
Testing starts with unit testing, during this phase, testers construct
and run tests designed to ascertain whether individual units of code
(e.g., functions) work properly.
This is followed by integration testing, which is designed to check
whether groups of units function properly when combined.
Finally, functional testing is used to check if the program as a whole
behaves as intended.
In industry, the testing process is often highly automated. Automating
the testing process facilitates the regression testing.
As programmers attempt to debug a program, it is all too common to
install a “fix” that breaks something, or maybe many things, that
used to work.
Whenever any change is made, no matter how small, you should check
that the program still passes all of the tests that it used to pass.
40/85
Next
Global Variables
Object-Oriented Programming
Runtime errors
The process of fixing flaws in software is known as debugging.
Runtime bugs can be categorized along two dimensions:
i. Overt/covert: An overt bug has an obvious manifestation, e.g.,
the program crashes or takes far longer (maybe forever) to run
than it should. A covert bug has no obvious manifestation. The
program may run to conclusion with no problem other than
providing an incorrect answer. Many bugs fall between the two
extremes, and whether the bug is overt can depend upon how
carefully you examine the behavior of the program.
ii. Persistent/intermittent: A persistent bug occurs every time the
program is run with the same inputs. An intermittent bug occurs
only some of the time, even when the program is run on the
same inputs and seemingly under the same conditions.
A lot of financial applications, e.g., Monte Carlo simulation, involve
programs that model situations in which randomness plays a role. In
programs of that kind, intermittent bugs are common.
41/85
Learning to debug
Debugging starts when it has become clear that the program behaves
in undesirable ways. Debugging is the process of searching for an
explanation of that behavior and fixing it.
The key to being consistently good at debugging is being systematic
in conducting that search.
For at least four decades people have been building tools called
debuggers, and debugging tools are built into all of the popular
Python IDEs.
These tools can help, but what is much more important is how you
approach the problem.
Start by studying all of the test results, and the program text, then
form a hypothesis that you believe to be consistent with all the data.
Next, design and run a repeatable experiment with the potential to
refute the hypothesis.
Finally, keep a record of what experiments you have tried.
42/85
How to debug
Next slides gather a few pragmatic hints about how to debug.
i. Look for the usual suspects.
a) Passed arguments to a function in the wrong order.
b) Misspelled a name, e.g., typed a lowercase letter when it should
be an uppercase one.
c) Failed to reinitialize a variable.
d) Tested that two-floating point values are equal (==) instead of
nearly equal (remember that floating-point arithmetic is not the
same as the arithmetic you learned in school).
e) Tested for value equality (e.g., compared two lists by writing the
expression L1 == L2) when you meant to test for object equality
(e.g., id(L1) == id(L2)).
f) Forgotten that some built-in function has a side effect.
g) Forgotten the () that turns a reference to an object of type
function into a function invocation.
h) Created an unintentional alias.
ii. Understand why the code is doing what it does, it is a good first
step in figuring out how to fix the program.
43/85
How to debug (ii)
iii. Keep in mind that the bug is probably not where you think it is.
If it were, you would have found it long ago. One practical way
to decide where to look is asking where the bug cannot be. As
Sherlock Holmes said, “Eliminate all other factors, and the one
which remains must be the truth.”
iv. Try to explain the problem to somebody else. Attempting to
explain the problem to someone will often lead you to see things
you have missed. Don’t believe everything you read. In
particular, don’t believe the documentation. The code may not
be doing what the comments suggest.
v. Stop debugging and start writing documentation. This will help
you approach the problem from a different perspective.
vi. Walk away and try again tomorrow. This may mean that bug is
fixed later than if you had stuck with it, but you will probably
spend less of your time looking for it. That is, it is possible to
trade latency for efficiency.
44/85
Debugging in VS Code
The main features of VS Code Python Debugging are:
i. Setting Breakpoints: Breakpoints are specific lines in your code
where you want the debugger to pause execution. To set a
breakpoint in VS Code, you can simply click on the gutter next
to the line number or use the keyboard shortcut (F9).
ii. Stepping Through Code: Once you have set breakpoints, you
can start debugging your Python code. When the execution
reaches a breakpoint, the debugger will pause, allowing you to
step through the code line by line. You can use the step over
(F10) or step into (F11) commands to navigate through your
code and see how variables change.
iii. Inspecting Variables: While debugging, it’s important to
inspect the values of variables at different points in your code.
VS Code provides a Variables panel that allows you to view and
track the values of variables. You can hover over variables in your
code or use the Watch panel to add specific variables to monitor.
45/85
Debugging in VS Code (ii)
iv. Debug Console: The Debug Console in VS Code allows you to
execute Python code and interact with your program while
debugging. This can be useful for testing specific code snippets
or evaluating expressions. The Debug Console supports both
single-line and multiline code execution.
v. Conditional Breakpoints: In addition to regular breakpoints,
VS Code also supports conditional breakpoints. With conditional
breakpoints, you can specify a condition that must be met for the
debugger to pause. This can be helpful when you want to break
only when a certain condition is true, such as when a variable
reaches a specific value.
vi. Exception Breakpoints: VS Code allows you to set breakpoints
on exceptions, so the debugger will pause whenever an exception
is raised. This can be useful for identifying and fixing errors in
your Python code. You can set exception breakpoints by opening
the Breakpoints view and clicking on the “+” button.
46/85
Next
Global Variables
Object-Oriented Programming
Python exceptions
During the execution of a program, exceptions are events that can
take place. If an exception occurs, an error message is shown, and the
program might terminate unless the exception is appropriately
handled. Common Python built-in exceptions are:
ZeroDivisionError: Raised when a division or modulo operation is
performed with zero as the divisor.
TypeError: Raised when an operation or function is applied to an
object of an inappropriate type.
ValueError: Raised when a function receives an argument of the
correct type but an inappropriate value.
IndexError: Raised when an index is out of range for a sequence.
NameError: Occurs when you try to use a variable, function, or
module that hasn’t been defined or imported correctly
FileNotFoundError: Raised when an attempt is made to open a file
that does not exist.
47/85
Assertions in Python
In Python, an assertion is a statement that allows to test a condition
and raise an AssertionError if the condition is not met.
The syntax for using an assertion in Python is:
assert condition , message
Global Variables
Object-Oriented Programming
Handling exceptions
Python provides a convenient mechanism, try-except, for catching
and handling exceptions.
The general form is
try
code block
except ( tuple with exception names ) :
code block
else :
code block
50/85
try-except example
Consider the code
s uc cess _ f a i l u r e _ r a ti o = num_successes / num_failures
print ( ' The success / failure ratio is ' ,
suc c e s s _ f a i l u r e _ rati o )
Most of the time, this code will work just fine, but it will fail if
num_failures happens to be zero.
The attempt to divide by zero will cause the Python runtime system
to raise a ZeroDivisionError exception, and the print statement will
never be reached. A possible solution is
try :
suc c e s s _ f a i l u r e_ rati o = num_successes / num_failures
print ( ' The success / failure ratio is ' ,
s u c c e s s _fai lure_ rati o )
except Ze roDivi sionError :
print ( ' No failures , so the success / failure ratio is
undefined . ')
51/85
Several except statements
If it is possible for a block of program code to raise more than one
kind of exception, the reserved word except can be followed by a
tuple of exceptions.
For example, in the following line of code
except ( ValueError , TypeError ) :
There are two except blocks associated with the try block.
53/85
Several except statements example (contd.)
If an exception is raised within the try block, Python first checks to
see if it is a ZeroDivisionError. If so, it appends a special value,
'nan', of type float to ratios.
The value nan stands for “not a number”, and there is no literal for it,
but it can be denoted by converting the string 'nan' or the string
'NaN' to type float. When nan is used as an operand in an
expression of type float, the value of that expression is also nan.
If the exception is anything other than a ZeroDivisionError, the
code executes the second except block, which raises a ValueError
exception with an associated string.
The second except block should never be entered, because the code
invoking get_ratios should respect the assumptions in the
specification of get_ratios.
However, it is probably worth practicing defensive programming and
checking anyway.
54/85
Next
Global Variables
Object-Oriented Programming
Exceptions as a control flow mechanism
Exceptions are a convenient flow-of-control mechanism that can be
used to simplify programs.
In many programming languages, the standard approach to dealing
with errors is to have functions return a value (often something
analogous to Python’s None) indicating that something is amiss.
In Python, it is more usual to have a function raise an exception when
it cannot produce a result that is consistent with the function’s
specification.
The Python raise statement forces a specified exception to occur, its
form is
raise exceptionName ( arguments )
57/85
Next
Global Variables
Object-Oriented Programming
Classes and Object-Oriented Programming
We now turn our attention to our last major topic related to
programming in Python: using classes to organize programs around
data abstractions.
Classes can be utilized in a multitude of ways, in the following their
usage, in the context of object-oriented programming (OOP), will be
explained.
The key to object-oriented programming is thinking about objects as
collections of both data and the methods that operate on that data.
Recall that objects are the core things that Python programs
manipulate. Every object has a type that defines the kinds of things
that programs can do with that object.
So far, we have relied upon built-in types such as float and str and
the methods associated with those types.
In this section, we will explore the mechanism that enables
programmers to define new types in Python.
58/85
Next
Global Variables
Object-Oriented Programming
Abstract Data Types and Classes
Inheritance and Data Encapsulation
Financial Examples
Abstract data types
An abstract data type is a set of objects and the operations on those
objects.
These are bound together so that programmers can pass an object
from one part of a program to another, providing both access to the
data attributes and to the operations to manipulate such data.
These components are interconnected in a way that allows
programmers to seamlessly transfer an object from one part of a
program to another.
This facilitates not only access to the data attributes held within the
object, but also the ability to perform operations and manipulate the
data effectively.
The interface of the data type defines the behavior of the operations.
It is an abstraction barrier that isolates the rest of the program from
the data structures, algorithms, and code involved in a realization of
the type abstraction.
59/85
Decomposition and abstraction
Programming is about managing complexity, two powerful
mechanisms are available for accomplishing this: decomposition and
abstraction.
Decomposition creates structure in a program, and abstraction
suppresses detail.
The key is to suppress the appropriate details. This is where data
abstraction hits the mark.
We can create domain specific types that provide a convenient
abstraction.
Ideally, these types capture concepts that will be relevant over the
lifetime of a program.
We have been using abstract data types so far, e.g., integers, lists,
floats, strings, and dictionaries.
This built-in abstract data types were utilized without any
consideration for their underlying implementation.
60/85
Python class definition example
In Python, a class definition begins with the reserved word class
followed by the name of the class and some information about how it
relates to other classes.
A class definition creates an object of type type and associates with
that class object a set of objects called attributes.
Consider the following example
class Toy ( object ) :
def __init__ ( self ) :
self . _elems = []
def add ( self , new_elems ) :
''' new_elems is a list . '''
self . _elems += new_elems
def size ( self ) :
return len ( self . _elems )
The first line indicates that Toy is a subclass of object, and the three
attributes associated with the class are __init__, add, and size.
These three attributes are of type function.
61/85
Python class definition example (ii)
Consequently, the code,
print ( type ( Toy ) )
print ( type ( Toy . __init__ ) , type ( Toy . add ) , type ( Toy . size ) )
yields
< class ' type ' >
< class ' function ' > < class ' function ' > < class ' function ' >
assignment will create a new instance of type Toy, and then call
Toy.__init__ with the newly created object as the input argument
that is bound to the formal parameter, i.e., the variable name, self.
62/85
Python class definition example (iii)
When invoked, Toy.__init__ creates2 the list object _elems, which
becomes part of the newly created instance of type Toy.
The list _elems is called a data attribute of the instance of Toy.
The code
t1 = Toy ()
print ( type ( t1 ) )
print ( type ( t1 . add ) )
t2 = Toy ()
print ( t1 is t2 ) # test for object identity
results in
< class ' __main__ . Toy ' >
< class ' method ' >
False
65/85
Magic methods
One of the design goals for Python was to allow programmers to use
classes to define new types that are as easy to use as the built-in
types of Python.
Using magic methods to provide class-specific definitions of built-in
functions such as str and len plays an important role in achieving
this goal.
Magic methods can also be used to provide class-specific definitions
for infix operators such as “==” and “+”.
The names of the methods available for infix operators are
“+”:__add__ “*”:__mul__ “/”:__truediv__
“-”:__sub__ “**”:__pow__ “%”:__mod__
“//”:__floordiv__ “|”:__or__ “<”:__lt__
“«”:__lshift__ “∧”:__xor__ “>”:__gt__
“»”:__rshsift__ “==”:__eq__ “<=”:__le__
“&”:__and__ “!=”:__ne__ “>=”:__ge__
66/85
Next
Global Variables
Object-Oriented Programming
Abstract Data Types and Classes
Inheritance and Data Encapsulation
Financial Examples
Inheritance
Many types have properties in common with other types. For
example, types list and str each have len functions that mean the
same thing.
Inheritance provides a convenient mechanism for building groups of
related abstractions.
It allows programmers to create a type hierarchy in which each type
inherits attributes from the types above it in the hierarchy.
The class object is at the top of the hierarchy. This makes sense,
since in Python everything that exists at runtime is an object.
As an example use of classes, imagine that you are designing a
program to help keep track of all the students professors, and staff at
IE University.
Each student would have a family name, a given name, a home
address, a year, some grades, etc.
This data could all be kept in a combination of lists and dictionaries.
67/85
Person class
Is there an abstraction that covers the common attributes of students,
professors, and staff?
As a starting point consider a class named Person as given in the file
“U2_7_class_person.py”, which first code lines are
''' Class Person example '''
import datetime
68/85
Person class (ii)
def get_name ( self ) :
''' Returns self 's full name . '''
return self . _name
69/85
Person class (iii)
def __lt__ ( self , other ) :
''' Assume other a Person .
Returns True if self precedes other in
alphabetical order , and False otherwise .
Comparison is based on last names , but if
these are the same full names are compared .
'''
if self . _last_name == other . _last_name :
return self . name < other . _name
return self . _last_name < other . _last_name
71/85
Subclasses and inheritance
class IE_person ( Person ) :
_next_id_num = 0 # identification number
The class IE_person inherits attributes from its parent class, Person,
including all of the attributes that Person inherited from its parent
class, object.
In the jargon of object oriented programming, IE_person is a subclass
of Person, and therefore inherits the attributes of its superclass.
72/85
Multiple levels of inheritance
The followng code adds another couple of levels of inheritance to the
class hierarchy
class Student ( IE_person ) :
pass
class UG ( Student ) :
def init_ ( self , name , class_year ) :
super () . __init__ ( name )
self . _year = class_year
def get_class ( self ) :
return self . _year
The reserved word pass indicates that the class has no attributes
other than those inherited from its superclass.
Introducing the class Grad allows to create two kinds of students and
use their types to distinguish one kind of object from another.
73/85
Substitution principle
In addition to what it inherits, a subclass can: (i) add new attributes;
and (ii) override, i.e., replace, attributes of the superclass.
When subclassing is used to define a type hierarchy, the subclasses
should be thought of as extending the behavior of their superclasses.
We do this by adding new attributes or overriding attributes inherited
from a superclass.
Sometimes, the subclass overrides methods from the superclass, but
this must be done with care.
In particular, important behaviors of the supertype must be supported
by each of its subtypes.
If client code works correctly using an instance of the supertype, it
should also work correctly when an instance of the subtype is
substituted.
Hence the phrase substitution principle for the instance of the
supertype.
74/85
Encapsulation and information hiding
The idea of encapsulation means the bundling together of data
attributes and the methods for operating on them.
Another important concept is information hiding, it is one of the keys
to modularity.
If those parts of the program that use a class (i.e., the clients of the
class) rely only on the specifications of the methods in the class, a
programmer implementing the class is free to change the
implementation of the class without worrying that the change will
break code that uses the class.
Programmers can make the attributes of a class private, so that clients
of the class can access the data only through the object’s methods.
Python 3 uses a naming convention to make attributes invisible
outside the class.
If the name of an attribute starts with “__” (double underscore) but
does not end with “__”, that attribute is not visible outside the class.
75/85
Generators
Any function definition containing a yield statement is treated in a
special way.
The presence of yield tells the Python system that the function is a
generator.
Generators are typically used with for statements.
At the start of the first iteration of a for loop that uses a generator,
the generator is invoked and runs until the first time a yield
statement is executed, at which point it returns the value of the
expression in the yield statement.
On the next iteration, the generator resumes execution immediately
following the yield, with all local variables bound to the objects to
which they were bound when the yield statement was executed, and
again runs until a yield statement is executed.
It continues to do this until it runs out of code to execute or executes
a return statement, at which point the loop is exited.
76/85
Next
Global Variables
Object-Oriented Programming
Abstract Data Types and Classes
Inheritance and Data Encapsulation
Financial Examples
Financial account
The code within “U2_8_financial_account.py” defines a class
called “FinancialAccount” that represents a financial account.
It has attributes for the account holder’s name and the current
account balance.
class FinancialAccount :
''' FinancialAccount class with attributes for the
account holder 's name ( account_holder ) and the
current account balance ( balance ) .
The class has methods for depositing ( deposit )
and withdrawing ( withdraw ) funds , as well as
getting the current balance ( get_balance ) .
The __str__ method is defined to provide a
string representation of the account . '''
77/85
Financial account (ii)
def deposit ( self , amount ) :
if amount > 0:
self . balance += amount
print ( f " Deposited $ { amount :.2 f }. New balance
: $ { self . balance :.2 f } " )
else :
print ( " Invalid deposit amount . Amount must
be greater than 0. " )
79/85
Mortgages
A mortgage consists in borrowing money from a bank and made a
fixed payment each month for the life of the mortgage, typically from
15 to 30 years.
At the end of that period, the bank had been paid back the initial loan
(the principal) plus interest, and the homeowner owned the house.
For example, “interest-only” mortgages, i.e., for some number of
months at the start of the loan the borrower paid only the accrued
interest and none of the principal.
Other loans involved multiple rates. Typically the initial rate (called a
“teaser rate”) was low, and then it went up over time.
Many of these were variable-rate, i.e., the rate to be paid would vary
depending upon some index intended to reflect the cost to the lender
of borrowing on the wholesale credit market.
To provide some experience in the incremental development of a set
of related classes the code within “U2_9_mortgage.py” is provided.
80/85
Mortgage base class
def find_payment ( loan , r , m ) :
''' Assumes : loan and r are floats , m an int .
Returns the monthly payment for a mortgage of size
loan at a monthly rate of r for m months . '''
return loan *(( r *(1+ r ) ** m ) /((1+ r ) ** m - 1) )
84/85
Mortgages comparison
def compare _mortg ages ( amt , years , fixed_rate , pts ,
pts_rate , var_rate1 , var_rate2 , var_months ) :
tot_months = years *12
fixed1 = Fixed ( amt , fixed_rate , tot_months )
fixed2 = Fixed_with_pts ( amt , pts_rate , tot_months ,
pts )
two_rate = Two_rate ( amt , var_rate2 , tot_months ,
var_rate1 , var_months )
morts = [ fixed1 , fixed2 , two_rate ]
for m in range ( tot_months ) :
for mort in morts :
mort . make_payment ()
for m in morts :
print ( m )
print ( f ' Total payments = ' +
'$ { m . get_total_paid () : ,.0 f } ')
85/85
References
Guttag, John V. (2021). Introduction to Computation and
Programming Using Python. with Application to Computational
Modeling and Understanding Data. Third Edition. Cambridge,
MA: The MIT Press.