Working with objects
Part of the free LogicWiz Python course, Module 9: Object-oriented programming. The examples and practice run in the app.
Shankar bhaiya wants the menu sorted by price. Meher typed sorted(menu) and Python shouted TypeError: '<' not supported between instances of 'Item' and 'Item'. Python doesn't know what "smaller" means for a dish, until you tell it.
You'll learn to
- Keep many objects in a list and loop over them
- Tell class attributes from instance attributes (and dodge the shared-list trap)
- Make objects comparable and sortable with `__eq__` and `__lt__`
- Write alternative constructors with `@classmethod`, and helpers with `@staticmethod`
Learn
A menu is a list of objects
class Item:
def __init__(self, name, price):
self.name = name
self.price = price
menu = [Item("Samosa", 15), Item("Chai", 10), Item("Dosa", 60)]
for it in menu:
print(it.name, it.price)
cheap = [it.name for it in menu if it.price < 20]
print(cheap)
print(sum(it.price for it in menu))
print(max(menu, key=lambda it: it.price).name)
key=lambda it: it.price tells max what to compare: the price of each item. Everything you learned about lists still works; the items are now objects.
Class attributes vs instance attributes
A name assigned in the class body belongs to the class: there is one copy, shared by every object. A name assigned with self.x = ... belongs to that one object.
When you read obj.x, Python looks on the object first, then on the class. When you assign obj.x = ..., Python always writes on the object, so it can hide ("shadow") the class value for that one object.
Watch the lookup order
class Item:
gst = 5 # class attribute: one shared copy
count = 0
def __init__(self, name):
self.name = name # instance attribute: one per object
Item.count += 1 # update the shared counter
a = Item("Samosa")
b = Item("Chai")
print(a.gst, b.gst, Item.count)
a.gst = 12 # makes a NEW attribute on a only
print(a.gst, b.gst, Item.gst)
The shared-list trap. class Order: items = [] gives every order the same list, so a chai added to one order shows up in all of them. Create mutable things per object inside __init__: self.items = [].
Also, self.count += 1 does not update a class counter. It reads the class value, then writes a new attribute on the object. Write Item.count += 1.
__eq__ and __lt__: teaching Python to compare
class Item:
def __init__(self, name, price):
self.name = name
self.price = price
def __eq__(self, other):
return self.name == other.name and self.price == other.price
def __lt__(self, other):
return self.price < other.price
def __repr__(self):
return f"{self.name}({self.price})"
a = Item("Chai", 10)
b = Item("Chai", 10)
print(a == b, a is b)
menu = [Item("Dosa", 60), a, Item("Samosa", 15)]
print(sorted(menu))
print(min(menu))
Without __eq__, == falls back to is ("same object?"), so a == b would be False. sorted, min and max only need __lt__.
@classmethod and @staticmethod
A normal method gets the object as self.
A @classmethod gets the class as cls. Its classic use is an alternative constructor: build an object from a CSV line or a dict. Because it calls cls(...), it also works for subclasses.
A @staticmethod gets neither. It is a plain function kept inside the class because it belongs there, like a price check.
Building an Item from a line of text
class Item:
def __init__(self, name, price):
self.name = name
self.price = price
@classmethod
def from_line(cls, line):
name, price = line.split(",")
return cls(name.strip(), int(price))
@staticmethod
def valid_price(p):
return 0 < p <= 1000
def __repr__(self):
return f"Item({self.name!r}, {self.price})"
print(Item.from_line("Vada Pav, 20"))
print(Item.valid_price(20), Item.valid_price(-5))
Three kinds of method
| Kind | First parameter and typical use |
|---|---|
| Instance method | self, the object. Reads or changes this object (total, discount) |
| @classmethod | cls, the class. Alternative constructors (fromline), class-wide counters |
| @staticmethod | Nothing extra. A helper that belongs with the class (validprice) |
In one line
Class attributes are shared, self.x is per object; __eq__/__lt__ make objects comparable and sortable; @classmethod gets cls (alternative constructors), @staticmethod gets nothing.
Interview corner
What is the difference between `@classmethod` and `@staticmethod`?
A classmethod receives the class (cls) and is used for alternative constructors or class-wide state; because it calls cls(...), it works correctly for subclasses. A staticmethod receives nothing implicit; it is a plain function namespaced inside the class. A normal method receives the instance (self).
What is the difference between a class attribute and an instance attribute?
A class attribute is defined in the class body and shared by all instances; an instance attribute is set on self and belongs to one object. Reading obj.x checks the instance first, then the class. Assigning obj.x = ... always creates or changes the instance attribute, which shadows the class one. Never use a mutable class attribute (like a list) for per-object data.
How do you make your objects sortable?
Define __lt__ (and usually __eq__), or decorate with functools.total_ordering to get the other comparisons from __eq__ plus one of them. Alternatively, leave the class alone and pass key= to sorted: sorted(menu, key=lambda i: i.price).
What happens to `__hash__` when you define `__eq__`?
Python sets __hash__ to None, so the objects can no longer be dict keys or set members (you get TypeError: unhashable type). If the object is effectively immutable, define __hash__ too, e.g. return hash((self.name, self.price)).