Classes
OOP in Python
Class
Classes created with operator class
:
class A:
attr = 10
When a class definition is entered, a new namespace is created, and used as the local scope — thus, all assignments to local variables go into this new namespace. In particular, function definitions bind the name of the new function here
Class objects support two kinds of operations: attribute references and instantiation.
Simple OOP examples
Here is the simplest class for Cat and two objects of this class:
🪄 Code:
class Cat:
pass
agata = Cat()
blanka = Cat()
print(agata, id(agata))
print(blanka, id(blanka))
📟 Output:
<__main__.Cat object at 0x7ff57dd25360> 140692354650976
<__main__.Cat object at 0x7ff57dd25540> 140692354651456
Cat class and object is "empty" - doesn't define any attributes and methods. They even don't have their proper name.
Let's add names:
🪄 Code:
agata.name = "Agata"
print(agata.name)
📟 Output:
Agata
We can assign attributes during initialization of the object - it would simplify things a lot.
The method starts with __
("dunder") is called a magic method. __init__
is one example, __str__
- method that defines a string representation of the object - is another.
self
in examples below is the reference to the object itself so we could use it in the function code - for example during str(agata)
Python will call agata.__str__()
which will need to return a string including agata.name
, and it does this using the reference self
.
🪄 Code:
class Cat:
def __init__(self, name="Stray"):
self.name = name
def __str__(self):
return f'<Cat "{self.name}">'
def __repr__(self):
return f'Cat("{self.name}")'
agata = Cat("Agata")
street_cat = Cat()
print(agata)
print(street_cat)
print(repr(agata))
📟 Output:
<Cat "Agata">
<Cat "Stray">
Cat("Agata")
Magic methods are not the only ones we can define - in fact we can add any method that would describe some action involving an object.
Here we also will set a class attribute accessible from all children.
🪄 Code:
class Cat:
default_sound = "Meow"
def __init__(self, name="Stray"):
self.name = name
#self.default_sound = "Rrrr" # We can override "global" class attribute with "local" instance attribute
def __str__(self):
return f'<Cat "{self.name}">'
def sound(self, times=3):
return " ".join([self.default_sound] * times)
blanka = Cat("Blanka")
print(blanka.sound())
📟 Output:
Meow Meow Meow
Examples with more methods/attributes:
class Bus:
""" Sample Bus class """
# Class attributes
buses_count = 0 # IDEA: change with classmethod property -- get len(buses)
people_transferred = 0 # IDEA: change with dict()
money_collected = 0 # IDEA: change with dict()
buses = []
# Instance Initializer
def __init__(self, name="Some Bus", rate=7):
# instance attribute
self.name = name
self.people_transferred = 0
self.rate = rate
self.money_collected = 0
self.__class__.buses_count += 1 # self.__class__ instead of hardcoding Bus
self.__class__.buses.append(self) # self.__class__ instead of hardcoding Bus
# Instance method
def transfer(self, num=1):
self.people_transferred += num
self.__class__.people_transferred += num
self.money_collected += self.rate * num
self.__class__.money_collected += self.rate * num
def __str__(self):
return f"<Bus '{self.name}'>"
def info(self): # change to __str__
data = dict(
name=self.name, total_buses=Bus.buses_count, rate=self.rate,
num=self.people_transferred, total=Bus.people_transferred
)
return "Bus '{name}' (rate: {rate}€) (total: {total_buses}), " \
"transferred {num} from {total} ppl".format(**data)
def __repr__(self):
return f"Bus('{self.name}', {self.rate})"
b = Bus("Bus #40")
b.transfer(100) # --> Bus.transfer(b, 100)
b
📟 Output:
Bus('Bus #40', 7)
🪄 Code:
print(b)
📟 Output:
<Bus 'Bus #40'>
🪄 Code:
b.transfer(50)
print(b.info()) # --> Bus.info(b)
📟 Output:
Bus 'Bus #40' (rate: 7€) (total: 1), transferred 150 from 150 ppl
🪄 Code:
b2 = Bus("Tram #1", 8)
b2.transfer(50)
print(b2.info())
📟 Output:
Bus 'Tram #1' (rate: 8€) (total: 2), transferred 50 from 200 ppl
🪄 Code:
print(f"Bus.people_transferred = {Bus.people_transferred}")
print(f"Bus.money_collected = {Bus.money_collected}")
print(f"Bus.buses_count = {Bus.buses_count}")
print(f"Bus.buses = {Bus.buses}")
📟 Output:
Bus.people_transferred = 200
Bus.money_collected = 1450
Bus.buses_count = 2
Bus.buses = [Bus('Bus #40', 7), Bus('Tram #1', 8)]
Creation of an instance of the class - like calling a function (in fact it is exactly like this - firstly we calling magic method __new__()
then __init__()
🪄 Code:
bus_317 = Bus("# 317")
bus_317.transfer(20)
print(bus_317.info())
📟 Output:
Bus '# 317' (rate: 7€) (total: 3), transferred 20 from 220 ppl
🪄 Code:
b.transfer(23)
bus_317.transfer()
bus_317.transfer(55)
print(bus_317.info())
📟 Output:
Bus '# 317' (rate: 7€) (total: 3), transferred 76 from 299 ppl
Class variables and instance variables were changed:
🪄 Code:
print(Bus.people_transferred)
print(bus_317.people_transferred)
📟 Output:
299
76
Inheritance
Inheritance from some class (called "base" or super
class) allows to create a new class "borrowing" all attributes and methods definitions as they were in the super class. It allows to:
Reuse the code (DRY principle).
Create abstractions.
For example here is the class for the Robot:
class Robot:
sounds = ["Beeep", "Bzzzt", "Oooooh"]
def __init__(self, name, weigth=1000):
self.weigth = weigth
self.name = name
def __str__(self):
"""Info about this robot"""
return "Robot {obj.name} ({obj.weigth} kg)".format(obj=self)
def say(self):
"""Say something"""
import random
return f"{self.name} says: {random.choice(self.sounds)}"
It is completely usable:
🪄 Code:
bip = Robot("Bip 1.0")
print(bip)
print(bip.say())
📟 Output:
Robot Bip 1.0 (1000 kg)
Bip 1.0 says: Oooooh
We can re-use this class to create a robot from Futurama using the inheritance. For this we need to specify base/super class in parenthesis during new class definition.
class BendingRobot(Robot):
sounds = ["Kill all humans", "Kiss my shiny metal face", "Oh, your God!",
"Oh wait you’re serious. Let me laugh even harder."]
🪄 Code:
bender = BendingRobot("Bender")
print(bender)
print(bender.say())
📟 Output:
Robot Bender (1000 kg)
Bender says: Oh, your God!
As we can we still can use say
method defined in the base class.
Multiple Inheritance
Python supports a limited form of multiple inheritance as well. A class definition with multiple base classes looks like this:
🪄 Code:
class A:
a = "a from A"
class B(A):
x = "x from B"
class C(A):
a = "a from C"
x = "x from C",
class D(B, C): # change to D(C, B) and check...
pass
d = D()
print(D.__mro__) # D.mro()
print(d.a, d.x)
📟 Output:
(<class '__main__.D'>, <class '__main__.B'>, <class '__main__.C'>, <class '__main__.A'>, <class 'object'>)
a from C x from B
Let's enhance our Robot
example by inheriting from two classes at once.
🪄 Code:
class Mail:
def send_message(self, msg):
print(f"*** SENDING MESSAGE: <<<{msg}>>> ***")
Mail().send_message("Test")
📟 Output:
*** SENDING MESSAGE: <<<Test>>> ***
🪄 Code:
class BendingMailingRobot(Robot, Mail):
sounds = ["Kill all humans", "Kiss my shiny metal face", "Oh, your God!",
"Oh wait you’re serious. Let me laugh even harder."]
bender2_0 = BendingMailingRobot("Bender 2.0")
bender2_0.send_message(bender2_0.say())
📟 Output:
*** SENDING MESSAGE: <<<Bender 2.0 says: Kiss my shiny metal face>>> ***
More advanced example:
🪄 Code:
class BendingMailingRobot(BendingRobot, Mail):
def mail(self):
return self.send_message(self.say())
bender3_0 = BendingMailingRobot("Bender 3.0")
bender3_0.mail()
📟 Output:
*** SENDING MESSAGE: <<<Bender 3.0 says: Oh, your God!>>> ***
And even more advanced example (with overloading of the existing send_message
method with super()
function covered later):
🪄 Code:
class BendingMailingRobot3(BendingRobot, Mail):
def send_message(self):
return super().send_message(self.say())
bender4_0 = BendingMailingRobot3("Bender 4.0")
bender4_0.send_message()
📟 Output:
*** SENDING MESSAGE: <<<Bender 4.0 says: Kiss my shiny metal face>>> ***
Methods
Methods can be:
instance methods
class methods
static methods
Instance method
By default all methods (except of __new__
are instance methods).
The method that should be called with the instance as it's first argument. This method is bound to instance so if calling as it's method passing instance is not required. Example:
🪄 Code:
class Example:
def cool_method(self):
print(f"I am instance method, my instance is: {self}")
ex = Example()
ex.cool_method()
# Example.cool_method(ex) # <-- same
print(ex.cool_method)
📟 Output:
I am instance method, my instance is: <__main__.Example object at 0x7ff57df07610>
<bound method Example.cool_method of <__main__.Example object at 0x7ff57df07610>>
Class methods
The method with class as the first argument. Useful to run some code without need of creating the instace.
To mark the method as class method it is required to use builtin decorator @classmethod
🪄 Code:
class Example:
attr = 5
@classmethod
def class_method(cls):
print("CLS:", cls.attr)
def instance_method(self):
print("INSTANCE:", self.attr)
ex = Example()
ex.attr = 25
ex.class_method()
ex.instance_method()
Example.class_method() # Example.class_method(Example)
📟 Output:
CLS: 5
INSTANCE: 25
CLS: 5
Let's add some class method to Bus
class. Please note that we don't need any existing buses to call general_info
method on Bus2
:
🪄 Code:
class Bus2(Bus):
# creating isolated class attributes to not use the ones from base Bus class
buses_count = 0
buses = []
people_transferred = 0
money_collected = 0
@classmethod
def general_info(cls):
print(f"* People_transferred = {cls.people_transferred}")
print(f"* Money_collected = {cls.money_collected}")
print(f"* Total buses = {cls.buses_count}")
print(f"* Buses = {cls.buses}")
@classmethod
def remove_bus(cls, bus):
try:
cls.buses.remove(bus)
cls.buses_count -= 1
print(f"{bus} removed")
except ValueError:
print("No such bus registered!")
Bus2.general_info()
📟 Output:
* People_transferred = 0
* Money_collected = 0
* Total buses = 0
* Buses = []
Let's add some buses, check the general info and remove buses:
🪄 Code:
bus = Bus2()
bus.transfer(100)
Bus2.general_info()
📟 Output:
* People_transferred = 100
* Money_collected = 700
* Total buses = 1
* Buses = [Bus('Some Bus', 7)]
Removing also works good:
🪄 Code:
Bus2.remove_bus(bus)
Bus2.remove_bus(bus)
📟 Output:
<Bus 'Some Bus'> removed
No such bus registered!
Static method
This method doesn't require to pass instance/class at all.
To mark the method as static method it is required to use builtin decorator @staticmethod
🪄 Code:
class Example:
@staticmethod
def cool_method():
Example.attr = 123123
return "I am a static method, I don't have access to anything :("
@staticmethod
def other_stat_method(str_):
return str_[::-1]
ex = Example()
print(ex.cool_method())
print(Example.cool_method())
print(ex.other_stat_method("Radar"))
print(Example.attr)
📟 Output:
I am a static method, I don't have access to anything :(
I am a static method, I don't have access to anything :(
radaR
123123
We can use static method for creating some helper function for `Bus` class, let's add simple one for calculating money collected:
class Bus:
...
@staticmethod
def calc_money(ppl, rate):
return ppl * rate
def transfer(self, num=1):
self.people_transferred += num
self.__class__.people_transferred += num
self.money_collected += self.calc_money(num, self.rate)
self.__class__.money_collected += self.calc_money(num, self.rate)
...
Old and New classes
This chapter is only viable for Python 2 - as in Python 3 there are no such distinguishing as "old/new" classes.
Before Python 2.5 the format for creating a class was:
class Old:
pass
This was resulted in various problems with MRO and types. So some code redesigned, for this new format introduced:
class New(object):
pass
Differences:
classobj
type asbuiltin
typesCorrect horizontal-then-vertical
MRO
(earlier it was vertical)No
__new__()
method in old classAdditional functionality:
__slots__
(Python 3.x)__getattribute__
__getattr__
and__getattribute__
don't look for magic methods in instance
Last updated
Was this helpful?