Python Training at Symantec - Chennai -- Day 4

March 27-31, 2017
Anand Chitipothu

These notes are available online at https://notes.pipal.in/2017/symantec

© Pipal Academy LLP

Home | Day 1 | Day 2 | Day 3 | Day 4 | Day 5

Topics

  • Dictionaries
  • Classes
  • Exception Handling

Dictionaries

In [1]:
d = {"x": 1, "y": 2, "z": 3}
In [2]:
print(d)
{'y': 2, 'x': 1, 'z': 3}

Dictionaries are unordered collections.

In [3]:
d['x']
Out[3]:
1
In [4]:
d['y']
Out[4]:
2
In [5]:
d['x'] = 11
In [7]:
print(d)
{'y': 2, 'x': 11, 'z': 3}

Q: Should the key be enclosed in quotes?

Need not be.

In [8]:
d = {"x": 1, "y": 2, "z": 3}
In [9]:
d['x']
Out[9]:
1
In [10]:
key = "x"
d[key]
Out[10]:
1

The keys of dictionary need not be strings.

In [11]:
records = {1: "x", 2: "y", 99: "z"}
In [12]:
records[1]
Out[12]:
'x'
In [13]:
records[99]
Out[13]:
'z'

In fact, keys can be tuples too.

In [14]:
d = {}
d["a", 1] = 123
In [15]:
d
Out[15]:
{('a', 1): 123}

Let us look at on more simple example.

In [16]:
person = {"name": "Alice", "email": "alice@example.com"}
In [17]:
print(person)
{'name': 'Alice', 'email': 'alice@example.com'}
In [18]:
"name" in person
Out[18]:
True
In [19]:
"email" in person
Out[19]:
True
In [20]:
"phone" in person
Out[20]:
False
In [21]:
"phone" not in person
Out[21]:
True

The dictionaries provide a useful method get.

In [22]:
person.get("name")
Out[22]:
'Alice'
In [23]:
person.get("name", "default-value")
Out[23]:
'Alice'
In [24]:
person.get("phone", "not provided")
Out[24]:
'not provided'
In [25]:
host1 = {"name": "mail-1", "ip": "1.2.3.4", "status": "up"}
host2 = {"name": "mail-2", "ip": "1.2.3.5", "status": "down"}
In [26]:
hosts = [host1, host2]
In [27]:
hosts
Out[27]:
[{'ip': '1.2.3.4', 'name': 'mail-1', 'status': 'up'},
 {'ip': '1.2.3.5', 'name': 'mail-2', 'status': 'down'}]
In [28]:
host3 = {"name": "web-1", "ip": "1.2.3.100"}
In [29]:
hosts.append(host3)
In [30]:
hosts
Out[30]:
[{'ip': '1.2.3.4', 'name': 'mail-1', 'status': 'up'},
 {'ip': '1.2.3.5', 'name': 'mail-2', 'status': 'down'},
 {'ip': '1.2.3.100', 'name': 'web-1'}]

The status of the last host is not known.

In [31]:
for h in hosts:
    print("{} {} {}".format(h["name"], h["ip"], h.get("status", "unknown")))
mail-1 1.2.3.4 up
mail-2 1.2.3.5 down
web-1 1.2.3.100 unknown
In [32]:
for h in hosts:
    print("{:10s} {:10s} {}".format(h["name"], h["ip"], h.get("status", "unknown")))
mail-1     1.2.3.4    up
mail-2     1.2.3.5    down
web-1      1.2.3.100  unknown

The keys, values and items of a dictionary can be accesed using keys(), values() and items() methods.

In [33]:
d = {"x": 1, "y": 2, "z": 3}
In [34]:
d.keys()
Out[34]:
dict_keys(['y', 'x', 'z'])
In [35]:
d.values()
Out[35]:
dict_values([2, 1, 3])
In [36]:
d.items()
Out[36]:
dict_items([('y', 2), ('x', 1), ('z', 3)])
In [37]:
for k in d.keys():
    print(k)
y
x
z
In [38]:
for v in d.values():
    print(v)
2
1
3
In [39]:
for k, v in d.items():
    print(k, v)
y 2
x 1
z 3

If you ever want to delete a key from a dictionary, you can use the del statement.

In [40]:
d
Out[40]:
{'x': 1, 'y': 2, 'z': 3}
In [41]:
del d['x']
In [42]:
d
Out[42]:
{'y': 2, 'z': 3}

Also, we can iterate over the dictionary directly, which will go over the keys.

In [43]:
for k in d:
    print(k)
y
z

We can use the update method to combine two dictionaries.

In [82]:
d1 = {"x": 1, "y": 2}
d2 = {"y": 20, "z": 30}
In [83]:
d1.update(d2)
In [84]:
d1
Out[84]:
{'x': 1, 'y': 20, 'z': 30}
In [ ]:
 

Example: Marks of a student

In [44]:
marks = {
    "english": 89,
    "maths": 78,
    "science": 68
}
In [45]:
marks
Out[45]:
{'english': 89, 'maths': 78, 'science': 68}
In [47]:
for k in marks:
    print(k, marks[k])
english 89
science 68
maths 78
In [48]:
for k, v in marks.items():
    print(k, v)
english 89
science 68
maths 78
In [53]:
for subject, score in marks.items():
    print(subject, score)
print("-----------")
print("total", sum(marks.values()))
english 89
science 68
maths 78
-----------
total 235

Example: Word Frequency

Let us write a program to compute the number of occurances of each word in the given file.

In [54]:
%%file words.txt
five
five four
five four three
five four three two
five four three two one
Writing words.txt
In [63]:
%%file wordfreq.py
"""Program to compute the number of occurances of each word in the given file.

USAGE: python wordfreq.py filename.txt
"""
import sys

def read_words(filename):
    """Returns all words in the given file.
    """
    return open(filename).read().split()

def wordfreq(words):
    """Computes the frequency of each word in the given words.
    
        >>> wordfreq(["a", "b", "a])
        {'a': 2, 'b': 1}
    """
    freq = {}
    for w in words:
        # if w in freq:
        #     freq[w] = freq[w] + 1
        # else:
        #     freq[w] = 1
        freq[w] = freq.get(w, 0) + 1
    return freq

def print_freq(freq):
    # TODO: improve this
    print(freq)

def main():
    filename = sys.argv[1]
    words = read_words(filename)
    freq = wordfreq(words)
    print_freq(freq)

if __name__ == "__main__":
    main()
Overwriting wordfreq.py
In [64]:
!python wordfreq.py words.txt
{'five': 5, 'four': 4, 'two': 2, 'one': 1, 'three': 3}
In [67]:
%%file test_wordfreq.py

from wordfreq import wordfreq

def test_wordfreq():
    assert wordfreq([]) == {}
    assert wordfreq(["a"]) == {"a": 1}
    assert wordfreq(["a", "a"]) == {"a": 2}
    assert wordfreq(["a", "b", "a"]) == {"a": 2, "b": 1}    
Overwriting test_wordfreq.py
In [68]:
!py.test test_wordfreq.py
============================= test session starts ==============================
platform darwin -- Python 3.5.2, pytest-3.0.2, py-1.4.31, pluggy-0.3.1
rootdir: /Users/anand/trainings/2017/symantec, inifile: 
collected 1 items 

test_wordfreq.py .

=========================== 1 passed in 0.07 seconds ===========================

Problem: Improve the above program to print one word per line, like shown below (order is not important).

five 5
three 3
two 2
four 4
one 1

Problem: Improve the program further to print the words sorted by count, with most common words on the top.

five 5
four 4
three 3
two 2
one 1
In [69]:
freq = {'five': 5, 'four': 4, 'two': 2, 'one': 1, 'three': 3}
In [70]:
for w, count in freq.items():
    print(w, count)
two 2
four 4
one 1
five 5
three 3
In [71]:
# let us try sorting it...
for w, count in sorted(freq.items()):
    print(w, count)
five 5
four 4
one 1
three 3
two 2
In [72]:
sorted(freq.items())
Out[72]:
[('five', 5), ('four', 4), ('one', 1), ('three', 3), ('two', 2)]
In [74]:
def get_value(item):
    # FIX ME
    print(item)
    return item[1]

sorted(freq.items(), key=get_value)
('two', 2)
('four', 4)
('one', 1)
('five', 5)
('three', 3)
Out[74]:
[('one', 1), ('two', 2), ('three', 3), ('four', 4), ('five', 5)]
In [75]:
sorted(freq, key=freq.get)
Out[75]:
['one', 'two', 'three', 'four', 'five']
In [77]:
def get_value_of_key(key):
    return freq[key]
sorted(freq, key=get_value_of_key)
Out[77]:
['one', 'two', 'three', 'four', 'five']

Dictionary Comprehensions

In [78]:
words = ["alice", "bob", "charlie", "dave"]
In [79]:
{w: len(w) for w in words}
Out[79]:
{'alice': 5, 'bob': 3, 'charlie': 7, 'dave': 4}

This is similar to a list comprehension.

In [80]:
[(w, len(w)) for w in words]
Out[80]:
[('alice', 5), ('bob', 3), ('charlie', 7), ('dave', 4)]

Example: Parsing /etc/hosts file

Let us write a program to parse /etc/hosts file format.

In [98]:
%%file hosts.txt
127.0.0.1 localhost
1.2.3.4 myhost www.myhost
1.2.3.5 foo bar
Writing hosts.txt

We want the program to give back a mapping from hostname to ip address.

In [96]:
%%file hosts.py
"""Module to parse /etc/hosts file format.
"""

def parse(filename):
    """Parses the given file and returns a dictionary mapping
    the hostnames to ip addresses.
    """
    h = {} # hostname -> ip
    lines = open(filename).readlines()
    for line in lines:
        h.update(parse_line(line))
    return h
    
def parse_line(line):
    """Parses a single line of /etc/hosts file.
    
        >>> parse_line("1.2.3.4 myhost www.myhost")
        {"myhost": "1.2.3.4", "www.myhost": "1.2.3.4"}
    """
    line = line.strip()
    if not line or line.startswith("#"):
        return {}
    
    parts = line.split()
    ip = parts[0]
    hostnames = parts[1:]
    return {h: ip for h in hostnames}
    
Overwriting hosts.py
In [94]:
%%file test_hosts.py
from hosts import parse_line

def test_parse_line():
    assert parse_line("") == {}
    assert parse_line("#comment") == {}
    assert parse_line("1.2.3.4 x") == {"x": "1.2.3.4"}    
    assert parse_line("1.2.3.4 x y") == {"x": "1.2.3.4", "y": "1.2.3.4"}    
Overwriting test_hosts.py
In [95]:
!py.test test_hosts.py
============================= test session starts ==============================
platform darwin -- Python 3.5.2, pytest-3.0.2, py-1.4.31, pluggy-0.3.1
rootdir: /Users/anand/trainings/2017/symantec, inifile: 
collected 1 items 

test_hosts.py .

=========================== 1 passed in 0.04 seconds ===========================
In [100]:
import hosts
h = hosts.parse("hosts.txt")
In [101]:
print(h)
{'bar': '1.2.3.5', 'localhost': '127.0.0.1', 'www.myhost': '1.2.3.4', 'foo': '1.2.3.5', 'myhost': '1.2.3.4'}
In [102]:
h['myhost']
Out[102]:
'1.2.3.4'

How to write a hosts file given the host to IP mapping?

In [105]:
ipdict = {} # mapping from ip to hosts
for host, ip in h.items():
    # make sure there is an entry in the ipdict for this ip address
    ipdict[ip] = ipdict.get(ip, [])
    ipdict[ip].append(host)
In [106]:
ipdict
Out[106]:
{'1.2.3.4': ['www.myhost', 'myhost'],
 '1.2.3.5': ['bar', 'foo'],
 '127.0.0.1': ['localhost']}
In [107]:
ipdict = {} # mapping from ip to hosts
for host, ip in h.items():
    # make sure there is an entry in the ipdict for this ip address
    ipdict.setdefault(ip, [])
    ipdict[ip].append(host)
print(ipdict)    
{'1.2.3.4': ['www.myhost', 'myhost'], '127.0.0.1': ['localhost'], '1.2.3.5': ['bar', 'foo']}
In [108]:
ipdict = {} # mapping from ip to hosts
for host, ip in h.items():
    ipdict.setdefault(ip, []).append(host)
print(ipdict)    
{'1.2.3.4': ['www.myhost', 'myhost'], '127.0.0.1': ['localhost'], '1.2.3.5': ['bar', 'foo']}

Now that we have ip to hostnames mapping, how to convert this into a string?

In [109]:
def tostring(ip, hostnames):
    return ip + " " + " ".join(hostnames)
In [110]:
tostring('1.2.3.4', ['www.myhost', 'myhost'])
Out[110]:
'1.2.3.4 www.myhost myhost'
In [111]:
lines = [tostring(ip, hostnames) for ip, hostnames in ipdict.items()]
In [112]:
lines
Out[112]:
['1.2.3.4 www.myhost myhost', '127.0.0.1 localhost', '1.2.3.5 bar foo']
In [113]:
print("\n".join(lines))
1.2.3.4 www.myhost myhost
127.0.0.1 localhost
1.2.3.5 bar foo
In [116]:
print("\n".join([ip + " " + " ".join(hostnames) for ip, hostnames in ipdict.items()]))
1.2.3.4 www.myhost myhost
127.0.0.1 localhost
1.2.3.5 bar foo

Classes

In [117]:
class Point:
    def __init__(self, x1, y1):
        self.x = x1
        self.y = y1
In [118]:
p = Point(3, 4)
print(p.x, p.y)
3 4

Calling Point(3,4) creates a new instance of Point class. It does the following things.

- creates an empty object of type Point
- initializes that by calling the __init__ method
- return back the created object
In [119]:
isinstance(p, Point)
Out[119]:
True
In [120]:
type(p)
Out[120]:
__main__.Point
In [121]:
isinstance(1, int)
Out[121]:
True
In [122]:
isinstance(1, str)
Out[122]:
False

Now let us see how to write methods.

In [125]:
class Point:
    def __init__(self, x1, y1):
        self.x = x1
        self.y = y1
        
    def getx(self):
        return self.x
    
    def display(self):
        print(self.x, self.y)
        
    def add(self, p):
        x = self.x + p.x
        y = self.y + p.y
        return Point(x, y)
In [126]:
p1 = Point(2, 3)
p2 = Point(10, 20)
In [127]:
p1.display()
2 3
In [128]:
p2.display()
10 20
In [129]:
p1.getx()
Out[129]:
2
In [130]:
Point.getx(p1)
Out[130]:
2
In [131]:
p3 = p1.add(p2)
In [132]:
p3.display()
12 23

Problem: Add a method double to the Point class. It should return a new point with both x and y coordinates doubles.

>>> p = Point(2, 3)
>>> p2 = p.double()
>>> p2.display()
4 6
In [138]:
class Point:
    def __init__(self, x1, y1):
        self.x = x1
        self.y = y1
        
    def getx(self):
        return self.x
    
    def display(self):
        print(self.x, self.y)
        
    def add(self, p):
        x = self.x + p.x
        y = self.y + p.y
        return Point(x, y)
    
    def double(self):
        x = 2 * self.x
        y = 2 * self.y
        return Point(x, y)
    
    def double2(self):
        return self.add(self)
In [139]:
p = Point(1, 2)

p2 = p.double()
p2.display()

p2 = p.double2()
p2.display()
2 4
2 4

The __str__ and __repr__ methods

In [140]:
p = Point(1, 2)
In [141]:
print(p)
<__main__.Point object at 0x1099dd390>
In [155]:
class Point:
    def __init__(self, x1, y1):
        self.x = x1
        self.y = y1
    
    def __str__(self):
        return "({}, {})".format(self.x, self.y)
    
    def __repr__(self):
        return "Point({}, {})".format(self.x, self.y)
In [156]:
p = Point(1, 2)
In [157]:
print(p)
(1, 2)
In [158]:
print([p])
[Point(1, 2)]

Python has two different ways to display an object.

In [159]:
print(1, "1")
1 1
In [160]:
print("hello", 1)
hello 1

There is something else called representation of an object.

In [161]:
print([1, "1"])
[1, '1']
In [162]:
print(str("1"))
1
In [163]:
print(repr("1"))
'1'
In [164]:
print(repr(p))
Point(1, 2)
In [165]:
[(1,2), p]
Out[165]:
[(1, 2), Point(1, 2)]

Why do we need classes?

Let us try to model a bank account.

In [167]:
%%file bank0.py

balance = 0

def deposit(amount):
    global balance
    balance = balance + amount

def withdraw(amount):
    global balance
    balance = balance - amount

def get_balance():
    return balance

def main():
    deposit(100)
    withdraw(20)
    print(get_balance())

    withdraw(40)
    print(get_balance())
    
if __name__ == "__main__":
    main()
Overwriting bank0.py
In [168]:
!python bank0.py
80
40

There is one big limitation of this program. It supports only one bank account. It is not possible to have more accounts.

Let us try to address that.

In [171]:
%%file bank1.py

def make_account():
    return {"balance": 0}

def deposit(account, amount):
    account["balance"] += amount

def withdraw(account, amount):
    account["balance"] -= amount
    
def get_balance(account):
    return account["balance"]

def main():
    a1 = make_account()
    a2 = make_account()
    
    deposit(a1, 100)
    deposit(a2, 50)
    print(get_balance(a1), get_balance(a2))

    withdraw(a1, 40)
    withdraw(a2, 20)    
    print(get_balance(a1), get_balance(a2))
    
if __name__ == "__main__":
    main()
Overwriting bank1.py
In [172]:
!python bank1.py
100 50
60 30

Let us try to model this using classes.

In [175]:
%%file bank2.py

class BankAccount:
    def __init__(self):
        self.balance = 0

    def deposit(self, amount):
        self.balance += amount

    def withdraw(self, amount):
        self.balance -= amount

    def get_balance(self):
        return self.balance

def main():
    a1 = BankAccount()
    a2 = BankAccount()
    
    a1.deposit(100)
    a2.deposit(50)
    print(a1.get_balance(), a2.get_balance())

    a1.withdraw(40)
    a2.withdraw(20)    
    print(a1.get_balance(), a2.get_balance())
    
if __name__ == "__main__":
    main()
Overwriting bank2.py
In [174]:
!python bank2.py
100 50
60 30

In fact, objects are glorified dictionaries.

In [176]:
from bank2 import BankAccount
In [178]:
a1 = BankAccount()
a1.deposit(100)

a2 = BankAccount()
a2.deposit(50)
In [179]:
a1.__dict__
Out[179]:
{'balance': 100}
In [180]:
a2.__dict__
Out[180]:
{'balance': 50}
In [ ]: