Skip to content
wannacode

Classes inside classes and data inside data

Problem

A class that only makes sense in one place

In the lesson about the linked list, Node was a separate class. Anyone in the program can make nodes, but they only make sense in the list. The same goes for an order line:

class OrderLine:
def __init__(self, product, quantity, price):
self.product = product
self.quantity = quantity
self.price = price
class Order:
def __init__(self):
self.lines = []

OrderLine without Order makes no sense. The name is long so it does not clash with the line of an invoice, InvoiceLine, or with the line of a warehouse. And nothing says that the two classes go together. Can the line live inside the order?

The idea

Nested class

It can. A class declared inside another class is called a nested class:

class Order:
class Line:
def __init__(self, product, quantity, price):
self.product = product
self.quantity = quantity
self.price = price
def total(self):
return self.quantity * self.price
def __init__(self):
self._lines = []
def add(self, line):
self._lines.append(line)
def total(self):
total = 0
for line in self._lines:
total += line.total()
return total
order = Order()
order.add(Order.Line("Coffee beans", 2, 12.5))
order.add(Order.Line("Mug", 4, 3.0))
print(f"Total: €{order.total()}")
Output
Total: €37.0
  • From the outside, the name is Order.Line. Order.Line(...) is made without an Order object, like a regular class.
  • Inside the methods of Order you also write Order.Line. Just Line is not visible there: the class is not a scope for its methods.
  • A nested class in Python is not tied to a specific order and does not see its attributes. In Java such a class is called static.
  • If it is _Line, by convention it says "for internal use only", like _lines.
class Order:
class Line:
pass
def make(self):
return Line()
try:
Order().make()
except NameError as e:
print(f"NameError: {e}")
print(type(Order.Line()).__qualname__)
Output
NameError: name 'Line' is not defined
Order.Line

In Python nested classes are less common than in Java. Often the small class just sits next to the big one, in the same file, or there is a dictionary instead of a class. That is what the next card is about.

The idea

Data inside data: dictionaries and lists

If the order line only holds values and has no methods, Python often has no class for it. The order is a dictionary that holds a list of dictionaries:

order = {
"customer": "Ivanova",
"lines": [
{"product": "Coffee beans", "quantity": 2, "price": 12.5},
{"product": "Mug", "quantity": 4, "price": 3.0},
],
}
total = 0
for line in order["lines"]:
total += line["quantity"] * line["price"]
print(order["lines"][0]["product"])
print(f"Total: €{total}")
Output
Coffee beans
Total: €37.0
  • order["lines"] is the list, order["lines"][0] is the first line, and order["lines"][0]["product"] is the product in it. Each [...] goes one level deeper.
  • The value in a dictionary can be anything: a number, text, a list, another dictionary.
  • A class is the better choice when the data has behavior (a total method) or rules that must be kept.

A dictionary of lists collects things into groups:

people = [("Ivan", "Sofia"), ("Maria", "Varna"), ("Ana", "Sofia")]
by_city = {}
for name, city in people:
if city not in by_city:
by_city[city] = []
by_city[city].append(name)
print(by_city)
print(by_city["Sofia"][1])
Output
{'Sofia': ['Ivan', 'Ana'], 'Varna': ['Maria']}
Ana
More precisely, if you are curious
  • A trap with nested lists: * 3 repeats the reference, it does not make three lists. The three rows are one and the same list:
grid = [[0] * 3] * 3
grid[0][0] = 1
print(grid)
grid = []
for row in range(3):
grid.append([0] * 3)
grid[0][0] = 1
print(grid)
Output
[[1, 0, 0], [1, 0, 0], [1, 0, 0]]
[[1, 0, 0], [0, 0, 0], [0, 0, 0]]

The idea

The nested object keeps the outer one

The cart has a discount for all products in it. Each product must know the discount of its own cart. In Python a nested class does not see the outer object, so the cart is passed explicitly:

class Cart:
class Item:
def __init__(self, cart, name, price):
self.cart = cart
self.name = name
self.price = price
def final_price(self):
return self.price * (100 - self.cart._discount_percent) / 100
def __init__(self, discount_percent):
self._discount_percent = discount_percent
self._items = []
def add(self, name, price):
self._items.append(Cart.Item(self, name, price))
def print_items(self):
for item in self._items:
print(f"{item.name}: €{item.final_price()}")
cart = Cart(10)
cart.add("Headphones", 80)
cart.add("Case", 20)
cart.print_items()
Output
Headphones: €72.0
Case: €18.0
Java has an inner class: each object of it keeps a hidden reference to the outer object. Python has no such thing, so the reference is visible: add passes self, and Item keeps it in self.cart. final_price reads the _discount_percent of the cart it is in. The underscore says "internal", and Item is inside Cart, so it is allowed.

cart = Cart(10)
cart.add("Headphones", 80)
cart.add("Case", 20)
cart.print_items()
Step by step, in words
  1. The cart is a Cart object with a 10% discount.
  2. add creates an Item and passes it the cart, self. Besides name and price, each Item keeps an arrow to its own cart in cart.
  3. The second Item points to the same cart.
  4. final_price of "Headphones" follows the arrow self.cart and reads _discount_percent: 80 * (100 - 10) / 100 = 72.0.
  5. final_price of "Case" follows the arrow self.cart and reads _discount_percent: 20 * (100 - 10) / 100 = 18.0.
More precisely, if you are curious
  • From the outside, an Item object is created with the cart as the first argument. Usually the outer class creates the inner ones itself, like add here:
cart = Cart(10)
cable = Cart.Item(cart, "Cable", 10)
print(cable.final_price())
Output
9.0
  • The cart field is exactly what the hidden Cart.this does in Java. Here you can simply see it.
  • If the nested class does not use the fields of the outer class, do not give it the outer object. That way it does not keep a reference it does not need.

Try it

Two carts

The Cart class is the same. One cart has no discount, and the other has 20% for club members.

What will the program print?

regular = Cart(0)
member = Cart(20)
regular.add("Book", 15)
member.add("Book", 15)
regular.print_items()
member.print_items()
Answer

Try it

Find the bug

The line should show the currency of the order. With print(Order.Line("Coffee", 12.5).show()) the program stops:
Error
NameError: name 'currency' is not defined

Which line has the problem?

Pick the line with the bug.

The idea

A class in a function

A class can also be declared in a function. Then it is only visible there and is called a local class:

def print_best_seller(names, sold):
class Sale:
def __init__(self, name, count):
self.name = name
self.count = count
best = Sale(names[0], sold[0])
for i in range(1, len(names)):
if sold[i] > best.count:
best = Sale(names[i], sold[i])
print(f"Best seller: {best.name} ({best.count} sold)")
names = ["Coffee", "Tea", "Water"]
sold = [120, 45, 300]
print_best_seller(names, sold)
Output
Best seller: Water (300 sold)

Sale keeps the name and the count together. Nobody uses it outside print_best_seller, so it lives there. Such classes are rare, but it is good to know them.

The idea

A function instead of a class

The discounts in the shop can be an abstract class with one method, as in the "Inheritance" module:

class Discount(ABC):
@abstractmethod
def apply(self, price):
pass

And a class for each discount: class BlackFriday(Discount): .... In Java, for a discount used in only one place, there is the anonymous class: a class with no name, written right there. Python has no anonymous classes. It does not need them: when the "class" has one method, in Python you pass a function:

def black_friday(price):
return price * 70 / 100
def no_discount(price):
return price
def checkout(price, discount):
return discount(price)
print(checkout(100, black_friday))
print(checkout(100, no_discount))
Output
70.0
100
  • A function is a value. checkout(100, black_friday) passes the function, with no parentheses, and checkout calls it with discount(price).
  • checkout does not ask what discount is. It only needs to be callable with a price.
  • no_discount(100) returns the number it got, so it is 100, not 100.0.
More precisely, if you are curious
  • In the "Modern Python" module you will see a shorter way to write small functions like no_discount, right where they are passed.
  • A function declared inside another function sees its variables. That makes functions even handier for such cases.

The idea

key: sorting by your own rule

sorted sorted text and numbers. But it cannot sort students:

class Student:
def __init__(self, name, grade):
self.name = name
self.grade = grade
def __repr__(self):
return f"{self.name} {self.grade}"
students = [
Student("Ivan", 5.50),
Student("Maria", 6.00),
Student("Peter", 4.75),
Student("Ana", 5.50),
]
sorted(students)
Error
TypeError: '<' not supported between instances of 'Student' and 'Student'

Python does not know what to sort students by: by name, by grade, by age? < between two Student objects is not defined.

The rule is given as a function in key. It gets one element and returns the value to compare by. sorted calls it for each student and sorts by the result:

def by_grade(student):
return student.grade
print(sorted(students, key=by_grade, reverse=True))
Output
[Maria 6.0, Ivan 5.5, Ana 5.5, Peter 4.75]
  • key=by_grade passes the function, with no parentheses. reverse=True reverses the order, so the highest grade comes first.
  • Ivan and Ana have the same grade and stay in the order they were in. sorted does not swap equal elements, even with reverse=True.
  • print of a list shows the elements with __repr__, which is why Student has it.
  • For text and numbers in reverse order, reverse=True is enough:
cities = ["Sofia", "Varna", "Plovdiv"]
print(sorted(cities, reverse=True))
Output
['Varna', 'Sofia', 'Plovdiv']
More precisely, if you are curious
  • students.sort(key=by_grade, reverse=True) sorts the list itself, and sorted returns a new one.
  • Text and numbers are sorted without key, because they have <. Your class can have a natural order too, if you write __lt__. key is for cases where there are several orders.
  • A compare function like compare(a, b) in Java is also possible, with functools.cmp_to_key. But in Python key is the usual way: it is shorter and faster.

Try it

Which one is first?

What will the program print?

products = [
Product("Monitor", 220),
Product("Mouse", 25),
Product("Keyboard", 60),
]
def by_price(product):
return product.price
for p in sorted(products, key=by_price):
print(p.name)
Answer

Exercise

Classes that go together

Solve the tasks in your editor.

1. Shopping cart. A ShoppingCart class with a nested class Item (name and price) and the methods add_item(item), get_total() and count(). Add ShoppingCart.Item("Milk", 2.5), bread for €1.20 and coffee for €7.50. Print the total with two digits after the point.

Output
Products: 3
Total: €11.20

2. Web server monitor. A WebServer class with a nested class Request (address and status code) and the methods add_request(r) and print_stats(). The stats say how many requests there are, how many there are of each status code (sorted) and what percent are successful, that is, with a code from 200 to 299. Keep the counts in a dictionary. The requests are /home 200, /cart 200, /login 401, /home 200, /api/orders 500.

Output
Requests: 5
200: 3
401: 1
500: 1
Successful: 60%

3. Building manager. A Building class with a nested class Apartment (number, number of rooms, owner) and the methods add_apartment(a) and get_apartment_info(number). Keep the apartments in a dictionary from number to apartment. Add 101 with 3 rooms owned by Ivanova and 102 with 2 rooms owned by Petrov. Look up 101 and 999.

Output
Apartment 101: 3 rooms, owner Ivanova
There is no apartment 999.

4. Ranking. Sort the students from the lesson by grade, from the highest down. When the grades are equal, sort them by name, in alphabetical order. It all goes in one key function: let it return a tuple (-grade, name). Tuples are compared by the first element, and when that is equal, by the second. The minus reverses the order of the grade only.

Output
Maria 6.0
Ana 5.5
Ivan 5.5
Peter 4.75

More tasks

  • Computer configurator. A Computer class with the nested classes CPU (brand, speed in GHz), RAM (GB) and Storage (GB) and the methods set_cpu, set_ram, set_storage. __str__ describes the configuration: Intel 3.5 GHz, 16 GB RAM, 512 GB.
  • Trip planner, again. Do the task from the lesson about dictionaries once with a nested class TripPlanner.Destination that keeps the name and the list of activities, and once with just a dictionary of lists. Which one is handier?
  • Contact network, again. Move Contact from the task in the lesson about dictionaries inside ContactNetwork. Should Contact know its network?
  • Mental math, again. Move Question from the task in the lesson about lists inside MathGame.
  • Products in two ways. Sort a list of products by price from the most expensive, and by name when the price is the same. Then sort the same list by name only, in reverse order.
  • Your own discount. Write three discount functions: 10% over €100, a fixed €5 off and "second product at half price". Apply them to one and the same cart with one and the same checkout.

Test

Check yourself

1. class Line is inside class Order. Does Line see the attributes of a specific order?
2. Why does show() of Order.Line stop with NameError: name 'currency' is not defined?
3. What does Python use instead of an anonymous class with one method, like the discount?
4. What does sorted(students, key=by_grade) do?
5. Which one sorts the students by grade, from the highest down?
6. grid = [[0] * 3] * 3 and then grid[0][0] = 1. What is in grid?

Made by Ivo Gergov as part of Evo Tech's campaign for accessible programming education ·