Python Programming Case Studies PDF
Python Programming Case Studies PDF
Studies in Python
Göktürk Üçoluk r Sinan Kalkan
Introduction
to Programming
Concepts with Case
Studies in Python
Göktürk Üçoluk Sinan Kalkan
Department of Computer Engineering Department of Computer Engineering
Middle East Technical University Middle East Technical University
Ankara, Turkey Ankara, Turkey
Purpose
Approach
This book introduces concepts by starting with the Q/A ‘WHY?’ and proceeds by
the Q/A ‘HOW?’. Most other books start with the Q/A ‘WHAT?’ which is then fol-
lowed by a ‘HOW?’. So, this book introduces the concepts starting from the grass-
roots of the ‘needs’. Moreover, the answer to the question ‘HOW?’ is somewhat
different in this book. The book gives pseudo-algorithms for the ‘use’ of some CS
concepts (like recursion or iteration). To the best of our knowledge, there is no other
book that gives a recipe, for example, for developing a recursive solution to a world
problem. In other textbooks, recursion is explained by displaying several recursive
solutions to well-known problems (the definition of the factorial function is the most
famous one) and hoping for the student to discover the technique behind it. That is
why students following such textbooks easily understand what ‘recursion’ is but get
stunned when the time comes to construct a recursive definition on their own. This
teaching technique is applied throughout the book while various CS concepts got
introduced.
This book is authored in concordance with a multi-paradigm approach, which is
first ‘functional’ followed by ‘imperative’ and then ‘object oriented’.
The CS content of this book is not hijacked by a programming language. This
is also unique to this book. All other books either do not use any PL at all or first
introduce the concepts only by means of the PL they use. This entanglement causes
v
vi Preface
a poor formation of the abstract CS concept, if it does at all. This book introduces
the concepts ‘theoretically’ and then projects it onto the Python PL. If the Python
parts (which are printed on light greenish background) would be removed, the book
would still be intact and comprehensible but be purely theoretical.
Audience
This book is intended for freshman students and lecturers in Computer science or
engineering as a text book of an introductory course frequently named as one of:
– Introduction to Programming
– Introduction to Programming Constructs
– Introduction to Computer Science
– Introduction to Computer Engineering
Acknowledgments
We would like to thank Faruk Polat and İ. Hakkı Toroslu from the Middle East
Technical University’s Department of Computer Engineering and Reda Alhajj from
the Department of Computer Science of University of Calgary for their constant
support. We would also like to thank Chris Taylor for her professional proofreading
of the manuscript and our student Rowanne Kabalan for her valuable comments on
the language usage. Moreover, we are very grateful to Aziz Türk for his key help in
the official procedures of publishing the book.
Last but not least, we thank our life partners Gülnur and Gökçe and our families:
without their support, this book would not have been possible.
Department of Computer Engineering, Göktürk Üçoluk
Middle East Technical University, Sinan Kalkan
Ankara, Turkey
Contents
vii
viii Contents
Leaving the television media context to one side, in its most general meaning, a
‘program’ can be defined as:
1 ‘Oracle machine’ has nothing to do with the world-wide known database company ‘ORACLE’.
2 Actually ‘digital’ does not necessarily mean ‘binary’. But to build binary logic electronic circuits
is cheap and easy. So, in time, due to the technological course all digital circuits are built to support
binary logic. Hence, ‘digital’ became a synonym for binary electronic circuity.
1 The World of Programming 3
A very important aspect of this architecture is that it is always the CPU that
determines the address that is to be accessed in the memory. In other words, it
is the CPU that generates and sends the address information via the address bus.
There is no chance that the memory ‘sends back’ an address.
The CPU sets the address bus to carry a certain address and also sets the R/W line
depending on whether the content at the given address will be read or written. If
it is a write action, then the CPU also sets the data bus to the content that will be
stored at the specific ‘page’ with that address. If it is a read action, this time it is
the memory that sets the data bus to carry a copy of the content of the ‘page’ at
the given address. Therefore, the address bus is unidirectional whereas the data
bus is bidirectional.
• When the CPU is powered on or after the CPU has carried out a unit of action,
it reads its next action, so called instruction, from the memory.3 Depending on
the outcome of last instruction, the CPU knows exactly, in the memory, where
its next instruction is located. The CPU puts that address on the address bus, sets
the R/W line to ‘read’ and the memory device, in response to this, sends back the
instruction over the data bus. The CPU gets, decodes (understands, or interprets)
and then performs the new instruction. Performing the action described by an
instruction is called ‘executing the instruction’. After this execution, the CPU
will go and fetch the next instruction and execute it. This continuous circle is
called the Fetch-Decode-Execute cycle (Fig. 1.2).
• The memory holds instructions and the information to be consumed directly by
those instructions (see Fig. 1.3 for a simple example that illustrates this). This
data is immediately fetched by the CPU (actually, if the data bus is large enough,
α is fetched right along with the instruction byte) and used in that execute cycle.
3 In the binary representation of instructions and data there exists some degree of freedom. Namely,
Fig. 1.3 The memory holds the instructions and the information to be consumed by the instruc-
tions. Also some results generated by the CPU are stored to the memory, for later use. In the figure
you see a segment of the memory (the address range: [8700–8714]) holding a small machine code
program. The program loads the first register with the integer 6452, the second with 1009 and then
adds these leaving the result in the first. Then stores this integer result to the memory position that
starts at 8713. Making use of a jump instruction the program ‘steps over’ this position that holds
the result and continues its instruction fetch from the address 8715
However, it is not only the instructions and this adjunct information that fills
the memory. Any data which is subject to be processed or used is stored in the
memory, as well. Individual numbers, sequences of numbers that represent digi-
tized images, sounds or any tabular numerical information are also stored in the
memory. For example, when you open an text file in your favorite text editor,
the memory contains sequences of words in the memory without an associated
instruction.
It is essential to understand the Von Neumann architecture and how it functions
since many aspects of ‘programming’ are directly based on it.
Programming, in the context of the Von Neumann architecture, is generating
the correct memory content that will achieve a given objective when executed
by the CPU.
1.1 Programming Languages 5
Programming a Von Neumann machine by hand, i.e., constructing the memory lay-
out byte-by-byte is an extremely hard, if not impossible, task. The byte-by-byte
memory layout corresponding to a program is called the machine code.
Interestingly, the first programming had to be done the most painful way: by
producing machine code by hand. Now, for a second, consider yourself in the fol-
lowing position: You are given a set of instructions (encoded into a single byte), a
schema to represent numbers (integers and real numbers) and characters (again all
in bytes), and you are asked to perform the following tasks that a programmer is
often confronted with:
• Find the average of a couple of thousand numbers,
• Multiply two 100 × 100 matrices,
• Determine the shortest itinerary from city A to city B,
• Find all the traffic signs in an image.
You will soon discover that, not to end up in a mental institution, you need to be
able to program a Von Neumann machine using a more sophisticated set of instruc-
tions and data that are more understandable by humans, and you need a tool which
will translate this human comprehensible form of the instructions and data (numbers
and characters, at least), into that cumbersome, ugly looking, incomprehensible se-
quence of bytes; i.e., machine code.
Here is such a sequence of bytes which multiplies two integer numbers sitting in two
different locations in the memory and stores the result in another memory position:
01010101 01001000 10001001 11100101 10001011 00010101 10110010 00000011
00100000 00000000 10001011 00000101 10110000 00000011 00100000 00000000
00001111 10101111 11000010 10001001 00000101 10111011 00000011 00100000
00000000 10111000 00000000 00000000 00000000 00000000 11001001 11000011
...
11001000 00000001 00000000 00000000 00000000 00000000
And, the following is a human readable form, where the instructions have been
given some short names, the numbers are readable, and some distinction between
instructions, instruction related data, and pure data is recognizable:
main:
pushq %rbp
movq %rsp, %rbp
movl alice(%rip), %edx
movl bob(%rip), %eax
imull %edx, %eax
movl %eax, carol(%rip)
6 1 The World of Programming
Now, having the working example of an assembler in front of us, why not take the
concept of ‘translation’ one step further? Knowing the abilities of a CPU at the
instruction level and also having control over the whole space of the memory, why
not construct a machine code producing program that ‘understands’ the needs of
the programmer better, in a more intelligent way and generates the corresponding
machine code, or even provides an environment in which it performs what it is asked
for without translating it into a machine code at all. A more capable translator or a
command interpreting program!
But, what are the boundaries of ‘intelligence’ here? Human intelligence? Cer-
tainly not! Even though this type of translator or command interpreter will have
intelligence, it will not be able to cope with the ambiguities and heavy background
information references of our daily languages; English, Turkish, French or Swazi.
Therefore, we have to compromise and use a restricted language, which is much
more mathematical and logical compared to natural languages. Actually, what we
1.2 Programming Paradigms 7
would like is a language in which we can express our algorithms without much
pain. In particular, the pain about the specific brand of CPU and its specific set of
instructions, register types and count etc. is unbearable. We have many other issues
to worry about in the field of programming.
So, we have to create a set of rules that define a programming language both syn-
tax- and semantic-wise, in a very rigorous manner. Just to refresh your vocabulary:
syntax is the set of rules that govern what is allowed to write down, and semantics
is the meaning of what is written down.
Such syntactic and semantic definitions have been made over the past 50 years,
and now, we have around 700 programming languages of different complexities.
There are about a couple of thousands more, created experimentally, as M.Sc. or
Ph.D. theses etc.
Most of these languages implement high-level concepts (those which are not
present at the machine level) such as
• human readable form of numbers and strings (like decimal, octal, hexadecimal
representations for numbers),
• containers (automatic allocation for places in the memory to hold data and nam-
ing them),
• expressions (calculation of formulas based on operators which have precedences
the way we are used to from mathematics),
• constructs for repetitive execution (conditional re-execution of code parts),
• functions,
• pointers (a concept which fuses together the ‘type of the data’ and the ‘address of
the data’),
• facilities for data organization (ability to define new data types based on the prim-
itive ones, organizing them in the memory in certain layouts).
Before diving into the variety of the programming languages, let us have a
glimpse at the problem that we introduced above, i.e., multiplication of two inte-
gers and storage of the result, now coded in a high level programming language, C:
int alice = 123;
int bob = 456;
int carol;
main(void)
{
carol = alice*bob;
}
This is much more understandable and shorter, isn’t it?
For example, one world view is regarding the programming task as transforming
some initial data (the initial information that defines the problem) into a final form
(the data that is the answer to that problem) by applying a sequence of functions.
From this perspective, writing a program is defining some functions which then are
used in a functional composition; a composition which, when applied to some initial
data, yields the answer to the problem. The earliest realization of this approach was
the LISP language, designed by John McCarthy in 1958 at MIT. After LISP, more
programming languages have been developed, and more world views have emerged.
The Oxford dictionary defines the word paradigm as follows:
paradigm |’parU,dïm|
noun
A world view underlying the theories and methodology of a particular scien-
tific subject.
These world views in the world of programming are known as programming
paradigms. Below is a list of some of the major paradigms:
• Imperative
• Functional
• Logical-declarative
• Object oriented
• Concurrent
• Event driven
In this paradigm, the programmer states the relations among the data as facts or
rules (sometimes also referred to as relations).
For example, facts can be the information about who is whose mother and the
rule can be a logical rule stating that X is the grandmother of Y if X is the mother
of an intermediate person who is the mother Y. Below is such a program in Prolog
a well-known logical programming language.
mother(matilda,ruth).
mother(trudi,peggy).
mother(eve,alice).
mother(zoe,sue).
mother(eve,trudi).
mother(matilda,eve).
mother(eve,carol).
grandma(X,Y) :- mother(X,Z), mother(Z,Y).
The mother() rule tells the computer about the existing motherhood relations
in the data (i.e., the people); for example, mother(mathilda,ruth) states
that mathilda is the mother of ruth. Based on the mother() rule, a new rule
grandma() is easily defined: grandma(X,Y) is valid if both mother(X,Z)
and mother(Z,Y) are valid for an arbitrary person Z, which the computer tries to
find among the rules that are given to it by the programmer.
Now, a question (technically a query) that asks for all grandmas and granddaugh-
ters:
?- grandma(G,T).
will yield an answer where all solutions are provided:
G=matilda, T=alice
G=matilda, T=trudi
G=matilda, T=carol
Contrary to other programming paradigms, we do not cook up the solution in the
logical programming paradigm: We do not write functions nor do we imperatively
give orders. We simply state rule(s) that define relations among the data, and ask for
the data that satisfies the rules.
An object has some internal data and functions, so called methods. It is possible
to create as many replicas (instances) of an object as desired. Each of these instances
has its own data space, where the object keeps its ‘private’ information content.
Some of the functions of the object can be called from the outside world. Here,
the outside world is the parts of the program which do not belong to the object’s
definition. The outside word cannot access an object’s data space since it is private.
Instead, accessing the data stored in the object is performed by calling a function
that belongs to the object. This function serves as an interface and calling it is termed
message passing.
In addition to this concept of data hiding, the object oriented paradigm employs
other ideas as well, one of which is inheritance. A programmer can base a new
definition of an object on an already defined one. In such a case, the new object
inherits all the definitions of the existing object and extend those definitions or add
new ones.
The following is a simplified example that demonstrates the inheritance mech-
anism used in object-oriented programming. A class is the blueprint of an object
defined in a programming language. Below we define three classes. (The methods
in the class definitions are skipped for the sake of clarity):
class Item
{
string Name;
float Price;
string Location;
...
};
All these features help to represent real world problems more easily and generate
re-usable programs, enhancing the maintainability of the huge codes developed by
tens or hundreds of programmers. These are commercial assets of the object oriented
programming paradigm.
From this paradigm’s perspective, the programming world consists of events and
actions tied to these events. When an event occurs, the action tied to that event
is automatically triggered. This action may carry out some computational tasks as
well as give rise to some new events. The programmer’s job is to make a design of
this chain reaction clockwork and implement the actions as pieces of the program
(usually as functions).
• To serve all types of events (that can occur),
• not to get into deadlocks,
• handling events that occur while an action is being carried out
are kind of problems a programmer that does event driven programming has to deal
with.
This paradigm has extensive applications in Graphical User Interface4 (GUI) ori-
ented application development, since GUIs have excessive amount of diverse user
input (i.e., events).
4 A Graphical User Interface is the set of windows and all the stuff included in the windows that
take care of the exchange of information between the user and the program. For example, when
you open a browser, a window pops up; that window is the GUI for the browser.
12 1 The World of Programming
that can walk like an ostrich, run like a tiger, crawl like a snake, jump like kangaroo,
swim like a human, remain underwater like a fish and fly like an albatross?”.
Very similar to the problems in the design of such a super creature, the real-
ization of a super programming language would present extreme difficulties due to
conflicting requirements. Even if it were possible to construct such a language, it
would be far from being versatile, fast, portable and so on. The reality is that lan-
guages that serve at most two or three paradigms are feasible and can be realized by
programming language developers.
The development of programming languages is interesting in this sense. Fig-
ure 1.5 displays a historical overview of how this taxonomy developed.5
All paradigms, all programming languages and even all CPUs are equivalent. This is
called the Turing Equivalence. We will return to the subject of the Turing Machine
later in Sect. 3.3.1. For the moment, we are only introducing the term Turing equiv-
alence which states that two computers P and Q are Turing equivalent if P can sim-
ulate Q and Q can simulate P. The proof is simple: A Von Neumann computer can
simulate a Turing machine (a very simple, hypothetical, machine that has a mathe-
matical definition) and a Turing machine can simulate any real world computer (this
is a product of the Church–Turing thesis). The Church–Turing thesis is a conjecture
which cannot be formally proven, but has near-universal acceptance. It reads as:
Everything computable is computable by a Turing machine.
So what? Are we in the darkness of the meaninglessness, again? All CPUs are
equivalent! All paradigms are equivalent! All programming languages are equiva-
lent! What does it mean if we can do with one what we can do with the other?
For clarification, think about this example: A man with a shovel and a bulldozer
are equivalent too, if the task is displacing a certain amount of soil. For the common
sense the key point lies in what we call ‘efficiency’ or ‘easiness’. Turing equivalence
does not say anything about these concepts. It is merely an equivalence about ‘do-
ability’.
Therefore, it is meaningful, indeed, to choose a language based on the consider-
ation of efficiency and easiness. We make decisions based on these considerations
in all other aspects of our life, all the time.
There are different factors in choosing among the programming language
paradigms:
The domain and technical nature of the world problem: Consider three different
computing tasks:
Fig. 1.5 A brief taxonomic history (1956–2005) of high level programming language (From: Chen
et al. (2005). Updated by the authors)
(a) Finding the pixels of an image with RGB value of [202,130,180] with a
tolerance of 5.4% in intensity.
(b) A proof system for planar geometry problems.
(c) A computer game platform to be used as a whole or in parts and may be
extended or even rewritten by programmers at various levels.
(d) Payroll printing.
1.3 The Zoo of Programming Languages 15
Any wise programmer will pick a fast imperative language (like C) for (a), a
declarative language (like Prolog) for (b), an object oriented language (like C++)
for (c) and Cobol for (d).
What would be the foolish way to go? That is fun to answer! Cobol for (a), (b)
and (c).
Why is this so, because C is the fastest high-level language and the defined im-
age processing task (a) is highly imperative; Prolog is the most profound and
well-structured logic-declarative language especially suited for AI applications,
in which domain the ‘proof system’ (b) belongs; Game programming is speed
hungry and the re-usability issue of the task (c) is best fulfilled by an object ori-
ented language, which leads a programmer to C++; Finally, payroll printing (d)
can actually be done in any language but the archaic language Cobol is designed
only to serve the domain of business, finance, and administrative systems for
companies and governments.
Personal taste and experience: Among those 700 programming languages, cer-
tainly some are alike; or, some are alike as far as the task is concerned (i.e.,
the difference is (ir)relevant for that specific task); or, for instance, the program-
mer has a rationale to favor a specific paradigm or language. As an example,
think of a case where a programmer prefers the Lisp language (a profound rep-
resentative of the functional paradigm) over Prolog (the best known declarative
paradigm representative) because s/he has a library of programs at hand to carry
out rule-based AI programming in Lisp (which s/he may have accumulated over
his/her extensive years of AI programming experience).
Microsoft, used C, C++ and assembler in its Windows operating system. Apple,
in its Mac OS X operating system used C, C++ and Objective C. The non-
proprietary operating system Linux used C and assembler. For other program-
mers, taking also the other constraints into account, it is a matter of taste to
choose among C++, Delphi or Objective-C.
Circumstance-imposed constraints: It is an interesting empirical fact that there is
always a shortage of time when it comes to writing a program. However, some-
times, the project constraints are so tight that you know from the start that you
have to go for a suboptimal solution; i.e., a solution with compromises. You
pick a scripting language (Perl, Php, Python, Ruby) for fast prototyping, know-
ing that it will be slow compared to an implementation in C. You select for Java
being aware that it will be less efficient compared to C++, but knowing there is
a huge pile of reusable code available in Java. As it is with life, choices made in
the world of programming have their pros and cons.
Writing an experimental program to prove a concept developed in connection
with your Ph.D. thesis and putting the concept into a commercial product are
also different. The first code is presumably understandable only ‘by you’ and
‘for you’ whereas the second will be ‘by a team of programmers in a software
company’ and ‘for thousands of end users’. In the second case, you have to
seriously consider the ease-of-use, maintainability, re-usability and portability
(compatibility with another operating system), etc., and, all these will impact on
your choice of programming language.
16 1 The World of Programming
Current trend: One way or another, current trends influence our choice of program-
ming language for various reasons such as:
• Trendy languages usually come with gizmos for contemporary issues.
• There is a large professional interest, which, in turn, means dynamic answers
in forums, documents, etc.
• The availability of a rich environment for learning (lectures, books, videos).
• Great collections of program libraries (even mostly free).
• It is easy to find a substitute programmer if someone drops out of the team.
• Customers are impressed to hear that their project will be realized using
a trendy language since the disadvantages of older languages are more
widespread.
There are many attempts to measure and inform the professional community
about the latest trend. Job advertisements mentioning a certain programming
language, lines of code written by the open software community, the teaching
books sold and search engine clicks are all counted and statistically evaluated
for this purpose.
In Fig. 1.6 the popularity change over the last 8 years from the TIOBE PCI is dis-
played. The TIOBE PCI (programming community index)6 is a widely known
and acknowledged indicator for the popularity of programming languages. As
denoted on the TIOBE web page, the index is updated once a month; the rat-
ings are based on the number of skilled engineers world-wide, courses and third
party vendors; the popular search engines Google, MSN, Yahoo!, Wikipedia and
YouTube are used to calculate the ratings.
Up to this point, you should have a fair idea of what a high level programming
language is.
When it comes to implementation, now we will discuss two ways to proceed
(later, we will also introduce an intermediate path). One approach is to set up a
machinery, a program, which takes as an input a program P in a high level language
and processes it, then produces a machine code equivalent of it. Then, whenever
we want the program to function, we make the Von Neumann machine execute the
translated machine code. This is the compilative approach. An alternative way is
to devise a machinery that, instead of producing the machine code, actually carries
out what the program P describes; immediately, statement by statement. This is the
interpretive approach.
6 http://www.tiobe.com/index.php/content/paperinfo/tpci/.
1.4 How Programing Languages Are Implemented 17
Fig. 1.6 Popularity change over years for highly used programming languages (Source:
www.tiobe.com)
In this approach, the high level language program is written by the programmer
using a text editor in a text file which is then submitted to a program that does
the translation. Such a translator program is called a compiler. The output of the
compilation is a file that contains the machine code translation of the high level
language program and some other information (Fig. 1.7). This content is called the
object code. Why is the output of the compilation process not exactly the machine
18 1 The World of Programming
code that will be run? Because it is quite likely that the programmer has made use of
some code that he has not defined. For example, a print directive, or an arctangent
calculation. Such codes are provided by the compiler manufacturer as a separate
entity (not embedded into the compiler program), usually as a huge file, full of
machine code pieces, which is called a library. The calls to these code pieces are
left as holes in the machine code. The object code starts with a table followed with
the machine code with holes in it. The table provides information about which hole
is going to be filled with which code’s reference.
After the first stage of compilation, a program called the linker, analyzes both
the object code and the library. It creates a combined file where the compiled code
is followed by the copies of the external code pieces that come from the library and
were used by the programmer. Looking at the table of the object code and the library,
the linker finds the matching names (for the code pieces) and substitutes into the
holes the corresponding references (addresses). The result is a stand-alone machine
code ready for execution. Actually this picture (Fig. 1.8) is a little simplified. In
reality, it is also possible to use codes which are coming from other object files.
Such combinations of object codes are possible because an object also carries a
table about the names of the codes (functions and variables) it implements/defines.
So, the linker is capable of analyzing more than one object code plus the library and
fill the holes accordingly.
After the linking is carried out, the user, whenever s/he wants to, loads the ma-
chine code into the memory and starts executing it. There is a program called loader
which is responsible for loading and starting machine codes (Fig. 1.9).
The compilative approach produces fast and executable machine codes, but it
is not so suitable for an incremental development of a program. Compilation and
linking, even in these days, take some time. Therefore, making small tweaks in the
program or locating/correcting errors cannot be done interactively.
1.4 How Programing Languages Are Implemented 19
Fig. 1.9 Compilative approach: The whole program is first translated into an executable code
(with the help of a compiler and a linker), loaded into the memory at a future time, run, then the
result of the whole program is returned to the programmer
Fig. 1.10 Interpretive approach: Each high-level statement from the programmer is executed and
the result is immediately returned to the programmer
The interpretive approach is more user-friendly and elegant, but it has the huge
drawback of being slow, sometimes to the point of unbearable! This is due, in part,
to reinterpreting what might have been already interpreted; looping structures or
functions for example. To overcome this problem, it is possible to compile, on the
fly, such an expression into machine code. Then, when it is time to reuse it, instead
of reinterpreting it, the interpreter simply executes the freshly compiled machine
code (of that piece of code). If there is no repetition, this process would actually add
to the processing time, but, in practice, most programs contain repetition to a great
extent. This technique is called as on-the-fly compilation.
Compiling to machine code has its drawbacks too. There is certainly no standard-
ization on the processors nor on the machine code. All manufactures are proud of
having their own low-level languages for their CPU’s. Though, they are all Von Neu-
mann machines, they differ in the internal hardware components such as in number
and the types/purposes of registers, the circuitry to undertake math calculations and
other instructions. Therefore, a machine code compiled for a CPU brand A, will not
run on a CPU brand B. One way to solve this problem is to write an emulator on the
computer with CPU B, for the machine code of CPU A; i.e., a kind of machine code
interpreter. This approach has some realizations. Of course, the interpretation has a
time cost which is a factor around three for CPUs of equal speed. However, there
exists another solution: to set a low-level language which is not biased towards any
CPU; i.e., a kind of a machine language where the machine does not exist as a hard-
ware, in other words, as a CPU. Then, for every brand of CPU, an interpreter will be
1.5 How a Program Gets “Written” 21
written, that takes the code expressed in this low-level language in, and carries out
the instructions. The idea is to keep this interpretation at a very simple level. This
pseudo machine code is generically called as p-code. A good example is the popular
language java. The interpreter that accepts java code is called jvm, an abbreviation
for the java virtual machine. You can find the jvm for almost every platform, from
powerful contemporary CPUs to the mobile phone CPUs.
Even a relatively small project of about a couple of thousand lines of code will have
a functional break-down; i.e., a division into a set of sub-components that are func-
tionally distinct from each other. The first level of such a break-down corresponds
to modules; for example consider a software for a library of books and periodicals.
The modules for this library software might be the following:
• The user interface module: The piece of code that is responsible for commu-
nicating with the user sitting in front of the computer and using the developed
or software. This module usually has screens containing informative text, parts
of which are active (e.g., clickable with the mouse), menus and text-fields to be
filled in by the user.
• The database module: The piece of code that is responsible for storing and re-
trieving information. In this case, it is about library material (i.e., books and pe-
riodicals) and the users of the library (their id, name, material they have checked
out, etc.).
• The control module: The piece of code that is responsible for all the ‘librarian’-
style actions such as, asking for the library id, checking out books, warning about
overdue items and generating lists of various items. While carrying out these
actions, most probably, this module will heavily communicate with (i.e., make
use of) the ‘database module’ as well as the ‘user interface module’.
22 1 The World of Programming
1.5.2 Testing
While a program is being developed, a constant and continuous effort has to be made
to ensure that the program conforms with the specifications (i.e., user requirements),
and meets the expectations. The former of these efforts is called verification and the
latter validation. In other words, verification is asking the question
Is the program built right?
While validation is asking
Is it the right program?
There are two fundamental approaches to respond to these questions. The first is
to experiment with the behavior of the software product, which is merely dynamic
testing. The other is static testing which means analyzing the product (not only the
code that makes up the program material but also its documentation etc.) and then
to decide whether it will operate correctly by examining the logical consequences
of its design. We will not go into the details about static testing, as this is subject
to theoretical research. Companies, therefore take ‘testing’ (apart from the careful
inspection of specifications and code by their in-house programming experts) to
mean dynamic testing. Actually, both approaches are complementary and should be
used together. It is also important to understand that, by testing, one cannot prove
the complete absence of errors. In other words, simply because the test suite of trial
examples for a system did not detect any errors, it cannot be concluded that the
system does not contain any error. So, the verification is just a relative verification,
‘modulo’ your tests.
Usually, software projects are so large, in scale, that it is unrealistic to test them
as a single unit. The way such systems are realized is in terms of smaller units, i.e.,
modules, which in turn are built out of procedures, functions, etc.
1.5 How a Program Gets “Written” 23
Structure-wise, there are two testing approaches that are used in testing: top-
down and bottom-up testing. Behavior-wise there are again two types of testing
approaches: black-box and white-box testing.
Top-Down Testing
In the top-down approach, testing starts at a subsystem level. Even though some
modules may not have actually been implemented before testing starts, they can be
substituted by stubs. A stub is a program module which has exactly the same mod-
ule interface as the intended final module, but is much simpler (in a sense hollow)
in implementation; i.e., it ‘fakes’ the subsystem by producing a simulated action or
output. This is achieved by random data generation, or returning its input as the out-
put, or directly consulting the tester for a result. Eventually, all stubs are replaced by
their actual code. Clearly, this technique does not need to wait until all the modules
are completed, and indeed it should to be started as soon as possible. It should be
used in parallel with the development, so that as soon as a part of the software is
coded, it is possible to start testing it.
Bottom-Up Testing
Bottom-up testing proceeds by starting from the smallest components. These com-
ponents are tested individually, and then the module that integrates them is tested.
As mentioned above, modules make up the subsystems; so, when all the modules
have been tested, the subsystems are tested. Bottom-up testing has the disadvantage
that until the very last line of code is written, the system is not ready for demonstra-
tion.
It is reasonable to suggest that a mixture of top-down and bottom-up testing
should be used. In this mixed approach, the implementation may start with a single
module, in which all kinds of bottom-up tests are performed and then this is mounted
in a top-down test frame. This scheme is continued until all the modules are coded.
Black-Box Testing
White-Box Testing
1.5.3 Errors
Fig. 1.11 Photo of what is possibly the first real bug found in a computer. (Photo courtesy of U.S.
Naval Historical Center Online Library Photograph—NH 96566-KN)
Hopper was not actually the one who found the insect, as she readily acknowledged.
The date in the log book was 9 September 1947, although sometimes erroneously
reported as 1945. The operators who did find it, including William “Bill” Burke, later
of the Naval Weapons Laboratory, Dahlgren, Virginia, were familiar with the engi-
neering term and, amused, kept the insect with the notation “First actual case of bug
being found.” Hopper loved to recount the story. This log book is on display in the
Smithsonian National Museum of American History, complete with moth attached.
(See Fig. 1.11.)
Generally speaking, there are four types of programming errors (bugs).
• Syntax errors (also known as compile-time errors): The programmer does not
obey the syntax rules of the programming language. For example, instead of en-
tering:
area = 3.1415 * R * R
the programmer types:
area = 3.1415 x R x R
and since ‘*’ is the infix operator for multiplication for this programming lan-
guage not ‘x’ the programmer has made a syntax error. Syntax errors are the eas-
26 1 The World of Programming
iest errors to resolve. The compiler or the interpreter will issue the error, mostly
indicating where exactly it occurred, with even in some cases, pointers on how to
correct it.
• Run-time errors: Some errors cannot be caught at the moment the program is
written. For example, assume that we are defining a function that will compute
the square root of the discriminant of a quadratic equation:
SqrtDelta(a, b, c) = b2 − 4ac
All high-level imperative languages will allow a function to be defined for this
computation. The following is an attempt to implement and test it in Python:
The last line causes a run-time error: the inputs given by the user lead to taking
the square-root of a negative number, which is not allowed in the floating number
domain.
Similarly, an attempt to divide a number by zero or accessing a memory portion
that is not reserved for or forbidden to the running program will also cause a
run-time error.
• Logic errors: Whenever you look at an outcome of a program and say:
It wasn’t supposed to do that!
you have a logic error. So, a logic error is when your program runs in an unex-
pected way.
Here is an example. Assume we want to compute the first root of a quadratic
equation. So, we want to do the following
√
−b + b2 − 4ac
root1 =
2a
and you code this equation in Python as:
Such a mistake will surface as ‘unexpected results’, which are not so easy to spot
compared with a compile-time error.
• Design errors are like logic errors. The difference is that, in logic errors, your
intention is correct but the way you convert it into a program contains a mistake,
but, in design errors, your intention was incorrect.
To have a concrete example, assume that you are given a cubic equation in the
form of the following equation:
x 3 + ax 2 + bx + c = 0
and you are cooking up the solution to find the first root by the method which
works only for a quadratic equation:
√
−b + b2 − 4ac
root1 =
2a
This is a design error.
1.5.4 Debugging
Removing a bug from a program is called debugging. The majority of the effort
in a debugging phase will go into the debugging of logic errors. There are various
debugging techniques, some of them are given below:
Use compiler features: Most contemporary compilers attempt to do semantic
checks (e.g., for the use of uninitialized variable or the forgotten return value
in a function definition) and issue warnings. Configure your compiler so that it
gives warnings about the semantics of what you are compiling. This is usually
done by some comment line flags.
RTFM: The polite meaning is Read-the-Fine-Manual (for a different meaning of
the term you can do a web search). Obscurities may occur especially when you
are using library functions and the only cure is RTFM.
print insertion: This is the oldest debugging technique. Knowing the flow of the
program, you insert printing statements at key positions. This way you can check
either for the flow of the program (i.e., whether the program flow reached that
spot) or for the values of certain variables (that you suspect).
Intelligently inserted print statements is a very powerful tool in nailing down
bugs.
Defensive programming: Insert statements that make sure that the variable values
remain in their domains. Here are two self-explanatory examples:
if not -1<s<12 : print "s is out of range"
if n%2 == 1 : print "n expected to be even but found odd"
Use a debugger: A debugger is a computer program that is used to execute another
program (your buggy program) in a controlled manner, sometimes step-by-step
or instruction-by-instruction. As the debugger steps through your program, you
28 1 The World of Programming
can inspect the content of the variables in your program. In a decent debugger,
you can put breakpoints (i.e., stops) in the program so that, when the flow of
execution comes to a breakpoint, the execution halts and you can, for example,
investigate or alter the content of the variables.
Explain to a bystander: The bystander is a human innocent, ready-to-help-type
programmer. As you try to explain your code to him/her, very often you will
discover by yourself the idiocy you made.
The following rules of programming style are from the book “The Elements of Pro-
gramming Style” by Kernighan and Plauger (published by McGraw Hill, 1978; with
kind permission of the authors):
1. “Write clearly—don’t be too clever.”
2. “Say what you mean, simply and directly.”
3. “Use library functions whenever feasible.”
4. “Avoid too many temporary variables.”
5. “Write clearly—don’t sacrifice clarity for ‘efficiency’.”
6. “Let the machine do the dirty work.”
7. “Replace repetitive expressions by calls to common functions.”
8. “Parenthesize to avoid ambiguity.”
9. “Choose variable names that won’t be confused.”
10. “Avoid unnecessary branches.”
11. “If a logical expression is hard to understand, try transforming it.”
12. “Choose a data representation that makes the program simple.”
13. “Write first in easy-to-understand pseudo language; then translate into whatever
language you have to use.”
14. “Modularize. Use procedures and functions.”
15. “Avoid gotos completely if you can keep the program readable.”
16. “Don’t patch bad code—rewrite it.”
17. “Write and test a big program in small pieces.”
18. “Use recursive procedures for recursively-defined data structures.”
19. “Test input for plausibility and validity.”
20. “Make sure input doesn’t violate the limits of the program.”
21. “Terminate input by end-of-file marker, not by count.”
22. “Identify bad input; recover if possible.”
23. “Make input easy to prepare and output self-explanatory.”
24. “Use uniform input formats.”
25. “Make input easy to proofread.”
26. “Use self-identifying input. Allow defaults. Echo both on output.”
27. “Make sure all variable are initialized before use.”
28. “Don’t stop at one bug.”
29. “Use debugging compilers.”
1.6 Meet Python 29
In this book, we will be using Python as the programming language that will
serve as the example or, a testbed for the concepts we introduce. The parts of
the book in Python have a different background color (i.e., green), and after
covering a topic, it will be exemplified and discussed in Python.
ability), its standard library with wide range of tools and its support for multiple
programming paradigms (i.e., imperative, functional and object-oriented) and
many other advantages it provides. Python was in its design phases at the end
of 1980s and the first release appeared in 1991. With the release of the second
version in 2000, it started attracting the interest of a wider population.
Python is an interpreted language; i.e., as you have learned in Sect. 1.4, the
computer can ‘understand’, or ‘interpret’ your code and compute the result of
your code without producing an object file, or an executable file.
In this book, we will assume that you are using python in a Unix-like envi-
ronment; however, the topics that we will cover, the codes that we are going to
analyze and the illustrations that we will present are applicable in other plat-
forms, though with minor modifications in rare cases.
In a Unix-like environment, you start talking to Python with the command
python (if your operating system complains to you about not being able to
find the command python, check your installation or your $PATH environ-
ment variable):
skalkan@divan:~$ python
Python 2.5.2 (r252:60911, Jan 24 2010, 17:44:40)
[GCC 4.3.2] on linux2
Type "help", "copyright", "credits" or "license" for more information.
>>>
Now, the Python interpreter is ready to interact with us, which is denoted
by the >>> characters (These characters are called the prompt string of the
interpreter and can be changed by the programmer). The first line after the
python command tells us about the version (along with the information on
the specific release number and the date) of the Python interpreter that we are
using. The second line gives information about the compiler that was used to
compile the Python interpreter: Do not be surprised; it is quite common that a C
compiler like GCC is used to compile the interpreters for different programing
languages. The third line gives the user pointers for more information and help
about the interpreter. The fourth line tells the user that the interpreter is ready for
interaction. To exit the interpreter, you can type CTRL-D (obtained by pressing
first the CTRL key on your keyboard then while keeping it pressed also press
the D key) or exit().
The version of the interpreter is quite important since there are some ma-
jor incompatibilities between versions. There are two major versions: (1) 2.x
(namely, 2.5, 2.6 or 2.7) and (2) 3.x. Unfortunately, for some reasons that are
not clear even to us, these two major versions are not compatible with each
other. Since the 2.5, 2.6 and 2.7 versions of Python have a wider range of tools,
facilities and applications available, we have adopted the 2.5 version of the in-
terpreter throughout the book.
1.7 Our First Interaction with Python 31
1.8 Keywords
The important concepts that we would like our readers to understand in this chapter
are indicated by the following keywords:
32 1 The World of Programming
Program Programming
Von Neumann Architecture CPU
Memory Imperative Programming Paradigm
Functional Programming Paradigm Object-Oriented Programming Paradigm
Logical Declarative Programming Paradigm Concurrent Programming Paradigm
1.10 Exercises
1. Propose the most suitable programming paradigm for each of the following prob-
lems:
(a) A computational environment where mathematical theorems or rules are
proven.
(b) A management system for a university where transfer of all the administra-
tive documents between the departments as well as information about the
students, the courses and the staff are managed.
(c) A software to be run on a car that assists the driver.
(d) The monitoring system of the airway control tower at a busy airport.
2. Propose whether a compilative or an interpretive programming language would
be more suitable for each of the following problems:
(a) A computational environment where mathematical theorems or rules are
proven.
(b) A management system for a university where transfer of all the administra-
tive documents between the departments as well as information about the
students, the courses and the staff are managed.
(c) A software to be run on a car that assists the driver.
(d) The monitoring system of the airway control tower at a busy airport.
3. Why do you think it is (currently) not possible to write programs using plain
English, or any other language spoken by humans? When do you think we can
write computer programs in our daily languages? (Hint: Have a look at the Turing
Test and the Chinese Room Argument.)
4. Linux is an open-source operating system that is developed by hundreds, even
thousands of programmers world-wide. If you were part of a team (composed
of thousands of open-source programmers located world-wide) to develop an
operating system (like Linux) from scratch,
(a) give one reason to choose imperative programming paradigm.
(b) give one reason to choose object-oriented paradigm.
34 1 The World of Programming
5. The nervous system in humans is one type of a computing machine where the
machine is composed of unit cells, called neurons; each neuron can work in par-
allel and perform its own computation; and, neurons can communicate with each
other (i.e., transfer information) using electrical currents. The nervous system is
composed of the connections between neurons (of different types) and therefore,
might be called the Connectionist Machine.
(a) What are the differences between the Connectionist and the Von Neumann
architectures?
(b) State, with reasons if possible, for which programming paradigm(s) the Con-
nectionist Architecture is more suitable.
Reference
Chen Y, Dios R, Mili A, Wu L, Wang K (2005) An empirical study of programming language
trends. IEEE Softw 22(3):72–79
Chapter 2
Data: The First Ingredient of a Program
The Von Neumann architecture has some implications even on high-level program-
ming languages. Below is an overview of these aspects:
• The Von Neumann architecture makes a clear distinction between the processing
unit, namely the CPU, and the memory.
• The content of the memory is highly mixed containing:
The orders to the CPU about all the actions: Register ⇔ memory transfer oper-
ations; arithmetic, comparison and bitwise operations on the registers; opera-
tions effecting the execution flow.
Adjunct information needed to carry out some instructions: Addresses for the
transfer operations, constants involved in arithmetic or bitwise operations.
Raw information to be processed: Integer or floating point values, or sequences
of them; address information of such raw data.
All these types of information live in the memory. However, still, there is a dis-
tinction between them. Anything that is stored in the memory falls into one of
these categories, and an error-free machine code, when executed, will consider
this distinction: Actions will be treated as actions, adjunct information as adjunct
information and raw information as raw information.
• Access to the memory is strictly address-wise. If you do not know the address of
what you are looking for, you cannot locate it unless you compare every memory
content with what you are looking for. In other words: Content-wise addressing
is not possible.
• All information subject to processing by the Von Neumann architecture must be
transformed to a binary representation.
Among these implications of the Von Neumann architecture, the main implica-
tion is the distinction between ‘actions’ and the ‘information’ because this distinc-
tion affects the way we approach any World problem. Here, the term World problem
refers to a problem of any subject domain, where a computerized solution is sought.
Below are a few examples:
• Find all the wheat growing areas in a satellite image.
Fig. 2.1 Most of the time, a world problem is solved by a set of algorithms that act on structured
data
• Given students’ homework, lab and examination grades, calculate their letter
grades.
• Change the amplitude of a sound clip for various frequencies.
• Predict China’s population for the year 2040, based on the changes in the popu-
lation growth rate up to date.
• Compute the launch date and the trajectory for a space probe so that it will pass
by the outermost planets in the closest proximity.
• Compute the internal layout of a CPU so that the total wiring distance is mini-
mized.
• Find the cheapest flight itinerary from A to B, given departure and return dates.
• Simulate a war between two land forces, given (i) the attack and the defense plans,
(ii) the inventories and (iii) other attributes of both forces.
In such World problems, the first task of the programmer is to identify the infor-
mation to be processed to solve the problem. This information is called data. Then,
the programmer has to find an action schema that will act upon this data, carry out
those actions according to the plan, and produce a solution to the problem. This
well-defined action schema is called an algorithm. This separation of data and algo-
rithm is visualized in Fig. 2.1. There can be more than one algorithm that can solve
a problem and this is usually the case in practice. Actually, in Chap. 5, we will talk
about the means for comparing the quality of two algorithms that solve the same
problem.
2.1 What Is Data? 37
Fig. 2.2 An array becomes computationally inefficient when a new item needs to be inserted in
the middle
automatically building and modifying such linked data structures. The ‘list’ con-
struct in these languages is a good example of such embedded features.
2.3.1 Integers
Integer is the data type used in almost all discrete mathematical applications,
like enumeration, counting, combinatorics, number theoretics, geometry and many
more.
The most common way to representing integers is Two’s Complement using
which we can represent integers in the range of [−2n−1 , 2n−1 − 1] given n bits.
In Two’s Complement notation, a positive number has the leading bit as 0 whereas
a negative number’s leading bit in Two’s Complement notation is 1. To convert a
(positive or negative) decimal number x into its Two’s Complement representation:
1. Convert |x| into base-2, call it bn−2 . . . b1 b0 .
2. If x > 0, 0bn−2 . . . b0 is the Two’s Complement representation.
3. If x < 0,
(a) flip each bit—i.e., ci = 1 − bi for i = 0, . . . , n − 2.
(b) add 1 to cn−2 . . . c0 ; i.e., dn−2 . . . d0 = cn−2 . . . c0 + 1.
(c) 1dn−2 . . . d0 is the Two’s Complement representation.
An important advantage of Two’s Complement representation is that addition, sub-
traction and multiplication do not need to check the signs of the numbers. Another
1 The exact value depends on how the CPU represents negative numbers.
2.3 Basic Data Types 41
important advantage is the fact that +0 and -0 have the same representations, unlike
other notations.
The CPU arithmetic works rather fast (faster compared to floating points) in the
integer domain. Furthermore, some obscure precision losses, which exist in repre-
senting floating point numbers, is not present for integers. Therefore, integers are
favored over floating points when it is possible to choose between the two. It is
frequently the case that even problems that are defined in a domain of reals (like
computer graphics) is carried over to the integer domain, because of the gain in
speed.
Some high-level languages do provide arbitrary precision integers, also known as
bignums (short for big numbers), as a part of the language. In some cases, the usage
is seamless and the programmer does not have to worry about choosing among
the representations. Lisp, Prolog, ML, Python are such programming languages. In
languages like C, Pascal and C++, facilities for bignums are available; therefore, the
functionality is provided but not seamless (i.e., the user has to make a choice about
the representation type). As explained in the preceding section, bignums are not
directly supported by the CPU; therefore, the provider has to represent them with a
data structure and has to implement the algorithms himself to perform the arithmetic
operations on them. Usually, bignums are represented as arrays of integers, each
element of which is a digit in base-n, where n is close to the square root of the
biggest integer that can be handled by the CPU’s arithmetic (Why? Think about it).
It is possible that a high-level language offers more than one fixed-size integer
type. Usually, their sizes are 16, 32, 64 or 128 bits. Sometimes, CPUs have support
for two or three of them (having also different operations for different types of inte-
gers; for example, there are two different instructions to add integers of size 32 and
64, respectively).
Floating point is the data type used to represent non-integer real numbers. The in-
ternal representation is organized such that the number is converted into a binary
fractional part and a multiplicative exponent part (i.e., F × 2E , where F is the frac-
tion, E the exponent). After this conversion, the fractional part is truncated to a fixed
length in bits, and stored along with the exponent.
You might remember from your Mathematics courses that irrational numbers do
not have fractional parts that can be truncated, neither do most of the rationals.
In “. . . the fractional part is truncated. . . ”, “truncated” means “approximated”. To
observe this, feel free to take the square root of (let’s say) 2.0, and then square the
result, in any high-level language. You will never get back the answer, 2.0.
Let us have a closer look at the internal representation of floating points to un-
derstand what is going on and where the precision gets lost. Below is the IEEE 754
binary floating point standard, converting a real number into the internal represen-
tation (see also Fig. 2.5):
42 2 Data: The First Ingredient of a Program
1. The whole part (the value to the left of the point) and the fractional part of a real
number are expressed in binary.
2. The fraction ‘point’ is moved to the left (or right) so that the whole part becomes
exactly 1. To compensate for the move and not to alter the value of the real
number, a multiplicative power of 2 is introduced. It is possible, of course, that
this power is negative.
3. The binary number 1 right before the point is skipped over, and the 23 digits
(following the fraction point) in the fraction part are stored as the mantissa.
4. 127 is added to the power of the multiplicative factor and stored as the exponent.
5. If the original real number is negative, the sign bit is set to 1, otherwise to 0.
6. The values ‘0’, ±∞, and ‘NaN’ (Not-a-Number) are represented by some excep-
tional combinations of mantissa and exponent values.
Therefore, mathematically, a real number is approximated to:
23
−n
1+ bit[23−n] × 2 × 2exponent−127
n=1
Where exactly is the loss? The answer is as follows: if the summation were extended
to infinity, then any real number could be represented precisely. However, we do not
have infinite number of bits; we have only 23 of them. Therefore, the truncation after
the first 23 elements in the summation causes the loss. For example, the binary rep-
resentation for a simple real number 4.1 has the whole part equal to 100, and yet, the
fraction part has infinitely many binary numbers: i.e., 000110011001100110011 . . . .
Hence, using only 23 bits for real numbers such as 4.1 introduces a precision loss.
Is this a big problem? Yes, indeed it is. Here are some examples of everyday
problems that occur in scientific computing:
• Let us say we have 32 bits for representing floating points. Therefore, you have
only 232 real numbers that can be correctly represented. However, we know from
Mathematics that even in the range [0, 1], there are infinitely many real numbers
(actually, it is worse than that: to be precise, there are ‘uncountably many’). In
other words, uncountably many real numbers are approximated to one real num-
ber that is representable by the computer. We call this precision loss roundoff
error.
What makes it even worse is that we easily make wrong estimates on roundoff
errors. In fact, there is no correlation between the representability in the decimal
notation and representability in the binary notation. For example, to us, 0.9 might
seem less prone to roundoff errors compared to 0.9375. Actually, it is just the
other way around: 0.9375 is one of the rare real numbers that is represented with-
out any loss, and 0.9, despite its innocent look, suffers from the roundoff error
(take your pencil and paper and do the math! When you get tired of it, you can
2.3 Basic Data Types 43
go and watch the movie “Office Space” (1999), where you can learn how to make
millions out of roundoff errors).
• Many numerical computations are based on multiplicative factors which are dif-
ferences of two big numbers (by big numbers, we mean numbers whose whole
parts 0). The whole parts are represented in the mantissa, and as a result, the
fractional parts lose precision. Therefore, for example, (1.0023 − 1.0567) yields
a different result from (1000.0023 − 1000.0567), although, mathematically, the
results should be the same (Try it!).
• The irrational number π is extensively used in scientific and engineering com-
putations. To get a close-to-correct internal floating point representation for π ,
we have to type 3.1415926535897931. The sinus of π , i.e., sin(π), should yield
zero, but it does not: the result of sin(π) on a computer is 1.2246467991473532 ×
10−16 , which is definitely a small number but not zero. Therefore, a comparison
of sin(π) against 0.0 would fail.
• You might remember that, in your Mathematics courses, you were told that
addition is associative. Therefore, (a + b) + c would yield the same result as
a + (b + c). This is not the case with floating number computations. The losses
in the intermediate computations will differ, and you will have a different result
for different ways numbers are added. As an example:
set a = 1234.567, b = 45.67834 and c = 0.0004:
(a + b) + c results in 1280.2457399999998,
a + (b + c) results in 1280.2457400000001.
• Precision does not mean accuracy. Assume that you are working with the (IEEE
754 standard) 32-bit float representation introduced above. You want to compute
an area and you take π as 3.141. You add, multiply, divide and what you get are
numbers that are precise in 7 decimal digits. However, your accuracy is not more
than 4 digits in any computation that involves π , since the π value (3.141) you
have taken is accurate only up to 4 digits: All those higher digits are nonsense as
far as accuracy is concerned.
What is the bottom line then? Here are some rules of thumb about using floating
points:
• If you can transform the problem to a problem in the integer domain, do so: As
much as you can, refrain from using floating points.
• Use the most precise type of floating point in your choice of high-level language.
C, for example, has float, double and long double, which, these days,
correspond to 32, 64 and 128 bit representations, respectively.
• Use less precision floating points only when you are short of memory.
• It is very likely that you will have catastrophic roundoff errors when you subtract
two floating points close in value.
• If you have two addends that are magnitude-wise incomparable, you are likely to
lose the contribution of the smaller one. That will yield unexpected results when
you repeat the addition in a computational loop where the looping is so much that
the accumulation of the smaller addends is expected to become significant. It will
not.
44 2 Data: The First Ingredient of a Program
• The contrary happens too: Slight inaccuracies might accumulate in loops to sig-
nificant magnitudes and yield non-sense values.
• You better use well-known, decent floating point libraries instead of coding float-
ing point algorithms yourself.
Just to remind you our first interaction with Python from Chap. 1, let us have a
look at a simple computation involving some numbers:
>>> 3+4
7
In this interaction, the numbers 3 and 4 are integers, and Python has a certain
name, i.e., a type, for all integers: int. If you ask Python what int is, it will
tell you that it is a type:
>>> int
<type ’int’>
We could also ask the type of a constant number, or a combination of them:
>>> type(3)
<type ’int’>
>>> type(3+4)
<type ’int’>
The int type in Python has fixed-size representation, which depends on the
CPU. If you need to work with integers that exceed the fixed-size limit of your
CPU, you can use the long data type in Python. If you want your constant
numbers to be represented as long integers, you need to enter the L letter after
them:
>>> type(3L)
<type ’long’>
>>> type(3L+4L)
<type ’long’>
>>>
int type in Python has fixed-size represen-
tation (based on the CPU) whereas long
type is only limited by the size of available
memory.
>>> type(3.4)
<type ’float’>
>>>
and similar to int numbers, we can do simple calculations with the float
data:
>>> 3.4+4.3
7.7
>>> 3.4 / 4.3
0.79069767441860461
Let us have a look at some useful simple operations with numerical values in
Python (in Chap. 3, we will look at more operations and how they are inter-
preted by the CPU):
• Absolute value of a number: The abs(Number) function can be used for
the absolute value of a number.
• Hexadecimal or octal representation of an integer: The hex() and oct()
functions can be used for this purpose.
• Exponent of a number: Typing pow(Number1, Number2) or
Number ** Number2 in Python results in Number1Number2 .
• Rounding a floating point number: The round(Float) function rounds
the given floating point number to the closest integer.
• Conversion between numbers: You can use the int(Number),
float(Number) and long(Number) as constructors to convert a
given number to int, float and long respectively:
>>> long(3.6)
3L
>>> float(3L)
3.0
>>> int(3.4)
3
Note from the examples that converting a float number to an integer num-
ber results in losing the fraction part (without rounding it).
2.3.4 Characters
As stated earlier in this book, we attempt to generate computer solutions for some
of our world problems. These problems are not always about numbers, they can also
46 2 Data: The First Ingredient of a Program
for the ordering of these letters. However, this makes working with such extension
tables inherently slower. It is an undeniable fact that since many of such existing
mappings are limited in size and scope, incompatible with multilingual environ-
ments, and cause programmers no end of trouble.
Years later, in the late 80’s, a nonprofit organization, the Unicode Consortium,
was formed with a goal to provide a replacement for the existing character tables
that is also (backward) compatible with them. Their proposed encoding (represen-
tation) scheme is called the Unicode Transformation Format (UTF). This encoding
scheme has variable length and can contain 1-to-4 8-bit-wide components (in the
case of UTF-8), or 1-to-2 16-bit-wide components (in the case of UTF-16). Gain-
ing wide popularity, UTF is now becoming part of many recent high level language
implementations such as Java, Perl, Python, TCL, Ada95 and C#.
Characters in Python
Python has a data type for a collection of characters (called ‘string’) and does
not have a separate data type for individual character. However, taking a charac-
ter as a subset of the string class (i.e., a character is a string of length one), you
can write programs making use of characters. We will come back to characters
in Python when we introduce strings.
Although characters are not explicitly represented with a type in Python, we
have the following functions that allows us to work with characters:
• The function chr(ascii) returns the one-character string corresponding
to the ASCII value ascii.
• The ord(CharString) is the reverse of chr(ascii) in that it returns
the ASCII value of the one character string CharString.
2.3.5 Boolean
Boolean is the type of data that represents the answer to questions like 3.4 > 5.6
?
or 6/2 = 3. CPUs has built-in support for asking such questions and acting accord-
ingly to the answers. Therefore, the concept of True and False must be understood
by CPUs. CPUs recognize 0 as the representation of False (falsity). Mostly, any
value other than 0 stands for True (truthness). If the CPUs process any boolean cal-
culation, the result will be either 0 or 1; 1 representing the True truth value that
CPUs generate.
High-level languages have a similar mapping. Generally, they implement two
keywords (like TRUE, FALSE or T, F) to represent the two boolean values.
2.3 Basic Data Types 49
When the interpreter or the compiler sees these keywords, it translates them into in-
ternal representations of values 0 or 1. High-level languages make additional checks
to ensure that those values are created
• either by the programmer entering the keyword, or
• as a result of a comparison operation.
Although they will be represented as 1s and 0s internally, on the surface (i.e., in the
high-level language) these keywords will not be treated as integers.2
You can check yourself whether or not Python has a separate data type for
boolean values by asking it a simple comparison:
>>> 3 > 4
False
>>> type(3 > 4)
<type ’bool’>
Therefore, Python has a separate data type for boolean values and that data
type is called bool. The bool data type can take only two values: True and
False.
We can use the not keyword for negating a boolean value. For example,
not True, and not 4 > 3 are False.
Since everything is internally represented as binary numbers, we can check
for the mapping of the True and False values to the integers:
>>> int(False)
0
>>> int(True)
1
which tells us that False is internally represented as 0 (zero), and True as 1
(one).
In Python, like many high-level languages, numerical values (other than 0
and 1) have a mapping to boolean values as well:
>>> not 0
True
>>> not 1
False
>>> not 2.5
False
2 Except in C and its descendants. C does not provide a distinct boolean type and assumes that the
From this interaction, we understand that Python interprets any numerical other
than 0 as True. Moreover, any non-empty instance of a container data type,
which we will see in the remainder of this chapter, is interpreted as True; and
otherwise, any data is False. For example:
>>> not ""
True
>>> not "This is some text"
False
>>> not ()
True
>>> not (1, 2, 3)
False
2.4.1 Strings
Strings are the containers to store a sequence of characters. The need for strings
is various; world problems intensively contain textual information and Computer
2.4 Basic Organization of Data: Containers 51
Science itself relies heavily on strings. For example, the implementations of pro-
gramming languages themselves, compilers as well as interpreters, is partly a string
processing task.
The internal representation of strings is based on storing the binary representa-
tions of characters that make up the string, scanned from left to right, in adjacent
memory cells in the increasing address order. The count of characters in a string
is not fixed: i.e., we can have a string instance with three characters or a thousand
characters. The count of characters that make up the string is named as the length of
the string. Zero-length strings, i.e. empty strings, are also allowed.
Since there is a flexibility in the length of a string, there is a problem of de-
termining where the string ends in the memory. There are two solutions for this
representational problem:
1. Store at a fixed position (usually just before the string starts), the length of the
string as an integer.
2. Store at the end of the string a binary code which cannot be a part of any string.
In other words, put an ending marker which is not a character that could take a
place in a string.
Strings in Python
Using brackets with an index number, i.e., [index], after a string, we can ac-
cess the character of the string residing at the given index number. For example,
"Hello?"[0] returns the character ’H’, and "Hello?"[4] the character
’o’. Note that indexing the elements of a string starts at zero and not at one.
This means that to access the last element of the string, we would need to pro-
vide len(string)-1 as the index.
Luckily, we don’t need to use len(string)-1 to access the last element
of a string since Python provides a very useful tool for that: negative indexing.
In other words, if the index number is negative, the indexing starts from the end
of the string; i.e., "Hello?"[-1] gives us ’?’ and "Hello?"[-2] the
character ’o’. Note that negative indexing starts with one (as the last element
in the list) and in general, an index of -i refers to the character of the string at
position len(string) - i in positive indexing.
Python provides additional tools to access a subset of a string using ranged
indexing; i.e., [start:end:step], where start is the starting index, end
is the ending index, and step specifies that every stepth element of the string
until the index end will be returned. For example:
>>> "Hello?"[0:4:2]
’Hl’
>>> "Hello?"[2:4]
’ll’
>>> "Hello?"[2::2]
’lo’
>>> "Hello?"[::2]
’Hlo’
As seen in these examples, we can skip any of the start, end and step
values in [start:end:step] (i.e., we can just write [start::step],
2.4 Basic Organization of Data: Containers 53
[start::], [::step] etc.), and in these cases, Python will assume that
the following default values are given: start: 0, end: the end of the string
and step: 1. However, if a negative step value is specified, then the start
corresponds to the last element, and end corresponds to the first element. In
other words:
>>> "ABC"[::-1]
’CBA’
In Python, strings, lists and tuples have similar representations and therefore,
as we will see later, they have similar means for accessing their elements.
– Constructing Strings
2.4.2 Tuples
The term tuple is borrowed from set theory, a subfield of mathematics. A tuple is an
ordered list of elements. Different from tuple in Mathematics, in Computer Science
a tuple can have elements of heterogeneous types. A very similar wording will be
2.4 Basic Organization of Data: Containers 55
given in the following subsection for the definition of lists. Having almost the same
definition for lists and tuples could be somewhat confusing but this can be resolved
by understanding their difference in use and their function in life. Tuples are used
to represent a static form of aggregation. There is a known prescription and you,
the programmer, will bring together the components that make up that prescribed
aggregate.
For example, in a computer simulation of a physical event, you will extensively
be dealing with coordinates, i.e., a 3-tuple of floating point numbers. You will send
‘coordinates’ to functions, and form variables of them. However, they will neither
become a 2-tuple nor a 4-tuple; like the 3-musketeers, the grouping is unique. Sim-
ilarly, assume you want to represent an ‘employee record’ which may consist of six
fields of information:
As you can see, the types of the fields are not at all homogeneous. ID is an integer;
FIRSTNAME, LASTNAME and the ADDRESS are three strings, whereas SALARY
is a floating point, and STARTDATE is likely to be a 3-tuple. Since after setting the
structure of the aggregate information once (in the example that is what makes up
an employee record), it is illogical to change it in the subparts of the program, thus,
you make it a tuple.
We will see that lists, too, serve the purpose of grouping information but they are
dynamic; i.e., the information in a list can be altered, new elements can be added
and removed, etc.
As far as high-level language implementations are concerned, the immutability
constraint is sometimes turned into an advantage when it comes to compilation.
Compared to lists, tuples lead to fast and simple machine code. However, apart from
this, the only use of tuples in favor of lists is to prevent programming errors.
Tuples in Python
Python provides the tuple data type for tuples. You can provide any number
of elements of any type between parentheses () to create a tuple in Python:
>>> (1, 2, 3, 4, "a")
(1, 2, 3, 4,’a’)
>>> type((1, 2, 3, 4, "a"))
<type ’tuple’>
Tuples have exactly the same indexing functionalities as strings to access the
elements; i.e., you can use the following that was introduced for strings:
56 2 Data: The First Ingredient of a Program
– Constructing Tuples
that unlike strings, cannot use the count() function for counting the oc-
currence of a subtuple within a tuple.
• Concatenating two tuples: Python provides the plus sign + to concatenate
two tuples: (1, 2) + (3, 4) yields (1, 2, 3, 4).
2.4.3 Lists
Similar to tuples, lists store a sequence of ordered information. From the program-
mers point of view, the only difference between tuples and lists is that lists are mu-
table and tuples are not. You can extend a list by inserting new elements and shrink
it by removing elements. However, this ability has deep implementational impacts.
Once a tuple is created every member’s position in the memory is fixed but this is
not the case for a list. In lists, both the size and content can be dynamically adjusted.
This brings the problem of structuring the data (the elements of the list) so that these
changes to the size and content can be easily and efficiently performed. There exists
various implementation alternatives for lists, which fall into two categories:
(a) Dynamic array solutions.
(b) Linked data structures solutions.
In type (a) implementations, reaching any member takes a constant number of steps
(i.e., reaching the first, the middle, the end of the list, or any other member in the
list requires the same number of steps) which is better than type (b). However, in
type (b), the insertion or deletion of elements takes a constant time which is not so
for type (a). High-level languages are divided on this subject; for example, Lisp and
Prolog implement lists as linked structures whereas the Python implementation is
based on dynamic arrays.
The list as a container concept is quite often confused with the linked list data
structure. Linked list data structures can serve as implementations of the list abstract
data type (the container). As stated, there can be other types of implementations of
lists.
Lists are heterogeneous, i.e., the members of a list do not have to be of the same
type. Mostly, lists themselves can be members of other lists, which is extremely
powerful and useful for some problems. When you have this power at hand, all
structures that are not self-referential3 become representable by means of lists. Some
languages, like Lisp, allow even self reference in lists. Therefore, complex structures
like graphs become efficiently representable.
3 Structures are self-referential if they are (recursively) defined in terms of themselves. We will
Lists in Python
Tuples are immutable, i.e., unchangeable, sets of data. For mutable sets of data,
Python provides the list data type:
>>> [1, 2, 3, 4, "a"]
[1, 2, 3, 4,’a’]
>>> type([1, 2, 3, 4, "a"])
<type ’list’>
Note that, unlike tuples, a list in Python uses brackets.
Lists have exactly the same indexing functionalities as strings and tuplesto ac-
cess the elements; i.e., you can use the following that we already introduced for
strings and tuples:
• Positive Indexing: [1, 2, 3, 4, "a"][2] returns 3.
• Negative Indexing: [1, 2, 3, 4, "a"][-1] returns ’a’.
• Ranged Indexing, i.e., [start:end:step]: [1, 2, 3, 4,
"a"][0:4:2] leads to [1, 3].
– Constructing Lists
– Modifying a List
You can substitute an element at a given index with a new one: i.e.,
List[index] = expression. Alternatively, Python allows changing a
subset of the list with new values:
>>> L = [3, 4, 5, 6, 7, ’8’, 9, ’10’]
>>> L[::2]
[3, 5, 7, 9]
>>> L[::2] = [4, 6, 8, 10]
>>> L[::2]
[4, 6, 8, 10]
>>> L[]
[4, 4, 6, 6, 8, ’8’, 10, ’10’]
Note that, in the above example, we have given the name L to the list in the
first line. We call L a variable, and later in this chapter we will discuss in detail
giving names to data (i.e., variables).
Python provides the L.append(item) function to add one item to the
end of the list L:
>>> L = [4, 4, 6, 6, 8, ’8’, 10, ’10’]
>>> L.append("a")
>>> L
[4, 4, 6, 6, 8, ’8’, 10, ’10’, ’a’]
Alternatively, you can add more than one item to a list using the
L.extend(seq) function:
>>> L.extend(["a", "b"])
>>> L
[4, 4, 6, 6, 8, ’8’, 10, ’10’, ’a’, ’a’, ’b’]
If you want to add a new element in the middle of a list, you can use the
L.insert(index, item) function:
>>> L=[1, 2, 3]
>>> L
[1, 2, 3]
>>> L.insert(1, 0)
>>> L
[1, 0, 2, 3]
To remove elements from a list, you can use either of the following:
• del statement: del L[start:end]
>>> L
[1, 0, 2, 3]
60 2 Data: The First Ingredient of a Program
In this example, we also see the difference between the reverse() func-
tion and the [::-1] indexing (which gives the elements of a list in reverse
order): [::-1] does not change the list whereas the reverse() function
does (i.e., it is in-place)!
• Searching a list: index = L.index(item) function
The index() function returns the index of the item in the list L. If item
is not a member of L, an error is returned.
• Counting the occurrence of an element in a list: n = L.count(item)
function
The count() function counts the number of occurrence of the item in the
list L.
• Range of a list: value = min(L) and value = max(L) functions
We can easily find the minimum- and maximum-valued items
in a list using the min() and max() functions. For example,
min([-100, 20, 10, 30]) is -100.
• Sorting a list: L.sort() or L2 = sorted(L) functions
There are two options for sorting the elements of a list: L.sort() or
L2 = sorted(L). They differ in that sort() modifies the list (i.e., it
is in-place) whereas sorted() does not.
2.5.1 Naming
The symbolic name is the handle that the programmer uses for reaching the variable.
What this name can consist of differs from language to language. Mostly, it is an
identifier which starts with a letter from the alphabet and continues either with a let-
ter or a digit. Examples of possible names are x, y, x1, xmax, xmin2max,
a1a, temperature. Whether there is a distinction between upper case and
lower case letters, and the inclusion of some of the punctuation characters in the
alphabet (e.g. ‘_’, ‘-’, ‘$’, ‘:’) is dependent on the language. Moreover, some lan-
guages limit the length of the identifiers whereas others do not.
62 2 Data: The First Ingredient of a Program
The way variables are created differ among high-level languages. Some need a dec-
laration of the variable before it is used whereas some do not. Some high-level lan-
guages allow the creation and annihilation of a variable for subparts of a program
where some do this automatically. The subparts of a program can exist in various
forms. Although not restricted to this, they are mostly in the form of subroutines, a
part of the program which is designed to carry out a specific task and is, to a great
extent, independent of other parts of the code. Subroutines are functional units that
receive data (through variables that are named as parameters) process it and as a
result, produce action(s) and/or data.
In addition to the parameters, many high level languages also allow the definition
of local variables in subroutines. The situation can become quite complex when
a high-level language (like Pascal) allows subroutine definitions inside subroutine
definitions. This is done with an intention of limiting the subroutine’s existence and
usability to a locality.
All this creation and annihilation of variables, the nest-ability, both definition-
wise and usage-wise, in subparts of a program result in the emergence of two im-
portant properties of variables:
• Scope
• Extent (or lifetime)
Scope is about where the language allows or disallows accessing a variable by re-
ferring to its name; i.e., scope determines the program parts in which a variable
is usable. Disallowing mostly occurs when a subpart (e.g. a subroutine) defines a
variable with the exact same name of a variable that already exists in the global
environment (the part of the program which is exterior to that subpart but is not
another (sibling) subpart). The variable of the global environment is there, lives
happily but is not visible from the subpart that has defined a variable with the
same name.
Extent or so called lifetime is the time span, from the creation to the annihilation of
the variable, during the flow of the program execution.
2.5.3 Typing
Depending on the high-level language, variables may or may not be defined with a
restriction on the type of data they will hold. The difference stems from the language
being;
• statically typed, or
• dynamically typed.
In the statically-typed case, the language processor (the compiler or interpreter)
knows exactly for what kind of data the variable has been established. It is going to
2.5 Accessing Data or Containers by Names: Variables 63
hold a single type of data, and this is declared by a statement in the program (prior to
using the variable). Therefore, whenever a reference in the program is made to that
variable, the interpreter or compiler knows exactly what type of content is stored in
the corresponding memory location. This is the case in C, Pascal and Java.
If it is a dynamically-typed language, the content of the variable can vary as far
as types are concerned. For example, the same variable can first store an integer
then, in accordance with the programmer’s demand, it can store a floating point. In
this case, every piece of data in the memory is tagged (preceded) with a byte that
specifies the type of the data. Lisp, Perl and Python are such languages.
Statically-typed languages are relatively faster, since at run time (i.e., when the
program is running) there is no need to perform a check on the type of the data
and make a decision about how to proceed with an action (e.g. to decide whether a
multiplication instruction is that of a floating point or an integer type).4
The answer is simple: you can do whatever you wanted to do with the ‘data’ stored
in the variable. You can replace the data, or use the data. Certain uses (syntaxes)
of a variable (name) have the semantic (meaning) of “I want to replace the content
of the variable with some new value (data)”. All other uses of the variable (name)
have the semantics of “I want to have the data (or sometimes a copy of it) inserted
at this position”. An assignment instruction example, common to many high-level
imperative languages, would read:
average = (x + y) / 2.0
Here, average, x and y are three distinct variables. The use of average is syn-
tactically different from the use of x and y. Being on the left of the assignment
operator,5 the equal sign, the data stored in the variable average is going to be re-
placed with a new value. This new value is the result of the calculation on the right
side of the assignment operator and that calculation also makes use of two variables:
x and y. Since they appear on the right-hand side of the assignment operator this
time, the semantic is different, meaning “use in the calculation the values stored in
those two variables”. In the course of the evaluation, those two values will be added
then the sum will be divided by two.
4 Processors have different sets of instructions for floating point and integer arithmetic.
5 The assignment operator exists in all imperative languages. ‘=’, ‘:=’ are the most common nota-
tions used for assignment. ‘<-’, ‘«’, ‘=:’, ‘:’ are also used, but less frequently.
64 2 Data: The First Ingredient of a Program
The scope and the extent of a variable in Python depends on where it is defined.
Since we will cover more complex Python constructs later in the book, we will
return to the topic of scope and the extent of variables in the following chapters.
You can use variables in Python as you would use data: you can assign a tuple,
a string or a list to a variable and manipulate it in any way that you would
manipulate the data, for example:
>>> a = (1, 2, 3, ’a’)
>>> type(a)
<type ’tuple’>
>>> a[1]
2
>>> a[-1]
’a’
66 2 Data: The First Ingredient of a Program
Each data (or object) in Python is assigned a unique identifier (basically, an in-
teger) which can be accessed by the id() function. Having unique identifiers,
Python manages memory space such that multiple occurrences of the same data
are stored only once whenever possible. For example:
>>> a = 1
>>> b = 1
>>> id(1)
135720760
>>> id(a)
135720760
>>> id(b)
135720760
Here, we see that the data ‘1’ and the variables a and b all hold the same
content; i.e., the data ‘1’ is represented once and all these three cases make use
of only one stored ‘1’.
However, it is safe to change the content of one variable without affecting
the content of the other (following the previous interaction):
>>> a = 2
>>> b
1
>>> id(a)
135720748
>>> id(b)
135720760
In the following example for lists (which are mutable containers), however,
the variables are linked to the same memory location and changing one variable
means changing the other one, due to mutability:
>>> a = [’a’, ’b’]
>>> b = a
>>> id(a)
3083374316L
>>> id(b)
3083374316L
>>> b[0] = 0
>>> a
[0, ’b’]
In this example, although we did not explicitly access the list pointed to by the
variable a, we could change it since variable b became an alias to the variable
a in the assignment b = a.
2.6 Keywords 67
2.6 Keywords
The important concepts that we would like our readers to understand in this chapter
are indicated by the following keywords:
2.8 Exercises
1. Find the Two’s Complement representation of the following numbers: 4, −5, 1,
−0, 11. How many bits do you require for all these numbers?
2. Find the IEEE 754 32-bit representation of the following floating points: 3.3,
3.37, 3.375.
3. You can use either a comma , or a plus sign + to combine different values or
variables in a print statement. Experiment with the Python interpreter to see the
difference.
4. Write a Python code that inputs two numbers from the user and then displays
their arithmetic, geometric and harmonic means. Do you see any relation be-
tween the arithmetic mean, geometric mean and the harmonic mean? (Hint: For
a set of numbers x0 , . . . , xN , arithmetic,
geometric and harmonic means are
respectively defined as: 1/N xi xi , 1/N xi xi and N/ xi 1/xi .)
5. Write a Python code that inputs the coefficients of two lines y = a1 x + b1 and
y = a2 x + b2 from the user and finds their intersection. (1st Hint: You can as-
sume that the lines are not parallel. 2nd Hint: The x coordinate of the intersec-
tion can be found by equaling a1 x + b1 to a2 x + b2 . Putting that x coordinate in
the equation of one of the lines leads us to the y coordinate of the intersection.)
6. Write a Python code that inputs the coordinates of the corners of a triangle (i.e.,
(x0 , y0 ), (x1 , y1 ), (x2 , y2 )) and compute the area of the triangle.
7. What happens when you call the append() function on a list with another
list as the argument? (For example: For a list L = [1, 2, 3], what is the
output of L.append([4, 5])?)
8. What happens when you call the extend() function on a list with another list
as the argument? For example: For a list L = [1, 2, 3], what is the output
of L.extend([4, 5])?
9. Write a piece of Python code that takes the first half of a list, reverses it and
appends it to the end of the second half. For example: For a list L = [1, 2,
10, 20], the result of your Python code should be [10, 20, 1, 2].
10. Write a piece of Python code that checks whether a word is palindrome. A palin-
drome is a word or phrase or number which reads the same forward and back-
wards. Examples are; radar, racecar, wasitaratisaw. (Hint: You can make use
of the == operator to check the equality of two sequences.)
11. Write the following in Python and study their outputs: help(help),
help(type), help(int), help(long), help(float), help(bool),
help(str), help(list).
12. From the help page of the list type, find out how to calculate the number of
bytes that a list occupies.
13. What happens when you try to index a string with a number greater than the
length of the string? Do you get the same result for tuples and lists?
14. What happens when you try to change the elements of a tuple? For example:
for a tuple T = (1, 2, 3), what is the result of T[0] = 3 ?
15. Do the sort() and the sorted() functions sort in increasing or decreasing
order? How can you change the sorting order? (Hint: Use the help page of the
list data type or the sorted() function.)
2.8 Exercises 69
16. Type import sys and help(sys) into your Python interpreter and look
for how you can access the following:
(a) The maximum int, float and long that can be represented by your
Python interpreter on your machine.
(b) The version of the Python interpreter.
(c) The name of the platform that the interpreter is running on.
Chapter 3
Actions: The Second Ingredient of a Program
In the previous chapter, we have introduced data an essential but static component
of programming. It is static in the sense that data has no ability to act: Data cannot
change itself, create new data, or even display itself. It is the actions of a high-level
language that will provide all these. Actions are what will be performed in terms of
CPU instructions at the machine code level.
printer, launching a missile, sending a request to Google over the Internet via a
modem connection are examples.
You can make use of either of the following to get input from the user:
raw_input function: raw_input([prompt string]): string
The raw_input function reads the characters from the keyboard until
an EOF (End of File) is provided (EOF is CTRL-D on Unix and CTRL-
Z&Return on Windows). If the optional prompt string argument is provided,
the raw_input function displays the argument string as the prompt to the
user before reading the input. The entered text can be used for example, as
follows:
>>> a = raw_input("--> ")
--> Do as I say
>>> a
’Do as I say’
>>> type(a)
<type ’str’>
input function: input([prompt string]): value
The input function is similar to the raw_input function with the
difference that the entered text is evaluated in Python and the resulting
value is returned. As specified in the help page of the raw_input func-
tion (you can reach the help page of the raw_input function by typ-
ing help(raw_input) at the interpreter), the raw_input function
is equivalent to eval(raw_input([prompt])), where the eval()
function evaluates, or interprets, the input string as a Python expression and
raises an error if the entered text is not valid in Python.
Since the entered text is evaluated as a Python expression, caution against
malicious input should be taken in the usage of the input function.
The following example demonstrates the difference between the input and
raw_input functions:
3.1 Purpose and Scope of Actions 75
Python provides two different ways to format a string for printing it to the user:
1. C style string formatting: In this style, the programmer can call the print
function as follows: print "I am %f meters tall" % 1.86,
76 3 Actions: The 2nd Ingredient of a Program
Identifier Description
d, i Integer
f, F Floating point
e, E Floating point in exponent form
s Using the str() function
r Using the repr() function
% The % character itself
Note the extra zeros at the end of 1.860000. You can use %s if you don’t
want to see extra zeros:
2. New style string formatting: In the new style formatting, the delimiters (i.e.,
%f, %d, etc.) of the C style are replaced by the curly braces ({}). For exam-
ple: print "I am {0} meters tall".format(1.86), which
would print ’I am 1.86 meters tall’.
The number within the curly braces specifies which data supplied as the
argument to format() corresponds to the identifier. If we have more than
one data, then:
Since we can give names to the data and the data identifiers, we can use a
data more than once, or even we can change the order:
>>> print "I am {height} tall, {age} years old.\
I am {height} tall.".format(age=20, height=1.86)
I am 1.86 tall, 20 years old. I am 1.86 tall.
3.2.1 Expressions
An expression can contain many operands and operations and now, we will take a
closer look at the evaluation scheme in order to understand how big and complex
78 3 Actions: The 2nd Ingredient of a Program
expressions are interpreted by the computer. There is a reason for this: things are
not exactly as we would expect them to be.
In mathematics, the evaluation of a mathematical expression is said to have the
so-called “Church–Rosser property”, which is about reduction or rewriting systems.
Such systems are defined as sets of reductions or rewriting rules on some objects.
An object, by means of application of the rules, will finally be transformed into
(i.e., reduced to) a form to which no further rule can be applied anymore. In a re-
duction system, the sequence of rule applications might not be unique. For example,
a reduction system on strings may have a single rule which says
• “If both ends of a string are consonants, remove any one.”
Now, assume that we start with the string BRING. Figure 3.3 displays a tree of
possible actions the rule set (which has actually a single rule) allows. Each path that
leads to a leaf (underlined in red) is a valid reduction. As clearly visible in Fig. 3.3,
various reductions can lead to different end-results. Starting with any object, if all
the results obtained for that object are the same, this reduction system would be said
to possess the Church–Rosser property.
In mathematics, expression evaluation has the Church–Rosser property, but, in
programming, expression evaluation does not have the Church–Rosser property.
3.2 Action Types 79
2
is the way we denote the operation. When any of the operands is a different opera-
tion, then the situation becomes complicated. Let us say is another operator and
3.2 Action Types 81
is an operand
2
Which operation is going to be performed first? In this case, there is a hidden code.
Beside the semantics (meaning) of the operators, we need to think about the prece-
dence of the operators; i.e., the relative priorities of the operators. For example,
multiplication has a higher precedence over addition, and exponentiation has the
highest precedence. If two operators of equal precedence clash then we consult the
associativity. We are taught in school that apart from exponentiation, all arithmeti-
cal operators are left associative. Last but not least, if you are dissatisfied with what
the hidden code imposes, you can override it by using parentheses. Although this
works, it is complicated. We don’t have this feeling when we perform arithmetic
with pen and pencil because that is a rewriting technique. We are absolutely free to
jump to any part of the expression, sort it out and even undertake some rewritings in
parallel. We use our experience and intelligence in doing this. Furthermore, mostly
we have an overall grasp of the ‘whole expression’. However, this is certainly not
the way a computer can proceed.
The irregularity (having to jump from a subpart to another subpart of the expres-
sion) in the calculation is due to the existence of parentheses and precedences.
Two other denotations, namely the prefix and the postfix notations, are free of
these troublemakers. As their name implies, the operands follow the operator in the
prefix notation, and the operator follows the operands in the postfix notation:
2 2 2
INFIX PREFIX POSTFIX
Arithmetics Programming
Addition (+) +
Subtraction (−) -
Multiplication (×) *
Division (÷) /
Exponentiation ^ or **
A-B^C^D*(E-(F-G-H))/K
-A/*^B^CD-E--FGHK
ABCD^^EFG-H--*K/-
The postfix notation is algorithmically very easy to evaluate and convert to from the
infix notation.
The name comes from the resemblance to the task of putting railroad cars into a
desired order according to their destination. The way to do this is to have a shunting-
yard, as shown in Fig. 3.4. So, how does it function? Let us see it on an example.
Assume that we have a train which is in the order of a red car, followed by a yellow
car, then by a green car. We want to change the order to red, green and yellow.
Here is how it is done:
Making use of the shunting-yard, we are able to transform an infix expression
into a postfix expression as follows (see Fig. 3.5):
• Place the infix expression at the INITIAL position on the shunting-yard.
• From left to right, consider, one by one, each item (operator, operand or paren-
thesis) of the expression as a railroad car to be moved.
• If the item is an operand, move it straightaway to the end of the sequence formed
at the FINAL position.
• If the item is an operator, it will go to the STACK, but the following conditions
must be fulfilled prior to this move:
– The operator to be placed on the STACK must have exactly a lower precedence
than the operator that will leave the STACK first. If the operator to be placed is
right associative, then equality in precedence is also allowed.
– If it is an opening parenthesis that would leave the STACK first, then the oper-
ator is allowed to the STACK.
– If none of the conditions above holds and the STACK contains elements, move
the operators out of the STACK straightaway to the FINAL position, until ei-
ther an opening parenthesis becomes the first in the STACK or the STACK is
emptied.
After these conditions are fulfilled, move the operator to the STACK.
3.2 Action Types 83
input queue is the infix expression placed at the INITIAL position, as a sequence of
tokens. Removal (get) is always from the left.
output queue is the place marked as FINAL where the postfix expression is formed
as a sequence of tokens. Append (add) is always from the left.
Now we are ready for a more formal definition of the shunting-yard algorithm,
shown in Algorithm 3.1.
It is time to have a complete walk-through of an example. We will consider the
mathematical infix expression given above, and applying the shunting-yard algo-
rithm, we will obtain the postfix form of the expression. Figure 3.6 displays the
progress of the algorithm step-by-step for each token.
Similar to phase 1, this phase also employs a stack. However, this time it is not
the operations but the values that are pushed and popped from the stack. It has a
relatively easier algorithm as shown below (see also Algorithm 3.2):
• Process the postfix expression from left to right. Consider each token.
• If the token is an operand, then evaluate it. There are three possibilities:
– The operand is a constant; push that value onto the stack.
– The operand is a variable; fetch the value associated with it from the memory,
and push that value onto the stack.
– The operand is a function call; perform the function call, obtain the return
value, push it onto the stack.
3.2 Action Types 85
Fig. 3.6 A step-by-step illustration of how the infix to postfix conversion is carried out. The ex-
ample continues in the next page
• If the token is an operator, then perform a pop operation twice. The first pop
operation will provide the value of the right operand and the second the value of
the left operand. Now, perform the operation between these two values and push
the result onto the stack. (If it is a compilation, then, at this position, the compiler
will insert some machine code that will perform a pop operation from the stack
into a register and then a second pop operation into another register. This piece
of code is followed by a machine code instruction that performs the operation on
the registers. The resulting value will be generated in a register, which thereafter
is pushed onto the stack.)
• When all tokens are exhausted, there will be only one single value on the stack
and that is the resulting value for the whole expression.
A Corollary
Now, let us consider the first phase. All the operands are directly moved to the
postfix expression. Since the token consumption is strictly left to right, the relative
order of the operands will not change during the conversion from infix to postfix. In
other words, if we erase all the operators from the infix expression and do the same
for the postfix expression, what remains (i.e., a sequence of operands) is the same.
So, Phase-1 does not alter the operand ordering.
Now, let us consider the second phase. This phase is also a strict left to right
one pass algorithm. When tokens are consumed from the postfix expression, the
operands are evaluated and pushed onto the stack. So, it is obvious that any operand
is evaluated before any operator to its right and after any operator to its left.
However, the order of the operands was not changed neither in phase-1 nor in
phase-2. So, the operands will get evaluated strictly in the left to right order as they
86 3 Actions: The 2nd Ingredient of a Program
appear in the infix expression. They will be evaluated but the values they are reduced
to will enter the stack and wait for a while. This is a proof that the wide-spread
‘parentheses are evaluated first’ dogma is simply incorrect.
The arithmetic operators in Python are common to both int and float data.
Below is a list of the arithmetic operators for numerical values:
Once you start working with the operators and the numbers, you will soon
realize that sometimes Python may not give you what you expected:
>>> 6/4
1
Python tells you that the division of 6 by 4 is 1, which is intriguing. The reason
for this unexpected behavior is that if the arguments of the division operator
/ are both integers, it performs integer division, neglecting the remainder of
the division. If you do not want the remainder to be neglected, you have to use
float numbers:
>>> 6.0/4.0
1.5
In fact, we could have just typed 6.0/4 and obtained the same result since
having only one float argument is sufficient for Python to perform floating
point division.
Alternatively, we could explicitly call a function to convert a number to a
float:
>>> float(6)/4
1.5
>>> 6.0 / 4
1.5
In general, if the types of the operands of an operator are not the same,
Python converts the data type of the smaller capacity (for example, a bool is
smaller than an int which is smaller than a float) to the type of the bigger
capacity:
>>> True / 4
0
>>> True / 4.0
0.25
In the first case (True/4), True is converted to the type of the second operand
(i.e., int), and the division operator performs integer division (hence, the result
is 0 due to 1/4). In the second case, the second operand is a float; the True
value is converted to a float, and floating point division is performed (hence,
the result is 1.0/4.0, which is 0.25).
operator is interpreted first (this is called operator precedence) and if the op-
erators have equal precedence, how they are combined (which is called op-
erator associativity). For example, the expression a + b + c * d is un-
derstood by Python as ((a + b) + (c * d)) and not as, for example,
(a + ((b + c) * d)).
The precedences of the operators specify the order of operators of differ-
ent precedence. For example, in a + b + c * d, multiplication (*) has a
higher precedence than addition, and therefore d is multiplied by c and not
b+c or a+b+c. Associativity, on the other hand, determines how the opera-
tors of the same precedence will be combined; i.e., is the expression a+b+c
equivalent to (a+b)+c or a+(b+c)? Note that although, in mathematics,
these two expressions might be the same, in programming, they do not have
to be the same. This was discussed in relation to floating point numbers in the
previous chapter, and in this chapter related to the side effects. Since the addi-
tion operator is left-to-right associative, the expression a+b+c is interpreted as
(a+b)+c.
Below is the precedence and associativity of arithmetic operators (from high-
est precedence, at the top, to the lowest at the bottom):
Containers, or sequences (i.e., strings, tuples and lists) have very different se-
mantics from numbers; therefore, the set of operators that can be applied on
them is different. Although in some cases the symbols may be the same, the
semantics is different:
• Concatenation: Python can concatenate two sequences with the + operator:
>>> "I" + " am a string"
’I am a string’
>>> [1, 2, 3] + [3, 4, 5]
[1, 2, 3, 3, 4, 5]
>>> (1, 2) + (3, 4)
(1, 2, 3, 4)
• Repetition: Python provides a handy operator * for repeating sequences:
90 3 Actions: The 2nd Ingredient of a Program
>>> "Noo"*3
’NooNooNoo’
>>> [1, 2] * 3
[1, 2, 1, 2, 1, 2]
>>> (3, 4) * 2
(3, 4, 3, 4)
• Membership: Python has in and not in operators for checking the mem-
bership of an item:
>>> "o" in "Noo"
True
>>> 2 in [1, 2, 1, 2, 1, 2]
True
>>> 4 not in (3, 4)
False
• Indexing: The indexing mechanisms (that we have talked in the previous
chapter) that use brackets ([]) are also operators.
Below is the updated list of the precedence and associativity of the operators in
Python:
You will often need to compare the values of two data types, and Python pro-
vides the following operators for comparing different data. In Python, relational
operators are defined for every data type, whether it is basic (e.g., int, float
or bool) or a container (e.g., tuple, str or list).
As in arithmetic operators, if the operands of a relational operator are not of
the same data type, the smaller one is converted to the data type of the larger
one. Whenever this conversion is not possible (such as in checking the equality
between [1, 2, 3] and 3.0), the result of the relational operation is False
3.2 Action Types 91
(i.e., the interpreter does not complain to you about the incompatibility of the
operands!).
== (Equality Operator): The equality operator returns the bool value True
if the operands are equal. Below is a description of what equal means for
different data types:
• Two values of basic data types are equivalent if the values are exactly the
same. For example, 3 equals 3.0, True equals 1, etc.
• Two strings are equivalent if they represent exactly the same set of char-
acters in the same order. For example, "Book" is not equivalent to
"Books" or "book" (i.e., equality on strings is case-sensitive!).
• Two tuples are equivalent if they have exactly the same elements in the
same order.
• Similar to strings and tuples, two lists are equivalent if they have the same
elements in the same order.
< (Less-than Operator): The less-than operator returns the bool value True
if the value of the first operand comes earlier than the value of the second
operand. The comes-earlier is determined based on the data type, and below
is a description of what comes-earlier means for different data types:
• A value of basic data type comes earlier than another basic-data-type
value if the first value is smaller than the second value. For example, 3 is
less than (comes earlier than) 4.0, -4.45667 is less than -1.25, and
True is less than 2, etc. (what is the result of False < True?).
• A string comes earlier than another string if the first is earlier than the
second string in the lexicographical ordering based on the ASCII val-
ues of the characters (Reminder: you can check the ASCII value of a
character using the ord() function). In other words, "b" < "bac",
"b" < "xyz" are True, whereas "b" < "BACK", "b" < "1"
and "b" < "/" are False.
• A tuple comes earlier than another tuple if the first differing elements of
the tuples satisfy the less-than operator. If the first differing element does
not exist in one of the tuples (because one of them is shorter), the shorter
tuple is less-than the longer one. Example: (1, 2) < (1, 2, 3) re-
turns True, so do (2, 2) < (3, 3, 3) and (2, 2, 1, 2, 3,
4) < (4, 1, 2, 3), whereas (2, 2) < (1, 2, 3) would re-
turn False.
• The less-than operator behaves in exactly the same way for
lists. In other words, [2, 2] < [3, 3, 3] is True whereas
[2, 2] < [1, 2, 3] is False.
<= (Less-than-or-Equal Operator): The less-than-or-equal operator between
two operands op1 and op2 returns True if either op1 < op2 or
op1 == op2 returns True; otherwise, the result is False.
92 3 Actions: The 2nd Ingredient of a Program
Below is the updated list of the precedence and associativity of the operators in
Python:
For manipulating truth values, Python provides and, or and not operators:
and Operator: Boolean-Expr-1 and Boolean-Expr-2
The and of two boolean expressions is True if both arguments of the op-
erator are True; otherwise, the result is False.
or Operator: Boolean-Expr-1 and Boolean-Expr-2
The or of two boolean expressions is True if either argument of the oper-
ator is True; otherwise, the result is False.
3.2 Action Types 93
Below is the updated list of the precedence and associativity of the operators in
Python:
3.2.3 Statements
Using the equal sign (=), we can assign data to variables. The equal sign is
called the assignment statement (not an operator!). Python provides shortcuts
for assignments of the form a = a OP b and allows the programmer to write
it equivalently as ‘a OP= b’. OP can be any of the arithmetic operators that
we introduced in previous sections (i.e., +, -, *, %, //, /, **). We call assign-
ments of the form ‘a OP= b’ as combined assignment. Note, however, that
the variable ‘a’ must be an existing variable.
Below is an example:
>>> b += 4
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
NameError: name ’b’ is not defined
>>> b = 5
>>> b **= 2
>>> b
25
Multiple assignment can be performed simultaneously like a = b = c =
10, in which case all the variables get the same values.
Now, we will introduce the Turing Machine, which we have already referred to
several times. This will give an insight into why some components of computation
are designed in the way they are. In particular, you will see that conditionals are an
inherent property of the Turing Machine.
3.3 Controlling Actions: Conditionals 97
state and fills out the tape; then, it changes state and continues with what the machine
with the written tape would do.
The members of the symbol set are certainly not restricted to being alphanumer-
ical, but in many examples a subset of alphanumerics are used. Below is a widely
known example, the three-state busy beaver Turing Machine.
The machine has three non-final states {A, B, C} and one final state HALT. A is
the initial state. The symbol set is {0, 1} where 0 is the blank symbol.
State now Read symbol Write symbol Move tape Next state
A 0 1 RIGHT B
A 1 1 LEFT C
B 0 1 LEFT A
B 1 1 RIGHT B
C 0 1 LEFT B
C 1 1 RIGHT HALT
As an exercise, you are invited to get a pencil and paper to investigate what this
machine actually does.
Before we end this introduction to the Turing Machine, one special Turing Ma-
chine is worth mentioning: The Universal Turing Machine (UTM). The UTM is a
Turing Machine that can simulate any Turing machine (with its tape) that is pro-
vided to it on tape (with some encoding). Actually, the UTM is an abstraction of the
Von Neumann architecture.
The Turing machine abstraction is used in proving various theorems about com-
puting. We will not go into their details, but let us observe some of the properties
of the Turing Machine. Due to the Church–Turing conjecture, any such observed
properties will also be the properties of any processor, any programming language
and any program that is written.
Discreteness: The machine has finite number of discrete states and symbols. The
Universe though is a continuum. Many physical events are based on real num-
bers. The states in a Turing Machine are countable but real numbers are not.
Therefore, there is simply not enough number of states to compute real numbers.
The same is true even for functions on natural numbers. There are uncountably
many such functions that are not computable by any Turing Machine (TM).
Determinism: You can run a TM a zillion times, it will carry out the same sequence
of transitions a zillion times. We call this property determinism. It is possible to
simulate nondeterminism on a TM if the nondeterminism is bounded (a subclass
of nondeterminism). However, even this equivalence refers solely to what can be
computed, and not how quickly. The payment is an extra non-polynomial time
factor, but in real life, this ‘time’ factor is the main criteria that counts.
Conditional: The transition function can be viewed as a set of conditionals. Each
row of the table then reads as:
if the machine is in state X and the symbol read is Y then . . .
So, a conditional structure exists per se in the TM.
3.3 Controlling Actions: Conditionals 99
3.3.2 Conditionals
Conditionals exist in all high level languages. Mostly in the form given below:
if boolean expression then action
It is the way to have an action take place depending on the outcome of a boolean
expression evaluation. In the programming context, an ‘if’ has a different meaning
to its causal meaning in natural language. Consider the statement below that you
might read in a magazine
If you refrain from consuming fat, sugar and wheat products, you will have a
longer life.
This conditional statement of English conveys us an information about ‘probabili-
ties’. The outcome is not 100% guaranteed: maybe in five years, you will be run over
by a truck and die. What the sentence says is that the probability of living longer
increases with the mentioned diet. Actually, it also contains a hidden assertion that:
Consuming fat, sugar and wheat products shortens your life.
In programming, the causal relation is not probabilistic and does not contain any
hidden information. If the conditional is going to be carried out, first the boolean
expression is evaluated; if the outcome is TRUE, the action, which was subject to
the conditional, is carried out.
At the machine code level, the instruction that exists with a conditional meaning
is called conditional branching. It is an instruction which alters the content of the
instruction counter depending on the value of a register. A conditional action in a
high-level language, for example;
if boolean expression then action
will translate into
compute the boolean expression,
leave the result in the relevant register r
?
branch to α if r = 0
carry out action
α: some actions that follow the if
In this section, we cover how we can change the flow of execution in Python.
– if Statements in Python
1 i f < c o n d i t i o n −e x p r e s s i o n > :
2 < s t a t e m e n t s −1>
3 else :
4 < s t a t e m e n t s −2>
where the first line specifies the condition that must be met for execut-
ing the statements that start on the next line; i.e., if the <condition-
3.3 Controlling Actions: Conditionals 101
1 a = 3
2 b = 5
3 if a < b :
4 p r i n t "a i s l e s s than b"
5 c = a
6 i f c >= a :
7 p r i n t " c i s g r e a t e r −t h a n −or−e q u a l −t o a "
and if we write this into a file test.py and ask Python to interpret it using
the command python test.py in a terminal window, we would obtain the
following output:
skalkan@divan:~$ python test.py
a is less than b
c is greater-than-or-equal-to a
– – Multiple if Statements
1 i f < c o n d i t i o n −e x p r e s s i o n −1> :
2 <statements >
3 e l i f < e x p r e s s i o n −2> :
102 3 Actions: The 2nd Ingredient of a Program
4 <statements >
5 .
6 .
7 .
8 e l i f < e x p r e s s i o n −M> :
9 <statements >
10 else :
11 <statements >
where the condition expressions are checked one by one until one of them is
met, in which case the corresponding statements are executed (again, they have
to be indented). If none of the expressions is True, then the statements in the
else part are executed. If there is no else part, the execution continues with
the rest of the code.
– – Nested if Statements
1 i f < c o n d i t i o n −e x p r e s s i o n −1> :
2 < s t a t e m e n t s −1>
3 i f < c o n d i t i o n −e x p r e s s i o n −2>:
4 < s t a t e m e n t s −2>
5 else :
6 < s t a t e m e n t s −3>
7 else :
8 < s t a t e m e n t s −4>
• The returned value is the new created object. The first print statement at line
12 prints this returned value.
• The second print statement at line 13 prints the untouched value of s.
If a call strategy is ‘call by value’, there is no possibility of altering the value at
the calling point (the value attached to s in the example). All changes are made
on the copy.
(b) is an example of call-by-reference.
• When the function is called at line 12, the variable x refers to s itself (that is
why it’s named as call-by-reference).
• The first element of x (also known as s) is changed to “jennie” at line 2.
• The next line (line 3) in the function creates a new object: the [“suzy”,
“mary”, “daisy”] list. The former content of x (namely [“jennie”, “arthur”])
is purged and the new object is assigned to the variable x. What has changed
is actually the content of s because x is just a renaming for s. The state of the
memory after line 3 is executed looks as follows:
3.4 Reusable Actions: Functions 107
• The returned value is the newly created object. The first print statement at
line 12 prints this returned value.
• The second print statement at line 13 prints the value of s. Since the value
of s was changed to [“suzy”, “mary”, “daisy”], the printing of s outputs this
value.
So, in this strategy, computations and changes are done on the original (calling
point) variables. The parameters are just renames.
(c) is an example of call-by-sharing.
• When the function is called at line 12, the variable x is created. This new
variable point to the same object that s does. This is not a copy: There is a
single [“bob”, “arthur”] object in this case, which is assigned to both s and x.
In this case, the variables actually hold the addresses of the object, not the
object itself. Therefore, ‘assigning the object to a variable’ means ‘storing the
address of the object in the variable’. So, it is quite possible that two or more
variables are set to contain the same address.
• The returned value at line 4 is the newly created object. The first print state-
ment at line 12 prints this returned value.
• The second print statement at line 13 prints the value of s namely the
[“jennie”, “arthur”] object.
In call-by-sharing, x and s are two variables sharing the same value at the mo-
ment of calling function f . Changes can be performed on the shared object by
means of variable x. However, an assignment to x means breaking the sharing
(i.e., the address stored in both of them is no longer the same) and assigning a
‘new’ (address) value to x.
1 d e f f u n c t i o n −name ( p a r a m e t e r −1, . . . , p a r a m e t e r −N ) :
2 s t a t e m e n t −1
3 .
4 .
5 s t a t e m e n t −M
def is the keyword that specifies that a new function is being defined. The def
keyword is followed by the name of the function. The naming conventions for
functions are the same as the ones with variables (see Chap. 2). Following the
name of the function, a list of parameters separated by comma is provided in
parentheses; these parameters are the information that the function will manip-
ulate. The list of parameters (i.e., parameter-1, ..., parameter-N)
is followed by a column ‘:’. The intended statements after the column define
what the function does. It is important to emphasize the indentation: Like in if
statements, the statements belonging to a function definition have to be indented
properly.
Let us give an example definition for a function that reverses the digits in a
number:
1 d e f r e v e r s e _ n u m ( Number ) :
2 ’ ’ ’ r e v e r s e _ n u m : R e v e r s e t h e d i g i t s i n a number ’ ’ ’
3 s t r _ n u m = s t r ( Number )
4 p r i n t " R e v e r s e o f " , Number , " i s " , s t r _ n u m [ : : − 1 ]
>>> reverse_num(123)
Reverse of 123 is 321
>>> reverse_num(123.45)
Reverse of 123.45 is 54.321
>>> reverse_num("Car")
Reverse of Car is raC
1 d e f f ( Number ) :
2 r e t u r n Number ∗∗ Number
3
4 d e f g ( Number ) :
5 a = 10
6 b = 20
7 print f (a) ∗ f (b)
1 d e f r e v e r s e _ n u m ( Number = 1 2 3 ) :
2 ’ ’ ’ ’ ’ ’ reverse_num : Reverse
3 t h e d i g i t s i n a number ’ ’ ’ ’ ’ ’
4 s t r _ n u m = s t r ( Number )
5 p r i n t " R e v e r s e o f " , Number , " i s " , s t r _ n u m [ : : − 1 ]
In this example, the lines enclosed within ””” are documentation strings and are
printed when help(reverse_num) is called.
110 3 Actions: The 2nd Ingredient of a Program
In this case, the following calls are possible: f("A"), f("A", 20) or
f("A", 20, "B").
1 d e f f (N ) :
2 N = N + 20
3
4 def g ( ) :
5 A = 10
6 print A
7 f (A)
8 print A
If you call function g(), you would get the following result:
>>> g()
10
10
The reason for this rather annoying result is that (as explained in Sect. 3.4) in
Python, functions receive parameters by sharing (call-by-sharing). In Python,
3.4 Reusable Actions: Functions 111
1 def f ( List ) :
2 L i s t [ 0 ] = ’A’
3
4 def g ( ) :
5 L = [1 , 2 , 3]
6 print L
7 f (L)
8 print L
So, the conclusion is that if you have to change non-mutable data types, you
should use the return statement to get the changes made in a function. If the
type of the data to be changed is mutable, you can change the elements of your
data. However, as seen in the following example, if your changes are not limited
to the elements of the list, you will lose the changes:
1 def f ( List ) :
2 List = List [:: −1]
3
4 def g ( ) :
5 L = [1 , 2 , 3]
6 print L
112 3 Actions: The 2nd Ingredient of a Program
7 f (L)
8 print L
>>> g()
[1, 2, 3]
[1, 2, 3]
One way to share data between functions is to use global variables. Below is an
example:
1 N = 10
2 def f ( ) :
3 global N
4 d e f g ( Number ) :
5 C = 20
6 r e t u r n N ∗ Number
7 N = g (N)
1 d e f f (N ) :
2 Number = N
3 def g ( ) :
4 C = 20
5 r e t u r n N ∗ Number
6 p r i n t " Number " , N, " and i t s s q u a r e : " , g ( )
Simple operations can be performed easily on all the elements of a list with
the syntax: [<expr> for <var> in <list>], where <expr> is an ex-
pression involving variable <var>, and for each element in <list> as a value
for variable <var>, the expression <expr> is evaluated.
For example:
[3*x for x in [1, 2, 3, 4, 5]] yields [3, 6, 9, 12, 15].
1 d e f P o s i t i v e (N ) :
2 i f N > 0 : r e t u r n True
3 return False
1 d e f Mod4 (N ) :
2 return N % 4
1 d e f g r e a t e r (A, B ) :
2 return A if A > B else B
3.7 Keywords
The important concepts that we would like our readers to understand in this chapter
are indicated by the following keywords:
3.9 Exercises
1. What are the values of the following expressions?
• 2 - 3 ** 4 / 8 + 2 * 4 ** 5 * 1 ** 8
• 4 + 2 - 10 / 2 * 4 ** 2
• 3 / 3 ** 3 * 3
2. Show the interpretation of the following expressions with parentheses (Exam-
ple: The interpretation of the expression 2 + 4 * 3 can be shown with paren-
theses as: 2 + (4 * 3)).
3.9 Exercises 117
(a) a - 3 * +a + -b ** -c
(b) a < b < c + d == d ** -f
(c) f ** -g + h
(d) a == b <= c == d
(e) not a == b + d < not e
3. Assuming that a is True, b True and c False, what would be the values of
the following expressions?
(a) not a == b + d < not a
(b) a == b <= c == True
(c) True <= False == b + c
(d) c / a / b
4. What is the output of the following Python code?
1 a = 10
2 b = a
3 i f a <= b :
4 p r i n t "A"
5 i f a / 10 ! = 1 :
6 p r i n t "B"
7 else :
8 p r i n t "C"
9 i f b >= a :
10 p r i n t "C"
1 a = 10
2 b = a +1
3 c = b+1
4 i f a <= b <= c :
5 p r i n t "A"
6 if c > b > a:
7 p r i n t "B"
8 if (c > b) > a
9 p r i n t "C"
10 i f ( c > a ) o r ( c == a )
11 p r i n t "D"
12 else :
13 p r i n t "E"
6. What is the output of calling function h() after the following Python code?
1 N = 10
2 d e f f (N ) :
118 3 Actions: The 2nd Ingredient of a Program
3 N = N ∗ N
4 def h ( ) :
5 global N
6 p r i n t "N = " , N
7 p r i n t "N = " , N
8
9 def g ( ) :
10 N = 0
11 p r i n t "N = " , N
12
13 d e f h ( ) :
14 global N
15 f (N)
16 g()
7. Write a piece of Python code that obtains a string from the user and performs
the following:
• If the string contains only one character, your code should display the type
of the character: Letter, Number, Whitespace, Operator, Other.
• If it is not a letter, it should display whether it is a numerical value.
• Otherwise, it should gracefully display the string back to the user.
8. Write a piece of Python code that gets a duration in terms of milliseconds and
displays back to the user whether that duration is bigger than:
• a minute (but less than an hour)
• an hour (but less than a day)
• a day (but less than a week)
• a week (but less than a month)
• a month (but less than a year)
• a year.
9. The
Euclidean distance between two points (a, b) and (c, d) is defined as
(a − c)2 + (b − d)2 . Write a function distance() that takes a, b, c and
d and computes the Euclidean distance. Your function should take (c, d) as
(0, 0) if left empty.
10. Modify the distance() function in exercise-9 to take two lists of arbitrary
length and compute the Euclidean distance between them. In other words, we
want the following Euclidean distance:
N
2
distance(L1, L2) = L1[i] − L2[i] .
i=0
You can assume that the two lists have the same number of elements of the same
type.
3.9 Exercises 119
11. Write a piece of Python code that removes lists in a list. For example, for a list
[1, 2, [4, 5]], the output should be [1, 2].
12. Write a piece of Python code that forms a string out of the elements of a list.
For example, [1, 2, 3, 4] should be transformed into ’1234’. You can
assume that the lists are not nested.
13. Write a piece of Python code that changes the case of a string (i.e., if a character
in the string is lowercase, it should be made uppercase and vice versa). For
example, if the string is boOK, the output should be BOok.
14. Write a piece of Python code that finds the acronym of a set of words, which are
supplied as elements of a list. For example, the list ["Middle", "East",
"Technical", "University"] should yield ’METU’.
15. Using the functional tools of Python, find the greatest divisor of a number. Note
that a divisor of a number n is another number k which is less than n and which
satisfies n = k ∗ x for a number x < k. (Hint: range().)
16. Write a descriptive length function desc_length() that calculates the
length of a list in a different way: The length of a list is the sum of the number
of characters and digits that are possessed by the elements of the list. For exam-
ple, the list [12, 0, "ab"] has the length 5 since 12 has two digits, 0 has
one digit and "ab" has two characters.
17. Write a Python function that computes the factorial of a number. (Hint: What
you have learned in this chapter is sufficient for you to implement such a func-
tion without iterations or recursion.)
18. Write a piece of Python code that inputs an equation of the form ax + b, where
a and b are numerical constants, and a value for x, call it xi , and displays the
result axi + b. For example, if the user supplies the string "3x+4" and the
value 8 for the variable x, your Python code should display the evaluation of
the following expression: 3*8+4.
Chapter 4
Managing the Size of a Problem
Job(bag_of_letters) is defined as
1. If the bag is empty, do nothing.
2. else take out and deliver the first letter in the bag.
3. Do Job(bag_with_one_letter_less)
However, this type of approach is very helpful in functional programming. There-
fore, we had better take a closer look at it. Consider the common definition of fac-
torial. Literally, it is defined as:
Except for zero, the factorial of a natural number is defined as the number
obtained by multiplying all the natural numbers (greater than zero) less than
or equal to it. The factorial of zero is defined as 1.
In mathematical notation, this can be written as in Eq. 4.1 (denoting the factorial
with the postfix operator (!)):
N! = 1 × 2 × · · · × (N − 1) × N N ∈ N, N > 0
(4.1)
0! = 1
N ! = (N − 1)! × N N ∈ N, N > 0
(4.2)
0! = 1
There is a very fundamental and important difference between the two mathematical
definitions (Eqs. 4.1 and 4.2). The later makes use of the ‘defined term’ (the factorial
function) in the definition itself (on the right hand side of the definition) whereas the
former does not.
Unless such a relation (as in the second definition in Eq. 4.2) boils down to a
tautology and maintains its logical consistency, it is valid. Such relations are called
recurrence relations and algorithms which rely on them are called recursive algo-
rithms.
4.1 An Action Wizard: Recursion 123
Now, let us have a look at the recursive algorithm (written down in pseudo code)
which evaluates the factorial of a natural number:
define factorial(n)
?
if n = 0 then
return 1
else
return n × factorial(n − 1)
Notice the similarity to the mathematical definition (Eq. 4.2) and the elegance of
the algorithm.
Recurrence can occur in more complex forms. For example, the evenness and
oddness of natural numbers could be recursively defined as:
• 0 is even.
• 1 is odd.
• A number is even if one less of the number is odd.
• A number is odd if one less of the number is even.
This definition is recursive since we made use of the concept which is being defined
in the definition. Such definitions are called mutually recursive.
Are all problems suitable to be solved by a recursive approach? Certainly not.
The problems that are more suitable for recursion have the following properties:
• ‘Scalable problems’are good candidates to be solved by recursion. The term ‘scal-
able’ means that the problem has a dimension in which we are able to talk about
a ‘size’. For example, a problem of calculating the grades of a class with 10 stu-
dents is ‘smaller’ in ‘size’ compared to a problem of calculating the grades of a
class with 350 students. Presumably, the reader has already recognized that the
‘size’ mentioned above has nothing to do with the ‘programming effort’. In this
context, ‘size’ is merely a concept of the ‘computational effort’. For example,
assume that you are asked to calculate the grade of a single student, which is
obtained through a very complicated algorithmic expression, and that this algo-
rithmic expression will cost you a great deal of ‘programming effort’. Now, if
the algorithmic expression were not so complicated, you would spend less ‘pro-
gramming effort’; however, this does not imply that this problem is suitable for
recursive programming.
• A second propertyof a recursively solvable problem is that whether any instance
of the problem is ‘downsizable’ as far as the ‘sizable’ dimension is concerned.
This needs some explanation: By removing some entities from the data that rep-
resents the sizable dimension of the problem, can we obtain a ‘smaller’ problem
of exactly the same type?
• The third propertyis strongly related to the second one. It should be possible to
‘construct’ the solution to the problem from the solution(s) of the ‘downsized’
problem. The ‘downsized’ problem is generated by removing some data from the
‘sizable dimension’ of the original problem and obtaining (at least) one sub-part,
which is still of the sizeable dimension’s data type, but now smaller.
124 4 Managing the Size of a Problem
Figure 4.1 shows a pictorial representation of recursion. Here, the mincing ma-
chine represents a recursive function. In this simplified example, the input to the
machine corresponds to the argument, and the outcome from the machine corre-
sponds to the result of the function. The transparent view of the internal structure
represents the body of the function. The input which is ‘partitionable’ is split
into a ‘small’ and a ‘large’ piece. The large piece is of the same type with the
original argument. The arrows internal to the machine represent any operation the
data undergoes. Therefore, the small part on its way to the output is processed and
turned into the square-box . The large piece is thrown into the mincing machine in
a recursive manner. The wonderful aspect of this process is that nobody has to worry
about what is going on in the internally activated mince machine. We just assume it
produces the correct result, namely . Then, the ‘correctly produced’ result for
the large part and the value (the square-box) for the small part is merged (through a
process) to form the result of the original input.
In this described process, one point is missing. Namely, what will happen when
there is no more input and the handle of the mincing machine keeps turning with the
hope of producing a result? This has to be checked and a value has to be produced;
a value which is manifestly agreed upon. This value entirely depends on the specific
problem. It can be a representation for the end of the data; a numerical value or
sometimes even nothing! Hence, our ‘revised’ machine looks like the one in Fig. 4.2.
What is new? Now, the machine has a sensor which detects the ‘no meat’ case
and triggers the immediate output of ( ). Therefore, no further recursion is done
when there is no more meat.
4.1 An Action Wizard: Recursion 125
Now the machinery in action: In this particular case, it will mimic the recursive
calculation of the factorial. To be more precise, if ‘the recursive factorial machine’
is to calculate 5!, the mincing machine looks like Fig. 4.3.
RULE I
Look at the (data) content of the problem and decide on a suitable data rep-
resentation. The chosen data representation should allow easy shrinking and
expansion with the change in the problem’s data.
For example, if the scalable dimension of the problem is about age, then naturally
you would decide to use integer as the data type. If it is about a set of human names,
then any container of strings would be acceptable. If it is about a sparse graph prob-
lem, then maybe a list structure of edge informations would be the best choice. Here,
the main principle is to choose a data structure which is easy to shrink in relation to
the downscaling. The string data type is, for example, easy to shrink or grow. On the
other hand, arrays are more troublesome. Usually, if an array is chosen for the data
representation, the shrinking is not carried out directly on the representation. The
preferred technique is to keep and update the start and end indexes (integer values)
that refer to the actual array.
126 4 Managing the Size of a Problem
Generally speaking, dynamic data structures (e.g. lists, trees, which we will talk
about in detail in Chap. 6) are the most suitable data structures as far as ‘shrinking’
is concerned.
RULE II
Start with the terminating (or, minimal) condition. Here, you check whether the
input data to the function cannot be shrunk any further. If it is the case, perform
what the function is expected to do for this situation (Depending on what the
function is devised for, either you return an appropriate value or you take an
appropriate action).
The recursive definition canonically will start with a test for this condition. If
the test succeeds, the function ceases by returning the result for the minimal case
or carries out a finishing action (if the function is devised not to return a value but
perform some actions).
RULE III
Handle the non-terminating condition of the input data. At this step, you have
to imagine that you partition the input data. The key idea of this partitioning is
that at least one of the pieces should be of the scalable type of the partitioned
data. You have the right (without any cause) to assume that a further call of
the same function with the piece(s) that have remained as being of the same
scalable type will return/do the correct result/action. It might be possible to
carry out the partitioning in different ways.
4.1 An Action Wizard: Recursion 127
It is quite often the case that alternatives exist for this stage. The alternatives
vary depending on the problem and the data structure chosen. In addition, the im-
plementation and handling of the data structures also differ from language to lan-
guage. Whereas some languages provide the user with built-in dynamic data struc-
tures (lists, trees, dynamic arrays) and operators on them, some don’t. Hence, the
decision preferences of this stage are somewhat language and data structure depen-
dent. However, with time, the necessary experience builds up, so you can make the
correct order of choice.
The parts obtained after the partitioning may
1. all be of the scalable type, or
2. have non-scalable pieces in addition to some (at least one) scalable type pieces.
In practice, both cases occur frequently. In both cases, you have the right to assume
that any recursive call on any scalable type pieces will return the correct result (or,
if it is a non-value returning task, carry out the correct action).
RULE IV
Having in one hand the correct resulting values returned or the actions per-
formed for the pieces that were still scalable, and in the other hand, the pieces
themselves which have not conserved the type (if any), we need to find the
answer to the following question:
How can I construct the desired value/action for the original input data?
The answer to the question should includes the action(s) that checks whether
the scalable parameter is in the terminating, or minimal, condition and the ac-
tion(s) for the non-terminating condition.
If you cannot figure out a solution, consider the other partitioning alternatives
(if any) that surfaced with the application of RULE III.
This is the point where ‘human intelligence’ takes its turn. If the data representa-
tion and the partitioning are implemented correctly, this step should not be compli-
cated: Normally, it is just a few lines of code. Unfortunately, it is also possible that
all your efforts turn out to be in vain, and there is no recursive solution that can be
possibly defined.
Now, we now consider the factorial problem for which we seek a recursive solution
by applying the golden rules.
Application of RULE I
First question: What data type shall we use?
The subject to the factorial operation is a natural number and the result is again
a natural number. Therefore, the suitable data type for representing the input
data is the integer type; but, due to the nature of the problem, we will restrict the
integers only to be natural numbers.
128 4 Managing the Size of a Problem
Second question: Is this data type partitionable in the terms explained above?
Yes, indeed. There exist operations like subtraction and division defined over
the integers which produce smaller pieces (in this case, all of the integer types
as well).
Application of RULE II
Having decided on the data type (integer in this case), now we need to deter-
mine the terminating condition. We know that the smallest integer the facto-
rial function would allow as an argument is 0, and the value to be returned
for this input value is 1. Therefore, we have the following terminating condi-
tion:
define factorial(n)
?
if n = 0 then
return 1
else
...
Application of RULE III
The question is: How can we partition the argument (in this case n), so that at
least one of the pieces remains an integer?
Actually, there are many ways to do it. Before presenting the result, here we
state some first thoughts (some of which will turn out to be useless).
Partitionings where all the pieces are of the same type
• For example, we can partition our number n in half; namely into two
nearly equal pieces n1 and n2 so that either n1 = n2 or n1 = n2 + 1. Cer-
tainly, it is true that each of the ‘pieces’ n1 and n2 is ‘smaller’ than n,
the actual argument. Furthermore, each of the pieces remain as the same
type of n; in other words, they are all of an integer type and hence, it is
possible to pass them to the factorial function (actually, RULE II says that
it is sufficient if one of the pieces is an integer type; here, we have all of
them satisfying this property).
• Alternatively, for example, it is possible to partition n into its additive con-
stituents so that n = n1 + n2 + · · · + nk . There are many ways to make this
partition and as it was for the factors above, each ni will remain smaller
than n and each one will satisfy the property of being an integer. There-
fore, it is possible to pass them to the factorial function as an argument.
Partitionings with one ‘small’ piece and a ‘large’ piece
For example,
• n can be additively split into a large piece: n − 1 and a small piece: 1.
• n can be additively split into a large piece: n − 2 and a small piece: 2 (This
will require n ≥ 2).
.
• ..
Due to the nature of the results of those integer operations, all the pieces
(including the small ones) will possess the property of ‘being the same type
4.1 An Action Wizard: Recursion 129
as the argument of the factorial function’, namely the integer type, but in
fact, this is not our concern: we require this condition to be met only for the
large pieces.
Application of RULE IV
Now, we have various ways of partitioning the input data n and of course, the
input argument n itself. For every partitioning scheme, we have the right to make
the very important assumption that
A further call of the factorial function with the ‘pieces’ that preserved the
type (i.e., positive integer) will produce the correct result.
Certainly, we will not trace all the thought experimentations related to every
way of partitioning. However, as an example, consider the n = n1 + n2 partition
where n1 differs from n2 by 1 at the most. Due to the ‘right of assumption’
(stated above), we can assume that n1 ! and n2 ! are both known. The question
that follows is:
Given n, n1 , n2 , n1 !, n2 ! can we compute n! in an easy manner?
Take your time, think about it; but, you will discover that there is no easy way to
obtain n! from these constituents. Except for one of the partitions, all will turn
out to be useless. The only useful one is the additive partitioning of n as n − 1
and 1. Considering also the ‘right of assumption’, we have at hand
• n
• 1
• n−1
• 1!
• (n − 1)!
It is, as a human being, your duty to discover (or know) the fact that
n! = n × (n − 1)!
And, there you have the solution. (Some readers may wonder why we have not
used all that we have at hand. It is true that we have not and there is nothing
wrong with that. Those are the possible ingredients that you are able to use. To
have the ability to use them does not necessarily mean that you have to use them
all. In this case, the use of n and (n − 1)! suffices.)
For the sake of completeness, let us write the complete algorithm:
define factorial(n)
?
if n = 0 then
return 1
else
return n × factorial(n − 1)
130 4 Managing the Size of a Problem
The second example that we will consider is the problem of reversing a given list. If
the desired function is named reverse, we expect to obtain the result [y, n, n, u, f ]
from calling reverse([f, u, n, n, y]).
We assume that head and tail operations are defined for lists which respectively
correspond to the first item of the list and all items but the first item of the list; in
other words, head([f, u, n, n, y]) = f and tail([f, u, n, n, y]) = [u, n, n, y]. More-
over, we assume that we have a tool which does the inverse of this operation (i.e.,
given the head and the tail of a list, it combines them into one list). Therefore, it
seems to be quite logical to put the list (in our example [f, u, n, n, y]) into two
pieces; namely, its head: f and tail: [u, n, n, y]. Is G OLDEN RULE III satisfied?
Yes, indeed. The tail is still of the list type. The same rule says that if we have
satisfied this criterion, then we have the right to assume that a recursive call with
the tail as argument will yield the correct result. Therefore, we have the right to as-
sume that the recursive call reverse(tail([f, u, n, n, y])) will return the correct result
[y, n, n, u]. Now, let us look at what we have in our inventory:
• [f, u, n, n, y] the argument
• f the head of the argument
• [u, n, n, y] the tail of the argument
• [y, n, n, u] result of a recursive call with the tail as argument
G OLDEN RULE IV says that we should look for an easy way that will generate
the correct result using the entities in the inventory. We observe that the desired
outcome of [y, n, n, u, f ] is easily obtained by the operation
[u, n, n, y] · [ f ]
tail([f,u,n,n,y]) head([f,u,n,n,y])
also taking into account the trivial case of an empty set (which fulfills the require-
ment of RULE II), we are able to write the full definition (⊕ denotes list concatena-
tion):
define reverse(S)
?
if S = ∅ then
return ∅
else
return reverse(tail(S)) ⊕ [head(S)]
Recursive solutions are so elegant that sometimes we can miss unnecessary invoca-
tions of recursion and can re-compute what have already been computed.
4.1 An Action Wizard: Recursion 131
fib1,2 = 1
fibn = fibn−1 + fibn−2 n > 2
Based on this definition, if we devise a recursive algorithm that calculates the nth
fibonacci number, we may end up with:
define fibonacci(n)
if n < 3 then
return 1
else
return fibonacci(n − 1) + fibonacci(n − 2)
This definition will work in the sense that it will produce correct results. How-
ever, each single invocation of the function with an argument bigger than 2 will
invoke two recursive calls of fibonacci(). Like the explosion of an atomic bomb, a
computation of fibonacci(n) will trigger approximately fibonaccin many invocations
of the function (take a pencil and paper, and draw a tree of this triggering for, let’s
say, fibonacci(6), and then count the function calls). Being exponential in n, this is
a terrible situation. The problem actually lies in the fact that the fibonacci(n − 1)
computation requires the computation of fibonacci(n − 2), which is computed (re-
cursively) and then its use in the computation of fibonacci(n − 1) is simply forgot-
ten. The computation of the second additive term of fibonacci(n − 2), which was
required for computing fibonacci(n), does not know anything about this forgotten
result. Recursively, this value is recomputed.
It is possible to fix this mistake. The solution is to save the intermediate results
and hence, prevent them from being forgotten. We do this by defining a modified
version of the fibonacci calculating function. This modified version, which we will
call mammothfibonacci(n) takes the same argument as fibonacci(n) but returns a list
of fibonaccin and fibonaccin−1 :
[fibonaccin , fibonaccin−1 ]
define calculatenext(s)
return [s[0] + s[1], s[0]]
define fibonacci(n)
return (mammothfibonacci(n))[0]
132 4 Managing the Size of a Problem
a n = a n−1 + a n−2 + 1.
a 2 = a + 1.
Time(n) ∝ n.
– Factorial of a Number
1 d e f f a c t (N ) :
2 i f N < 1:
3 return 1
4 r e t u r n N ∗ f a c t (N−1)
where the terminating condition is checked on the 2nd and the 3rd lines, and
the recursive call is initiated on the last line.
1 d e f gcd (A, B ) :
2 i f B == 0 :
3 return A
4 r e t u r n gcd ( B , A % B )
One of the clearest illustrations of recursion can be seen in searching for an item
in a set of items. If we have a set of items s : [x1 , . . . , xn ], we can recursively
search for an item e in s as follows:
⎧
⎪
⎨False if s = ∅,
search(e, s) = True if e = head(s),
⎪
⎩
search(e, tail(s)) otherwise.
1 d e f s e a r c h (A, B ) :
2 i f l e n ( B ) == 0 : # B i s e m p t y
3 return False
4 i f A == B [ 0 ] : # The f i r s t i t e m o f B
5 # matches A
6 return True
4.1 An Action Wizard: Recursion 135
1 d e f i n t e r s e c t (A, B ) :
2 i f l e n (A) == 0 :
3 return [ ]
4 i f A[ 0 ] i n B :
5 r e t u r n [A [ 0 ] ] + i n t e r s e c t (A [ 1 : ] , B )
6 else :
7 r e t u r n i n t e r s e c t (A [ 1 : ] , B)
The union of two sets, A and B, is denoted formally as A ∪ B and defined as:
(A ∪ B) = {e | e ∈ A or e ∈ B}. To find the union of the two sets, A and B, we
can make use of the following formal description:
⎧
⎪
⎨B if A = ∅,
union(A, B) = {head(A)} ⊕ union(tail(A), B) if head(A) ∈
/ B,
⎪
⎩
union(tail(A), B) otherwise.
1 d e f u n i o n (A, B ) :
2 i f l e n (A) == 0 :
3 return B
4 i f A[ 0 ] n o t i n B :
5 r e t u r n [A [ 0 ] ] + u n i o n (A [ 1 : ] , B)
6 else :
7 r e t u r n u n i o n (A [ 1 : ] , B )
Another suitable example for recursion is the removal of an item e from a set
of items s:
⎧
⎪
⎨∅ if s = ∅,
remove(e, s) = remove(e, tail(s)) if e = head(s),
⎪
⎩
head(s) ⊕ remove(e, tail(s)) otherwise.
1 d e f remove ( e , s ) :
2 i f l e n ( s ) == 0 :
3 return [ ]
4 i f e == s [ 0 ] :
5 r e t u r n remove ( e , s [ 1 : ] )
6 else :
7 r e t u r n [ s [ 0 ] ] + remove ( e , s [ 1 : ] )
The power set of a set s is the set of all subsets of s; i.e., power(s) = e | e ⊂ s.
The recursive definition can be illustrated on an example set {a, b, c}. The
power set of {a, b, c} is {∅, {a}, {b}, {c}, {a, b}, {a, c}, {b, c}, {a, b, c}}. If we
look at the power set of {b, c}, which is {∅, {b}, {c}, {b, c}}, we see that it is
a subset of the power set of {a, b, c}. In fact, the power set of {a, b, c} is the
union of the power set {b, c} and its member-wise union with {a}. In other
words,
power {a, b, c} = a ∪ X | X ∈ power {b, c} ∪ power {b, c}
With this intuition, we can formally define a recursive power set as follows:
4.2 Iteration 137
{∅} if s = ∅,
power(s) =
{head(s) ∪ X | X ∈ power(tail(s))} ∪ power(tail(s)) otherwise.
1 def powerset ( s ) :
2 i f l e n ( s ) == 0 :
3 return [ [ ] ]
4
5 sub_power_set = powerset ( s [ 1 : ] )
6 return sub_power_set + \
7 [ [ s [ 0 ] ] + x for x in sub_power_set ]
should be restricted to a single locality (not scattered over the iteration code). Ac-
cordingly, four common patterns in the layout of the iterations have surfaced. A very
high percentage of iterations exhibit one of the patterns displayed in Figs. 4.4
and 4.5.
In the Figures,
4.2 Iteration 139
• represents the whole environment that is used and/or modified in the itera-
tion.
• represents the part of the environment that is modified and used in testing for
termination.
• represents all used and/or modified parts of the environment which are not
used in testing for termination.
In addition to these four patterns, there is a fifth one, which is seldom used:
A body where termination tests are performed at multiple points.
From the four patterns displayed, the least preferable one is the premod-
posttesting (Fig. 4.5(a)) because it does not prevent undesired (due to some unantic-
ipated ingredient) modifications from being used.
The conditional, as stated previously, has to be present in all programming lan-
guages. Therefore, to implement these four iterative patterns, it is sufficient that the
language provides a branching instruction; imperative languages have branching
instruction whereas pure functional languages do not. However, due to the over-
whelming use of these patterns plus the anti-propaganda by the structured program-
mers against the use of branching instructions, many imperative languages provide
a syntax for those patterns of iteration. The idea is to decouple the test and the body.
Many languages provide a syntax that serve to implement pre-testing patterns and
in addition, some provide a syntax for post-testing. The one for pre-testing is known
as the while construct and in many languages, it has the structure of:
while test do some_action(s)
The syntax for post-testing is known as the repeat. . . until construct and usually has
the structure:
repeat some_action(s) until test
In this case, test is a termination condition (the looping continues until the test
evaluates to TRUE). Sometimes, a variant of repeat exists where the test is for
continuation (not for termination):
do some_action(s) while test
Some languages take it even further and provide syntax with dedicated slots for the
initialization, test, execution and modification (or some of them).
In the Python part of this section, you will find extensive examples and informa-
tion about what Python supports in terms of those patterns and constructs.1
As we have mentioned earlier, the structured programming style denounces the
explicit use of branching instructions (gotos). For this reason, to fulfill the need of
ending the looping (almost always based on a decision) while the body is being exe-
cuted, some languages implement the break and continue constructs. A break
1 Interestingly, you will observe that Python does not support the post-testing with a syntax, at all.
140 4 Managing the Size of a Problem
causes the termination of the body execution as well as the iteration itself and per-
forms an immediate jump to the next action that follows the iteration. A continue
skips over the rest of the actions to be executed and performs an immediate jump to
the test.2
Additional syntax exists for some combined needs for the modification and test-
ing parts. Many iterative applications have needs that:
(a) Systematically explore all cases, or
(b) Systematically consider some or all cases by jumping from one case to another
as a function of the previous case.
Below are some examples of these types of requirements:
• For each student in a class, compute the letter grade.
[example to type (a)]
• Given a road map, for each city, display the neighboring cities.
[example to type (b)]
• Given a road map, find the shortest path from a given city to another.
[example to type (b)]
• For all the rows in a matrix, compute the mean.
[example to type (a)]
• Find all the even numbers in the range [100000, 250000].
[example to type (a)]
• For all coordinates, compute the electromagnetic flux.
[example to type (a)]
• Find the root of a given function using the Newton–Raphson method.
[example to type (b)]
• Compute the total value of all the white pieces in a stage of a chess game.
[example to type (a)]
• Compute all the possible positions to which a knight can move in three moves (in
a chess game).
[example to type (b)]
• For all the pixels in a digital image, compute the light intensity.
[example to type (a)]
• Knowing the position of a white pixel, find the white area this pixel belongs to,
which is bound by non-white boundary.
[example to type (b)]
Especially regarding the (a) type of needs, many imperative languages provide the
programmer with two kinds of iterators. In Computer Science, an iterator is a con-
struct that allows a programmer to traverse through all the elements of
• a container, or
• a range of numbers.
2 Minor variations among languages can occur about the location to which the actual jump is made.
4.2 Iteration 141
The first kind of iterators, namely iteration over the members of a container, usually
have the following syntax:
foreach variable in container do some_action(s)
Certainly, keywords may vary from language to language.
The second kind of iterators, namely iteration over a range of numbers, appear in
programming languages as:
for variable in start_value to end_value step increment do
some_action(s)
Here too, the keywords may vary. It is also possible that the order and even the syn-
tax is different. However, despite all these minor syntactic variations, the semantic
and the ingredients for this semantic remains common.
1. Work with a case example. Pay attention to the fact that the case should cover
the problem domain (i.e., the case is not a specific example of a subtype of the
problem).
2. Contrary to recursion, do not start by concentrating on the initial or terminal con-
ditions. Start with a case example where the iteration has gone through several
cycles and partially built the solution.
3. If you take the pre-mod approach:
Determine what is going to change and what is going to remain the same in this
cycle. Perform the changes that are doable.
If you take the post-mod approach:
Determine what must have been changed (from the previous cycle) and must not
be incorporated in the solution and what will remain the same in this cycle.
4. Having the half-completed solution to hand, determine the way you should mod-
ify it to incorporate the ‘incremental’ changes that were performed in the period
prior to this point (those changes subject to (3) that were performed and have not
yet been incorporated in the solution).
5. Determine the termination criteria. Make sure that the looping will terminate for
all possible task cases.
6. For each variable whose value is used throughout the iteration, look for a proper
value being assigned to it, prior to the first use. The initialization part is ex-
actly for this purpose. Those variables, used without having had a proper value
assigned, should be initialized here. The initialization values are, of course, task-
dependent.
7. Consider the termination situation. If the task is going to produce a value, de-
termine this value, secure it if necessary, and if the iteration is wrapped in a
function, then do not forget to properly return that value.
8. If you have to do some post processing (e.g. memory cleaning up, device discon-
nection, etc.), consider them at this step. If you have written a function, do not
forget that these actions must go before the return action.
142 4 Managing the Size of a Problem
Python basically provides two types of iterative statements: namely, for and
while statements, which are basically pre-testing types of loops with post-
modification. As we mentioned before, Python does not provide statements for
iterations with post-testing.
for statements have very similar syntax to the List Comprehension that we
have seen in Sect. 3.5:
1 f o r x i n [ 2 , 4 , −10, "c" ] :
2 p r i n t x , "@"
while statements are different from for statements in that the iterations are
not restricted to a set of values that a variable can take. Instead, a set of state-
ments are executed while a condition is true:
1 L = [2 , 4 , −10, "c" ]
2 i = 0
3 while i < len (L) :
4 p r i n t L [ i ] , "@"
5 i += 1
In Python, like most other programming languages, you can put loops within
other loops. There is no limit on the level of nesting (i.e., the number of loops
you can nest within other loops); however, indentation becomes a difficult issue
when the level of nesting increases. Below is a simple example:
1 for i in range ( 1 , 1 0 ) :
2 p r i n t i , ":" ,
3 for j in range (1 , i ) :
4 p r i n t j , "-" ,
5 p r i n t ""
Like most other programming languages, Python provides the following state-
ments:
• break
If you want to terminate a loop prematurely, you can use the break state-
ment. When you use the break statement, the execution of the loop finishes
and the execution continues with the next statement after the loop. The fol-
lowing example illustrates what the break statement does (Note that this is
just an illustration of the semantics and there are several ways of combining
a break with other statements (especially with if statements) in a loop):
1 x = 4
2 L i s t = [ 1 , 4 , −2, 3 , 8 ]
3 for m in L i s t :
4 print m
5 i f m == x :
6 p r i n t "I have found a match"
7 break
ing example illustrates what the continue statement does (Note that this
is just an illustration of the semantics and there are several ways for combin-
ing a continue with other statements (especially with if statements) in a
loop):
1 List = [ 1 , 4 , −2, 3 , 8 ]
2 i = 0
3 while i < len ( List ) :
4 i f List [ i ] < 0:
5 i += 1
6 continue
7 print List [ i ]
8 i += 1
In Python, all loops can have an else statement as shown below (the same
holds for for statements) which is used for post-loop handling:
1 w h i l e <cond > :
2 <statements >
3 else :
4 < e l s e −s t a t e m e n t s >
The statements in the else part are executed when the loop exits (i.e., when
the test condition becomes False in a while loop, or the list is exhausted
in the for loop) normally; it means that when the loop exists with a break
statement, the statements in the else part are not executed.
146 4 Managing the Size of a Problem
In contrary to the built-in facilities for arithmetical calculations and conditional con-
trol of the execution flow, processors are not designed to natively support user de-
4.3 Recursion Versus Iteration 147
fined function calls. High-level languages provide this facility by a series of pre-
cisely implemented instructions and a stack-based short-term data structure (techni-
cally named as activation records on acall stack). Therefore, when a function call is
made, the processor has lots of additional instructions to execute (in addition to the
instructions of the function), which means time. Furthermore, each call has some
spatial cost in the memory, i.e., storage cost. Even for the ‘intelligently’ rewritten
recursive Fibonacci(n) function, which fires only one recursion per call, this pro-
cessing time and memory space overhead will grow in proportion to the value of n.
On the other hand, a non-recursive definition of Fibonacci that does not need a stack
will not suffer from any n proportional call costs. Of course, one should not confuse
this cost with the cost of the algorithmic computation: Both the recursive and the
non-recursive versions of Fibonacci consume additional time for adding n numbers,
which is proportional to n.
We can summarize the resource cost for a function call with the sizable dimen-
sion parameter as n in Table 4.1. What is the bottom line then?
• Resource wise the best is to have an algorithm which is iterative and is not needing
a stack.
• Resource wise the second best is an iterative algorithm that requires a stack.
• A Recursive algorithm is resource wise the worst.
• Since it is not an inherit technique of the human mind, ‘recursion’, at the first
sight, is easy to understand but not easy to construct. Therefore, the novice pro-
grammer’s first reaction is to avoid recursion and stick to ‘iteration’. As the pro-
grammer masters recursion, the shyness fades away and recursion gets frequently
preferred over iteration. This is so because, for experienced programmers, recur-
sive solutions are more apparent than imperative ones.
• Recursive solutions are more elegant, clean and easy to understand. They need
fewer lines in coding compared to iterative ones. When clean code is more impor-
tant than fast code, especially in the development phase, professionals will prefer
a recursive solution and consider it for iterative optimization at a later stage, if
necessary.
148 4 Managing the Size of a Problem
4.4 Keywords
The important concepts that we would like our readers to understand in this chapter
are indicated by the following keywords:
4.6 Exercises
1. What do you think the following Python code does? What happens when you
call the function like as follows: f(0)? How about f(-2)?
4.6 Exercises 149
1 def f (B ) :
2 i f B:
3 f ( B−1)
2. Write a recursive version of the Python code that checks whether a string is
palindrome.
3. Write a recursive match function that checks whether two containers are iden-
tical (content-wise and type-wise) without using Python’s == operator.
4. Implement the following Ackermann function with and without using recur-
sion:
⎧
⎪
⎨n + 1 if m = 0,
A(m, n) = A(m − 1, 1) if m > 0 and n = 0,
⎪
⎩
A(m − 1, A(m, n − 1)) if m > 0 and n > 0.
5. What does the variable c hold after executing the following Python code?
1 c = l i s t ( "address" )
2 for i in range ( 0 , 6 ) :
3 i f c [ i ]== c [ i + 1 ] :
4 for j in range ( i , 6 ) :
5 c [ j ]= c [ j +1]
6. How many times does # get printed after executing the following Python code?
1 i = j = 0
2 while i < 10:
3 p r i n t "#" ,
4 j = i
5 while j < 10:
6 p r i n t "#" ,
7 i += 1
(a) We can derive two new three-digit numbers by sorting the three digits. Let
us use A to denote the new number whose digits are in increasing order and
B the number whose digits are in decreasing order. For example, for 392,
A and B would respectively be 239 and 932.
(b) Subtracting A from B, we get a new number C (i.e., C = |B − A|).
(c) If the result C has less than three digits, pad it from left with zeros; i.e., if
C is 63 for example, make it 063.
(d) If C is not 1, continue from (a) with C as the three-digit number.
It turns out that, for three-digit numbers, C eventually becomes 495 and never
changes. This procedure outlined above is called the Kaprekar’s process and
the number 495 is known as the Kaprekar’s constant for three digits.
Write a piece of Python code that finds the Kaprekar’s constant for four digit
numbers.
11. Write an iterative version of the recursive greatest-common-divisor function
that we have provided in the chapter. Which version is easier for you to write
and understand?
12. Write an iterative version of the recursive power-set function that we have pro-
vided in the chapter. Which version is easier for you to write and understand?
13. Some integers, called self numbers, have the property that they can be generated
by taking smaller integers and adding their digits together. For example, 21 can
be generated by summing up 15 with the digits of 15; i.e., 21 = 15 + 1 + 5. On
the other hand, 20 is a self number since it cannot be generated from a smaller
integer (i.e., for any two digit number ab < 20, the equation 20 = ab + a + b
does not hold; or, more formally, ∀ab (ab < 20) ⇒ (20 = ab + a + b)). Write
a piece of Python code for finding self numbers between 0 and 1000.
14. Write a piece of Python code to check whether the following function converges
to a constant n value (Hint: It does).
f (n/2) if n mod 2 = 0,
f (n) =
f (3n + 1) if n mod 2 = 1.
Chapter 5
A Measure for ‘Solution Hardness’: Complexity
Up to this point, we have described the basic ingredients to build an algorithm and
thereafter to implement it as a program. Soon, one discovers that usually there are
multiple ways in constructing an algorithm that serves a particular purpose. How
can we choose between the alternatives? It would be good to have an assertion that
says “take whatever approach you like, since they will all do the same job with the
same efficiency”. This is unfortunately not the case. “All roads lead to Rome!” but
“some roads are shorter!”. How can we make an assertion about the efficiency of an
algorithm? To answer this question, we need to define the aspects in which we seek
efficiency:
R1. Time required by the computer to run the algorithm and perform the task.
R2. Size of the memory required to store data to carry out the task.
R3. Count of instructions and the code complexity of the algorithm.
As you can see, it is all about resources: those required to run the algorithm and
the human resources to implement the algorithm. The latter, interestingly, is not
often emphasized when choosing an algorithm. This aspect is called algorithmic
complexity (also known as the Kolmogorov complexity) and is investigated merely
of academic interest. It starts with the assertion that all output can be expressed in
the form of a string, and then proceeds by investigating the content of the string.
For a string ξ , the Kolmogorov complexity C(ξ ) is the length of the shortest binary
program that generate it. Though of much academic interest, aspect (R3) is less
important compared to R1 and R2, since they are much more influential on practical
applications because they are about the run-time observables.
To understand the time and memory efficiency of an algorithm, let us start with time
and immediately consider an example.
Let us assume that you are given a collection of words in a sorted order. You
have the whole collection in a container that is indexable such that accessing any
item in the collection takes constant time (apparently it is an array structure).
The problem, for example as a subtask of writing a spell checker, is to check
whether a given word exists in the sorted collection. We display two alternative
algorithms, exists1 and exists2, which serve the same purpose. Our first algorithm is
this:
define exists1(word, collection)
pivot ← 0
countofwords ← length(collection)
while pivot < countofwords do
?
if word = collection[pivot] then
return TRUE
else if word < collection[pivot] then
return FALSE
else
pivot = pivot + 1
return FALSE
Our second algorithm is:
define exists2(word, collection)
start ← 0
end ← length(collection) − 1
while start < end do
pivot ← (start + end)/2
if word ≤ collection[pivot] then
end = pivot
else
start = pivot + 1
?
if word = collection[pivot] then
return TRUE
else
return FALSE
Let us try to estimate the time that will be spent for each algorithm for a collection
that has n elements:
• Time required for exists1:
First, we consider the exists1 algorithm. As a shorthand notation, we will repre-
sent
Time exists1(word, anthology) |anthology| = n.
as Time(exists1(n)).
5.1 Time and Memory Complexity 153
The time spent by exists1, i.e., Time(exists1(n)), is the sum of time spent on two
different parts: Tprior , the time prior to the while and Twhile , the time spent in the
while:
Time exists1(n) = Tprior + Twhile .
Tprior has two ingredients: the first is an assignment of a constant value to a vari-
able, the second is a function call and the assignment of the result of the function
call to a variable.
Though we cannot know the exact running-time value of the first assignment (i.e.,
“pivot ← 0”), we do know that it is a fixed value. In other words, it is independent
of the n value (i.e., the length of the collection). We will denote the time spent on
the assignment of this constant value with c0 .
The second time element (i.e., “countofwords ← length(collection)”), on the
other hand, is a function call, the time cost of which is unknown. There are two
possibilities about how the length() function is implemented:
– The language that provides the container also keeps, as a part of the internal
container information, the element count of the container; therefore, providing
the length information is just the retrieval of a stored value with a constant time
cost, or,
– the implementation of the container does not keep the count of elements, which
means that it has to be ‘measured’ (by counting all the elements), which will
have a time cost that will increase proportional to the length of the container,
in our case n.
We denote this time as Tlength :
either c1
Tlength =
or c2 + c3 · n
A close investigation of the body of the while loop reveals that in the worst case,
the loop will iterate n times and at every iteration, a constant time (let us call c4 )
will be spent on the body of the while loop. Therefore, Twhile = c4 · n. Putting all
the time-consuming elements together:
either c1
Time exists1(n) = c0 + + c4 · n.
or c2 + c3 · n
But, either way, the equation above can be combined into a linear equation:
either a1 + a2 · n
Time exists1(n) =
or b1 + b2 · n
A similar analysis for exists2 can be performed. With a similar naming, the Tprior
value boils down to:
either c1
Tprior = c0 +
or c2 + c3 · n
The while loop of exists2 is somewhat trickier than that of exists1. The control of
the while loop is performed over the values of start and end. At each cycle, either
start or end are moved towards each other by the half distance between them. In
other words, the distance between start and end is halved at each iteration. For
the algorithm to terminate, this distance has to be reduced to one (then, in the next
cycle, they will become equal). What is the cycle count required to achieve this
value?
Cyclecount
1
n· =1
2
It is straightforward that this yields:
Here, c5 is a new constant because the time spent during a single cycle is not
necessarily the same as in the exists1(n), namely c4 . Putting all of these together,
we have:
either c1
Time exists2(n) = c0 + + c5 · log2 (n),
or c2 + c3 · n
In computer science, what interests us is the way the time cost is affected when
the size of the problem n is increased for really large values of n. The big question
is:
What type of functional dependency to n exhibits the time function of the
algorithm for considerably larger n values?
Here, the emphasis is on two concepts:
1. The dependency on the size of the problem n and nothing else.
2. The asymptotic behavior of this dependency (for n → ∞).
5.1 Time and Memory Complexity 155
At the end of the 19th century, Bachmann, a number theorist introduced a concept
to describe the limiting behavior of functions. Soon after, the famous mathematician,
Edmund Landau, adding notation used and popularized Bachmann’s concept. This
notation is now referred to as the Bachmann–Landau notation. In 1976, Donald
Knuth, the well-known computer scientist, imported the concept into the analysis of
algorithms. The formal definition of the notation reads as follows:
Let n take values on a subset of real numbers for a given function f(n) and a
function g(n). It is said that:
f (n) is Θ g(n) ,
if there exists some positive c1 , c2 values such that for all n > n0 values:
c1 · g(n) ≤ f (n) ≤ c2 · g(n). (5.1)
Formally speaking, “f (n) is Θ(g(n))” means “f (n) is bounded both above and
below by g(n) asymptotically”.1
Now reconsider our exist1 algorithm which we discovered to have a time function
Time exists1(n) = a · n + b,
where a and b are constants. Now in Eq. 5.1, set c1 = a and c2 = 1.001 · a and take
g(n) = n. We can claim that the function (n) is a bound both from above and below
on (a · n + b) since
Interestingly, we could have taken g(n) = 7 · n (nothing special about the 7. It is just
a constant, any other number would do as well). Redefining c1 = a/7 and c2 = a/7,
we can insert our new g(n) into the defining inequality and get:
a a
· |7 · n| ≤ |a · n + b| ≤ · |7 · n|
7 7
So, this would give us the right to write:
1 For historical reasons, especially among mathematicians (and also among some computer scien-
tists), the denotation uses an equal sign (=) in the place of is. Others prefer to use the membership
operator (∈). Certainly this asymmetric overloading of the equality sign is a misuse of denota-
tion. It very easily leads to the incorrect assumption that if A = B then B = A which is not so in
this case. Computer scientists have less of a problem with the equal sign, presumably due to the
(i = i + 1) practice. The use of the membership operator is correct but implies a set theoretical
view, which is not exactly what (and how) we are focusing on here.
To be on the safe side, we introduce the denotation “is” which is certainly asymmetric. For
example, (human is mammal) does not mean (mammal is human) (plural forms are omitted for
the sake of simplicity).
156 5 Complexity
Time exists1(n) is Θ(7 · n)
2 The reader should note that Θ is not a function. It defines an infinite set of functions for each
argument it is given. Some authors, misusing the mathematical notation, treat Θ as A function and
m
make it subject to functional composition, such as F (Θ(g(n))) (e.g. eΘ(n ) , (log(n))Θ(1) ). unless
the meaning is defined explicitly, such a use is incorrect.
5.1 Time and Memory Complexity 157
Now, applying all these rules, we can conclude that the exists1 algorithm has
Θ(n) complexity. What about exists2? If the internal implementation of the length
function had a time cost proportional to n, then it would be:
b1 + b3 · n + c5 · log2 (n).
On the contrary, if that time cost were constant, then we would have:
a1 + c5 · log2 (n).
This gives us two different asymptotic complexities, namely Θ(n) and Θ(log n),
respectively. The first case is interesting in that an algorithm which implements a
more clever technique than exists1 cannot get a reduction in complexity just because
of a time consuming internal function (the length function). This is a case which
happens from time to time. If you observe an unexpected complexity behavior, the
first thing you should investigate is the possibility of such hidden complexities.
How do the basic ingredients of complexity compare? The graph in Fig. 5.1
displays the most common functions that appear in complexity analysis. Note that
the y-axis is logarithmically scaled (that is why the function n is not drawn as a
line).
Some common complexity names used in computer science:
Very similar to our interest in the analysis of the time aspects of an algorithm, we
should also be attentive to the memory requirements of an algorithm because mem-
ory is another delicate resource we have to keep an eye on. The memory that is
subject to complexity analysis is about data: as with ‘time’, in memory complex-
ity analysis, the algorithm is inspected for the size of the ‘memory’ it requires as a
function of the size n of the ‘sizable’ input of the algorithm.
Similar to the case with the time resource, it is possible that two algorithms serv-
ing exactly the same purpose can vary in their memory requirements. For example,
let us compare three well-known array sorting algorithms for their time and auxil-
iary memory3 complexity:
The assumption that memory requirements are inversely related to the time require-
ment is not always true (as seen above), though, sometimes, this is the case. This is
3 Auxiliary memory, in this context, is the additional memory other than that required to hold the
input.
5.1 Time and Memory Complexity 159
due to the structure of the Von Neumann machine. The time cost to access any place
(given an address) in the memory is constant (i.e., a time complexity of Θ(1)). If
we give up this random accessibility due to memory space consumption and replace
it by a search in the memory, then depending on the search, a time complexity is
introduced which varies form logarithmic to linear.
The most popular one is Big-Oh. Given a function f (n), Big-Oh defines the set of
all the functions that remain above f (n) when n approaches infinity. If f (n) = n
for example, it is perfectly legitimate to say ‘f (n) is O(n2 )’, or ‘f (n) is O(nn )’.
Of course, it is also legitimate to say ‘f (n) is O(n)’. Remember that we had a
similar problem with Big-Theta where we were allowed to have any multiplicative
constant factor plus any function that remained smaller. Now, with Big-Oh, this
freedom extends to the use of any function asymptotically greater than f (n). Please
note that Big-Oh is the super set of Big-Theta. According to the similar reasoning
we made for Big-Theta, we seek the simplest (and yet the smallest) function that is
still Big-Oh of f (n). With this implicit desire, the outcome of the task of seeking
Big-Oh of a function becomes the same as that of Big-Theta. So, commonly, when
we are expressing the Big-Oh of a linear time dependency, we say that it is O(n).
Some well-known authors, such as Cormen and Knuth, pinpoint this misuse of Big-
Oh (i.e. Using Big-Oh with the intention of meaning a ‘tight asymptotic bound’
5.2 Keywords
The important concepts that we would like our readers to understand in this chapter
are indicated by the following keywords:
For more information on the topics discussed in this chapter, you can check out the
sources below:
• Algorithm Analysis:
– http://en.wikipedia.org/wiki/Analysis_of_algorithms
• Computational Complexity:
– http://en.wikipedia.org/wiki/Computational_complexity_theory
– L. Fortnow, S. Homer, “A Short History of Computational Complexity”, Bul-
letin of the EATCS 80: 95–133, 2003.
• Time Complexity:
– http://en.wikipedia.org/wiki/Time_complexity
• Big-Theta and Big-Oh Notations:
– http://en.wikipedia.org/wiki/Big_Theta_notation
– http://en.wikipedia.org/wiki/Asymptotic_analysis
• Best-, Worst- and Average-Case Analysis:
– http://en.wikipedia.org/wiki/Best,_worst_and_average_case
5.4 Exercises
1 def f (L ) :
2 i f L == [ ] :
3 return 0
4 r e t u r n ( f ( L [ 0 ] ) i f t y p e ( L [ 0 ] ) == l i s t e l s e 1 ) \
5 + f (L [ 1 : ] )
3. What is the running time complexity of the following Python function? Use both
the Big-Theta and the Big-Oh notations.
1 d e f f (N ) :
2 r e t u r n N i f N == 0 or N == 1 e l s e \
3 ( T r u e i f n o t f (N−1) e l s e F a l s e )
4. What is the running time complexity of function h() defined below? Use both
the Big-Theta and the Big-Oh notations.
1 def f (L ) :
2 i f L == [ ] :
3 return 0
4 r e t u r n ( f ( L [ 0 ] ) i f t y p e ( L [ 0 ] ) == l i s t e l s e 1 ) \
5 + f (L [ 1 : ] )
6 d e f g (N ) :
7 r e t u r n N i f N == 0 or N == 1 e l s e \
8 ( T r u e i f n o t f (N−1) e l s e F a l s e )
9
10 d e f h ( L ) :
11 while L != [ ] :
12 i f g ( len (L ) ) :
13 L . pop ( )
14 L . pop ( )
5. What is the running time complexity of function f defined below? Use both the
Big-Theta and the Big-Oh notations.
1 def count (L ) :
2 cnt = 0
162 5 Complexity
3 i f L != [ ] :
4 for x in L:
5 c n t = c n t +1
6 return cnt
7
8 def f (L ) :
9 i = 0
10 w h i l e i < c o u n t ( L) −1:
11 i f L[ i ] > L[ i +1]:
12 return False
13 return True
6. What is the running time complexity of function f defined below? Use both the
Big-Theta and the Big-Oh notations.
1 def count (L ) :
2 cnt = 0
3 i f L != [ ] :
4 for x in L:
5 c n t = c n t +1
6 return cnt
7
8 def f (L ) :
9 i = 0
10 w h i l e i < c o u n t ( L) −1:
11 j = 0
12 while j < i :
13 i f L [ j ] > L [ i ] and i ! = j :
14 return True
15 return False
7. Consider the following two sorting algorithms in Python. Compare their auxil-
iary memory complexities.
1 def f1 ( L i s t ) :
2 for i in range (1 , len ( L i s t ) ) :
3 save = L i s t [ i ]
4 j = i
5 w h i l e j > 0 and L i s t [ j − 1 ] > s a v e :
6 L i s t [ j ] = L i s t [ j − 1]
7 j −= 1
8 L i s t [ j ] = save
9
10 d e f f 2 ( L i s t ) :
5.4 Exercises 163
11 List2 = []
12 for i in range (0 , len ( L i s t ) ) :
13 save = L i s t [ i ]
14 j = 0
15 w h i l e j < i and L i s t 2 [ j ] < s a v e :
16 pass
17 j += 1
18 List2 . i n s e r t ( j , save )
19 List [ : ] = List2 [ : ]
1 d e f c o p _ c a t a n 1 ( boys , g i r l s ) :
2
3 # m i x e d = c a r t e s i a n p r o d u c t o f b o y s and g i r l s .
4 mixed = [ ( b , g ) f o r b i n b o y s f o r g i n g i r l s ]
5 allowed = [ ]
6
7 f o r c o u p l e i n mixed :
8 i f couple [ 0 ] [ 1 ] + couple [ 1 ] [ 1 ] < 60:
9 al l o w e d . append ( c o u p l e )
10
11 return allowed
12
13 d e f c o p _ c a t a n 2 ( boys , g i r l s ) :
14
15 allowed = [ ]
16 for b in boys :
17 for g in g i r l s :
18 i f b [1]+ g [1] < 60:
19 allowed . append ( ( b , g ) ) ;
20
21 return allowed
22
23 # Example r u n :
24 # boys =[(" A l i " , 20) , (" V e l i " , 30) ,
25 # (" Deli " , 40)]
26 # g i r l s = [ ( " A y s e " , 5 0 ) , ( " Fatma " , 4 0 ) ,
27 # (" Hayriye " ,30)]
28 # c o p _ c a t a n 1 ( boys , g i r l s )
29 # c o p _ c a t a n 2 ( boys , g i r l s )
Chapter 6
Organizing Data
While implementing computer solutions to real world problems, you will very
soon discover that you frequently encounter certain types of data such as: num-
bers, strings, ‘yes’/‘no’ type of information. Moreover, you will encounter the need
to group some certain information together. For example, whenever we are asked
to undertake some programming related to spatial data (programming about geo-
graphical positions, flight simulators, first-person-shooter games, architectural de-
sign, etc.), a desperate need for representing ‘coordinates’ emerges. Alternatively, in
any e-government or e-commerce applications, there is the concept of ‘private data’
(like name, surname, address, etc.).
In computer science,
• identifying a data kind and its organization in the memory, and
• identifying operations that can be performed on that kind of data (and its organi-
zation)
are named as defining a data type.
Computer languages provide home-brew solutions for some data types, which
we glimpsed in Chap. 2—“Data: The First Ingredient of a Program”. These were
the basic or primitive data types and were provided by almost all decent high level
programming language.
While implementing computer solutions to real world problems, we come across
points where we decide about how we are going to organize the data in the memory.
Sometimes a simple container suffices, but sometimes the nature of the problem do-
main or a subpart of the problem, combined with the programming demands related
to the data enforces more sophisticated data organizations. There is a collection
of patterns of use of such data organizations, which occur so frequently in a wide
spectrum of problems that obviously the specific data domain needs to personalize.
Those patterns of use of the data organization and their properties form a kind of
abstraction, which is independent of what the data actually is, what domain it comes
form, what it is used for etc. These patterns of use represented as a bundle of certain
operations is called Abstract Data Type, commonly abbreviated to ADT.
6.2.1 Stack
Verbal definition: A collection of items in which the next item to be removed is the
item most recently stored and no other items can be removed or investigated.
The most recently stored item is called the top of the stack.
A stack is also known as a push-down-list or as a Last-in-First-out (LIFO) struc-
ture.
There are two operations that modify the stack: push adds a new item to the
stack and pop removes the topmost item. Some imperative approaches overload
1 If none of the lines matches in such a multi-line function definition and the argument is legitimate,
the pop operation with two tasks: (1) modify the stack by removing the top item,
(2) return the removed item as the value of the function. As stated above, this
is something we dislike in a functional approach so we will separate these two
tasks: Task 1 will be implemented by the popoff function, and task 2 by the top
function.
In addition, there are two other functions: new creates a stack and isempty checks
whether the stack is empty or not.
Formal definition: We represent the push(item, stack) function, which returns a
stack with the pushed item on top, by the infix operation
item stack
and an empty stack with ∅. S represents a stack, ξ represents an item that can be
placed on that stack.
• new() → ∅
• popoff (ξ S) → S
• top(ξ S) → ξ
• isempty(∅) → TRUE
• isempty(ξ S) → FALSE
Usage: Stacks are used whenever we have to remember the trace we have followed
in the course of carrying out a computation. The first to remember is the most
recent visited.
Certainly the most widespread usage is the ‘call stack’. As stated in previous
chapters, CPUs do not provide a built-in facility for function calls. This very
handy feature is implemented by a complicated series of actions. It is quite
possible that a function calls another function (that is what we call ‘functional
composition’). The mechanism that performs the evaluation of a function has
to know where to return when the evaluation of the innermost function is com-
pleted. A stack is used for this purpose.
Another usage is when, for a kind of systematic exploration or search in a high-
dimensional space (e.g., searching for the list of moves leading to a check-mate
from the current board configuration in a chess game), an alternative solution is
sought. This is technically called ‘backtracking’.
Reversing or matching up related pairs of entities (such as palindromes, and
matching parentheses) are also tasks which require stack usage. Generally
speaking, any task that can be solved recursively will mostly require a stack
to be solved iteratively.
6.2.2 Queue
Verbal definition: A collection of items in which the next item to be removed is the
item in the collection which was stored first. Apart from that item, no item can
168 6 Organizing Data
be removed or investigated. The first stored item of the queue is called the front
of the queue.
A queue is also known as a first-in-first-out (FIFO) structure.
There are two operations that modify the queue: add, that adds a new item to
the queue and remove, that removes the front item. As with the stack, some
imperative approaches overload the remove operation with two tasks: (1) modify
the queue by removing the front item, (2) return as value the removed item. With
the same reasoning, we will separate these tasks into (1) remove and (2) front.
In addition to these, there are two other functions. new creates a queue and
isempty checks whether the queue is empty or not.
Formal definition: We represent the add(item, queue) function, which returns a
queue with the added item to the end of the queue, by the infix operation
item queue
and an empty queue with ∅. Q represents a queue, ξ represents an item that can
be added to that queue.
• new() → ∅
• front(ξ ∅) → ξ
• front(ξ Q) → front(Q)
• remove(ξ ∅) → ∅
• remove(ξ Q) → ξ remove(Q)
• isempty(∅) → TRUE
• isempty(ξ Q) → FALSE
Usage: In the world of programming, queues are used
• when there is a limitation on the resources; all processes that require the re-
sources have to wait for turn before execution.
• when a real life simulation is to be performed (e.g. simulation of any sort of
vehicle traffic, production or service line simulation etc.).
• where a decision/exploration/search process needs to keep an order of first-
in-first-out (e.g. Breadth-first search in graphs, A path finding).
bine them into a single action (a single ‘delete’ action that returns a value as
well as causes a side-effect). Then, we will have a separate highest-priority-item
returning function that we call highest.
In addition to these, there are two other functions. As usual, new creates an
empty PQ and isempty tells whether a PQ is empty or not.
Formal definition: We represent the insert(item, PQ) function, which returns the
PQ with the item inserted, by the infix operation
item PQ
6.2.4 Bag
6.2.5 Set
Verbal definition: An unordered collection of items where each item occurs at most
once. A set is defined over a universe. The universe is made up of all the elements
allowed to be a member of the set. Having the universe to hand, an item is either
in the set (is a member of the set) or not.
A set has two modifier functions: add adds a member to the set. remove removes
a member. member is a predicate function that tells whether an item is a member
or not.
Formal Definition: We represent the add(item, set) function, which returns the set
with the item inserted, by the infix operation
item set
6.2 Abstract Data Types 171
and an empty bag with ∅. S represents a set, ξ , η represent two different items
that can be added to the set.
• new() → ∅
• remove(ξ, ∅) → ∅
• remove(ξ, ξ S) → S
• remove(ξ, η S) → η remove(ξ, S)
• member(ξ, ∅) → FALSE
• member(ξ, ξ S) → TRUE
• member(ξ, η S) → member(ξ, S)
• isempty(∅) → TRUE
• isempty(ξ S) → FALSE
Some definitions also add the complement operation with respect to the univer-
sal set. If that is the case the definitions have to be extended by an additional
argument which will carry the universal set.
6.2.6 List
A collection of items accessible one after another. The list begins at the head.
Verbal definition: A collection of items accessible one after another beginning at
the head and ending at the tail.
A list has three modifier functions: cons, that inserts an item into the list so it
becomes the head; head, a function that provides the first (head) item of the list;
tail, a function that returns a list with its first item removed.
Formal Definition: We represent the cons(item, list) function, which returns the set
with the item inserted, by the infix operation
item set
and an empty list with []. This list is also commonly referred to as nil. L repre-
sents a list, ξ an item that can be inserted into the list.
• new() → []
• head(ξ L) → ξ
• tail(ξ L) → L
• isempty([]) → TRUE
• isempty(ξ L) → FALSE
6.2.7 Map
Verbal definition: Map is a collection of items where each item is (efficiently) ac-
cessible by a key. The key can be any arbitrary type. So, the relation between
keys and the items is exactly a function.
172 6 Organizing Data
Hashes are the best implementations of the map ADT (see Fig. 6.1).
6.2.8 Tree
Verbal definition: A tree is a collection of nodes where each tree has a distinguished
node r, called the root, and zero or more nonempty (sub)trees T1 , T2 , . . . , Tk ,
each of which root is connected by a directed edge from r. The root of each of
those subtrees (Ti ) is called a child of r, and r is the parent of each subtree root
(see Fig. 6.2).
Here is some terminology on trees.
• Nodes may have any type of data items attached, which are called the datum.
• Nodes with no children are called leaves or terminals.
• Nodes that have the same parent are called siblings.
• A path from a start node n1 to an end node nk is defined as a sequence of
nodes n1 , n2 , . . . , nk such that for all 1 ≤ i < k, ni+1 is a child of ni (see
Fig. 6.3).
• The length of a path is one less the cardinality of the path, in other words, it
is the number of edges on the path, namely k − 1.
174 6 Organizing Data
• The depth or level of a node is the length of the unique path from the root to
the node (see Fig. 6.4).
• The height of a node is the length of the longest path from the node to a leaf.
• The sibling nodes, or siblings, are the immediate children of the same node
(see Fig. 6.5(a)).
• The height of a tree is defined as the height of the root.
• If there is a path from α to β, then α is an ancestor of β, and β is a descendant
of α. Furthermore, if α = β, then the ancestor and descendant are called the
proper ancestor and proper descendant, respectively (see Fig. 6.5(b)).
A tree has two modifiers: The first is makenode(item) that creates a tree (consist-
ing of a single node) having the item attached as datum to the node.3 The second
is addchild which admits only trees and insert a new child at the top node. The
child becomes the ‘first’ child. datum and haschild act as their names imply.
firstchild is a function, given a tree returns the subtree which is the ‘first’ child
of the top node. remchild returns a tree with the ‘first’ child (subtree) removed.
Some common operations on trees are:
Visiting all the nodes: This is also called tree traversal or walking a tree. The
question is: Which node is visited first and which is the next one? This sys-
tematic is important and names the traversal. When we arrive at a node there
are two tasks to be carried out:
• Process the information at the node (the datum),
• Visit the children (yes that was a recursive definition).
Therefore, for all kind of trees there are two possibilities:
• First process the datum then visit the children, or,
• first visit the children and then process the datum.
These two recursively definable alternatives are called pre-order-traversal
and post-order-traversal. If the tree is a binary tree (a tree with two-and-
only-two children), then there is also a third possibility: Visit the left child,
process the datum, visit the right child. This is called in-order-traversal.
Altering a tree: Quite often there is a need to make a change in the tree. Com-
mon changes are:
• Insert a new child at any position. This can be a single node as well as a
huge, new subtree. Inserting a subtree is called grafting.
• Delete a child at any position. If the child has children, in other words if
it is interior node, then the removal is called pruning
Searching a tree: Trees are powerful tools for organizing data for searching.
The trivial search is, of course, to visit all the nodes. But, it is possible to
organize the data so that,
(1) Start at the root.
(2) Consider the datum, is it what you are looking for? Otherwise depending
on the datum, make a decision to which child to descend to.
(3) Descend to the child.
(4) Go to step (2).
The trick here is to have the data organized in such a way that, at any node,
the search is continued by descending to a single child only. The datum at
that node will direct the way to proceed.
3 Different then some other definitions we do not consider the emptyset as an instance of a tree.
Though denotationally it looks attractive, having an empty set as a tree clutters the subtree defini-
tion. That way a dangling edge to a subtree, a child, which does not exist at all, becomes possible.
176 6 Organizing Data
We will return to this topic when we consider the usages of the trees.
Formal Definition: T , t represents two distinct trees, δ is an item that can be at-
tached (as datum) to a node of tree.
• datum(makenode(δ)) → δ
• datum(addchild(T , t)) → datum(T )
• firstchild(addchild(T , t)) → t
• remchild(addchild(T , t)) → T
• haschild(addchild(T , t)) → TRUE
• haschild(makenode(δ)) → FALSE
Usage: Trees have a broad range of uses. The main uses can be classified as below:
• Representing a hierarchical relational structure: When any world knowledge
which has a hierarchy in itself has to be represented as structured data, it is
the ‘tree’ ADT that we select as the data type. Moreover, most ontological4
information can be represented as trees. Figures 6.6 and 6.7 display examples
of such hierarchical structures from the real world.
• Representing an action logic: Sometimes depending on the answer to a par-
ticular question, a certain action (from a set of possible actions) is carried
out. Thereafter, a new question will be answered, and as a function of the
answer, another action (from another set of possible actions) is carried out.
This . . . question-answer-action-question. . . sequence continues until a ter-
mination criteria is met. Trees are also convenient in representing such action
logics. Trees of this kind are called decision trees, and they can be constructed
automatically (using different inference mechanisms) or manually (by a hu-
man). Decision trees fall into the classification algorithms category of com-
puter intelligence and are widely used.
In Fig. 6.8, you can see two examples of decision trees, which are self ex-
planatory.
And-Or trees are a variant of decision trees. In a decision tree, upon the an-
swer, we proceed to the next question by descending to one of the children,
whereas in an and-or tree, each node is either a conjunction (and) or disjunc-
tion (or) of the children. The children are either further conjunctions/disjunc-
tions or simple questions that can be answered with true or false. And-or trees
are heavily used by inference and proof systems.
Figure 6.9 displays a paranoid logic rule system expressed as an and-or tree.
Nodes that have an angle arc on their links to their children are conjunctions
(and nodes), others are disjunctions (or nodes).
Another group of trees are concerned with encoding and decoding actions.
In this field, a set of atomic information, called the alphabet, is coded into
of knowledge as a set of concepts within a domain, and the relationships between those concepts.
It is used to reason about the entities within that domain, and may be used to describe the domain.
6.2 Abstract Data Types 177
A-SIMPLE-STRING-TO-BE-ENCODED-USING-A-MINIMAL-NUMBER-
OF-BITS
There are eleven dashes, three A’s, three B’s, etc. The frequency table for this
string is shown below:
A B C D E F C I L M N O P R S T U –
3 3 1 2 5 1 2 6 2 4 5 3 1 2 4 3 2 11
The Huffman coding algorithm would construct the tree in Fig. 6.11.
The small numbers at the nodes are only for information: any such number
depicts the frequency sum of the subtrees of that node. Now encoding a letter
is achieved by traversing the path form the root to that letter and recording
‘0’ for a left branching and ‘1’ for a right branching. For example, the letter
‘A’, starting from the root, is reached by a “left, right, right, left” move which
180 6 Organizing Data
therefore can be encoded as 0110. For the reader curious about the result, the
final encoding would be:
011011110010011010110101100011100111100111011101110
010000111111111011010011101011100111110000011010001
001011011001011011110000100100100001111111011011110
6.2 Abstract Data Types 181
100010000011010011010001111000100001010010111001011
11110100011101110101001110111001
That is a binary code of 236 bits. We started with a string of 44 char-
acters from an 18 character alphabet which, uncompressed, would require
60 × log2 18 bits. That is 300 bits. So we obtained a compression ratio of
(300 − 236)/300 = 21%
• Organizing data for a quick search: Trees are a convenient way of organizing
data in such a way that a search for any data item can be performed by making
a decision at the root and then, if necessary, descend to one of the children to
continue the search by making a decision at that child node. The benefit here
is that if the tree is such that the depth of all leaves are more or less equal, then
at every descent, the number of elements in the subtree is reduced by a factor
of 1/n, where n is the count of branching at the node. So, if at every level a
branching into n children occurs and there are N data items in the tree, then to
reach a leaf will take logn (N ) descents. If the tree keeps data items at interior
182 6 Organizing Data
nodes as well as leaves, it is quite possible that the search will terminate even
earlier.
Though theoretically the branching count n can be any number in search trees,
using 2 is very common in practice. The reason for this is simple: the time
complexity of such a search is Θ(log N ) because logn (N ) is proportional
to log N by a constant factor. So, why not chose the simplest one, n = 2?
This will also ease the coding of the branching condition by having a single
conditional (if statement). Based on this idea, various binary search trees have
been developed:
– Red-Black tree
– AVL tree
– Splay tree
– T-tree
– AA tree
– Scapegoat tree
These are all binary search trees, having at worst as average, Θ(log N )
search/insertion/deletion time complexities. Red-black and AVL trees are the
most preferred ones. Having all leaves more or less at the same depth is called
the balance of the tree. A binary tree, even if it starts out balanced, has to keep
its balance against insertion and deletions. There are different algorithmic ap-
proaches in ‘keeping the balance’, which is why we have so many different
binary search trees.
One type of binary tree, which is actually not a search tree, is outstanding in
its simplicity but still useful. A heap is a binary tree where each node holds
a data item which is larger than any of its children’s. The root, in this case,
holds the largest data item in the tree. This is exactly the aspect that a heap
is used for: to provide the largest item among a set of data. However, this is
what we actually expect from the priority queue (PQ) ADT. Therefore, heaps
are very convenient for implementing PQs.
Heaps are used extensively for sorting. The idea is to (i) extract the largest (the
root) element out of the heap and store it and append it to the sequence (which
will finally become the sorted data), then (ii) update the heap and continue the
process until no elements are left on the heap. The update phase is extremely
cheap in time complexity (costs only Θ(log N )). So, the whole sorting has a
complexity of Θ(N log N ).
What is even more interesting is that it is possible to have a tree view on
an array, an Θ(1) indexable container which usually holds the data during
sorting. So, there is even no need to have an additional heap structure. The
method is as follows:
(1) We start with the array holding the unsorted data.
(2) The array content is made into a heap (this is called heapification).
(3) The root element of the heap is placed in the first position of the array
(that element is in its final location according to its value).
(4) The remaining elements are heapified.
6.3 Abstract Data Types in Python 183
(5) Now the next largest is the root, and is moved to the place just after the
last placed (largest) one in the array.
(6) If there are still unsorted elements we continue from (4).
Consider an array of 10 elements:
Python provides a dict data type, which is mutable (i.e., it can be modified)
and sequenced (i.e., container data type such as strings, tuples and lists). Es-
sentially, a dict type corresponds to the map ADT described above. It is an
unordered set (or, sequence) of key-value pairs, which follows the syntax given
below:
{key_1:value_1, key_2:value_2, ..., key_N:value_N}.
where the keys are unique (i.e., they are all different from each other). Note
the curly braces (i.e., {}) and the columns (i.e., :) between key-value pairs.
The keys in a dictionary can be any immutable data type; i.e., strings, tuples,
numbers and boolean values.
6 If the indexing started at [1] instead of [0], the left child would have been A[2k] and the right
The items in a dictionary can be accessed using a key value within brackets. For
example, person = {’age’: 20, ’name’:’Mike’} is a dict type
whose items can be accessed like person[’age’] and it can be updated like
person[’age’] = 30. If the key does not exist in the dictionary, Python
gives an error unless it is an assignment:
>>> person = {’age’: 30, ’name’:’Mike’}
>>> person
{’age’: 30, ’name’: ’Mike’}
>>> person[’ssn’]
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
KeyError: ’ssn’
>>> person[’ssn’] = 124
>>> person
{’age’: 30, ’name’: ’Mike’, ’ssn’: 124}
The list of keys and values in a dictionary can be accessed using the keys()
and the values() functions:
>>> person
{’age’: 30, ’name’: ’Mike’, ’ssn’: 124}
>>> person.keys()
[’age’, ’name’, ’ssn’]
>>> person.values()
[30, ’Mike’, 124]
– Creating Dictionaries
– Modifying Dictionaries
In Python, lists can be used for implementing a stack. As you can see above,
the stack ADT should provide the following operations:
• new() → ∅: An empty stack is created. A function returning [] can be used
for the functionality of the new() operation; i.e., S = [].
• popoff (ξ S) → S: The item at the top of the stack is removed. The pop()
function of lists satisfy the popoff () operation; i.e., S.pop().
• push(item): The item is put at the top of the stack. In Python, lists provide
the append(item) function that achieves what is required by the push()
operation; i.e., S.append(item).
• top(ξ S) → ξ : The top item is returned without the stack being modified.
The top() operation can be easily implemented in Python by List[-1];
i.e., S[-1].
• isempty(S): Checks whether the stack is empty or not. In Python, S == []
can be used.
Below is an example of the use of a stack in Python. The function accepts a
postfix expression as a string and evaluates it:
1 def p o s t f i x _ e v a l ( S t r i n g ) :
2 ’ ’ ’ Example S t r i n g : 3 4 + 5 6 ∗ + ’ ’ ’
3 Stack = [ ]
4 for token in S t r i n g . s p l i t ( ) :
5 i f ’0’ <= t o k e n <= ’9’ : # Operand
6 S t a c k . a p p e n d ( t o k e n ) # Push o p e r a t i o n
7 e l s e : # Operator
8 o p e r a n d 2 = S t a c k . pop ( )
9 o p e r a n d 1 = S t a c k . pop ( )
10 r e s u l t = eval ( operand1 + token + operand2 )
11 S t a c k . a p p e n d ( s t r ( r e s u l t ) ) # Push o p e r a t i o n
12 return Stack [ 0 ]
talk about the complexities of different operations. The following table lists the
complexities of the operations for the Stack implementation mentioned above:
In Python, lists can be used for the implementing a queue. As we have seen
above, the queue ADT should provide the following operations:
• new() → ∅: Returns an empty queue; Q = [].
• front(Q): Returns the element at the front; Q[0].
• add(item, Q): Adds the element (at the end); Q.append(item).
• remove(Q): Removes the element at the front; Q.pop(0).
• isempty(Q): Checks whether the queue is empty; Q == [].
Below is a hypothetical example of a queue at a bank:
1 def bank_queue ( ) :
2 Queue = [ ]
3
4 while True :
5 i f a_new_customer_arrived :
6 new_customer = g e t _ c u s t o m e r ( )
7 Queue . a p p e n d ( n e w _ c u s t o m e r )
188 6 Organizing Data
8
9 c u s t o m e r _ t o _ b e _ s e r v e d = Queue . pop ( 0 )
10 serve_customer ( customer_to_be_served )
The following table lists the complexities of the operations for the Queue
implementation mentioned above:
The priority queue (PQ) ADT (described above) can be implemented in Python
using lists, like the previous ADTs.
There are two options for representing PQs in Python (and most other PLs):
• Keeping the queue sorted
• Finding the maximum when needed
The latter is described here and the former is left as an exercise. Below we
describe how the operations on a PQ can be implemented in Python:
• new() → : PQ = [].
• insert(item, priority): PQ.append((item, priority)).
• highest(PQ):
PQ[[y for (x,y) in PQ].index(max([y for (x,y)
in PQ]))][0]
• deletehighest(PQ):
del PQ[[y for (x,y) in PQ].index(max([y for (x,y)
in PQ]))].
• isempty(PQ): PQ == [].
The following table lists the complexities of the operations for the Priority
Queue implementation mentioned above:
6.3 Abstract Data Types in Python 189
Note that these complexities only apply for the implementation discussed
above.
Until the next chapter, we will use lists, nested lists or nested dictionaries to
implement the tree ADT (described above) in Python. Below is an example
tree:
be used. For leaf nodes, either one-item lists (i.e., [3]) or a list with empty
branches (e.g., [3, [], []]) could be used.
The example tree in the above figure can be implemented with lists in any of
the following ways:
– [10, [5, [3, [], []], [8, [], []]], [30, [], []]].
– [10, [5, [3, ’#’, ’#’], [8, ’#’, ’#’]], [30, ’#’,
’#’]], where the empty branches are marked with ’#’.
– [10, [5, [3], [8]], [30]].
If your tree does not require modifications (i.e., insertions or deletions of
nodes), then tuples can be used as well. When using lists, one can, of course,
start with an empty tree and then insert nodes and branches one by one.
• Representing Trees with Dictionaries:
Alternatively, you can use the dict type in Python, where each
node is equivalent to a dictionary like: {’value’: <node_value>,
’LeftTree’: <Left_Tree>, ’RightTree’:<Right_Tree>}
Using the dict type, we can represent the example tree (from above) as
follows:
Tree = \
{ ’value’ : 10, \
’left’ : {’value’: 5, \
’left’: {’value’: 3, \
’left’: {}, \
’right’: {}},\
’right’: {’value’: 8, \
’left’: {}, \
’right’: {}}}, \
’right’ : {’value’: 30, \
’left’: {}, \
’right’: {}}\
}
When using dictionaries, you can, of course, start with an empty tree and
then insert nodes and branches one by one.
Assuming that the trees are represented using lists with empty lists as empty
branches (the case with dictionaries are left as an exercise), we will see different
ways of visiting every node in a tree in Python. Since each of these traversals
visit each node once, the overall complexities of these traversals is O(n).
6.3 Abstract Data Types in Python 191
• Pre-order Traversal:
In pre-order traversal, the node is visited (or processed) before its left and
right branches:
1 def p r e o r d e r _ t r a v e r s e ( Tree ) :
2 i f T r e e == [ ] :
3 return
4 value = Tree [ 0 ]
5 print value
6 p r e o r d e r _ t r a v e r s e ( Tree [ 1 ] )
7 p r e o r d e r _ t r a v e r s e ( Tree [ 2 ] )
For the example tree [10, [5, [3, [], []], [8, [], []]], [30,
[], []]], the preorder_traverse would print: 10 5 3 8 30.
• In-order Traversal:
In in-order traversal, the node is visited (or processed) after its left branch
but before its right branch:
1 def i n o r d e r _ t r a v e r s e ( Tree ) :
2 i f T r e e == [ ] :
3 return
4 value = Tree [ 0 ]
5 i n o r d e r _ t r a v e r s e ( Tree [ 1 ] )
6 print value
7 i n o r d e r _ t r a v e r s e ( Tree [ 2 ] )
For the example tree [10, [5, [3, [], []], [8, [], []]], [30,
[], []]], the inorder_traverse would print: 3 5 8 10 30.
• Post-order Traversal:
In post-order traversal, the node is visited (or processed) after its left branch
and right branches:
1 def p o s t o r d e r _ t r a v e r s e ( Tree ) :
2 i f T r e e == [ ] :
3 return
4 value = Tree [ 0 ]
5 p o s t o r d e r _ t r a v e r s e ( Tree [ 1 ] )
6 p o s t o r d e r _ t r a v e r s e ( Tree [ 2 ] )
7 print value
For the example tree [10, [5, [3, [], []], [8, [], []]], [30,
[], []]], the postorder_traverse would print: 3 8 5 30 10.
192 6 Organizing Data
6.4 Keywords
The important concepts that we would like our readers to understand in this chapter
are indicated by the following keywords:
ADT Stacks
Queues Priority Queues
Trees Tree Traversal
Pre-order, In-order, Post-order Traversal Binary Tree
Binary-search Tree Huffman coding
For more information on the topics discussed in this chapter, you can check out the
sources below:
• ADT:
– http://en.wikipedia.org/wiki/Abstract_data_type
• Stack:
– http://en.wikipedia.org/wiki/Stack_%28data_structure%29
• Queue:
– http://en.wikipedia.org/wiki/Queue_%28data_structure%29
• Priority Queue:
– http://en.wikipedia.org/wiki/Priority_queue
• Last-in First-out (LIFO):
– http://en.wikipedia.org/wiki/LIFO_%28computing%29
• First-in First-out (FIFO):
– http://en.wikipedia.org/wiki/FIFO_%28computing%29
• Tree:
– http://en.wikipedia.org/wiki/Tree_%28data_structure%29
– http://xlinux.nist.gov/dads//HTML/tree.html
• Hufman Coding:
– http://en.wikipedia.org/wiki/Huffman_coding
6.6 Exercises
1. Add a length() operation to the formal definition of the stack ADT. Note that
the length of an empty stack is zero. Hint: You can use a recursive rule.
2. Add a reverse() operation to the formal definition of the queue ADT.
3. Is it possible to implement a stack using a queue? If not, why not? If yes, how?
Assume that you can add new operations to the queue ADT.
6.6 Exercises 193
4. Implement a priority queue ADT in Python by keeping the data sorted in such
a way that each front operation can be performed by one access to the list.
5. What is the difference between a bag, a set and a list?
6. Consider an add(item, DQ) function, which returns a DQ with the added item
to the DQ by the infix operation
item ◦ DQ
An empty DQ is represented with ∅. ξ and η represent items that can be added
to that DQ. There is a second constructive operation on a DQ which can be
formalized as:
• inject(ξ, ∅) → ξ ◦ ∅
• inject(ξ, η ◦ DQ) → η ◦ inject(ξ, DQ)
Other operations on a DQ are defined as follows:
• new() → ∅
• pilot(ξ ◦ DQ) → ξ
• remove(ξ ◦ DQ) → DQ
• eject(ξ ◦ ∅) → ∅
• eject(ξ ◦ DQ) → ξ ◦ eject(DQ)
• beaver(ξ ◦ ∅) → ξ
• beaver(ξ ◦ DQ) → beaver(DQ)
• len(∅) → 0
• len(ξ ◦ DQ) → 1 + len(DQ)
What is the result of the functional composition below?
remove(eject(add(1, add(1, (eject(add(0, remove(add(1, add(1, new()))))))))))
7. What is expected to be found in the variable a after the following code exe-
cutes?
a = add(1, add(2, new()))
while len(a) < 6
do
s = beaver(a) ∗ pilot(a) + pilot(a)%beaver(a)
if s%2 == 0 then
a = add(s, a)
else
a = inject(s, a)
done
8. Why do you think Python does not allow mutable data types as key values?
9. Re-write the three tree traversal functions (i.e., preorder_traverse,
inorder_traverse, postorder_traverse) for trees implemented
with dictionaries.
10. Write a Python function depth that takes a tree (in nested-list representation)
as an argument and returns the maximum depth in the tree. Note that the depth
194 6 Organizing Data
of the root node is 0, and the depth of a node other than the root node is one
plus the depth of its parent node.
11. Write a Python function duplicates that takes a tree (in nested-list repre-
sentation) as an argument and returns the list of duplicate nodes (i.e., nodes that
have the same values).
12. Write a Python function flip that takes a tree (in nested-list representation)
as an argument and returns the horizontally flipped version of the tree; in other
words, the mirror image of the tree along a vertical axis is returned as a result
of the flipping operation.
13. Write a Python function that prints the values of the nodes such that a node x
gets printed before another node y only if the level of node x is less than or
equal to the level of node y.
Chapter 7
Objects: Reunion of Data and Action
1 This is certainly not the only solution to information processing. Our brain (i.e., a connectionist
machine, where every function is handled in the connections of identical self-functioning units),
for example, does not make this black and white distinction; you can hardly pinpoint a neuron (nor
a group of them) and claim that item stores the age of your grandmother (and nothing else).
definitions of an entity are made in terms of the entities just one level below (tech-
nically speaking, if the entity is of a tree structure, its definition will refer to its
children only (and not to any further descendants)). Therefore, a battalion can be
modeled in terms of companies and some commanding officers and not in terms of
jeeps and individual platoons (as shown in Fig. 7.1).
Fig. 7.2 Definition of a CAR object and a set of its instances, i.e., CAR objects
This is not the first time that, at the higher levels of programming, we con-
struct some entities which do not exist at lower levels: as introduced in the pre-
vious chapters, functions do not exist at the machine instruction level, for example,
but any high-level programming language allows implementing them. Other exam-
ples include arbitrarily-sized integers, complex numbers or matrices; although they
do not exist at the machine-instruction level, many high level languages provide
them.
The object-oriented programming life is organized around the ‘object’ concept. For
the sake of integrity, modularity and efficiency, there are some properties imposed
on the object, some of which are productive while others are restrictive. Below, we
will investigate these properties in detail, But, before we proceed, there is an aspect
that has to be introduced.
There is a distinction between the definition of an object and the actual creation
of that object for use (see, e.g., Fig. 7.2). The definition of a car object, for example,
includes the properties of a car and the actions that can be performed on its proper-
ties and its sub-entities. Making such a definition of an object resembles drawing the
architectural blueprint of a building. Later, when demanded, one or many buildings
can be manufactured from this blueprint. It is also possible that not a single building
will be constructed from the blueprint, and the blueprint will turn to dust on a shelf.
198 7 Objects: Reunion of Data and Action
2 To make things more complicated, some authors use the word ‘object’ as a synonym for the
‘instance of a class’.
7.2 Properties of Object-Oriented Programming 199
Actually, these concepts do not have a causal connection among themselves. The
fabric of programming, which claims to use the ingredients that resemble (or it is
better to say, are inspired by) the objects of real life, ‘naturally’ brings them together.
7.2.1 Encapsulation
Fig. 7.4 An object (or class) hierarchy tree with the inherited items highlighted
7.2.2 Inheritance
‘Inheritance’ in OOP does not exactly represent the concept used in biology. In
OOP, an object’s definition can be based on an already defined object. Inheritance
is the automatic copying of the data field declarations and method definitions of
the ‘already defined object’ to the newly defined object. Presumably, the newly de-
fined class will have also additional data fields and methods. It is also possible that
some of the method definitions that were copied (inherited) are abandoned and got
redefined.
In inheritance, we can speak of a class hierarchy, which can be displayed as a
tree. At the root is a base class, whose children are the classes that were derived
from it (it is quite possible that more than one class is derived, for which reason
we talk about children and not a single child). Moreover, more new classes can be
derived from the children of the root node. Therefore, we have a tree, called a class
hierarchy tree or a diagram, sometimes, it is also referred to as an inheritance tree.
Figure 7.4 displays an example inheritance tree.3
7.2.3 Polymorphism
The word ‘polymorphism’ stems from the Greek meaning ‘many forms’. In pro-
gramming, polymorphism refers to the fact that a method may have the same name
but perform different actions (i.e., ‘have different forms’) on different object types;
or, alternatively said, there are a bunch of objects but all respond to the same mes-
sage (i.e., method) in their own way. The first thought that comes to mind is the very
reasonable question:
Since the responses are different and the objects are different, why are the
method names the same?
In a sensible use of polymorphism, the shared name will refer to same semantic. For
example, consider that we are constructing a program for drawing and editing planar
geometric shapes such as squares, triangles and circles. In object-oriented program-
ing, these shapes will be internally represented by different objects. We will have a
square object, a triangle object as well as a circle object. Needless to say, each shape
on the screen will be an instance of one of these objects. Naturally, such a program
will provide some common functionalities for all the shapes: Painting a shape with
a color is a good example; moving them on the screen is another. The existence of
polymorphism in OOP allows us to have all those different geometric objects im-
plement a different painting algorithm under the same name: ‘paint’. Therefore, an
OOP programmer is able to send any of the objects the ‘paint’ message, presumably
with the color information attached, and get the shape painted.
The way polymorphism is implemented in a programming language is a delicate
matter since polymorphism is a broad concept and what you can expect from it
depends on how the language implements this concept.
Below are some possible polymorphism expectations of a programmer (and the
answers):
• I want to define a generic class for sorted lists. For the moment, I do not
want to restrict the data type of the elements. In other words, I want to
implement a ‘sorted list of X’ object. Later, when I need a sorted list of
floats, for example, I should be able to use that generic class, specifying
that the undetermined type X is a float and the name of this ‘sorted list
of floats’ class should be sorted_float_list. Parallel to this, I want
7.2 Properties of Object-Oriented Programming 203
1 c l a s s <class_name >:
2 < v a r i a b l e s w i t h v a l u e s > OR < f u n c t i o n d e f i n i t i o n s >
3 < v a r i a b l e s w i t h v a l u e s > OR < f u n c t i o n d e f i n i t i o n s >
4 ...
5 < v a r i a b l e s w i t h v a l u e s > OR < f u n c t i o n d e f i n i t i o n s >
1 c l a s s Person :
2 name = ""
3 a g e = −1
4 def P r i n t ( s e l f ) :
5 p r i n t "Name:" , s e l f . name , " age:" , s e l f . a g e
where name and age are member variables and the function Print is a mem-
ber function of the Person class. When an instance of a class is created, the
object has the member variables and the member functions defined in the class,
as shown below:
>>> mark = Person()
>>> mark
<_ _main_ _.Person instance at 0xb7dd0a8c>
With the line mark = Person(), we have created an instance of the
Person class, i.e., an object, which is assigned to the variable mark. Now,
we can work with the member variables and the member functions:
>>> mark.Print()
Name: age: -1
>>> mark.name = "Mark"
>>> mark.age = 20
>>> mark.Print()
Name: Mark age: 20
7.3 Object-Oriented Programming in Python 205
Objects in Python are mutable; i.e., their content can be changed after they are
created. This allows classes to be defined and the objects created from them
to be modified (i.e., added new member variables or functions) on the run. For
example, consider a modified version of the Person class above:
1 c l a s s Person2 :
2 pass
which now does not have any members. However, we can add new members to
the objects created from them on the run:
>>> mark = Person2()
>>> mark.age
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
AttributeError: Person2 instance has no attribute ’age’
>>> mark.age = 10
>>> mark.age
10
>>> def f():
... print "A humble function at your service"
...
>>> mark.f = f
>>> mark.f()
A humble function at your service
>>> del mark.age
>>> mark.age
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
AttributeError: Person2 instance has no attribute ’age’
As shown in the above example, we can add members to an object and delete
them. However, the modifications on an object are not reflected in the class
itself. In other words, if we create another instance of the Person2 class (name
it jill), jill will only have what is defined in the Person2 class and will
not be affected by the changes in mark.
If you want your modifications to affect all subsequent instances of the class,
you can modify the class directly (considering the empty Person2 definition):
>>> Person2.age = 10
>>> Person2.name = ""
>>> mark = Person2()
>>> mark.age
10
206 7 Objects: Reunion of Data and Action
In most cases, the member functions will change the member variables, make
some calculations on them or just return them. The public member functions
which just return the values of the member variables are called accessors and
the public member functions that modify the member variables (usually, by just
setting them to the parameters) are called modifiers. Accessors and modifiers
allow appropriate access to an object and determine what you can do with the
object.
Whether accessor or modifier, the functions that need to access the member
variables of the object need to have at least one parameter, the first of which
always points to the current object. Using this first parameter, the member vari-
ables and functions of an object can be accessed. Below is a simple example:
1 c l a s s Person3 :
2 ’ ’ ’ D e f i n i t i o n of Person3 :
3 _name and _age a r e p r i v a t e ’ ’ ’
4 _name = ""
5 _ a g e = −1
6 d e f getName ( s e l f ) :
7 r e t u r n s e l f . _name
8
9 d e f setName ( s e l f , new_name ) :
10 s e l f . _name = new_name
11
12 def getAge ( s e l f ) :
13 return s e l f . _age
14
15 d e f s e t A g e ( s e l f , new_age ) :
16 s e l f . _ a g e = new_age
17
18 def g r e e t ( s e l f ) :
19 i f s e l f . _ a g e ! = −1:
20 p r i n t "My name is" , s e l f . _name , ", and I am" , \
21 s e l f . _age , "years old. Nice to meet you."
22 else :
23 p r i n t "<Uninitialized>"
example:
>>> mark = Person3()
>>> mark.greet()
<Uninitialized>
>>> mark.setName("Mark")
>>> mark.setAge(20)
>>> mark.greet()
My name is Mark , and I am 20 years old. Nice to meet you.
A stricter way to make member variables private is to use two leading un-
derscores in their names:
>>> class Person4:
... _ _a = 10
... def f(self):
... print self._ _a
...
>>> mark = Person4()
>>> mark._ _a
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
208 7 Objects: Reunion of Data and Action
where, in fact, Python transforms the name of the variables so that it is a little
bit more difficult to access it.
1 c l a s s Person4 :
2 ’ ’ ’ D e f i n i t i o n of Person4 :
3 _name and _age a r e i n i t i a l i z e d upon c r e a t i o n ’ ’ ’
4
5 d e f _ _ i n i t _ _ ( s e l f , name="" , a g e = −1):
6 s e l f . _name = name
7 s e l f . _age = age
8
9 def g r e e t ( s e l f ) :
10 i f s e l f . _ a g e ! = −1:
11 p r i n t "My name is" , s e l f . _name , ", and I am" , \
12 s e l f . _age , "years old. Nice to meet you."
13 else :
14 p r i n t "<Uninitialized>"
As shown in the example, the _ _init_ _ function has to take a self ar-
gument and any number of additional arguments. It is a good practice to provide
default values for all of the arguments other than the first one.
7.3 Object-Oriented Programming in Python 209
In Python, you can use the following syntax to define new classes inheriting
from others:
1 c l a s s Student ( Person3 ) :
2 ’ ’ ’ S t u d e n t Class : Child Class o f Person4 Class ’ ’ ’
3 _grades = []
4 _ y e a r = −1
5
6 def getGrades ( s e l f ) :
7 return s e l f . _grades
8
9 def s e t G r a d e s ( s e l f , new_grades ) :
10 s e l f . _grades = new_grades
11
12 def getYear ( s e l f ) :
13 return s e l f . _year
14
15 def s e t Y e a r ( s e l f , new_year ) :
16 s e l f . _year = new_year
Note that tom is an instance of the Student class that inherits the member
variables and the functions from the Person3 class.
1 class Father :
2 ’ ’ ’ Parent c l a s s ’ ’ ’
210 7 Objects: Reunion of Data and Action
3
4 d e f humble ( s e l f ) :
5 p r i n t "In father object"
6
7 d e f mumble ( s e l f ) :
8 s e l f . crumble ( )
9
10 c l a s s C h i l d ( F a t h e r ) :
11 ’ ’ ’A d e r i v e d c l a s s t h a t i n h e r i t s from
12 the Father c l a s s ’ ’ ’
13
14 d e f humble ( s e l f ) :
15 p r i n t "In child object"
16 F a t h e r . humble ( s e l f )
17
18 def crumble ( s e l f ) :
19 p r i n t "I crumble"
Consider the Person3 class (the example is valid for other classes, too):
>>> a = Person3()
>>> b = Person3()
>>> a == b
False
>>> c = a
>>> c == a
True
7.3 Object-Oriented Programming in Python 211
In other words, in Python, by default, two objects are equal to each other only
if they share the same data. In the above example, a and b are different by
default although, content-wise, they are the same. If you want to check the
equality based on content, you need to define a new function (as given below)
or overload the equality operator (as shown in the next section):
>>> def Eq(Pers1, Pers2):
... return Pers1.getName() == Pers2.getName() \
... and Pers1.getAge() == Pers2.getAge()
...
>>> a = Person3()
>>> b = Person3()
>>> a == b
False
>>> Eq(a, b)
True
1 c l a s s Person5 :
2 name = ""
3 a g e = 20
4
5 d e f __eq__ ( s e l f , o t h e r ) :
6 r e t u r n s e l f . name == o t h e r . name \
7 and s e l f . a g e == o t h e r . a g e
Objects in Python are very handy with self-referential structures, like Trees.
Below is a simple example:
1 c l a s s Node :
2 ’ ’ ’ Node d e f i n i t i o n o f t h e t r e e ’ ’ ’
3 d e f _ _ i n i t _ _ ( s e l f , v a l u e ="" ) :
4 s e l f . _ l e f t = None
5 s e l f . _ r i g h t = None
6 s e l f . _value = value
7
8 # Left modifier & accessor
9 def g e t L e f t ( s e l f ) :
10 return s e l f . _ l e f t
11 def s e t L e f t ( s e l f , leftNode ) :
12 s e l f . _ l e f t = leftNode
13 # Right modifier & accessor
14 def getRight ( s e l f ) :
15 return s e l f . _ r i g h t
16 def s e t R i g h t ( s e l f , rightNode ) :
17 s e l f . _r ight = rightNode
18 # Value m o d i f i e r & accessor
19 def getValue ( s e l f ) :
20 return s e l f . _value
21 def setValue ( s e l f , value ) :
22 s e l f . _value = value
7.3 Object-Oriented Programming in Python 213
1 def p r e o r d e r _ t r a v e r s e ( Tree ) :
2 print Tree . getValue ( ) ,
3 i f T r e e . g e t L e f t ( ) ! = None :
4 p r e o r d e r _ t r a v e r s e ( Tree . g e t L e f t ( ) )
5 i f T r e e . g e t R i g h t ( ) ! = None :
6 p r e o r d e r _ t r a v e r s e ( Tree . g e t R i g h t ( ) )
You can implement the Stack ADT in Python using objects as follows:
1 class Stack :
2
3 def _ _ i n i t _ _ ( s e l f , items = [ ] ) :
4 s e l f . _items = items
5 def push ( s e l f , item ) :
6 s e l f . _ i t e m s . append ( item )
7 d e f pop ( s e l f ) :
8 r e t u r n s e l f . _ i t e m s . pop ( )
214 7 Objects: Reunion of Data and Action
9 def isempty ( s e l f ) :
10 r e t u r n s e l f . _ i t e m s == [ ]
11 def p r i n t S t a c k ( s e l f ) :
12 p r i n t "Stack:" , s e l f . _ i t e m s
7.4 Keywords
The important concepts that we would like our readers to understand in this chapter
are indicated by the following keywords:
Classes Objects
Data Abstraction Encapsulation
Inheritance Polymorphism
Member Variables Member Functions
Virtual Functions Operator Overloading
For more information on the topics discussed in this chapter, you can check out the
sources below:
• Data Abstraction or Information Hiding:
– http://mitpress.mit.edu/sicp/full-text/sicp/book/node26.html
– http://en.wikipedia.org/wiki/Abstraction_%28computer_science%29
#Abstraction_in_object_oriented_programming
• Encapsulation:
– http://en.wikipedia.org/wiki/Encapsulation_%28object-oriented_
programming%29
7.6 Exercises 215
– http://www.javaworld.com/javaworld/jw-05-2001/jw-0518-encapsulation.
html
• Polymorphism:
– http://en.wikipedia.org/wiki/Polymorphism_in_object-oriented_programming
• Operators and functions that can be overloaded:
– http://docs.python.org/reference/datamodel.html#basic-customization
7.6 Exercises
1. What is the difference between data abstraction and encapsulation?
2. Is the existence of virtual functions and operator overloading sufficient to have
polymorphism in an OOP language?
3. Since objects are mutable, they are affected by the aliasing problem. Write a
member function copy for the Person3 class that copies a given object to the
current instance without being affected by the aliasing problem.
4. Why is it a good idea to write accessors and modifiers for an object?
5. In Python, everything is an object, and objects are mutable but some objects, such
as tuples, strings, numbers are immutable. Define an object of your own which
is not mutable. Hint: look up _ _setattr_ _ and _ _delattr_ _ methods.
6. Define a new object in Python for implementing the Queue ADT.
7. Extend the object-based Tree definition provided above such that a node can have
more than two children.
Index
Conditionals, 99 mantissa, 42
Connectionist machine, 195 warnings, 43
Constructable problems, 123 for, 142
Containers, 50 for statement, 94
continue, 140, 144 Function, 103
count(), 54 arguments, 105
CPU, 2, 3, 35, 73, 195 call by reference, 106
call by sharing, 107
D call by value, 106
Data, 37 function call, 103
basic data, 40 nesting, 112
structured data, 37 parameters, 109
types, 37 passing arguments, 105
Data bus, 2 return value, 109
Debug, 25 Functional programming tools, 113, 143
Debugger, 27 filtering, mapping and reduction, 113
Debugging, 27 list comprehension, 113
def, 108
del, 59 G
Determinism, 98 global, 112, 115
dict, 183–186 Global variables, 112
Discreteness, 98 goto, 139
Disjktra’s algorithm, see Shunting-yard Grafting, 175
algorithm Graphemes, 46
Downsizable problems, 123 Graphical User Interface, 11
Dynamically typed, 62, 63 Greatest common divisor, 133
E H
elif, 101, 102 Hash, 173
else, 101–103, 145 head(), 130
endif, 100 Heap, 182, 183
Errors, 25 Huffman coding, 177
design errors, 27
logic errors, 26 I
run-time errors, 26 if, 103
eval(), 74 if statement, 94, 99, 100
Events, 11 Immutable, 50, 51, 58
Expression, 72, 77 in, 90
extend(), 59 index(), 53
Extent, 62 Indexing containers in Python, 52, 53, 90
in Python, 65 Infix, 80
Inheritance, 10
F input(), 56, 74
Factorial, 122, 123, 133 insert(), 59
False, 49 Instruction, 3
Fetch-Decode-Execute cycle, 3 int, 44
Fibonacci numbers, 131–133 Integers, 40
filter, 113 bignums, 41
Filtering, 113 Interpreter, 19
First-in first-out, FIFO, 168 Interrupt service routine, 73
float, 44 isalpha(), 54
Floating point, 41 isdigit(), 54
exponent, 42 Iteration, 121, 137
IEEE Standard, 41 in Python, 142
Index 219
T Tuples, 50, 54
tail(), 130 Turing Equivalence, 13
Terminating condition, 126 Turing Machine, 1, 97, 98
Testing, 22 Deterministic Turing Machine, 72
black-box testing, 23 Universal Turing Machine, 98
bottom-up testing, 23 Type of objects, 210
dynamic testing, 22
static testing, 22 U
top-down testing, 23 Unicode, 48
white-box testing, 24 Unprintables, 46
Tree, 173, 175, 176 upper(), 54
children, 173
common operations, 175
V
in Python, 189, 190, 212
nodes, 173 values(), 184
properties, 173 Variable, 61, 72
traversal, 190 global variables, 112
types, 176, 182 naming, 61
and-or, 176 naming in Python, 64
binary-search, 182 typing, 62
decision, 176 Von Neumann, 1, 4, 16, 35, 73, 98
dichotomic decision tree, 176
True, 49 W
tuple, 55 while, 142
tuple(), 56 while statement, 94