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 totalorder = Order()order.add(Order.Line("Coffee beans", 2, 12.5))order.add(Order.Line("Mug", 4, 3.0))print(f"Total: €{order.total()}")Total: €37.0- From the outside, the name is
Order.Line.Order.Line(...)is made without anOrderobject, like a regular class. - Inside the methods of
Orderyou also writeOrder.Line. JustLineis 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__)NameError: name 'Line' is not definedOrder.LineIn 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 = 0for line in order["lines"]: total += line["quantity"] * line["price"]print(order["lines"][0]["product"])print(f"Total: €{total}")Coffee beansTotal: €37.0order["lines"]is the list,order["lines"][0]is the first line, andorder["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
totalmethod) 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]){'Sofia': ['Ivan', 'Ana'], 'Varna': ['Maria']}AnaMore precisely, if you are curious
- A trap with nested lists:
* 3repeats the reference, it does not make three lists. The three rows are one and the same list:
grid = [[0] * 3] * 3grid[0][0] = 1print(grid)
grid = []for row in range(3): grid.append([0] * 3)grid[0][0] = 1print(grid)[[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()Headphones: €72.0Case: €18.0add 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
- The cart is a Cart object with a 10% discount.
- 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.
- The second Item points to the same cart.
- final_price of "Headphones" follows the arrow self.cart and reads _discount_percent: 80 * (100 - 10) / 100 = 72.0.
- 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
Itemobject is created with the cart as the first argument. Usually the outer class creates the inner ones itself, likeaddhere:
cart = Cart(10)cable = Cart.Item(cart, "Cable", 10)print(cable.final_price())9.0- The
cartfield is exactly what the hiddenCart.thisdoes 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()Each Item sees the discount of the cart that created it. The book in regular has 0%, and the one in member has 20%.
Try it
Find the bug
print(Order.Line("Coffee", 12.5).show()) the program stops:NameError: name 'currency' is not definedWhich line has the problem?
Pick the line with the bug.
currency is an attribute of an Order object, and Line has no order. Python looks for a bare name currency in the method and in the whole file and does not find it. The error only shows up when show is called, not earlier. The fix is for Line to get the order, Order.Line(order, "Coffee", 12.5), and to read self.order.currency. Another way is for currency to be a class attribute, Order.currency, if the currency is the same for all orders.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)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): passAnd 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))70.0100- A function is a value.
checkout(100, black_friday)passes the function, with no parentheses, andcheckoutcalls it withdiscount(price). checkoutdoes not ask whatdiscountis. It only needs to be callable with a price.no_discount(100)returns the number it got, so it is100, not100.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)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))[Maria 6.0, Ivan 5.5, Ana 5.5, Peter 4.75]key=by_gradepasses the function, with no parentheses.reverse=Truereverses the order, so the highest grade comes first.- Ivan and Ana have the same grade and stay in the order they were in.
sorteddoes not swap equal elements, even withreverse=True. printof a list shows the elements with__repr__, which is whyStudenthas it.- For text and numbers in reverse order,
reverse=Trueis enough:
cities = ["Sofia", "Varna", "Plovdiv"]print(sorted(cities, reverse=True))['Varna', 'Sofia', 'Plovdiv']More precisely, if you are curious
students.sort(key=by_grade, reverse=True)sorts the list itself, andsortedreturns 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__.keyis for cases where there are several orders. - A compare function like
compare(a, b)in Java is also possible, withfunctools.cmp_to_key. But in Pythonkeyis 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)by_price returns the price, and sorted sorts from the smallest: 25, 60, 220. To put the most expensive first, add reverse=True.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.
Products: 3Total: €11.202. 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.
Requests: 5200: 3401: 1500: 1Successful: 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.
Apartment 101: 3 rooms, owner IvanovaThere 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.
Maria 6.0Ana 5.5Ivan 5.5Peter 4.75More tasks
- Computer configurator. A
Computerclass with the nested classesCPU(brand, speed in GHz),RAM(GB) andStorage(GB) and the methodsset_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.Destinationthat keeps the name and the list of activities, and once with just a dictionary of lists. Which one is handier? - Contact network, again. Move
Contactfrom the task in the lesson about dictionaries insideContactNetwork. ShouldContactknow its network? - Mental math, again. Move
Questionfrom the task in the lesson about lists insideMathGame. - 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