Your own type
Problem
The prices got mixed up
You are writing the shopping cart of an online store. Every product has a name and a price, so you keep them in two lists. Index 0 holds the name and price of the first product, index 1 those of the second.
You want the most expensive one first, so you swap the prices:
names = ["Bread", "Milk", "Coffee"]prices = [1.20, 2.10, 7.50]
# Most expensive first: swap the first and the third price.prices[0], prices[2] = prices[2], prices[0]
for i in range(len(names)): print(f"{names[i]}: €{prices[i]}")Bread: €7.5Milk: €2.1Coffee: €1.2Bread now costs €7.50. The prices are swapped, but the names are not. The two lists must always change together, but nothing makes you do it. Add a third list for the quantities, and you have to keep three in mind.
The idea
One product, one place
The name and the price are not separate things. They are parts of one product. We want a "product" type, just like there is int for numbers and str for text.
You make a type like that yourself. It is called a class:
class Product: def __init__(self): self.name = "" self.price = 0.0 self.quantity = 0self.name, self.price and self.quantity are the fields of the product (in Python they are also called attributes). Every product will have its own name, its own price and its own quantity.
In Python you do not list the fields separately. You create them with a starting value in __init__: empty text, 0.0 and 0. For now, write the line def __init__(self): exactly like this. The code under it runs for every new product, and self is that product. You will understand it in detail in the lesson "Constructors and self".
The class is only a description: "every product has a name, a price and a quantity". There is no product yet.
The name of the class starts with a capital letter (Product), and the names of the fields with a lowercase one (name, price). This way you see at once what is a type and what is a value.
The idea
Product(): from the description to a product
You create a concrete product from the class by calling the class like a function: Product(). It is called an object. You reach its fields with a dot:
class Product: def __init__(self): self.name = "" self.price = 0.0 self.quantity = 0
bread = Product()bread.name = "Bread"bread.price = 1.20bread.quantity = 2
coffee = Product()coffee.name = "Coffee"coffee.price = 7.50coffee.quantity = 1
print(f"{bread.name}: €{bread.price * bread.quantity}")print(f"{coffee.name}: €{coffee.price * coffee.quantity}")Bread: €2.4Coffee: €7.5- The class is above the code that uses it. Python reads the file from top to bottom and must have seen it already. A file can have several classes.
- Python has no
new.Product()creates a new object and returns it. Every object has its own fields: the bread and the coffee do not get in each other's way. - You do not write a type in front of
bread, but the object still has a type:type(bread)gives<class '__main__.Product'>. bread.priceis "the price of the bread". You read it and write it like an ordinary variable.
Now the name and the price of the product are in one place. Move the product, and they move together.
Try it
Nothing is filled in
If you create a product and fill in nothing, what is in its fields?
What will the program print?
item = Product()print(item.name)print(item.price)print(item.quantity)In Python, fields have no "default" value. They have exactly what __init__ gave them: empty text (printed as an empty line), 0.0 and 0. A field that __init__ does not create simply is not there: item.color stops the program with AttributeError: 'Product' object has no attribute 'color'.
Try it
What is missing?
What goes in the blank so that there is a new product?
milk = ()
milk.name = "Milk"
milk.price = 2.10Product: the name of the class, called with parentheses, creates an object. Python has no new.See it
The variable points to the object
You want a sale on coffee, so you make a "copy" to change its price:
coffee = Product()coffee.price = 10sale = coffeesale.price = 8print(coffee.price) # 8Step by step, in words
- Product() makes the object, and __init__ gives its fields starting values. coffee gets an arrow to it.
- Follow the arrow from coffee: the price of the object becomes 10.
- sale = coffee copies the arrow, not the object. Two arrows, one object.
- Follow the arrow from sale: the price in the same object becomes 8.
- coffee points to the same object, so it prints 8.
The price of the coffee became 8 too. The reason is that the variable coffee is not the object itself. The object lives separately in memory, and coffee keeps where it is: an arrow to the object. A variable like that is called a reference.
sale = coffee copies the arrow, not the object. Now two arrows point to one object, so the change through sale shows through coffee too.
You saw the same thing in the lesson on lists: sale = prices gave a second name to the same list. A list is an object too.
The idea
The discount works now
In the "Functions" module, the discount did not work: the function got the price, changed its own parameter, and the change was lost. With an object it is different:
def discount(p, percent): p.price = p.price * (100 - percent) / 100
coffee = Product()coffee.price = 10discount(coffee, 10)print(coffee.price)9.0The parameter is still a copy, but a copy of the arrow. It points to the same object, so the change to p.price stays after the function too.
More precisely, if you are curious
If the function puts a new object in the parameter, it only moves its own copy of the arrow. The object outside does not change:
def reset(p): p = Product() p.price = 0
coffee = Product()coffee.price = 10reset(coffee)print(coffee.price)10= does: the parameter gets an arrow to the same object. If the function points its arrow somewhere else, that does not reach the variable outside.The idea
None: an arrow to nowhere
A variable may point to no object at all. Then it holds None: "nothing". If you try to read a field through it, there is no object to read it from:
gift = Noneprint(gift.name)The program starts, but it stops with an error when it reaches the line:
AttributeError: 'NoneType' object has no attribute 'name'This is the most common mistake with objects. What an "exception" is and how you catch one, you will see in the module "Errors and exceptions".
A list also holds arrows, not the products themselves. [None] * 3 makes three places that do not point to a product yet. Each one is None until you put Product() there:
cart = [None] * 3print(cart[0] is None)cart[0] = Product()cart[0].name = "Bread"print(cart[0].name)TrueBreadTry it
Find the bug
The program should print "Bread" and "Milk". Instead, it prints "Milk" twice. Which line has the bug?
Pick the line with the bug.
milk = bread does not make a new product. Now milk and bread point to the same object, and the name "Milk" replaces "Bread". The fix is milk = Product().
Exercise
Your classes
1. Book. A class Book with fields for the title and the author. Create two books and print them.
Under the Yoke by Ivan VazovTobacco by Dimitar Dimov2. Car. A class SimpleCar with a field for the speed. Create a car (its speed is 0), increase the speed by 10 and print it. In the lesson "Behavior" this will become a method of the car.
Speed after accelerating: 103. The cart. The cart from the first card, this time with a list of three Product objects: bread €1.20 (2 pieces), milk €2.10 (1 piece), coffee €7.50 (1 piece). Print the amount for each line and the total. Create each product with Product() first, then put it in the list.
Bread x2: €2.4Milk x1: €2.1Coffee x1: €7.5Total: €12.04. Most expensive first. Sort the same cart by price, from the most expensive to the cheapest, with the bubble sort from the lesson on lists. This time you swap whole products, so the name and the price move together.
Coffee: €7.5Milk: €2.1Bread: €1.2More tasks
- Student. A class
Studentwith a name and three grades (three fields). Create two students and print the average grade of each. - One book, two names. Create a book, store it in two variables and change the title through the second one. Print the title through the first one. Explain in words why it works this way.
Test