Class Objects
Classes¶
Classes are the object factories of many programming languages. The objects that classes create are typically called instances. Classes can also be used to organize code and/or data. Python Classes are similar to classes in other languages but in many ways they are quite different.
Class Instantiation & The Instance Object¶
When a class is called directly you get back an instance object.
class MyClass:
pass
instance_object = MyClass()
Magic methods¶
Also known as Dunder Methods – these are invoked by Python and do not need to be called directly. For example, the __call__()
method is automatically called when you call the object itself. See Callable Object below.
Define Fields with __init__()
¶
This is the Init Method. It is used to populate fields on the instance object. The init method allows us to load the instance object with fields, this is the last step of the instantiation process. Fortunately the object already has all the class variables, instance methods, static methods and class methods pre-loaded. Inside any instance method the instance object has the name: self, this is an implict argument. You need to declare it in the method def but it is not expected to be passed in – that’s the implicit part.
Sometime this __init__()
method is called the constructor, however it would be better to call it the initiallizer as the object has already been constructed at this point. There is another magic method __new__()
– this is the proper constructor. The __new__()
magic method will not be covered here as it is almost never used.
class Name:
def __init__(self, name):
self.name = name # instance variable
name_object = Name("Jim Bob Joe") # name passed to __init__
print(name_object.name)
Callable Object with __call__()
¶
In this example we’ll see how we can add to the instance objects the ability to call them as if they where functions.
class Callable:
fourty_two = 42 # class variable
def __call__(self):
return self.fourty_two
callable_obj = Callable()
print(callable_obj) # not called
print(callable_obj()) # called
Printable Object with __str__()
and/or __repr__()
¶
__str__()
: This magic method should return a string. This is used when the object is to be printed or any time the object is cast to a string.
__repr__()
: This magic method should also return a string. Typically this is a string of the class signature.
So long as one of these methods are defined, the objects will be printable directly.
class Printable:
class_answer = 42
def __str__(self):
return f"The answer is {self.class_answer}"
def __repr__(self):
return "Printable()"
answer = Printable()
print(answer)
print(repr(answer))
Inheritance¶
It can be said that Wizard & Fighter both inherit from Character. All fields and methods from any base classes will automatically be present in all derived classes. This is one way to share behavior and data across many classes.
class Character:
""" Base Class """
health = 10
class Wizard(Character):
""" Derived Class """
mana = 20
class Fighter(Character):
""" Derived Class """
power = 15
wizard_object = Wizard()
print("Wizard Health:", wizard_object.health)
print("Wizard Mana:", wizard_object.mana)
print()
fighter_object = Fighter()
print("Fighter Health:", fighter_object.health)
print("Fighter Power:", fighter_object.power)
Avoid Multiple Inheritance¶
The JunkYardShip below, only fires with the power of a StarFighter. This is due to the order that the base classes are inherited… JunkYardShip(StarFighter, IonCanon)
should be JunkYardShip(IonCanon, StarFighter)
, and this is weird. This seems backwards to anyone that knows how CSS works.
Multiple Inheritance is not considered Pythonic and generally it’s best avoided. Composition is a much better pattern, see the StarDestroyer()
class.
class StarFighter:
def fire(self):
return 10
class IonCanon:
def fire(self):
return 100
class JunkYardShip(StarFighter, IonCanon): # Don't do this
""" I have a bad feeling about this. """
pass
class StarDestroyer(StarFighter): # Do this instead
""" This class uses composition to gain
the full fire power of the IonCanon. """
primary_weapon = IonCanon()
def fire(self):
return self.primary_weapon.fire()
fighter = StarFighter()
print(f"StarFighter: {fighter.fire()}")
junk_ship = JunkYardShip()
print(f"JunkYardShip: {junk_ship.fire()}")
destroyer = StarDestroyer()
print(f"StarDestroyer: {destroyer.fire()}")
Polymorphism¶
The example below uses inheritance to achieve full polymorphism between Monsters and Bosses. All fields and methods match in name and logical behavior. They do not need to hold the same data. This allows the two types of objects to be used interchangeably – and yet leverage their logical differences. Inheritance is not the only way to achieve polymorphism.
import random
def dice(rolls, sides):
return sum(random.randint(1, sides) for _ in range(rolls))
class Monster:
creature_type = "Monster"
hit_dice = 8
damage_dice = 6
names = ("Goblin", "Troll", "Giant", "Zombie", "Ghoul", "Vampire")
def __init__(self, level=1):
self.level = level
self.name = self.random_name()
self.total_health = dice(self.level, self.hit_dice)
self.current_health = self.total_health
def take_damage(self, amount):
print(f"{self.name} takes {amount} damage!")
self.current_health -= amount
def deal_damage(self):
return dice(self.level, self.damage_dice)
def __str__(self):
output = (
f"{self.creature_type}: {self.name}",
f"Level: {self.level}",
f"Health: {self.current_health}/{self.total_health}",
)
return "\n".join(output)
def random_name(self):
return random.choice(self.names)
class Boss(Monster):
creature_type = "Boss"
hit_dice = 12
damage_dice = 8
names = (
"The Loch Ness Monster", "Godzilla", "Nero the Sunblade",
"The Spider Queen", "Palladia Morris", "The Blood Countess",
)
some_monster = Monster(10)
print(some_monster, '\n')
dungeon_boss = Boss(20)
print(dungeon_boss, '\n')
dungeon_boss.take_damage(some_monster.deal_damage())
print(dungeon_boss)
some_monster.take_damage(dungeon_boss.deal_damage())
print(some_monster)
Class Scope¶
This can be tricky. It’s better not to think of what is going on here as scope. But rather a blueprint to make objects. Sometimes the blueprint would like to refer to itself. This complicates things a great deal. What is self? Is it the class or the instance object? We want both abilities, and here we are. The convention is that when we use param ‘self’ we mean the instance object, when we actually mean the class, meaning in class methods, we will instead use the param ‘cls’.
In Java it’s required to declare what are known as ‘get’ and ‘set’ methods to read and write class fields. In Python we may we drink java, but we never write get or set methods. We have direct access to all fields all the time. This is only partially true, see class methods and static methods for exceptions to this rule.
class ClassScope:
# self does not exit yet.
class_variable = "class_variable"
def __init__(self):
"""
Local scope inside a method is just like function scope. However,
methods also have access to class scope and instance scope
through self. """
self.instance_variable = "instance_variable"
def instance_method(self):
""" This is a regular Instance Method.
We have access to everything from here.
Don't over think it, most of the time this is what you want.
While it is common to modify instance variables here, it is not wise to
declare them here. Use the `__init__()` method for that. Use instance
methods, like this one, to read and update instance variables. """
return self.instance_variable + ": via instance method"
@classmethod
def classy_method(cls):
""" This is a Class Method.
It's more restricted than regular methods. Instead of the `self`
param we use the `cls` param. This is a convention to indicate
we expect this method to live on a class that might possibly never
be instantiated. This is the whole point of having class methods.
This ability comes at a cost: everything we access from this scope
must live on the class itself, not an instance. Only static methods,
class methods and class variables are accessible here. """
return cls.class_variable + ": via class method"
@staticmethod
def selfless_method():
""" This is a Static Method.
It's way more restricted than regular methods. Static Methods
have no concept of `self` or `cls` and cannot access anything.
This is a prime candidate to refactor into a function. """
local_variable = "local_variable"
return local_variable + ": via static method"
# Class Scope
print("From the Class:")
print(ClassScope.class_variable) # There is no spoon, i mean...
print(ClassScope.classy_method()) # There is no instance.
print(ClassScope.selfless_method()) # But we have lots of class!
print()
# Instance Scope
print("From the Instance:")
instance_object = ClassScope() # instance object instantiated.
print(instance_object.instance_variable) # now we have everything...
print(instance_object.instance_method()) # ...except local variables.
print(instance_object.class_variable)
print(instance_object.classy_method())
print(instance_object.selfless_method())