Tutorial:PracticalPython/4 Classs objects
Classes and Objects
So far, our programs have only used built-in Python datatypes. In this section, we introduce the concept of classes and objects. You’ll learn about the class
statement that allows you to make new objects. We’ll also introduce the concept of inheritance, a tool that is commonly use to build extensible programs. Finally, we’ll look at a few other features of classes including special methods, dynamic attribute lookup, and defining new exceptions.
Classes
This section introduces the class statement and the idea of creating new objects.
Object Oriented (OO) programming
A Programming technique where code is organized as a collection of objects.
An object consists of:
- Data. Attributes
- Behavior. Methods which are functions applied to the object.
You have already been using some OO during this course.
For example, manipulating a list.
>>> nums = [1, 2, 3] >>> nums.append(4) # Method >>> nums.insert(1,10) # Method >>> nums [1, 10, 2, 3, 4] # Data >>>
nums
is an instance of a list.
Methods (append()
and insert()
) are attached to the instance (nums
).
The class
statement
Use the class
statement to define a new object.
class Player: def __init__(self, x, y): self.x = x self.y = y self.health = 100 def move(self, dx, dy): self.x += dx self.y += dy def damage(self, pts): self.health -= pts
In a nutshell, a class is a set of functions that carry out various operations on so-called instances.
Instances
Instances are the actual objects that you manipulate in your program.
They are created by calling the class as a function.
>>> a = Player(2, 3) >>> b = Player(10, 20) >>>
a
and b
are instances of Player
.
Emphasize: The class statement is just the definition (it does nothing by itself). Similar to a function definition.
Instance Data
Each instance has its own local data.
>>> a.x 2 >>> b.x 10
This data is initialized by the __init__()
.
class Player: def __init__(self, x, y): # Any value stored on `self` is instance data self.x = x self.y = y self.health = 100
There are no restrictions on the total number or type of attributes stored.
Instance Methods
Instance methods are functions applied to instances of an object.
class Player: ... # `move` is a method def move(self, dx, dy): self.x += dx self.y += dy
The object itself is always passed as first argument.
>>> a.move(1, 2) # matches `a` to `self` # matches `1` to `dx` # matches `2` to `dy` def move(self, dx, dy):
By convention, the instance is called self
. However, the actual name used is unimportant. The object is always passed as the first argument. It is merely Python programming style to call this argument self
.
Class Scoping
Classes do not define a scope of names.
class Player: ... def move(self, dx, dy): self.x += dx self.y += dy def left(self, amt): move(-amt, 0) # NO. Calls a global `move` function self.move(-amt, 0) # YES. Calls method `move` from above.
If you want to operate on an instance, you always refer to it explicitly (e.g., self
).
Exercises
Starting with this set of exercises, we start to make a series of changes to existing code from previous sctions. It is critical that you have a working version of Exercise 3.18 to start. If you don’t have that, please work from the solution code found in the Solutions/3_18
directory. It’s fine to copy it.
Exercise 4.1: Objects as Data Structures
In section 2 and 3, we worked with data represented as tuples and dictionaries. For example, a holding of stock could be represented as a tuple like this:
s = ('GOOG',100,490.10)
or as a dictionary like this:
s = { 'name' : 'GOOG', 'shares' : 100, 'price' : 490.10 }
You can even write functions for manipulating such data. For example:
def cost(s): return s['shares'] * s['price']
However, as your program gets large, you might want to create a better sense of organization. Thus, another approach for representing data would be to define a class. Create a file called stock.py
and define a class Stock
that represents a single holding of stock. Have the instances of Stock
have name
, shares
, and price
attributes. For example:
>>> import stock >>> a = stock.Stock('GOOG',100,490.10) >>> a.name 'GOOG' >>> a.shares 100 >>> a.price 490.1 >>>
Create a few more Stock
objects and manipulate them. For example:
>>> b = stock.Stock('AAPL', 50, 122.34) >>> c = stock.Stock('IBM', 75, 91.75) >>> b.shares * b.price 6117.0 >>> c.shares * c.price 6881.25 >>> stocks = [a, b, c] >>> stocks [<stock.Stock object at 0x37d0b0>, <stock.Stock object at 0x37d110>, <stock.Stock object at 0x37d050>] >>> for s in stocks: print(f'{s.name:>10s} {s.shares:>10d} {s.price:>10.2f}') ... look at the output ... >>>
One thing to emphasize here is that the class Stock
acts like a factory for creating instances of objects. Basically, you call it as a function and it creates a new object for you. Also, it must be emphasized that each object is distinct—they each have their own data that is separate from other objects that have been created.
An object defined by a class is somewhat similar to a dictionary–just with somewhat different syntax. For example, instead of writing s['name']
or s['price']
, you now write s.name
and s.price
.
Exercise 4.2: Adding some Methods
With classes, you can attach functions to your objects. These are known as methods and are functions that operate on the data stored inside an object. Add a cost()
and sell()
method to your Stock
object. They should work like this:
>>> import stock >>> s = stock.Stock('GOOG', 100, 490.10) >>> s.cost() 49010.0 >>> s.shares 100 >>> s.sell(25) >>> s.shares 75 >>> s.cost() 36757.5 >>>
Exercise 4.3: Creating a list of instances
Try these steps to make a list of Stock instances from a list of dictionaries. Then compute the total cost:
>>> import fileparse >>> with open('Data/portfolio.csv') as lines: ... portdicts = fileparse.parse_csv(lines, select=['name','shares','price'], types=[str,int,float]) ... >>> portfolio = [ stock.Stock(d['name'], d['shares'], d['price']) for d in portdicts] >>> portfolio [<stock.Stock object at 0x10c9e2128>, <stock.Stock object at 0x10c9e2048>, <stock.Stock object at 0x10c9e2080>, <stock.Stock object at 0x10c9e25f8>, <stock.Stock object at 0x10c9e2630>, <stock.Stock object at 0x10ca6f748>, <stock.Stock object at 0x10ca6f7b8>] >>> sum([s.cost() for s in portfolio]) 44671.15 >>>
Exercise 4.4: Using your class
Modify the read_portfolio()
function in the report.py
program so that it reads a portfolio into a list of Stock
instances as just shown in Exercise 4.3. Once you have done that, fix all of the code in report.py
and pcost.py
so that it works with Stock
instances instead of dictionaries.
Hint: You should not have to make major changes to the code. You will mainly be changing dictionary access such as s['shares']
into s.shares
.
You should be able to run your functions the same as before:
>>> import pcost >>> pcost.portfolio_cost('Data/portfolio.csv') 44671.15 >>> import report >>> report.portfolio_report('Data/portfolio.csv', 'Data/prices.csv') Name Shares Price Change ---------- ---------- ---------- ---------- AA 100 9.22 -22.98 IBM 50 106.28 15.18 CAT 150 35.46 -47.98 MSFT 200 20.89 -30.34 GE 95 13.48 -26.89 MSFT 50 20.89 -44.21 IBM 100 106.28 35.84 >>>
Inheritance
Inheritance is a commonly used tool for writing extensible programs. This section explores that idea.
Introduction
Inheritance is used to specialize existing objects:
class Parent: ... class Child(Parent): ...
The new class Child
is called a derived class or subclass. The Parent
class is known as base class or superclass. Parent
is specified in ()
after the class name, class Child(Parent):
.
Extending
With inheritance, you are taking an existing class and:
- Adding new methods
- Redefining some of the existing methods
- Adding new attributes to instances
In the end you are extending existing code.
Example
Suppose that this is your starting class:
class Stock: def __init__(self, name, shares, price): self.name = name self.shares = shares self.price = price def cost(self): return self.shares * self.price def sell(self, nshares): self.shares -= nshares
You can change any part of this via inheritance.
Add a new method
class MyStock(Stock): def panic(self): self.sell(self.shares)
Usage example.
>>> s = MyStock('GOOG', 100, 490.1) >>> s.sell(25) >>> s.shares 75 >>> s.panic() >>> s.shares 0 >>>
Redefining an existing method
class MyStock(Stock): def cost(self): return 1.25 * self.shares * self.price
Usage example.
>>> s = MyStock('GOOG', 100, 490.1) >>> s.cost() 61262.5 >>>
The new method takes the place of the old one. The other methods are unaffected. It’s tremendous.
Overriding
Sometimes a class extends an existing method, but it wants to use the original implementation inside the redefinition. For this, use super()
:
class Stock: ... def cost(self): return self.shares * self.price ... class MyStock(Stock): def cost(self): # Check the call to `super` actual_cost = super().cost() return 1.25 * actual_cost
Use super()
to call the previous version.
Caution: In Python 2, the syntax was more verbose.
actual_cost = super(MyStock, self).cost()
__init__
and inheritance
If __init__
is redefined, it is essential to initialize the parent.
class Stock: def __init__(self, name, shares, price): self.name = name self.shares = shares self.price = price class MyStock(Stock): def __init__(self, name, shares, price, factor): # Check the call to `super` and `__init__` super().__init__(name, shares, price) self.factor = factor def cost(self): return self.factor * super().cost()
You should call the __init__()
method on the super
which is the way to call the previous version as shown previously.
Using Inheritance
Inheritance is sometimes used to organize related objects.
class Shape: ... class Circle(Shape): ... class Rectangle(Shape): ...
Think of a logical hierarchy or taxonomy. However, a more common (and practical) usage is related to making reusable or extensible code. For example, a framework might define a base class and instruct you to customize it.
class CustomHandler(TCPHandler): def handle_request(self): ... # Custom processing
The base class contains some general purpose code. Your class inherits and customized specific parts.
“is a” relationship
Inheritance establishes a type relationship.
class Shape: ... class Circle(Shape): ...
Check for object instance.
>>> c = Circle(4.0) >>> isinstance(c, Shape) True >>>
Important: Ideally, any code that worked with instances of the parent class will also work with instances of the child class.
object
base class
If a class has no parent, you sometimes see object
used as the base.
class Shape(object): ...
object
is the parent of all objects in Python.
- Note: it’s not technically required, but you often see it specified as a hold-over from it’s required use in Python 2. If omitted, the class still implicitly inherits from
object
.
Multiple Inheritance
You can inherit from multiple classes by specifying them in the definition of the class.
class Mother: ... class Father: ... class Child(Mother, Father): ...
The class Child
inherits features from both parents. There are some rather tricky details. Don’t do it unless you know what you are doing. Some further information will be given in the next section, but we’re not going to utilize multiple inheritance further in this course.
Exercises
A major use of inheritance is in writing code that’s meant to be extended or customized in various ways–especially in libraries or frameworks. To illustrate, consider the print_report()
function in your report.py
program. It should look something like this:
def print_report(reportdata): ''' Print a nicely formated table from a list of (name, shares, price, change) tuples. ''' headers = ('Name','Shares','Price','Change') print('%10s %10s %10s %10s' % headers) print(('-'*10 + ' ')*len(headers)) for row in reportdata: print('%10s %10d %10.2f %10.2f' % row)
When you run your report program, you should be getting output like this:
>>> import report >>> report.portfolio_report('Data/portfolio.csv', 'Data/prices.csv') Name Shares Price Change ---------- ---------- ---------- ---------- AA 100 9.22 -22.98 IBM 50 106.28 15.18 CAT 150 35.46 -47.98 MSFT 200 20.89 -30.34 GE 95 13.48 -26.89 MSFT 50 20.89 -44.21 IBM 100 106.28 35.84
Exercise 4.5: An Extensibility Problem
Suppose that you wanted to modify the print_report()
function to support a variety of different output formats such as plain-text, HTML, CSV, or XML. To do this, you could try to write one gigantic function that did everything. However, doing so would likely lead to an unmaintainable mess. Instead, this is a perfect opportunity to use inheritance instead.
To start, focus on the steps that are involved in a creating a table. At the top of the table is a set of table headers. After that, rows of table data appear. Let’s take those steps and and put them into their own class. Create a file called tableformat.py
and define the following class:
# tableformat.py class TableFormatter: def headings(self, headers): ''' Emit the table headings. ''' raise NotImplementedError() def row(self, rowdata): ''' Emit a single row of table data. ''' raise NotImplementedError()
This class does nothing, but it serves as a kind of design specification for additional classes that will be defined shortly. A class like this is sometimes called an “abstract base class.”
Modify the print_report()
function so that it accepts a TableFormatter
object as input and invokes methods on it to produce the output. For example, like this:
# report.py ... def print_report(reportdata, formatter): ''' Print a nicely formated table from a list of (name, shares, price, change) tuples. ''' formatter.headings(['Name','Shares','Price','Change']) for name, shares, price, change in reportdata: rowdata = [ name, str(shares), f'{price:0.2f}', f'{change:0.2f}' ] formatter.row(rowdata)
Since you added an argument to print_report(), you’re going to need to modify the portfolio_report()
function as well. Change it so that it creates a TableFormatter
like this:
# report.py import tableformat ... def portfolio_report(portfoliofile, pricefile): ''' Make a stock report given portfolio and price data files. ''' # Read data files portfolio = read_portfolio(portfoliofile) prices = read_prices(pricefile) # Create the report data report = make_report_data(portfolio, prices) # Print it out formatter = tableformat.TableFormatter() print_report(report, formatter)
Run this new code:
>>> ================================ RESTART ================================ >>> import report >>> report.portfolio_report('Data/portfolio.csv', 'Data/prices.csv') ... crashes ...
It should immediately crash with a NotImplementedError
exception. That’s not too exciting, but it’s exactly what we expected. Continue to the next part.
Exercise 4.6: Using Inheritance to Produce Different Output
The TableFormatter
class you defined in part (a) is meant to be extended via inheritance. In fact, that’s the whole idea. To illustrate, define a class TextTableFormatter
like this:
# tableformat.py ... class TextTableFormatter(TableFormatter): ''' Emit a table in plain-text format ''' def headings(self, headers): for h in headers: print(f'{h:>10s}', end=' ') print() print(('-'*10 + ' ')*len(headers)) def row(self, rowdata): for d in rowdata: print(f'{d:>10s}', end=' ') print()
Modify the portfolio_report()
function like this and try it:
# report.py ... def portfolio_report(portfoliofile, pricefile): ''' Make a stock report given portfolio and price data files. ''' # Read data files portfolio = read_portfolio(portfoliofile) prices = read_prices(pricefile) # Create the report data report = make_report_data(portfolio, prices) # Print it out formatter = tableformat.TextTableFormatter() print_report(report, formatter)
This should produce the same output as before:
>>> ================================ RESTART ================================ >>> import report >>> report.portfolio_report('Data/portfolio.csv', 'Data/prices.csv') Name Shares Price Change ---------- ---------- ---------- ---------- AA 100 9.22 -22.98 IBM 50 106.28 15.18 CAT 150 35.46 -47.98 MSFT 200 20.89 -30.34 GE 95 13.48 -26.89 MSFT 50 20.89 -44.21 IBM 100 106.28 35.84 >>>
However, let’s change the output to something else. Define a new class CSVTableFormatter
that produces output in CSV format:
# tableformat.py ... class CSVTableFormatter(TableFormatter): ''' Output portfolio data in CSV format. ''' def headings(self, headers): print(','.join(headers)) def row(self, rowdata): print(','.join(rowdata))
Modify your main program as follows:
def portfolio_report(portfoliofile, pricefile): ''' Make a stock report given portfolio and price data files. ''' # Read data files portfolio = read_portfolio(portfoliofile) prices = read_prices(pricefile) # Create the report data report = make_report_data(portfolio, prices) # Print it out formatter = tableformat.CSVTableFormatter() print_report(report, formatter)
You should now see CSV output like this:
>>> ================================ RESTART ================================ >>> import report >>> report.portfolio_report('Data/portfolio.csv', 'Data/prices.csv') Name,Shares,Price,Change AA,100,9.22,-22.98 IBM,50,106.28,15.18 CAT,150,35.46,-47.98 MSFT,200,20.89,-30.34 GE,95,13.48,-26.89 MSFT,50,20.89,-44.21 IBM,100,106.28,35.84
Using a similar idea, define a class HTMLTableFormatter
that produces a table with the following output:
<tr><th>Name</th><th>Shares</th><th>Price</th><th>Change</th></tr> <tr><td>AA</td><td>100</td><td>9.22</td><td>-22.98</td></tr> <tr><td>IBM</td><td>50</td><td>106.28</td><td>15.18</td></tr> <tr><td>CAT</td><td>150</td><td>35.46</td><td>-47.98</td></tr> <tr><td>MSFT</td><td>200</td><td>20.89</td><td>-30.34</td></tr> <tr><td>GE</td><td>95</td><td>13.48</td><td>-26.89</td></tr> <tr><td>MSFT</td><td>50</td><td>20.89</td><td>-44.21</td></tr> <tr><td>IBM</td><td>100</td><td>106.28</td><td>35.84</td></tr>
Test your code by modifying the main program to create a HTMLTableFormatter
object instead of a CSVTableFormatter
object.
Exercise 4.7: Polymorphism in Action
A major feature of object-oriented programming is that you can plug an object into a program and it will work without having to change any of the existing code. For example, if you wrote a program that expected to use a TableFormatter
object, it would work no matter what kind of TableFormatter
you actually gave it. This behavior is sometimes referred to as “polymorphism.”
One potential problem is figuring out how to allow a user to pick out the formatter that they want. Direct use of the class names such as TextTableFormatter
is often annoying. Thus, you might consider some simplified approach. Perhaps you embed an if-
statement into the code like this:
def portfolio_report(portfoliofile, pricefile, fmt='txt'): ''' Make a stock report given portfolio and price data files. ''' # Read data files portfolio = read_portfolio(portfoliofile) prices = read_prices(pricefile) # Create the report data report = make_report_data(portfolio, prices) # Print it out if fmt == 'txt': formatter = tableformat.TextTableFormatter() elif fmt == 'csv': formatter = tableformat.CSVTableFormatter() elif fmt == 'html': formatter = tableformat.HTMLTableFormatter() else: raise RuntimeError(f'Unknown format {fmt}') print_report(report, formatter)
In this code, the user specifies a simplified name such as 'txt'
or 'csv'
to pick a format. However, is putting a big if
-statement in the portfolio_report()
function like that the best idea? It might be better to move that code to a general purpose function somewhere else.
In the tableformat.py
file, add a function create_formatter(name)
that allows a user to create a formatter given an output name such as 'txt'
, 'csv'
, or 'html'
. Modify portfolio_report()
so that it looks like this:
def portfolio_report(portfoliofile, pricefile, fmt='txt'): ''' Make a stock report given portfolio and price data files. ''' # Read data files portfolio = read_portfolio(portfoliofile) prices = read_prices(pricefile) # Create the report data report = make_report_data(portfolio, prices) # Print it out formatter = tableformat.create_formatter(fmt) print_report(report, formatter)
Try calling the function with different formats to make sure it’s working.
Exercise 4.8: Putting it all together
Modify the report.py
program so that the portfolio_report()
function takes an optional argument specifying the output format. For example:
>>> report.portfolio_report('Data/portfolio.csv', 'Data/prices.csv', 'txt') Name Shares Price Change ---------- ---------- ---------- ---------- AA 100 9.22 -22.98 IBM 50 106.28 15.18 CAT 150 35.46 -47.98 MSFT 200 20.89 -30.34 GE 95 13.48 -26.89 MSFT 50 20.89 -44.21 IBM 100 106.28 35.84 >>>
Modify the main program so that a format can be given on the command line:
bash $ python3 report.py Data/portfolio.csv Data/prices.csv csv Name,Shares,Price,Change AA,100,9.22,-22.98 IBM,50,106.28,15.18 CAT,150,35.46,-47.98 MSFT,200,20.89,-30.34 GE,95,13.48,-26.89 MSFT,50,20.89,-44.21 IBM,100,106.28,35.84 bash $
Discussion
Writing extensible code is one of the most common uses of inheritance in libraries and frameworks. For example, a framework might instruct you to define your own object that inherits from a provided base class. You’re then told to fill in various methods that implement various bits of functionality.
Another somewhat deeper concept is the idea of “owning your abstractions.” In the exercises, we defined our own class for formatting a table. You may look at your code and tell yourself “I should just use a formatting library or something that someone else already made instead!” No, you should use BOTH your class and a library. Using your own class promotes loose coupling and is more flexible. As long as your application uses the programming interface of your class, you can change the internal implementation to work in any way that you want. You can write all-custom code. You can use someone’s third party package. You swap out one third-party package for a different package when you find a better one. It doesn’t matter–none of your application code will break as long as you preserve keep the interface. That’s a powerful idea and it’s one of the reasons why you might consider inheritance for something like this.
That said, designing object oriented programs can be extremely difficult. For more information, you should probably look for books on the topic of design patterns (although understanding what happened in this exercise will take you pretty far in terms of using objects in a practically useful way).
Special Methods
Various parts of Python’s behavior can be customized via special or so-called “magic” methods. This section introduces that idea. In addition dynamic attribute access and bound methods are discussed.
Introduction
Classes may define special methods. These have special meaning to the Python interpreter. They are always preceded and followed by __
. For example __init__
.
class Stock(object): def __init__(self): ... def __repr__(self): ...
There are dozens of special methods, but we will only look at a few specific examples.
Special methods for String Conversions
Objects have two string representations.
>>> from datetime import date >>> d = date(2012, 12, 21) >>> print(d) 2012-12-21 >>> d datetime.date(2012, 12, 21) >>>
The str()
function is used to create a nice printable output:
>>> str(d) '2012-12-21' >>>
The repr()
function is used to create a more detailed representation for programmers.
>>> repr(d) 'datetime.date(2012, 12, 21)' >>>
Those functions, str()
and repr()
, use a pair of special methods in the class to produce the string to be displayed.
class Date(object): def __init__(self, year, month, day): self.year = year self.month = month self.day = day # Used with `str()` def __str__(self): return f'{self.year}-{self.month}-{self.day}' # Used with `repr()` def __repr__(self): return f'Date({self.year},{self.month},{self.day})'
Note: The convention for __repr__()
is to return a string that, when fed to eval()
, will recreate the underlying object. If this is not possible, some kind of easily readable representation is used instead.
Special Methods for Mathematics
Mathematical operators involve calls to the following methods.
a + b a.__add__(b) a - b a.__sub__(b) a * b a.__mul__(b) a / b a.__truediv__(b) a // b a.__floordiv__(b) a % b a.__mod__(b) a << b a.__lshift__(b) a >> b a.__rshift__(b) a & b a.__and__(b) a | b a.__or__(b) a ^ b a.__xor__(b) a ** b a.__pow__(b) -a a.__neg__() ~a a.__invert__() abs(a) a.__abs__()
Special Methods for Item Access
These are the methods to implement containers.
len(x) x.__len__() x[a] x.__getitem__(a) x[a] = v x.__setitem__(a,v) del x[a] x.__delitem__(a)
You can use them in your classes.
class Sequence: def __len__(self): ... def __getitem__(self,a): ... def __setitem__(self,a,v): ... def __delitem__(self,a): ...
Method Invocation
Invoking a method is a two-step process.
- Lookup: The
.
operator - Method call: The
()
operator
>>> s = Stock('GOOG',100,490.10) >>> c = s.cost # Lookup >>> c <bound method Stock.cost of <Stock object at 0x590d0>> >>> c() # Method call 49010.0 >>>
Bound Methods
A method that has not yet been invoked by the function call operator ()
is known as a bound method. It operates on the instance where it originated.
>>> s = Stock('GOOG', 100, 490.10) >>> s <Stock object at 0x590d0> >>> c = s.cost >>> c <bound method Stock.cost of <Stock object at 0x590d0>> >>> c() 49010.0 >>>
Bound methods are often a source of careless non-obvious errors. For example:
>>> s = Stock('GOOG', 100, 490.10) >>> print('Cost : %0.2f' % s.cost) Traceback (most recent call last): File "<stdin>", line 1, in <module> TypeError: float argument required >>>
Or devious behavior that’s hard to debug.
f = open(filename, 'w') ... f.close # Oops, Didn't do anything at all. `f` still open.
In both of these cases, the error is cause by forgetting to include the trailing parentheses. For example, s.cost()
or f.close()
.
Attribute Access
There is an alternative way to access, manipulate and manage attributes.
getattr(obj, 'name') # Same as obj.name setattr(obj, 'name', value) # Same as obj.name = value delattr(obj, 'name') # Same as del obj.name hasattr(obj, 'name') # Tests if attribute exists
Example:
if hasattr(obj, 'x'): x = getattr(obj, 'x'): else: x = None
Note: getattr()
also has a useful default value arg*.
x = getattr(obj, 'x', None)
Exercises
Exercise 4.9: Better output for printing objects
Modify the Stock
object that you defined in stock.py
so that the __repr__()
method produces more useful output. For example:
>>> goog = Stock('GOOG', 100, 490.1) >>> goog Stock('GOOG', 100, 490.1) >>>
See what happens when you read a portfolio of stocks and view the resulting list after you have made these changes. For example:
>>> import report >>> portfolio = report.read_portfolio('Data/portfolio.csv') >>> portfolio ... see what the output is ... >>>
Exercise 4.10: An example of using getattr()
getattr()
is an alternative mechanism for reading attributes. It can be used to write extremely flexible code. To begin, try this example:
>>> import stock >>> s = stock.Stock('GOOG', 100, 490.1) >>> columns = ['name', 'shares'] >>> for colname in columns: print(colname, '=', getattr(s, colname)) name = GOOG shares = 100 >>>
Carefully observe that the output data is determined entirely by the attribute names listed in the columns
variable.
In the file tableformat.py
, take this idea and expand it into a generalized function print_table()
that prints a table showing user-specified attributes of a list of arbitrary objects. As with the earlier print_report()
function, print_table()
should also accept a TableFormatter
instance to control the output format. Here’s how it should work:
>>> import report >>> portfolio = report.read_portfolio('Data/portfolio.csv') >>> from tableformat import create_formatter, print_table >>> formatter = create_formatter('txt') >>> print_table(portfolio, ['name','shares'], formatter) name shares ---------- ---------- AA 100 IBM 50 CAT 150 MSFT 200 GE 95 MSFT 50 IBM 100 >>> print_table(portfolio, ['name','shares','price'], formatter) name shares price ---------- ---------- ---------- AA 100 32.2 IBM 50 91.1 CAT 150 83.44 MSFT 200 51.23 GE 95 40.37 MSFT 50 65.1 IBM 100 70.44 >>>
Defining Exceptions
User defined exceptions are defined by classes.
class NetworkError(Exception): pass
Exceptions always inherit from Exception
.
Usually they are empty classes. Use pass
for the body.
You can also make a hierarchy of your exceptions.
class AuthenticationError(NetworkError): pass class ProtocolError(NetworkError): pass
Exercises
Exercise 4.11: Defining a custom exception
It is often good practice for libraries to define their own exceptions.
This makes it easier to distinguish between Python exceptions raised in response to common programming errors versus exceptions intentionally raised by a library to a signal a specific usage problem.
Modify the create_formatter()
function from the last exercise so that it raises a custom FormatError
exception when the user provides a bad format name.
For example:
>>> from tableformat import create_formatter >>> formatter = create_formatter('xls') Traceback (most recent call last): File "<stdin>", line 1, in <module> File "tableformat.py", line 71, in create_formatter raise FormatError('Unknown table format %s' % name) FormatError: Unknown table format xls >>>