Python — OOP Part 2: Class and Instance
In this lesson we’ll learn about class and instance.
You’ve probably heard “Python is an object-oriented programming language. Everything in Python is an object — strings, lists, functions, even modules…” so many times your ears hurt. But what exactly is an object? An object is a data structure that contains both data (often called attributes) and functions (called methods when inside an object). And in Python these objects are first-class objects — they can be assigned to variables and passed as arguments to functions. For more on first-class objects, see the earlier lesson on first-class functions.
What is an object? #
An object is a logical grouping built using namespaces to make data easier to handle — like how a school separates students by grade and class. Whether you build a Python dictionary or define a class or module to group data, you’re creating an object that lets you store, modify, and access data more easily.
We’ll cover namespaces in detail when we get to modules.
The code below shows three different ways — different in form only — of building an object as a logical data grouping and accessing the data inside.
Create a Python file called oop_2.py in any directory and save the following code:
Using a dictionary:
student = {'name': 'Sanghee Lee', 'year': 2, 'class': 3, 'student_id': 35}
print('{}, year {} class {} no. {}'.format(student['name'], student['year'], student['class'], student['student_id']))Open a terminal or command prompt, navigate to the directory containing oop_2.py, and run:
Sanghee Lee, year 2 class 3 no. 35Using a class:
class Student:
def __init__(self, name, year, class_num, student_id): # `class` is a Python keyword and can't be used as a parameter name.
self.name = name
self.year = year
self.class_num = class_num
self.student_id = student_id
def introduce_myself(self):
return '{}, year {} class {} no. {}'.format(self.name, self.year, self.class_num, self.student_id)
student = Student('Sanghee Lee', 2, 3, 35)
print(student.introduce_myself())Sanghee Lee, year 2 class 3 no. 35Using a module:
In the same folder, make a file student.py and add this code:
name = 'Sanghee Lee'
year = 2
class_id = 3
student_id = 35import student
print('{}, year {} class {} no. {}'.format(student.name, student.year, student.class_id, student.student_id))Sanghee Lee, year 2 class 3 no. 35All three examples differ only in form — they all use objects as logical groupings to print the same data.
Let’s verify everything in Python really is an object and look inside one. First, is a string really an object?
text = 'string'
print(dir(text))Remember!
dir()is a Python standard built-in function. With no argument it returns the local names at module level; with an argument (an object), it returns all attributes and methods of that object. It’s heavily used for debugging.
['__add__', '__class__', '__contains__', '__delattr__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__getitem__', '__getnewargs__', '__getslice__', '__gt__', '__hash__', '__init__', '__le__', '__len__', '__lt__', '__mod__', '__mul__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__rmod__', '__rmul__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', '_formatter_field_name_split', '_formatter_parser', 'capitalize', 'center', 'count', 'decode', 'encode', 'endswith', 'expandtabs', 'find', 'format', 'index', 'isalnum', 'isalpha', 'isdigit', 'islower', 'isspace', 'istitle', 'isupper', 'join', 'ljust', 'lower', 'lstrip', 'partition', 'replace', 'rfind', 'rindex', 'rjust', 'rpartition', 'rsplit', 'rstrip', 'split', 'splitlines', 'startswith', 'strip', 'swapcase', 'title', 'translate', 'upper', 'zfill']You assigned only the 6-character 'string' to text, yet so much was printed. Why?!
Because text is an instance object created by the str data type, and it inherits all attributes and methods defined on str.
Let’s verify a few:
text = 'string'
print('# text class')
print(text.__class__) # <type 'str'>
print('\n# Is text an instance of str?')
print(isinstance(text, str)) # True
print('\n# str method')
print(text.upper()) # STRING# text class
<class 'str'>
# Is text an instance of str?
True
# str method
STRINGWe checked the class of text and called a method. Functions and modules are also objects — let’s confirm:
def my_function():
'''Documentation for my_function.'''
pass
print('# my_function attributes')
print(dir(my_function), '\n')
print('# my_function docstring')
print(my_function.__doc__, '\n')
print('# add a new attribute to my_function\n')
my_function.new_variable = 'a new variable'
print('# attributes after addition')
print(dir(my_function), '\n')
print('# value of the added attribute')
print(my_function.new_variable, '\n')# my_function attributes
['__annotations__', '__call__', '__class__', '__closure__', '__code__', '__defaults__', '__delattr__', '__dict__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__get__', '__getattribute__', '__globals__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__kwdefaults__', '__le__', '__lt__', '__module__', '__name__', '__ne__', '__new__', '__qualname__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__']
# my_function docstring
Documentation for my_function.
# add a new attribute to my_function
# attributes after addition
['__annotations__', '__call__', '__class__', '__closure__', '__code__', '__defaults__', '__delattr__', '__dict__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__get__', '__getattribute__', '__globals__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__kwdefaults__', '__le__', '__lt__', '__module__', '__name__', '__ne__', '__new__', '__qualname__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', 'new_variable']
# value of the added attribute
a new variableConfirmed — functions have many attributes, and you can even add arbitrary new ones.
Once you’ve got a feel for it, let’s create a new data type with a class and create instance objects of that type.
A class to manage employee data at a company:
class Employee:
pass
emp_1 = Employee()
emp_2 = Employee()
print('# emp_1 and emp_2 are separate objects with different memory addresses.')
print(id(emp_1))
print(id(emp_2))
print()
print('# emp_1 and emp_2 are instances of the same class.')
class_of_emp_1 = emp_1.__class__
class_of_emp_2 = emp_2.__class__
print(id(class_of_emp_1))
print(id(class_of_emp_2))# emp_1 and emp_2 are separate objects with different memory addresses.
3095355928096
3095355928048
# emp_1 and emp_2 are instances of the same class.
3095348371152
3095348371152We defined an Employee class and made instances emp_1 and emp_2. Using id() we confirmed they’re separate objects with different memory addresses. We also confirmed both are instances of the same class.
Now let’s add variables to emp_1 and emp_2 to store data:
class Employee:
pass
emp_1 = Employee()
emp_2 = Employee()
# Store data on the instance
emp_1.first = 'Sanghee'
emp_1.last = 'Lee'
emp_1.email = 'sanghee.lee@schoolofweb.net'
emp_1.pay = 50000
emp_2.first = 'Minjung'
emp_2.last = 'Kim'
emp_2.email = 'minjung.kim@schoolofweb.net'
emp_2.pay = 60000
# Access instance variables
print(emp_1.email)
print(emp_2.email)sanghee.lee@schoolofweb.net
minjung.kim@schoolofweb.netWe stored data on instances and accessed it. But there’s a problem with that code. Manually assigning instance variables one by one defeats the purpose of using a class. Use the __init__ method to assign the required data when an instance is created:
Note
__init__is also called the initializer, and in other languages the constructor. It runs automatically when an instance is created, and at that moment Python automatically passes the new instance object as the first argument namedself. Using the initializer, you can pass several pieces of data when creating an instance and store them as initial state on the object.
class Employee:
def __init__(self, first, last, pay):
self.first = first
self.last = last
self.pay = pay
self.email = first.lower() + '.' + last.lower() + '@schoolofweb.net'
emp_1 = Employee('Sanghee', 'Lee', 50000)
emp_2 = Employee('Minjung', 'Kim', 60000)
print(emp_1.email)
print(emp_2.email)
# Print emp_1's full name
print('{} {}'.format(emp_1.first, emp_1.last))sanghee.lee@schoolofweb.net
minjung.kim@schoolofweb.net
Sanghee Lee
Now we have concise code thanks to using a class.
But look at the last line that prints the full name. That's like the company asking an employee, "What's your name? Please tell me first name, then last name." Asking 100 employees that way would make your mouth tired. ㅋ It would be nice if every employee already knew how to answer the question "What's your name?" in a standard format. Let's make that happen.class Employee:
def __init__(self, first, last, pay):
self.first = first
self.last = last
self.pay = pay
self.email = first.lower() + '.' + last.lower() + '@schoolofweb.net'
def full_name(self):
return '{} {}'.format(self.first, self.last)
emp_1 = Employee('Sanghee', 'Lee', 50000)
emp_2 = Employee('Minjung', 'Kim', 60000)
# Print emp_1's full name
print(emp_1.full_name())Sanghee LeeA very common mistake beginners make when writing their first class: forgetting the self parameter when defining a method. What happens?
class Employee:
def __init__(self, first, last, pay):
self.first = first
self.last = last
self.pay = pay
self.email = first.lower() + '.' + last.lower() + '@schoolofweb.net'
def full_name(): # <--- self is missing.
return '{} {}'.format(self.first, self.last)
emp_1 = Employee('Sanghee', 'Lee', 50000)
emp_2 = Employee('Minjung', 'Kim', 60000)
# Print emp_1's full name
print(emp_1.full_name())Traceback (most recent call last):
File "C:\Users\CURTIS\Dev\Python\oop_2.py", line 16, in <module>
print(emp_1.full_name())
TypeError: full_name() takes 0 positional arguments but 1 was given
"TypeError: full_name() takes 0 positional arguments but 1 was given"The Python interpreter gives a cryptic message. What does it mean? It says full_name accepts 0 arguments but received 1. We called emp_1.full_name() with no arguments — so where did that 1 come from?! As mentioned, when you call a method on an instance, the instance itself is automatically passed as the first argument, self. The example below makes this clearer.
class Employee:
def __init__(self, first, last, pay):
self.first = first
self.last = last
self.pay = pay
self.email = first.lower() + '.' + last.lower() + '@schoolofweb.net'
def full_name(self):
return '{} {}'.format(self.first, self.last)
emp_1 = Employee('Sanghee', 'Lee', 50000)
emp_2 = Employee('Minjung', 'Kim', 60000)
# Call full_name through the class
print(Employee.full_name(emp_1))Sanghee LeeThe last line calls the method via the class. When you do that, the class doesn’t know which instance the method should run for, so you must pass the target instance as an argument. In effect, calling emp_1.full_name() runs Employee.full_name(emp_1) behind the scenes.
That wraps up this lesson. In the next lesson we’ll cover class variables, and along the way explore the difference between class and instance further.