CS331 - Datastructures and Algorithms

Version 1

Course webpage for CS331

Python Language Introduction

Agenda

  1. Language overview
  2. White space sensitivity
  3. Basic Types and Operations
  4. Statements & Control Structures
  5. Functions
  6. OOP (Classes, Methods, etc.)
  7. Namespaces and scope
  8. Immutable Sequence Types (Strings, Ranges, Tuples)
  9. Mutable data structures: Lists, Sets, Dictionaries
  10. Modules (libraries)

1. Language overview

Note: this is not a language course! Though I’ll cover the important bits of the language (and standard library) that are relevant to class material, I expect you to master the language on your own time.

Python …

  • is interpreted
  • is dynamically-typed (vs. statically typed)
  • is automatically memory-managed
  • supports procedural, object-oriented, imperative and functional programming paradigms
  • is designed (mostly) by one man: Guido van Rossum (aka “benevolent dictator”), and therefore has a fairly opinionated design
  • has a single reference implementation (CPython)
  • version 3 (the most recent version) is not backwards-compatible with version 2, though the latter is still widely used
  • has an interesting programming philosophy: “There should be one — and preferably only one — obvious way to do it.” (a.k.a. the “Pythonic” way) — see The Zen of Python

2. White Space Sensitivity

Python has no beginning/end block markers! Blocks must be correctly indented (4 spaces is the convention) to delineate them.

if True:
    print('In if-clause')
else:
    print('In else-clause')
In if-clause
for x in range(5):
    if x < 3:
        print('In if loop body')
    print('In for loop body')
print("After loop")
In if loop body
In for loop body
In if loop body
In for loop body
In if loop body
In for loop body
In for loop body
In for loop body
After loop
def foo():
    print('In function definition')
foo()
In function definition

3. Basic Types and Operations

In Python, variables do not have types. Values have types (though they are not explicitly declared). A variable can be assigned different types of values over its lifetime.

a = 2 # starts out an integer
print(type(a)) # the `type` function tells us the type of a value

a = 1.5
print(type(a))

a = 'hello'
print(type(a))
<class 'int'>
<class 'float'>
<class 'str'>

Note that all the types reported are classes. I.e., even types we are accustomed to thinking of as “primitives” (e.g., integers in Java) are actually instances of classes. All values in Python are objects!

There is no dichotomy between “primitive” and “reference” types in Python. All variables in Python store references to objects.

Numbers

# int: integers, unlimited precision
print(1)
print(500)
print(-123456789)
print(6598293784982739874982734)
1
500
-123456789
6598293784982739874982734
# basic operations
print(1 + 2)
print(1 - 2)
print(2 * 3)
print(2 * 3 + 2 * 4)
print(2 / 5)
print(2 ** 3) # exponentiation
print(abs(-25))
3
-1
6
14
0.4
8
25
# modulus (remainder) and integer division
print(10 % 3)
print(10 // 3)
print(11 // 3)
1
3
3
# floating point is based on the IEEE double-precision standard (limit to precision!)
print(2.5)
print(-3.14159265358924352345)
print(1.000000000000000000000001)
2.5
-3.1415926535892433
1.0
# mixed arithmetic "widens" ints to floats
print(3 * 2.5)
print(1 / 0.3)
7.5
3.3333333333333335

Booleans

print(True)
print(False)
True
False
print(not True)
print(not False)
print(not not (True))
False
True
True
print(True and True)
print(False and True)
print(True and False)
print(False and False)
True
False
False
False
print(True or True)
print(False or True)
print(True or False)
print(False or False)
True
True
True
False
# relational operators
print(1 == 1)
print(1 != 2)
print(1 < 2)
print(1 <= 1)
print(1 > 0)
print(1 >= 1)
print(1.0 == 1)
print(1.0000000000000000001 == 1)
print(type(1) == type(1.0))
print(type(1))
print(type(1.0))
True
True
True
True
True
True
True
True
False
<class 'int'>
<class 'float'>
# object identity (reference) testing
x = 1000
y = 1000
print(x == x)
print(x is x)
print(x is not x)
True
True
False
print(x == y)
print(x is y)
print(x is not y)
y = 1001
print(x == y)
True
False
True
False
# but Python caches small integers! so ...
x = 5
y = 5
print(x == y)
print(x is y)
True
True

Strings

# whatever strings you want
print('hello world!')
print("hello world!")
hello world!
hello world!
# convenient for strings with quotes:
print('she said, "how are you?"')
print("that's right!")
she said, "how are you?"
that's right!
print('hello' + ' ' + 'world')
print('hello world')
print('thinking... ' * 4)
print('*' * 80)
hello world
hello world
thinking... thinking... thinking... thinking...
********************************************************************************

Strings are an example of a sequence type; https://docs.ipython.org/3.5/library/stdtypes.html#typesseq

Other sequence types are: ranges, tuples (both also immutable), and lists (mutable).

All immutable sequences support the common sequence operations, and mutable sequences additionally support the mutable sequence operations

# indexing
greeting = 'hello there'
print(greeting[0])
print(greeting[6])
print(len(greeting))
print(greeting[len(greeting)-1])
h
t
11
e
# negative indexes
print(greeting[-1])
print(greeting[-2])
print(greeting[-len(greeting)])
e
r
h
# "slices"
print(greeting[0:11])
print(greeting[0:5])
print(greeting[6:11])
hello there
hello
there
# default slice ranges
print(greeting[:11])
print(greeting[6:])
print(greeting[:])
hello there
there
hello there
# slice "steps"
print(greeting[::2])
print(greeting[::3])
print(greeting[6:11:2])
hlotee
hltr
tee
# negative steps
print(greeting[::-1])
ereht olleh
# other sequence ops
print(greeting)
print(greeting.count('e'))
print(greeting.index('e'))
print(greeting.index('e', 2))
print('e' in greeting)
print('z' not in greeting)
print(min(greeting))
print(max(greeting))
print(min('bdca'))
hello there
3
1
8
True
True

t
a

Strings also support a large number of type-specific methods.

name = 'Peter'
address = '10 W 31st, Chicago'
print('Person: ' + name + ' lives at: ' + address)
print(f"Person: {name} lives at: {address}")
print(f"Initial: {name[0]}")
Person: Peter lives at: 10 W 31st, Chicago
Person: Peter lives at: 10 W 31st, Chicago
Initial: P

Type “Conversions”

Constructors for most built-in types exist that create values of those types from other types:

public class x {

    public static void main(String[] args) {
        int a = 3; // type variablename assignment
        a = (int) 3.0;
        System.out.println("" + a);
    }
}
3
a = 3;
print(type(a))
a = 'str'
print(type(a))
<class 'int'>
<class 'str'>
# making ints
print(int('123'))
print(int(12.5))
print(int(True))
print(int(False))

# floats
print(float('123.123'))

# strings
print(str(123))
123
12
1
0
123.123
123

Operators/Functions as syntactic sugar for special methods

print(5 + 6)
print((5).__add__(6))
11
11
class MyInt(int):
    def __add__(self, other):
        return self * other
a = MyInt(5)
b = MyInt(6)
print(a + b)
30
print(abs(-2.8))
print((-2.8).__abs__())
2.8
2.8
print('hello' + ' ' + 'world')
print('hello'.__add__(' ').__add__('world'))
hello world
hello world

None

None is like “null” in other languages

# often use as a default, initial, or "sentinel" value

x = None
print(x)
None

note: notebooks do not display the result of expressions that evaluate to None

print(None)
None

“Truthiness”

All objects in Ipython can be evaluated in a Boolean context (e.g., as the condition for an if statement). Values for most types act as True, but some act (conveniently, usually) as False.

if True: # try numbers, strings, other values here
    print('tests as True')
else:
    print('tests as False')
tests as True

What tests as False?

class MyBool(int):
    def __bool__(self):
        return self != 0
x = MyBool(1)
if x:
    print("1 is true")

x = MyBool(0)
if not x:
    print("0 is false")

x = MyBool(2)
if x:
    print("2 is true")
1 is true
0 is false
2 is true

4. Statements & Control Structures

Assignment

# simple, single target assignment

a = 0
b = 'hello'
print(f"{a} {b}")
0 hello
# can also assign to target "lists"

a, b, c = 0, 'hello', True
print(f"{a} {b} {c}")
0 hello True
# note: expression on right is fully evaluated, then are assigned to
#       elements in the "target" list, from left to right

x, y, z = 1, 2, 3
x, y, z = x+y, y+z, x+y+z
print(f"{x} {y} {z}")
3 5 6
# easy python "swap"

a, b = 'apples', 'bananas'
a, b = b, a
print(f"{a} {b}")
bananas apples
# note: order matters!

a, b, a = 1, 2, 3
#a = 1
#b = 2
#a = 3
print(f"{a} {b}")
3 2
# can also have multiple assignments in a row -- consistent with
# above: expression is evaluated first, then assigned to all targets
# from left to right (note: order matters!)

x = y = z = None
print(f"{x} {y} {z}")
None None None

Augmented assignment

a = 0  # a= 0
a += 2 # a = 2
a *= 3 # a = 6
print(a)
6

pass

pass is the “do nothing” statement

pass
def foo():
    pass
foo()
def foo():
    print("a")
foo()
a

if-else statements

from random import randint
score = randint(50, 100)
grade = None
if score >= 90:
    grade = 'A'
elif score >= 80:
    grade = 'B'
elif score >= 70:
    grade = 'C'
elif score >= 60:
    grade = 'D'
else:
    grade = 'E'

print(score, grade)
95 A

while loops

f0 = 0
f1 = 1
while f0 < 100:
    print(f0)
    f0, f1 = f1, f0+f1
0
1
1
2
3
5
8
13
21
34
55
89
i = 0
to_find = 4
while i < 5:
    i += 1
    if i == to_find:
        print('Found; breaking early')
        break # stop a loop early
    else:
        print('Not found; terminated loop')
Not found; terminated loop
Not found; terminated loop
Not found; terminated loop
Found; breaking early
i = 0
to_find = 10
while i < 100:
    i += 1
    if i == to_find:
        print('Found; breaking early')
        break
    else:
        print('Not found; terminated loop')
Not found; terminated loop
Not found; terminated loop
Not found; terminated loop
Not found; terminated loop
Not found; terminated loop
Not found; terminated loop
Not found; terminated loop
Not found; terminated loop
Not found; terminated loop
Found; breaking early

Exception Handling

# raise Exception('Boommmmmm!')

:results: nil:END:

# raise NotImplementedError()

:results: nil:END:

try:
    raise Exception('Boom')
except:
    print('Exception encountered!')
Exception encountered!
try:
    raise ArithmeticError('Eeek!')
except LookupError as e:
    print('LookupError:', e)
except ArithmeticError as e:
    print('ArithmeticError:', e)
except Exception as e:
    print(f'Just an Exception: {e}')
finally:
    print('Done')
ArithmeticError: Eeek!
Done

for loops (iteration)

print(range(10))
range(0, 10)
for x in range(10):
    print(x)
0
1
2
3
4
5
6
7
8
9
for i in range(9, 81, 9):
    print(i)
9
18
27
36
45
54
63
72
for c in 'hello world':
    print(c)
h
e
l
l
o

w
o
r
l
d
to_find = 50
for i in range(100):
    if i == to_find:
        break
    else:
        print('Completed loop')
Completed loop
Completed loop
Completed loop
Completed loop
Completed loop
Completed loop
Completed loop
Completed loop
Completed loop
Completed loop
Completed loop
Completed loop
Completed loop
Completed loop
Completed loop
Completed loop
Completed loop
Completed loop
Completed loop
Completed loop
Completed loop
Completed loop
Completed loop
Completed loop
Completed loop
Completed loop
Completed loop
Completed loop
Completed loop
Completed loop
Completed loop
Completed loop
Completed loop
Completed loop
Completed loop
Completed loop
Completed loop
Completed loop
Completed loop
Completed loop
Completed loop
Completed loop
Completed loop
Completed loop
Completed loop
Completed loop
Completed loop
Completed loop
Completed loop
Completed loop

Generalized iteration (iter and next)

r = range(10)
it = iter(r)
print(type(it))
<class 'range_iterator'>
print(next(it))
0
it = iter(r)
while True:
    try:
        x = next(it)
        print(x)
    except StopIteration:
        break
0
1
2
3
4
5
6
7
8
9
it = iter(r)
while True:
    try:
        x = next(it)
        y = next(it)
        print(x, y, x+y)
    except StopIteration:
        break
0 1 1
2 3 5
4 5 9
6 7 13
8 9 17

5. Functions

def foo():
    pass

:results: nil:END:

def mymulti():
    return 1,2,3
print(mymulti())
(1, 2, 3)
import math

def quadratic_roots(a, b, c):
    disc = b**2-4*a*c
    if disc < 0:
        return None
    else:
        return (-b+math.sqrt(disc))/(2*a), (-b-math.sqrt(disc))/(2*a)
print(quadratic_roots(1, -5, 6)) # eq = (x-3)(x-2)
(3.0, 2.0)
import math

def quadratic_roots(a: int, b: int, c: int):
    disc = b**2-4*a*c
    if disc < 0:
        return None
    else:
        return (-b+math.sqrt(disc))/(2*a), (-b-math.sqrt(disc))/(2*a)
try:
    print(quadratic_roots('asdsad', -5, 6)) # eq = (x-3)(x-2)
except Exception as e:
    print(e)
unsupported operand type(s) for -: 'int' and 'str'
print(quadratic_roots(a=1, b=-5, c=6))
(3.0, 2.0)
print(quadratic_roots(c=6, a=1, b=-5))
(3.0, 2.0)
def create_character(name, race, hitpoints, ability):
    print('Name:', name)
    print('Race:', race)
    print('Hitpoints:', hitpoints)
    print('Ability:', ability)
create_character('Legolas', 'Elf', 100, 'Archery')
Name: Legolas
Race: Elf
Hitpoints: 100
Ability: Archery
def create_character(name, race='Human', hitpoints=100, ability=None):
    print('Name:', name)
    print('Race:', race)
    print('Hitpoints:', hitpoints)
    if ability:
        print('Ability:', ability)
create_character('Michael')
Name: Michael
Race: Human
Hitpoints: 100
def create_character(name, race='Human', hitpoints=100, abilities=()):
    print('Name:', name)
    print('Race:', race)
    print('Hitpoints:', hitpoints)
    if abilities:
        print('Abilities:')
        for ability in abilities:
            print('  -', ability)
create_character('Gimli', race='Dwarf', abilities=("Dig", "small"))
Name: Gimli
Race: Dwarf
Hitpoints: 100
Abilities:
  - Dig
  - small
create_character('Gandalf', hitpoints=1000)
Name: Gandalf
Race: Human
Hitpoints: 1000
create_character('Aragorn', abilities=('Swording', 'Healing'))
Name: Aragorn
Race: Human
Hitpoints: 100
Abilities:
  - Swording
  - Healing
def create_character(name, *abilities, race='Human', hitpoints=100):
    print('Name:', name)
    print('Race:', race)
    print('Hitpoints:', hitpoints)
    if abilities:
        print('Abilities:')
        for ability in abilities:
            print('  -', ability)
create_character('Michael')
Name: Michael
Race: Human
Hitpoints: 100
create_character('Michael', 'Coding', 'Teaching', 'Sleeping', 'Eat', hitpoints=25, )
Name: Michael
Race: Human
Hitpoints: 25
Abilities:
  - Coding
  - Teaching
  - Sleeping
  - Eat

Functions as Objects

def foo():
    print('Foo called')

bar = foo
bar()
Foo called
def foo(f):
    f()

def bar():
    print('Bar called')

foo(bar)
Bar called
foo = lambda: print('Anonymous function called')

foo()
Anonymous function called
f = lambda x,y: x+y

f(1,2)
def my_map(f, it):
    for x in it:
        print(f(x))
print(my_map(lambda x: x*2, range(1,10)))
2
4
6
8
10
12
14
16
18
None
for x in map(lambda x: x*2, range(1,10)):
    print(x)
2
4
6
8
10
12
14
16
18
def foo():
    print('Foo called')

print(type(foo))
<class 'function'>
print(dir(foo))
['__annotations__', '__call__', '__class__', '__closure__', '__code__', '__defaults__', '__delattr__', '__dict__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__get__', '__getattribute__', '__globals__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__kwdefaults__', '__le__', '__lt__', '__module__', '__name__', '__ne__', '__new__', '__qualname__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__']
foo.__call__()
Foo called
print(foo.__str__())
<function foo at 0x10afe8290>

6. OOP (Classes, Methods, etc.)

class Foo:
    pass
print(type(Foo))
<class 'type'>
print(Foo())
<__main__.Foo object at 0x10c801c50>
print(type(Foo()))
<class '__main__.Foo'>
print(__name__) # name of the current "module" (for this notebook)
__main__
print(globals().keys()) # symbol table of the current module
dict_keys(['__name__', '__doc__', '__package__', '__loader__', '__spec__', '__builtin__', '__builtins__', '_ih', '_oh', '_dh', 'In', 'Out', 'get_ipython', 'exit', 'quit', '_', '__', '___', '_i', '_ii', '_iii', '_i1', 'l', '_i2', '_i3', '_i4', '_i5', '_i6', '_i7', '_i8', '_i9', '_i10', '_i11', '_i12', '_i13', '_i14', '_i15', '_i16', '_i17', '_i18', '_i19', '_i20', 'result', 'x', '_i21', '_i22', '_i23', 'adjs', 'nouns', '_i24', 'n', '_i25', 's', '_i26', '_i27', 't', '_i28', '_i29', '_i30', '_i31', '_i32', 'stri', '_i33', 'chars', '_i34', '_i35', '_i36', '_i37', '_i38', '_i39', '_i40', '_i41', '_i42', '_i43', 'u', '_i44', '_i45', '_i46', '_i47', '_47', '_i48', '_i49', 'mysubseteq', '_i50', '_i51', 'setop_subseteq', '_i52', '_i53', '_i54', 'd', '_i55', '_i56', '_i57', '_57', '_i58', '_i59', '_59', '_i60', '_i61', 'sentence', '_61', '_i62', '_i63', 'math', '_i64', 'trunc', 'pi', '_i65', 'm', '_i66', '_i67', '_i68', 'foo', '_i69', 'a', '_i70', '_i71', '_i72', '_i73', '_i74', '_i75', '_i76', '_i77', '_i78', '_i79', '_i80', 'y', '_i81', '_i82', '_i83', '_i84', '_i85', '_i86', 'greeting', '_i87', '_i88', '_i89', '_i90', '_i91', '_i92', '_i93', 'name', 'address', '_i94', '_i95', '_i96', '_i97', 'MyInt', '_i98', 'b', '_i99', '_i100', '_i101', '_i102', '_i103', '_i104', 'MyBool', '_i105', '_i106', '_i107', 'c', '_i108', 'z', '_i109', '_i110', '_i111', '_i112', '_i113', '_i114', '_i115', '_i116', 'randint', 'score', 'grade', '_i117', 'f0', 'f1', '_i118', 'i', 'to_find', '_i119', '_i120', '_i121', '_i122', '_i123', '_i124', '_i125', '_i126', '_i127', '_i128', '_i129', 'r', 'it', '_i130', '_i131', '_i132', '_i133', '_i134', '_i135', 'mymulti', '_i136', 'quadratic_roots', '_i137', '_i138', '_i139', '_i140', '_i141', '_i142', 'create_character', '_i143', '_i144', '_i145', '_i146', '_i147', '_i148', '_i149', '_i150', '_i151', '_i152', '_i153', 'bar', '_i154', '_i155', '_i156', 'f', '_156', '_i157', 'my_map', '_i158', '_i159', '_i160', '_i161', '_i162', '_i163', '_i164', 'Foo', '_i165', '_i166', '_i167', '_i168', '_i169'])
import sys
m = sys.modules['__main__'] # explicitly accessing the __main__b module
print(dir(m))
['Foo', 'In', 'MyBool', 'MyInt', 'Out', '_', '_156', '_47', '_57', '_59', '_61', '__', '___', '__builtin__', '__builtins__', '__doc__', '__loader__', '__name__', '__package__', '__spec__', '_dh', '_i', '_i1', '_i10', '_i100', '_i101', '_i102', '_i103', '_i104', '_i105', '_i106', '_i107', '_i108', '_i109', '_i11', '_i110', '_i111', '_i112', '_i113', '_i114', '_i115', '_i116', '_i117', '_i118', '_i119', '_i12', '_i120', '_i121', '_i122', '_i123', '_i124', '_i125', '_i126', '_i127', '_i128', '_i129', '_i13', '_i130', '_i131', '_i132', '_i133', '_i134', '_i135', '_i136', '_i137', '_i138', '_i139', '_i14', '_i140', '_i141', '_i142', '_i143', '_i144', '_i145', '_i146', '_i147', '_i148', '_i149', '_i15', '_i150', '_i151', '_i152', '_i153', '_i154', '_i155', '_i156', '_i157', '_i158', '_i159', '_i16', '_i160', '_i161', '_i162', '_i163', '_i164', '_i165', '_i166', '_i167', '_i168', '_i169', '_i17', '_i170', '_i18', '_i19', '_i2', '_i20', '_i21', '_i22', '_i23', '_i24', '_i25', '_i26', '_i27', '_i28', '_i29', '_i3', '_i30', '_i31', '_i32', '_i33', '_i34', '_i35', '_i36', '_i37', '_i38', '_i39', '_i4', '_i40', '_i41', '_i42', '_i43', '_i44', '_i45', '_i46', '_i47', '_i48', '_i49', '_i5', '_i50', '_i51', '_i52', '_i53', '_i54', '_i55', '_i56', '_i57', '_i58', '_i59', '_i6', '_i60', '_i61', '_i62', '_i63', '_i64', '_i65', '_i66', '_i67', '_i68', '_i69', '_i7', '_i70', '_i71', '_i72', '_i73', '_i74', '_i75', '_i76', '_i77', '_i78', '_i79', '_i8', '_i80', '_i81', '_i82', '_i83', '_i84', '_i85', '_i86', '_i87', '_i88', '_i89', '_i9', '_i90', '_i91', '_i92', '_i93', '_i94', '_i95', '_i96', '_i97', '_i98', '_i99', '_ih', '_ii', '_iii', '_oh', 'a', 'address', 'adjs', 'b', 'bar', 'c', 'chars', 'create_character', 'd', 'exit', 'f', 'f0', 'f1', 'foo', 'get_ipython', 'grade', 'greeting', 'i', 'it', 'l', 'm', 'math', 'my_map', 'mymulti', 'mysubseteq', 'n', 'name', 'nouns', 'pi', 'quadratic_roots', 'quit', 'r', 'randint', 'result', 's', 'score', 'sentence', 'setop_subseteq', 'stri', 'sys', 't', 'to_find', 'trunc', 'u', 'x', 'y', 'z']
print(m.Foo())
<__main__.Foo object at 0x10af1fc90>
f = Foo()
print(f)
<__main__.Foo object at 0x10af2bed0>
f.x = 100
f.y = 50
print(f.x + f.y)
150
g = Foo()
# g.x
class Foo:
    def bar():
        print('Bar called')
print(type(Foo.bar))
<class 'function'>
f = Foo()
print(type(f.bar))
<class 'method'>
print(Foo.bar())
Bar called
None
try:
    f.bar()
except Exception as e:
    print(e)
bar() takes 0 positional arguments but 1 was given
class Foo:
    def bar(x):
        print('Bar called with', x)
Foo.bar('test')
Bar called with test
f = Foo()
f.bar()
print(f)
Bar called with <__main__.Foo object at 0x10afe4b90>
<__main__.Foo object at 0x10afe4b90>
class Foo:
    def bar(self):
        self.x = 'Some value'
f = Foo()
f.bar()
print(f.x)
Some value
class Shape:
    def __init__(self, name):
        self.name = name

    def __repr__(self):
        return self.name

    def __str__(self):
        return self.name.upper()

    def area(self):
        raise NotImplementedError()
s = Shape('circle')
print(s) # s.__str__()
CIRCLE
print(str(s))
CIRCLE
try:
    print(s.area())
except NotImplementedError as e:
    print(e)

class Circle(Shape):
    def __init__(self, radius):
        super().__init__('circle')
        self.radius = radius

    def area(self):
        return 3.14 * self.radius ** 2
c = Circle(5.0)
print(c.__repr__())
print(c.area())
circle
78.5
class Circle(Shape):
    def __init__(self, radius):
        super().__init__('circle')
        self.radius = radius

    def area(self):
        return 3.14 * self.radius ** 2

    def __eq__(self, other):
        return isinstance(other, Circle) and self.radius == other.radius

    def __add__(self, other):
        return Circle(self.radius + other.radius)

    def __repr__(self):
        return 'Circle(r={})'.format(self.radius)

    def __str__(self):
        return self.__repr__()
c1 = Circle(2.0)
c2 = Circle(4.0)
c3 = Circle(2.0)

print(c1, c2, c3)
print(c1 == c2)
print(c1 == c3)
print(c1 + c2)
Circle(r=2.0) Circle(r=4.0) Circle(r=2.0)
False
True
Circle(r=6.0)

7. Namespaces and Scope

  • Python organizes identifiers in namespaces:
    • Built-In: reserved for build-in python
    • Global: global to a file / module (more on that later)
    • Enclosing: class and function definitions can be nested resulting in a hierarchy of enclosing namespaces
    • Local: this is a namespace local to a function or class definition

You can list which symbols are defined in a namespace using dir.

def f():
    """
This this the documentation of f.
    """
    pass
print(dir(f))
print(f.__doc__)
help(f)
['__annotations__', '__call__', '__class__', '__closure__', '__code__', '__defaults__', '__delattr__', '__dict__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__get__', '__getattribute__', '__globals__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__kwdefaults__', '__le__', '__lt__', '__module__', '__name__', '__ne__', '__new__', '__qualname__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__']

This this the documentation of f.

Help on function f in module __main__:

f()
    This this the documentation of f.

The scope of a symbol is the part of the code where it is visible. A more detailed description can be found here: https://docs.python.org/3/tutorial/classes.html

build-in namespace

print(dir(__builtins__))
['ArithmeticError', 'AssertionError', 'AttributeError', 'BaseException', 'BlockingIOError', 'BrokenPipeError', 'BufferError', 'BytesWarning', 'ChildProcessError', 'ConnectionAbortedError', 'ConnectionError', 'ConnectionRefusedError', 'ConnectionResetError', 'DeprecationWarning', 'EOFError', 'Ellipsis', 'EnvironmentError', 'Exception', 'False', 'FileExistsError', 'FileNotFoundError', 'FloatingPointError', 'FutureWarning', 'GeneratorExit', 'IOError', 'ImportError', 'ImportWarning', 'IndentationError', 'IndexError', 'InterruptedError', 'IsADirectoryError', 'KeyError', 'KeyboardInterrupt', 'LookupError', 'MemoryError', 'ModuleNotFoundError', 'NameError', 'None', 'NotADirectoryError', 'NotImplemented', 'NotImplementedError', 'OSError', 'OverflowError', 'PendingDeprecationWarning', 'PermissionError', 'ProcessLookupError', 'RecursionError', 'ReferenceError', 'ResourceWarning', 'RuntimeError', 'RuntimeWarning', 'StopAsyncIteration', 'StopIteration', 'SyntaxError', 'SyntaxWarning', 'SystemError', 'SystemExit', 'TabError', 'TimeoutError', 'True', 'TypeError', 'UnboundLocalError', 'UnicodeDecodeError', 'UnicodeEncodeError', 'UnicodeError', 'UnicodeTranslateError', 'UnicodeWarning', 'UserWarning', 'ValueError', 'Warning', 'ZeroDivisionError', '__IPYTHON__', '__build_class__', '__debug__', '__doc__', '__import__', '__loader__', '__name__', '__package__', '__spec__', 'abs', 'all', 'any', 'ascii', 'bin', 'bool', 'breakpoint', 'bytearray', 'bytes', 'callable', 'chr', 'classmethod', 'compile', 'complex', 'copyright', 'credits', 'delattr', 'dict', 'dir', 'display', 'divmod', 'enumerate', 'eval', 'exec', 'filter', 'float', 'format', 'frozenset', 'get_ipython', 'getattr', 'globals', 'hasattr', 'hash', 'help', 'hex', 'id', 'input', 'int', 'isinstance', 'issubclass', 'iter', 'len', 'license', 'list', 'locals', 'map', 'max', 'memoryview', 'min', 'next', 'object', 'oct', 'open', 'ord', 'pow', 'print', 'property', 'range', 'repr', 'reversed', 'round', 'set', 'setattr', 'slice', 'sorted', 'staticmethod', 'str', 'sum', 'super', 'tuple', 'type', 'vars', 'zip']

global namespace

x = 'I am global!'
def f():
    print(x)
print(x)
f()
I am global!
I am global!

local namespace

def f():
    y = "I am in local namespace"
    print(y)
f()
I am in local namespace

enclosing namespace

x = "I am a global x"

def outerMost():
    def slightlyOuter():
        def inner():
            x = "I am inners's x"
            print(x)

        x = "I am slightlyOuter's x"
        print(x)
        inner()
    x = "I am outerMost's x"
    print(x)
    slightlyOuter()

print(x)
outerMost()
I am a global x
I am outerMost's x
I am slightlyOuter's x
I am inners's x

Scope

Python searches for a symbol in the order shown below and the first symbol found is used

  • local namespace

  • enclosing namespace (inner to outer)

  • global

  • built-in

      x = "I am a global x"
    
      def outerMost():
          def slightlyOuter():
              def inner():
                  z = "I am inners's x"
                  print(x)
    
              y = "I am slightlyOuter's x"
              print(x)
              inner()
          x = "I am outerMost's x"
          print(x)
          slightlyOuter()
    
      print(x)
      outerMost()
    
    I am a global x
    I am outerMost's x
    I am outerMost's x
    I am outerMost's x
    

This means that, e.g., local variables hide enclosing variables and enclosing variables hide global variables. Since in python variables can be assigned to without having to declare them, this means without some extra syntax it is not possible to assign to a variable from some outer scope.

x = None

def f():
    x = 'test' # this is not assigning a new value to the global x, but creates a new x in the namespace of f

f()
print(x) # This is still the global x
None

So how can we assign to variables from an enclosing or global scope? Python provides keywords global and nonlocal to enable this.

x = None

def f():
    global x
    x = 'test' # this is assigning to the global x

f()
print(x) # This is not the global x
test

class namespaces

Objects of a class have their own namespace. As discussed before instance methods of a class have an explicit parameter self through which instance methods and fields can be accessed. Furthermore, variables declared in the scope of the class, but outside the scope of any method of the class are global to the class (not specific to any particular instance) and are referenced through Class.field.

x = 'global x'

class a:
    x = "a's x"

    def __init__(self):
        self.x = "object's x"

    @staticmethod
    def staticMethod():
        print(f"static method: x = {x}")
        print(f"static method: a.x = {a.x}")

    def f(self):
        print(f"self.x = {self.x}")
        print(f"x = {x}")
        print(f"a.x = {a.x}")

o = a()
o.f()
a.staticMethod()
self.x = object's x
x = global x
a.x = a's x
static method: x = global x
static method: a.x = a's x

8. Immutable Sequence Types: Strings, Ranges, Tuples

Recall: All immutable sequences support the common sequence operations. For many sequence types, there are constructors that allow us to create them from other sequence types.

Strings

s = 'hello'
print(s)
hello
print(s[0])
print(s[1:3])
print('e' in s)
print(s + s)
h
el
True
hellohello
try:
    s[0] = 'j'
except TypeError as e:
    print(e)
'str' object does not support item assignment
s = 'hello'
t = s
s += s # not mutating the string!
print(s)
print(t)
hellohello
hello

Ranges

r = range(150, 10, -8)
print(r)
range(150, 10, -8)
print(r[2])
print(r[3:7])
print(94 in r)
134
range(126, 94, -8)
True

Tuples

a = ()
print(a)
()
print((1, 2, 3))
(1, 2, 3)
print(('a', 10, False, 'hello'))
('a', 10, False, 'hello')
print(tuple(range(10)))
(0, 1, 2, 3, 4, 5, 6, 7, 8, 9)
print(tuple('hello'))
('h', 'e', 'l', 'l', 'o')
t = tuple('hello')
print('e' in t)
print(t[::-1])
print(t * 3)
print((1,2) * 2)
True
('o', 'l', 'l', 'e', 'h')
('h', 'e', 'l', 'l', 'o', 'h', 'e', 'l', 'l', 'o', 'h', 'e', 'l', 'l', 'o')
(1, 2, 1, 2)

9. Mutable data structures: Lists, Sets, Dicts

Lists

This list supports the mutable sequence operations in addition to the common sequence operations.

l = [1, 2, 1, 1, 2, 3, 3, 1]
print(l)
[1, 2, 1, 1, 2, 3, 3, 1]
print(len(l))
8
print(l[5])
3
print(l[1:-1])
[2, 1, 1, 2, 3, 3]
print(l + ['hello', 'world'])
[1, 2, 1, 1, 2, 3, 3, 1, 'hello', 'world']
print(l) # `+` does *not* mutate the list!
[1, 2, 1, 1, 2, 3, 3, 1]
print(l * 3)
print(l)
[1, 2, 1, 1, 2, 3, 3, 1, 1, 2, 1, 1, 2, 3, 3, 1, 1, 2, 1, 1, 2, 3, 3, 1]
[1, 2, 1, 1, 2, 3, 3, 1]
sum = 0
for x in l:
    sum += x
print(sum)
14

Mutable list operations

l = list('hell')
print(l)
['h', 'e', 'l', 'l']
l.append('o')
l.append('!')
print(l)
['h', 'e', 'l', 'l', 'o', '!']
print(l)
['h', 'e', 'l', 'l', 'o', '!']
l.append(' there')
print(l)
['h', 'e', 'l', 'l', 'o', '!', ' there']
del l[-1]
print(l)
['h', 'e', 'l', 'l', 'o', '!']
l.extend(' there')
print(l)
['h', 'e', 'l', 'l', 'o', '!', ' ', 't', 'h', 'e', 'r', 'e']
print(l)
['h', 'e', 'l', 'l', 'o', '!', ' ', 't', 'h', 'e', 'r', 'e']
print(l[2:7])
['l', 'l', 'o', '!', ' ']
del l[2:7]
print(l)
['h', 'e', 't', 'h', 'e', 'r', 'e']
print(l)
['h', 'e', 't', 'h', 'e', 'r', 'e']
print(l[:])
['h', 'e', 't', 'h', 'e', 'r', 'e']

List comprehensions

  • comprehension syntax: [ expr(x) for x in SEQUENCE]

      print([x for x in range(11)])
      # is equivalent
      result = []
      for x in range(11):
          result.append(x)
      print(result)
    
    [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
    [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
    
  print([2*x+1 for x in range(10)]) # odd numbers
[1, 3, 5, 7, 9, 11, 13, 15, 17, 19]
  adjs = ('hot', 'blue', 'quick')
  nouns = ('table', 'fox', 'sky')
  print([adj + ' ' + noun for adj in adjs for noun in nouns])
['hot table', 'hot fox', 'hot sky', 'blue table', 'blue fox', 'blue sky', 'quick table', 'quick fox', 'quick sky']
  # pythagorean triples
  n = 50
  print([(a,b,c) for a in range(1,n)
             for b in range(a,n)
             for c in range(b,n)
             if a**2 + b**2 == c**2])
[(3, 4, 5), (5, 12, 13), (6, 8, 10), (7, 24, 25), (8, 15, 17), (9, 12, 15), (9, 40, 41), (10, 24, 26), (12, 16, 20), (12, 35, 37), (15, 20, 25), (15, 36, 39), (16, 30, 34), (18, 24, 30), (20, 21, 29), (21, 28, 35), (24, 32, 40), (27, 36, 45)]

Sets

A set is a data structure that represents an unordered collection of unique objects (like the mathematical set).

s = {1, 2, 1, 1, 2, 3, 3, 1}
print(s)
{1, 2, 3}
t = {2, 3, 4, 5}
print(t)
{2, 3, 4, 5}
print(s.union(t))
{1, 2, 3, 4, 5}
print(s.difference(t))
{1}
print(s.intersection(t))
{2, 3}
stri = 'Hello World, this is CS331!'
chars = set(stri)
print(chars)
print('H' in chars)
print('O' in chars)
print('O' not in chars)
print({ 1, 2, 3 }.union({4, 5}))
{'l', 't', 'W', '!', 'H', 'e', 's', ',', 'h', 'r', 'C', 'o', ' ', '3', 'd', 'i', 'S', '1'}
True
False
True
{1, 2, 3, 4, 5}
stri = 'Hello World, this is CS331!'
chars = set(stri)
chars.add('O')
print(chars)
{'t', 'o', 'd', 'O', 'l', 'W', 's', 'r', ' ', '3', 'S', 'i', '1', 'H', '!', 'e', ',', 'h', 'C'}
s = { 1, 2, 3 }
t = { 2, 3, 4 }
u = { 1, 2, 3, 4, 5 }
print(s < u)
print(s < t)
print(s < s)
print(s <= s)
True
False
False
True
def mysubseteq(left,right):
    for e in left:
        if not e in right:
            return False
    return True

print(mysubseteq(s,u))
print(mysubseteq(s,t))
True
False
def setop_subseteq(left, right):
    return left.intersection(right) == left

print(setop_subseteq(s,u))
print(setop_subseteq(s,t))
True
False
def myintersection(left, right):
    pass # only uses union / difference to implement intersection

print(myintersection(s,t) == s.intersection(t))
print(myintersection(s,u) == s.intersection(u))
False
False

Dicts

A dictionary is a data structure that contains a set of unique key → value mappings.

d = {
    'Superman':  'Clark Kent',
    'Batman':    'Bruce Wayne',
    'Spiderman': 'Peter Parker',
    'Ironman':   'Tony Stark'
}
print(d['Ironman'])
Tony Stark
d['Ironman'] = 'James Rhodes'
print(d)
{'Superman': 'Clark Kent', 'Batman': 'Bruce Wayne', 'Spiderman': 'Peter Parker', 'Ironman': 'James Rhodes'}

Dictionary comprehensions

  • syntax: { KEY_EXPR(x) : VALUE_EXPR(x) for x in SEQUENCE }

      print({e:2**e for e in range(0,100,10)})
    
    {0: 1, 10: 1024, 20: 1048576, 30: 1073741824, 40: 1099511627776, 50: 1125899906842624, 60: 1152921504606846976, 70: 1180591620717411303424, 80: 1208925819614629174706176, 90: 1237940039285380274899124224}
    
  print({x:y for x in range(3) for y in range(10)})
{0: 9, 1: 9, 2: 9}
  sentence = 'a man a plan a canal panama'
  print(sentence.split())
  print({w:w[::-1] for w in sentence.split()})
['a', 'man', 'a', 'plan', 'a', 'canal', 'panama']
{'a': 'a', 'man': 'nam', 'plan': 'nalp', 'canal': 'lanac', 'panama': 'amanap'}

10. Modules and the Standard Library

Importing modules

To get access to functions and attributes of a module within a python file you have to import the module. We already saw some examples of how this works. import module makes module available for use in a python file. You can then refer to attributes and functions from the module by prefixing them with module name, e.g., module.function().

import math
print(math.trunc(math.pi))
3

With long module names, it may be tedious to always have to write out the full module name. Python allows you to define a short alias or import names directly (no prefixing is required after that).

from math import trunc, pi
print(trunc(pi))
3
import math as m
print(m.trunc(m.pi))
3

The Python Standard Library

Python comes with a comprehensive standard library. We will use several modules from this library in the course. However, there is no sense in going over the whole list of available functionality. We encourage you to explore the documentation: https://docs.python.org/3/library/

Installing Libraries: Pip and PyPI

PyPI (The *Py*thon *P*ackage *I*ndex) is a repository of python packages. You can install packages from PyPI using pip tool.

pip install numpy
Last updated on Monday, February 1, 2021
Published on Friday, January 1, 2021
 Edit on GitHub