Objects, names and identity
A name refers to an object; it isn't the object. Two names can share one object, a change through either shows through both, and two objects with the same data are still two objects.
- Explain why changing an object through one name shows through every name that refers to it
- Tell
is(the same object) from==, which on a plain class also means the same object - Make a separate object with the same data when a change must not reach the original
same = order doesn't copy the order. It makes same a second name for the one object order already refers to, just as with lists in the Lists & Tuples module. Change an attribute through either name and the other sees it, because there is only one object.
The same is true when an object is stored in a list or passed to a function: the list and the function's parameter refer to the original. That is useful (a function can update an order's status) and dangerous (a function you expected to leave things alone can change them).
a is b asks whether two names refer to the same object. a == b asks whether they are equal, and for a class you write yourself, Python doesn't know what equal should mean, so it falls back to identity: two separate orders with identical data are not ==. (the Data Models & Typing module shows how a class can define its own equality.)
Run each example to see what it prints. Running examples doesn't count toward progress.
class Order: def __init__(self, order_id, status, total): self.order_id = order_id self.status = status self.total = total order = Order("O100282", "paid", 35.84) same = order same.status = "shipped" print(order.status, order is same) copy_of_data = Order("O100282", "shipped", 35.84) print(order == copy_of_data, order is copy_of_data) print(order.total == copy_of_data.total)
class Order: def __init__(self, order_id, status, total): self.order_id = order_id self.status = status self.total = total order = Order("O100918", "returned", 55.72) print(order.status) print(order.totl)
When a change must not reach the original (a price simulation, an undo snapshot), make a separate object: call the class again with the old object's attribute values (and the one you want to change). The new object has its own attributes, so changing them leaves the original alone. If an attribute holds a list, pass list(old.items) so the two objects don't end up sharing it.
class Product: def __init__(self, sku, name, price): self.sku = sku self.name = name self.price = price def sale_prices(products, rate): sale = [] for product in products: cheaper = Product(product.sku, product.name, round(product.price * (1 - rate), 2)) sale.append(cheaper) return sale catalogue = [Product("SPO-11681", "Yoga mat", 31.0), Product("HOM-22847", "Desk lamp", 48.0)] on_sale = sale_prices(catalogue, 0.25) for before, after in zip(catalogue, on_sale): print(before.sku, before.price, "->", after.price, before is after)
When to use it: whenever a function receives objects, decide whether it is allowed to change them, and say so in its name or docstring (mark_shipped changes; sale_prices returns new ones).
Mistakes to avoid: new_list = list(products) and expecting new products (it copies the list, not the objects in it); comparing objects with == and expecting their data to be compared; assigning to a misspelt attribute name; and changing an object you received when the caller expects it unchanged.
- A name refers to an object; assignment, lists and function calls share it rather than copy it.
ismeans the same object; on a plain class==means the same thing.- To keep an original unchanged, make a new object from its attribute values.
- Next: a constructor that refuses invalid data, so every object that exists is valid.