def count_words(fileobj):
text = fileobj.read()
words = text.split()
return len(words)Session 11
- Class Inheritance
- Exception Handling
- Data Classes
Duck Typing
!cat files/five.txtone
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 StringIOf = 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_variableNameError: name 'no_such_variable' is not defined
def f():
return no_such_variablef()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
5Overwriting 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 0toint("NA")Bad number: NA
0
%load_problem sumfile-with-error-handlingWrite 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):
passdef 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: intPoint(10, 20)Point(x=10, y=20)
Point(x=10, y=20)Point(x=10, y=20)
p = Point(x=10, y=20)p.x10
p.y20
@dataclass
class Point:
x: int = 0
y: int = 0Point()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: dictimport requestsdata = 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_responseLatestAPI(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})