Session 11

Published

November 2, 2023

Topics Covered
  • Class Inheritance
  • Exception Handling
  • Data Classes

Duck Typing

def count_words(fileobj):
    text = fileobj.read()
    words = text.split()
    return len(words)
!cat files/five.txt
one
two
three
four
five
count_words(open("files/five.txt"))
5
class FakeFile:
    def read(self):
        return "one two three"
f = FakeFile()
count_words(f)
3
from io import StringIO
f = StringIO("One Two Three")
count_words(f)
3

Class Inheritance

class Point:
    def __init__(self, x, y):
        print("-- Point.__init__", x, y)
        self.x = x
        self.y = y

    def display(self):
        print("-- Point.display")
        
        print("x:", self.x)
        print("y:", self.y)
p = Point(10, 20)
p.display()
-- Point.__init__ 10 20
-- Point.display
x: 10
y: 20
class Point3d(Point):
    def __init__(self, x, y, z):
        print("-- Point3d.__init__", x, y, z)
        
        super().__init__(x, y)
        self.z = z

    def display(self):
        print("-- Point3d.display")
        
        super().display()
        print("z:", self.z)
p3 = Point3d(3, 4, 5)
-- Point3d.__init__ 3 4 5
-- Point.__init__ 3 4
p3.display()
-- Point3d.display
-- Point.display
x: 3
y: 4
z: 5

Example: Github APIs

Let’s build a library to expose github public API.

import requests
# v1

class Organization:
    def __init__(self, name):
        self.name = name
        
    def list_repos(self):
        url = f"https://api.github.com/orgs/{self.name}/repos"
        data = requests.get(url).json()
        return [Repository(d['owner']['login'], d['name']) for d in data]

    def __repr__(self):
        return f"<Org:{self.name}>"
    
class Repository:
    def __init__(self, owner, name):
        self.owner = owner
        self.name = name
        self.full_name = f"{owner}/{name}"

    def __repr__(self):
        return f"<Repo {self.owner}/{self.name}>"
#v2

class GithubResource:
    def get(self, path):
        url = "https://api.github.com" + path
        return requests.get(url).json()

    def list_resources(self, path, cls):
        data = self.get(path)
        return [cls.fromdict(d) for d in data]
        
class Organization(GithubResource):
    def __init__(self, name):
        self.name = name
        
    def list_repos(self):
        path = f"/orgs/{self.name}/repos"
        return self.list_resources(path, Repository)

    def __repr__(self):
        return f"<Org:{self.name}>"
    
class Repository(GithubResource):
    def __init__(self, owner, name):
        self.owner = owner
        self.name = name
        self.full_name = f"{owner}/{name}"

    def get_contributors(self):
        path = f"/repos/{self.owner}/{self.name}/contributors"
        return self.list_resources(path, GithubUser)

    def get_issues(self):
        """Returns all the issues of a repository.
        """
        path = f"/repos/{self.owner}/{self.name}/issues"
        return self.list_resources(path, GithubIssue)


    @staticmethod
    def fromdict(data):
        return Repository(data['owner']['login'], data['name'])        
        
    def __repr__(self):
        return f"<Repo {self.owner}/{self.name}>"

class GithubUser(GithubResource):
    def __init__(self, name):
        self.name = name

    def __repr__(self):
        return f"<User {self.name}>"

    def list_repos(self):
        path = f"/users/{self.name}/repos"
        return self.list_resources(path, Repository)

    @staticmethod
    def fromdict(data):
        return GithubUser(data['login'])        

class GithubIssue(GithubResource):
    def __init__(self, number, state, title):
        self.number = number
        self.state = state
        self.title = title

    @staticmethod
    def fromdict(data):
        return GithubIssue(data['number'], data['state'], data['title'])

    def __repr__(self):
        return f"<Issue#{self.number} {self.state}: {self.title}>"
Repository("python", "cpython")
<Repo python/cpython>
Organization("python")
<Org:python>
Organization("python").list_repos()
[<Repo python/getpython3.com>,
 <Repo python/community-starter-kit>,
 <Repo python/psf-docs>,
 <Repo python/historic-python-materials>,
 <Repo python/psf-chef>,
 <Repo python/psfoutreach>,
 <Repo python/pythondotorg>,
 <Repo python/mypy>,
 <Repo python/raspberryio>,
 <Repo python/pycon-code-of-conduct>,
 <Repo python/peps>,
 <Repo python/psf-salt>,
 <Repo python/docsbuild-scripts>,
 <Repo python/planet>,
 <Repo python/typing>,
 <Repo python/speed.python.org>,
 <Repo python/typeshed>,
 <Repo python/asyncio>,
 <Repo python/buildbot>,
 <Repo python/psf-packages>,
 <Repo python/typed_ast>,
 <Repo python/the-knights-who-say-ni>,
 <Repo python/devinabox>,
 <Repo python/pythontestdotnet>,
 <Repo python/pythonineducation.org>,
 <Repo python/tlsproxy>,
 <Repo python/devguide>,
 <Repo python/overload-sig>,
 <Repo python/pyperformance>,
 <Repo python/release-tools>]
Repository("python", "cpython").get_contributors()
[<User gvanrossum>,
 <User vstinner>,
 <User benjaminp>,
 <User birkenfeld>,
 <User freddrake>,
 <User serhiy-storchaka>,
 <User rhettinger>,
 <User pitrou>,
 <User jackjansen>,
 <User loewis>,
 <User tim-one>,
 <User brettcannon>,
 <User akuchling>,
 <User warsaw>,
 <User bitdancer>,
 <User ezio-melotti>,
 <User mdickinson>,
 <User nnorwitz>,
 <User tiran>,
 <User terryjreedy>,
 <User gpshead>,
 <User orsenthil>,
 <User vsajip>,
 <User merwok>,
 <User jeremyhylton>,
 <User 1st1>,
 <User zooba>,
 <User ned-deily>,
 <User berkerpeksag>,
 <User gward>]
user = GithubUser("gvanrossum")
user.list_repos()
[<Repo gvanrossum/500lines>,
 <Repo gvanrossum/asyncio>,
 <Repo gvanrossum/ballot-box>,
 <Repo gvanrossum/c-parser>,
 <Repo gvanrossum/cpython>,
 <Repo gvanrossum/ctok>,
 <Repo gvanrossum/devguide>,
 <Repo gvanrossum/exceptiongroup>,
 <Repo gvanrossum/guidos_time_machine>,
 <Repo gvanrossum/gvanrossum.github.io>,
 <Repo gvanrossum/http-get-perf>,
 <Repo gvanrossum/minithesis>,
 <Repo gvanrossum/mirror-cwi-stdwin>,
 <Repo gvanrossum/mypy>,
 <Repo gvanrossum/mypy-dummy>,
 <Repo gvanrossum/old-demos>,
 <Repo gvanrossum/path-pep>,
 <Repo gvanrossum/patma>,
 <Repo gvanrossum/pep550>,
 <Repo gvanrossum/peps>,
 <Repo gvanrossum/Pyjion>,
 <Repo gvanrossum/pythonlabs>,
 <Repo gvanrossum/pythonlabs-com-azure>,
 <Repo gvanrossum/pytype>,
 <Repo gvanrossum/pyxl3>]
repo = Repository("python", "cpython")
issues = repo.get_issues()
issues[0]
<Issue#111629 open: Segmentation fault during garbage collection>

Exception Handling

What happens when we use a variable that is not defined?

no_such_variable
NameError: name 'no_such_variable' is not defined
def f():
    return no_such_variable
f()
NameError: name 'no_such_variable' is not defined

Python gives a traceback of where the error is happening. That helps us to find which part of the code is causing the error.

int("bad-number")
ValueError: invalid literal for int() with base 10: 'bad-number'
def sumfile(filename):
    lines = open(filename).readlines()
    numbers = [int(line) for line in lines]
    return sum(numbers)
%%file numbers.txt
1
2
3
4
NA
5
Overwriting numbers.txt
sumfile("numbers.txt")
ValueError: invalid literal for int() with base 10: 'NA\n'

When we try to open a file that is not present, that is also an exception.

open("no-file.txt")
FileNotFoundError: [Errno 2] No such file or directory: 'no-file.txt'

handling exceptions

def toint(strvalue):
    try:
        return int(strvalue)
    except ValueError:
        print("Bad number:", strvalue)
        # ignore bad numbers and treat them as 0
        return 0
toint("NA")
Bad number: NA
0
%load_problem sumfile-with-error-handling
Problem: Sum File

Write a program sumfile.py that takes a filename as command-line argument and prints sum of all numbers in that file. It is expected that the file will have one number for every line. The program should ignore the line if it is not a valid number after printing a warning message. The warning message should be printed to sys.stderr.

$ python sumfile.py files/sumfile/numbers.txt
WARNING: Bad number 'N/A'
WARNING: Bad number 'xxx'
15

Here is a sample input file.

$ cat files/numbers.txt
1
2
3
N/A
4
xxx
5

Hint:

You can print a messge to stderr using:

print("message", file=sys.stderr)

You can verify your solution using:

%verify_problem sumfile-with-error-handling

%%file sumfile.py
# your code here


Writing sumfile.py
%verify_problem sumfile-with-error-handling
✗ python sumfile.py /opt/files/sumfile/numbers.txt
Expected:
15
Found:

💥 Oops! Your solution to problem sumfile-with-error-handling is incorrect or incomplete.

Raising our own exceptions

class GithubError(Exception):
    pass
def get_repo(username):
    if " " in username:
        raise GithubError("Invalid username")
get_repo("foo bar")
GithubError: Invalid username

Data Classes

from dataclasses import dataclass
@dataclass
class Point:
    x: int
    y: int
Point(10, 20)
Point(x=10, y=20)
Point(x=10, y=20)
Point(x=10, y=20)
p = Point(x=10, y=20)
p.x
10
p.y
20
@dataclass
class Point:
    x: int = 0
    y: int = 0
Point()
Point(x=0, y=0)
Point(x=10, y=20)
Point(x=10, y=20)
@dataclass
class Point:
    x: int = 0
    y: int = 0

    def double(self):
        x = self.x * 2
        y = self.y * 2
        return Point(x, y)
Point(10, 20).double()
Point(x=20, y=40)
@dataclass
class Polygon:
    points: list[int]
Polygon([Point(0, 0), Point(3, 4), Point(10, 20)]) 
Polygon(points=[Point(x=0, y=0), Point(x=3, y=4), Point(x=10, y=20)])

Example: Frankfurter API

!curl https://api.frankfurter.app/latest
{"amount":1.0,"base":"EUR","date":"2023-11-01","rates":{"AUD":1.6561,"BGN":1.9558,"BRL":5.2963,"CAD":1.461,"CHF":0.9572,"CNY":7.712,"CZK":24.677,"DKK":7.4644,"GBP":0.86945,"HKD":8.2436,"HUF":383.75,"IDR":16803,"ILS":4.2437,"INR":87.75,"ISK":148.1,"JPY":159.33,"KRW":1429.16,"MXN":18.9457,"MYR":5.0272,"NOK":11.796,"NZD":1.8068,"PHP":59.861,"PLN":4.4658,"RON":4.9679,"SEK":11.806,"SGD":1.4443,"THB":38.144,"TRY":29.852,"USD":1.0537,"ZAR":19.6349}}
from dataclasses import dataclass

@dataclass
class LatestAPI:
    amount: float
    base: str
    date: str
    rates: dict
import requests
data = requests.get("https://api.frankfurter.app/latest").json()
data
{'amount': 1.0,
 'base': 'EUR',
 'date': '2023-11-01',
 'rates': {'AUD': 1.6561,
  'BGN': 1.9558,
  'BRL': 5.2963,
  'CAD': 1.461,
  'CHF': 0.9572,
  'CNY': 7.712,
  'CZK': 24.677,
  'DKK': 7.4644,
  'GBP': 0.86945,
  'HKD': 8.2436,
  'HUF': 383.75,
  'IDR': 16803,
  'ILS': 4.2437,
  'INR': 87.75,
  'ISK': 148.1,
  'JPY': 159.33,
  'KRW': 1429.16,
  'MXN': 18.9457,
  'MYR': 5.0272,
  'NOK': 11.796,
  'NZD': 1.8068,
  'PHP': 59.861,
  'PLN': 4.4658,
  'RON': 4.9679,
  'SEK': 11.806,
  'SGD': 1.4443,
  'THB': 38.144,
  'TRY': 29.852,
  'USD': 1.0537,
  'ZAR': 19.6349}}
latest_response = LatestAPI(**data)
latest_response.base
'EUR'
latest_response.rates
{'AUD': 1.6561,
 'BGN': 1.9558,
 'BRL': 5.2963,
 'CAD': 1.461,
 'CHF': 0.9572,
 'CNY': 7.712,
 'CZK': 24.677,
 'DKK': 7.4644,
 'GBP': 0.86945,
 'HKD': 8.2436,
 'HUF': 383.75,
 'IDR': 16803,
 'ILS': 4.2437,
 'INR': 87.75,
 'ISK': 148.1,
 'JPY': 159.33,
 'KRW': 1429.16,
 'MXN': 18.9457,
 'MYR': 5.0272,
 'NOK': 11.796,
 'NZD': 1.8068,
 'PHP': 59.861,
 'PLN': 4.4658,
 'RON': 4.9679,
 'SEK': 11.806,
 'SGD': 1.4443,
 'THB': 38.144,
 'TRY': 29.852,
 'USD': 1.0537,
 'ZAR': 19.6349}
latest_response
LatestAPI(amount=1.0, base='EUR', date='2023-11-01', rates={'AUD': 1.6561, 'BGN': 1.9558, 'BRL': 5.2963, 'CAD': 1.461, 'CHF': 0.9572, 'CNY': 7.712, 'CZK': 24.677, 'DKK': 7.4644, 'GBP': 0.86945, 'HKD': 8.2436, 'HUF': 383.75, 'IDR': 16803, 'ILS': 4.2437, 'INR': 87.75, 'ISK': 148.1, 'JPY': 159.33, 'KRW': 1429.16, 'MXN': 18.9457, 'MYR': 5.0272, 'NOK': 11.796, 'NZD': 1.8068, 'PHP': 59.861, 'PLN': 4.4658, 'RON': 4.9679, 'SEK': 11.806, 'SGD': 1.4443, 'THB': 38.144, 'TRY': 29.852, 'USD': 1.0537, 'ZAR': 19.6349})