Python OOP part 1: classes, objects, self, constructors and variable types
Advertisement
Until now you have probably written programs as a sequence of functions that pass data around. Object-oriented programming (OOP) bundles the data and the functions that work on it into one unit, called an object. A bank account, a student, a shopping cart and a game character each have data (balance, name, items, health) and behaviour (deposit, enrol, add, move). OOP lets you describe that once, as a class, and create as many objects as you like. This first part covers classes and objects, self, constructors, the three kinds of variables, the three kinds of methods, and how Python cleans up objects. The code was run on Python 3.9.
What is a class?
A class is a blueprint: it describes what data its objects hold and what they can do. It uses no memory for any particular student; it is the plan every student object follows. A class is created with the class keyword, and by convention its name uses CapitalWords.
class Student:
"""A student with a name and marks."""
pass
print(Student, type(Student), Student.__name__)
# <class '__main__.Student'> <class 'type'> Student
This empty class does nothing yet, but it already exists. In Python even classes are objects: type(Student) is type.
What is an object?
An object (also called an instance) is one concrete thing built from the class. You create it by calling the class like a function:
s1 = Student()
s2 = Student()
print(type(s1), s1 is s2, isinstance(s1, Student))
# <class '__main__.Student'> False True
s1 and s2 are two different objects of the same class. An object has three parts: an identity (id(s1), its address in memory), a type (its class) and its state (the values of its variables).
Reference variables
The names s1 and s2 are reference variables: they do not contain the object, they point to it. Assigning one to another copies the pointer, not the object:
s3 = s1
print(s3 is s1, id(s3) == id(s1)) # True True
s3.course = "Python"
print(s1.course) # Python
Because s3 and s1 refer to the same object, a change through one is visible through the other. This is the same behaviour you saw with lists.
self and the constructor
Real classes need data. The special method __init__ is the constructor: Python calls it automatically right after creating an object, so it is the place to set up the object's starting data.
class Student:
def __init__(self, name, marks):
self.name = name
self.marks = marks
def grade(self):
return "pass" if self.marks >= 40 else "fail"
def __repr__(self):
return f"Student({self.name!r}, {self.marks})"
s1 = Student("Asha", 82)
s2 = Student("Ravi", 35)
print(s1, s2.grade(), s1.name, s2.marks)
# Student('Asha', 82) fail Asha 35
self is the first parameter of every instance method. It is the object the method is working on. When you write s1.grade(), Python calls Student.grade(s1), so self is s1:
print(Student.grade(s2), s2.grade()) # fail fail
The name self is a strong convention, not a keyword, but always use it. You never pass self yourself. Variables attached to self (like self.name) belong to that one object. Without self., a name inside the constructor would be a plain local variable that disappears when __init__ returns.
__repr__ returns the text shown when you print the object; without it you would see something like <__main__.Student object at 0x...>.
Constructor versus method
Constructor (__init__) |
Method | |
|---|---|---|
| Name | always __init__ |
any name |
| Runs | automatically, once, when the object is created | only when you call it |
| Purpose | set up the initial state | perform operations on the state |
| Returns | must return None |
can return any value |
A class has only one __init__; Python has no constructor overloading. Use default values to allow several call styles:
class Point:
def __init__(self, x=0, y=0):
self.x, self.y = x, y
print(Point().x, Point(3).x, Point(3, 4).y) # 0 3 4
Types of variables
Variables in a class live at three levels, depending on where you create them.
Instance variables (object level)
Created with self.name = .... Every object has its own copy. Use them for data that differs from one object to the next, such as a student's name.
class Counter:
def __init__(self):
self.value = 0
a, b = Counter(), Counter()
a.value += 5
print(a.value, b.value) # 5 0
Python lets you add instance variables later, in any method, or even from outside (a.extra = 1), but defining them all in __init__ keeps the class easy to understand.
Static (class-level) variables
Created inside the class body, outside any method. There is one copy, shared by all objects. Use them for shared facts or counters.
class Student:
school = "Green Valley"
count = 0
def __init__(self, name):
self.name = name
Student.count += 1
a, b = Student("Asha"), Student("Ravi")
print(Student.count, a.school, Student.school) # 2 Green Valley Green Valley
Student.school = "Blue Hills"
print(a.school, b.school) # Blue Hills Blue Hills
Read a class variable with the class name (Student.count) so it is obvious where it lives. A trap: assigning through an object creates a new instance variable that hides the class one for that object only.
a.school = "Own School"
print(a.school, b.school, Student.school) # Own School Blue Hills Blue Hills
print("school" in a.__dict__, "school" in b.__dict__) # True False
Each object keeps its instance variables in a dictionary called __dict__, which makes this easy to inspect.
Local variables (method level)
Created inside a method and visible only there. They disappear when the method returns:
class Calc:
def add(self, a, b):
total = a + b # local variable
return total
print(Calc().add(2, 3)) # 5
| Variable | Created in | Shared? | Accessed as |
|---|---|---|---|
| Instance | __init__ / methods, via self |
one per object | obj.name |
| Static / class | class body, or ClassName.x = ... |
one for the class | ClassName.x |
| Local | inside a method | no (temporary) | by name, inside the method |
Types of methods
A class can contain three kinds of methods. They differ in what their first parameter is.
class Student:
school = "Green Valley"
def __init__(self, name, marks):
self.name, self.marks = name, marks
def grade(self): # instance method
return "pass" if self.marks >= 40 else "fail"
@classmethod
def change_school(cls, new_name): # class method
cls.school = new_name
@classmethod
def from_text(cls, text): # alternative constructor
name, marks = text.split(",")
return cls(name, int(marks))
@staticmethod
def is_valid_mark(m): # static method
return 0 <= m <= 100
s = Student.from_text("Asha,82")
Student.change_school("Blue Hills")
print(s.grade(), s.school, Student.is_valid_mark(120))
# pass Blue Hills False
| Kind | Decorator | First parameter | Can use | Typical use |
|---|---|---|---|---|
| Instance | none | self (the object) |
instance and class variables | anything about one object |
| Class | @classmethod |
cls (the class) |
class variables | change shared data; alternative constructors |
| Static | @staticmethod |
none | neither | a utility that belongs with the class but needs no data |
A static method is just a function that lives inside a class for organisation. A class method receives the class, so cls(name, int(marks)) still works if someone later makes a subclass.
Getters, setters and properties
Because anyone can write s.marks = -500, classes often protect their data. Python's way is the @property decorator, which makes a method look like an attribute:
class Student:
def __init__(self, marks):
self.marks = marks # goes through the setter below
@property
def marks(self):
return self._marks
@marks.setter
def marks(self, value):
if not 0 <= value <= 100:
raise ValueError("marks must be between 0 and 100")
self._marks = value
s = Student(80)
s.marks = 95
print(s.marks) # 95
try:
s.marks = 120
except ValueError as e:
print(e) # marks must be between 0 and 100
The leading underscore in _marks is a convention that means "internal; do not touch from outside". Python does not enforce privacy, but the convention is respected. A double underscore (__marks) triggers name mangling, which makes accidental access harder.
Garbage collection
You never free memory yourself. Python counts the references to every object, and when the count reaches zero the object is removed automatically. This is reference counting, with an extra collector for objects that refer to each other in circles.
import sys, gc
class Thing: pass
t = Thing()
u = t
print(sys.getrefcount(t) - 1) # 2 (t and u; getrefcount counts its own argument)
del u
print(sys.getrefcount(t) - 1) # 1
print(gc.isenabled()) # True
gc.collect() forces a collection pass and returns the number of unreachable objects it found; you rarely need it.
Destructors
The special method __del__ is the destructor. Python calls it just before an object is destroyed:
class Resource:
def __init__(self, name):
self.name = name
print("opened", name)
def __del__(self):
print("closed", self.name)
r = Resource("db")
r2 = r
del r
print("still alive")
del r2
print("end")
opened db
still alive
closed db
end
del r only removed one name; the object lived on because r2 still pointed to it. It was destroyed when the last reference went. Do not rely on __del__ for important clean-up: the timing is not guaranteed on every Python implementation, and exceptions inside it are ignored. For files, connections and locks use a with statement (see file handling) or a try/finally block.
Putting it together: a small bank account
class Account:
interest_rate = 0.04
accounts = 0
def __init__(self, owner, balance=0):
self.owner = owner
self.balance = balance
Account.accounts += 1
def deposit(self, amount):
if amount <= 0:
raise ValueError("deposit must be positive")
self.balance += amount
return self.balance
def withdraw(self, amount):
if amount > self.balance:
raise ValueError("insufficient funds")
self.balance -= amount
return self.balance
def yearly_interest(self):
return self.balance * Account.interest_rate
def __repr__(self):
return f"Account({self.owner!r}, {self.balance})"
acc = Account("Asha", 1000)
acc.deposit(500)
acc.withdraw(200)
print(acc, acc.yearly_interest(), Account.accounts)
# Account('Asha', 1300) 52.0 1
Common mistakes
- Forgetting
selfas the first parameter of a method, which givesTypeError: ... takes 0 positional arguments but 1 was given. - Writing
name = nameinstead ofself.name = namein__init__. - Using a mutable class variable (such as a list) when each object needs its own; create it in
__init__. - Expecting
__init__to be able to return a value. - Relying on
__del__for clean-up. - Confusing a class (the plan) with an object (a thing built from the plan).
Practice
Write a Book class with title, author and price, a method that applies a discount percentage, and a class variable that counts how many books exist. Add a class method that builds a book from the text "Title|Author|250" and a static method that checks whether a price is positive. Then create three books, print them, and list their total price.
Continue
The next part, inheritance and the "is-a" and "has-a" relationships, builds on these ideas. Review functions if self still feels unfamiliar, and watch the matching video lessons: Episode 61: Python OOPs, Session 1, Episode 62: Python OOPs, Session 2 and Episode 63: Python OOPs, Session 3. The Python video course has the whole series.