Write your own class, build two objects from it, and read what self means on every line.
A class is a template for making objects. An object holds data and the functions that work on that data in the same place, so a.balance and a.deposit(50) belong to one thing.
I build two classes here. First an account with a balance and a deposit. Then a portfolio that stores its holdings and its prices and works out its own value and weights.
Step 1. An account that keeps its own balance
class Account: # the class is the templatedef__init__(self, start): # runs once, automatically, when an object is madeself.balance = start # store the starting value on this objectdef deposit(self, amount): # a method is a function that lives in the classself.balance =self.balance + amounta = Account(100) # two objects from the same templateb = Account(500) # each one carrying its own dataa.deposit(50) # changes aprint(a.balance) # -> 150print(b.balance) # -> 500print(type(a)) # -> <class '__main__.Account'>
150
500
<class '__main__.Account'>
a and b come from the same four lines of class and hold different numbers.
Step 2. The five words
Class.Account is the template. On its own it holds no data, the way a blueprint is not a house.
Object.a is one account built from the template. Account(100) builds it.
Attribute.a.balance is data stored on the object. Read it with a dot.
Method.deposit() is a function stored in the class. Call it with a dot and brackets.
self. The object the method is running on right now.
Step 3. Reading self out loud
Translate each line of the class into what it means for one object.
Inside the class I wrote self.balance = start. The object being built is a and start is 100, so that line runs as a.balance = 100.
Then I wrote a.deposit(50). Python turns that into Account.deposit(a, 50), putting the object in front as the first argument. Those two arguments land on the two parameters of def deposit(self, amount), so self becomes a and amount becomes 50. The body self.balance = self.balance + amount therefore runs as a.balance = a.balance + 50, which took a.balance from 100 to 150.
b never appears in that call, so b.balance stayed at 500.
Write the long form yourself and the deposit lands the same way:
Account.deposit(a, 50) # the same call as a.deposit(50), with self spelled outprint(a.balance) # -> 200
200
Step 4. The same job without a class
def deposit(balance, amount): # a plain function takes a number and returns a numberreturn balance + amountbalance =100balance = deposit(balance, 50) # I store the result myselfprint(balance) # -> 150
150
deposit(balance, 50) hands back a number and I choose where to keep it. a.deposit(50) changes the balance already sitting on a. With two hundred accounts, the class keeps two hundred balances and I never pass any of them around.
Step 5. A portfolio that knows its own holdings
A portfolio is a number of shares per ticker and a price per ticker: two dictionaries, keyed the same way. value() multiplies them and adds up the positions. weights() divides each position by that total.
class Portfolio:def__init__(self, holdings, prices):self.holdings = holdings # ticker to number of sharesself.prices = prices # ticker to price per sharedef value(self): # a method can take no arguments but selfreturnsum(self.holdings[t] *self.prices[t] for t inself.holdings)def weights(self): total =self.value() # a method can call another method on selfreturn {t: self.holdings[t] *self.prices[t] / total for t inself.holdings}def set_price(self, ticker, price):self.prices[ticker] = price # changes the prices on this portfoliop = Portfolio({"AAPL": 10, "MSFT": 5, "NVDA": 20}, {"AAPL": 185.00, "MSFT": 410.00, "NVDA": 130.00})q = Portfolio({"AAPL": 40}, {"AAPL": 185.00})print(p.value()) # -> 6500.0print(q.value()) # -> 7400.0for t, w in p.weights().items():print(t, round(w, 3))# -> AAPL 0.285# -> MSFT 0.315# -> NVDA 0.4print(round(sum(p.weights().values()), 10)) # -> 1.0
6500.0
7400.0
AAPL 0.285
MSFT 0.315
NVDA 0.4
1.0
p.value() reads as Portfolio.value(p), so inside the method self.holdings is p’s holdings and self.prices is p’s prices. Change one price and only that portfolio moves:
p.set_price("NVDA", 143.00) # 20 NVDA shares, now 143.00 eachprint(p.value()) # -> 6760.0print(q.value()) # -> 7400.0 <- q keeps its own prices
6760.0
7400.0
Step 6. Many objects become a table
One object with a date on it is one row. Give a class a date, a ticker and a close, build three objects, and they line up as a table.
import pandas as pdclass StockDay: # one object is one day of tradingdef__init__(self, date, ticker, close):self.date = date # each argument saved as an attributeself.ticker = tickerself.close = closeobs = [ StockDay("2024-01-02", "AAPL", 105.0), StockDay("2024-01-03", "AAPL", 103.0), StockDay("2024-01-04", "AAPL", 108.0),]print(obs[0].close) # -> 105.0 <- attribute on one objectdf = pd.DataFrame([{"Date": o.date, "Ticker": o.ticker, "Close": o.close} for o in obs])print(df.shape) # -> (3, 3)print(df)# -> Date Ticker Close# -> 0 2024-01-02 AAPL 105.0# -> 1 2024-01-03 AAPL 103.0# -> 2 2024-01-04 AAPL 108.0
105.0
(3, 3)
Date Ticker Close
0 2024-01-02 AAPL 105.0
1 2024-01-03 AAPL 103.0
2 2024-01-04 AAPL 108.0
The attributes became columns. df is an object too, built from the DataFrame class, so df.shape is an attribute on it and df.head() is a method on it, in the same way a.balance and a.deposit() sit on Account.
Your turn
Add a withdraw(self, amount) method to Account. When the amount is larger than the balance, make it print a message and leave the balance alone.
TipShow answer
class Account:def__init__(self, start):self.balance = startdef deposit(self, amount):self.balance =self.balance + amountdef withdraw(self, amount):if amount >self.balance: # check before changing anythingprint("insufficient funds")return# a bare return stops here and changes nothingself.balance =self.balance - amounta = Account(100)a.withdraw(30)print(a.balance) # -> 70a.withdraw(500) # -> insufficient fundsprint(a.balance) # -> 70 <- unchanged
NoteAttribute or column?
df["Close"] is a column of data. df.shape, df.columns and df.index are facts about the DataFrame object.
df.Close also reaches the column, so the two spellings look alike. Use df["Close"]. It still works when a column is called count, which collides with a DataFrame method, or when a column name contains a space.
self.balance uses a dot rather than self["balance"] because square brackets only work on objects written to support them. Dictionaries and DataFrames are. Account is not.
NoteDoes it have to be called self?
Python matches parameters by position, not by name, so this runs:
class Account2:def__init__(the_object, start): the_object.balance = startprint(Account2(10).balance) # -> 10
10
The convention is self. The other parameter names are free: start, initial_balance.