Spielend programmieren lernen

Programmierkurse für Kinder, Jugendliche und Erwachsene

User Tools

Site Tools


en:blog:2020:0325_class

Python class tutorial

This Python class example is meant to follow by copy the code into a python shell (like Idle) and experiment with it by altering the code.

Of course you can also write longer (and more comfortable) code pieces in your usual Python editor.

Why using a class

In Computer Science lessons, a class is often explained as a blueprint or plan to explain how a “thing” is to be constructed, and then with the help of the class, countless instances of this well-defined class can be constructed. For example, let's say you program a computer game in Python about cars, than you could one time define a class car and create a lot of individual cars (= instances of the car class).

However, you will often see code that creates a class and then only have a single instance of this class used. Like a game written in Python with a “Game” class, and without any multiplayer capacity… just one single instance of this is ever used. Makes this sense ?

Namespace

Yes, it does. Because classes in Python provide their own namespace to store variables. Typically, you see at the begin of many python programs some variables (or constants) defined at module level (at the left edge).

Such variables are read-only are in the “root namespace”, meaning other python code in the same root namespace has read and write access to this variables. Code inside functions or classes has it's own namespace. (Such code is “indented to the right). Code in a function- or class-namespace can still “see” the variables of the root namespace (read-access) but he can not manipulate them… no write access.

Example 1

# root namespace, at the left edge
gravity = 9.81
print("gravity in root namespace is: ", gravity)
 
def game():
    print("inside game function")
    print("gravity in the 'game' namespace is: ", gravity)
 
game() # call game function
print("back in root namespace")
print("gravity in root namespace is: ", gravity)

output:

gravity in root namespace is:  9.81
inside game function
gravity in the 'game' namespace is:  9.81
back in root namespace
gravity in root namespace is:  9.81

As you can see in the example above, the 'game' function has read-access to the gravity variable from the root namespace.

However, a function has by definition it's own namespace. If you define gravity inside the game function, you get a local gravity. The root-namespace gravity is not affected because root namespace and function namespace are different:

Example 2

# root namespace, at the left edge
gravity = 9.81
print("gravity in root namespace is: ", gravity)
 
def game():
    print("inside game function")
    print("creating a local gravity of -5")
    gravity = -5
    print("gravity in the 'game' namespace is: ", gravity)
 
game() # call game function
print("back in root namespace")
print("gravity in root namespace is: ", gravity)

output:

gravity in root namespace is:  9.81
inside game function
creating a local gravity of -5
gravity in the 'game' namespace is:  -5
back in root namespace
gravity in root namespace is:  9.81

In fact, the code inside the game function has no chance at all to manipulate a variable in the root namespace. the only way would be to either use a global variable (not recommended) or to return a value to the main program, like:

Example 3

gravity = 9.81
def game():
   gravity = -5
   return gravity # returns local gravity 
print("gravity before calling game:", gravity)
gravity = game()
print("gravity after calling game:", gravity)

output:

gravity before calling game: 9.81
gravity after calling game: -5

To come back to classes: Classes in Python provide their own namespace, like functions. But with a difference:

Code outside a class has read and write access to the class variables (the variables defined at the “root” of the class”, more on that later), so they act as global variables:

Example 4

class Settings:
    gravity = 9.81   # this is a class variable, or class attribute
 
print("gravity in root namespace:", Settings.gravity)
print("doubling gravity...")
Settings.gravity *= 2
print("gravity in root namespace:", Settings.gravity)
 
 
def game():
   print("gravity inside a function", Settings.gravity)
   print("halving gravity....")
   Settings.gravity *= 0.5
   print("gravity inside a function", Settings.gravity)
 
game() # calling game
print("gravity after calling game:", Settings.gravity)

output:

gravity in root namespace: 9.81
doubling gravity...
gravity in root namespace: 19.62
gravity inside a function 19.62
halving gravity....
gravity inside a function 9.81
gravity after calling game: 9.81

Notice that by convention, class names begin with an uppercase letter. Variables inside a class are called attributes. You can access the class attributes with the dot notation: 'Settings.gravity'.

Summary: A python class can serve as a container for (global) game variables, with read and write access from everywhere.

instances of classes

A function defined inside a class is called a method. In python, the most important method of a class is the __init__ method (sometimes called a constructor). The __init__ method is called automatically when a new instance of a class is created. Let's make a “Planet” class where each Planet instance gets randomized attributes (See the random module in the Python documentation) like number of moons, number of rings etc.

Important: Each class instance has it's own namespace! So the number of moons attribute ( self.moons ) is individual for each planet.

Example 5

import random
 
class Game:
    gravity = 9.81  
 
class Planet:
 
    def __init__(self):
        self.color = random.choice(("red", "blue", "orange", "brown"))
        self.mass = random.randint(1,250)
        self.radius = random.randint(1,30)
        self.moons = random.randint(0, 5)
        self.rings = random.randint(0,2)
 
#create 4 random planets
solarsystem = [Planet(), Planet(), Planet(), Planet()]
print("solarsystem:", solarsystem)
for p in solarsystem:
    print(p.__dict__)

output (Your output may be different, because of the random nature of the random module):

solarsystem: [<__main__.Planet object at 0x7f5fdd72c550>, <__main__.Planet object at 0x7f5fdd72c590>, <__main__.Planet object at 0x7f5fdd72c690>, <__main__.Planet object at 0x7f5fdd6b9390>]
{'color': 'orange', 'mass': 186, 'radius': 4, 'moons': 2, 'rings': 1}
{'color': 'orange', 'mass': 242, 'radius': 18, 'moons': 4, 'rings': 2}
{'color': 'blue', 'mass': 226, 'radius': 23, 'moons': 3, 'rings': 2}
{'color': 'brown', 'mass': 207, 'radius': 19, 'moons': 2, 'rings': 1}

You will notice that the whole Game class is not used in the example above…but it will soon. The planets are created in by the command 'Planet()'. This automatically creates a new instance of Planet class and executes the __init__ method.

You will also notice that our planets are no beauties: '<main.Planet object at 0x7f5fdd72c690>' does provide a lot of information, but somehow it's not a pretty sight. More useful is the __dict__ attribute provided by python. It shows a dictionary of all instance attributes.

Unlike a “normal” function declared with the “def” keyword, a method inside a class needs something more: the “self” parameter, indicating a reference to the class instance.

Let's upgrade this example by demanding a name when creating Planets, and make use of the in-buildt python functions __str__ to manipulate what happens when a planet get printed and __repr__ to manipulate the effect when the whole solarsystem get printed:

Example 6

import random
 
class Game:
    gravity = 9.81  
 
class Planet:
 
    def __init__(self, name):
        self.name = name 
        self.color = random.choice(("red", "blue", "orange", "brown"))
        self.mass = random.randint(1,250)
        self.radius = random.randint(1,30)
        self.moons = random.randint(0, 5)
        self.rings = random.randint(0,2)
 
    def __repr__(self):
        return self.name
 
    def __str__(self):
        return "{}, a {} planet with {} moon(s) and {} ring(s)".format(
        self.name, self.color, self.moons, self.rings )
 
#create 4 random planets
solarsystem = [Planet("Mercury"), Planet("Venus"), Planet("Earth"), Planet("Mars")]
print("solarsystem:", solarsystem)
for p in solarsystem:
    print(p)

output:

solarsystem: [Merury, Venus, Earth, Mars]
Mercury, a brown planet with 0 moon(s) and 1 ring(s)
Venus, a brown planet with 4 moon(s) and 0 ring(s)
Earth, a red planet with 5 moon(s) and 1 ring(s)
Mars, a brown planet with 2 moon(s) and 1 ring(s)

Example 7

import random
 
class Game:
    gravity = 9.81  
    solarsystem = {}
 
class CelestialBody:
    number = 0 # class attribute
    def __init__(self, name=None, boss_number=0, ring=False):
        self.number = CelestialBody.number  # id
        CelestialBody.number += 1
        if name is None:
            self.name = "object {}".format(self.number)
        else:
            self.name = name 
        self.boss_number = boss_number
        Game.solarsystem[self.number] = self
        self.color = random.choice(("red", "blue", "orange", "brown"))
        self.mass = random.randint(1,250)
        self.radius = random.randint(1,30)
        #self.moons = random.randint(0, 5)
        self.ring = ring
 
    def __repr__(self):
        return self.name
 
    def __str__(self):
        return "#{:>2}: '{}', a celestial body {}".format(
        self.number, self.name, "with a ring" if self.ring else "" )
 
#create 4 random planets
CelestialBody("Sun", boss_number = None) # object 0
CelestialBody("Merury") # boss_number is 0 by default 
CelestialBody("Venus")
CelestialBody("Earth")
CelestialBody("Mars")
CelestialBody("Jupiter")
CelestialBody("Saturn", ring=True)
CelestialBody("Neptune")
CelestialBody("Uranus")
CelestialBody("Pluto")
CelestialBody() # anonymous planet
 
 
print("solarsystem:", Game.solarsystem)
for p in Game.solarsystem.values():
    print(p)

Example 8

import random
 
class Game:
    gravity = 9.81      # class attribute
    solarsystem = {}    # class attribute
 
class CelestialBody:
 
    number = 0 # class attribute
 
    def __init__(self, name=None, boss_number=0, ring=False, small=False):
        self.number = CelestialBody.number  # id
        CelestialBody.number += 1
        if name is None:
            self.name = "object {}".format(self.number)
        else:
            self.name = name 
        self.boss_number = boss_number
        Game.solarsystem[self.number] = self
        self.ring = ring
        self.small = small
 
    def satellites(self):
        result = []
        for b in Game.solarsystem.values():
            if b.boss_number is None:
                continue
            if b.boss_number == self.number:
                result.append(b)
        return result
 
    def rank(self):
        """no boss -> sun
           boss is a sun -> planet (or Asteroid)
           boss is a planet -> moon (or Satellit)
           boss is a moon or else -> object"""
        if self.boss_number is None:
            return "sun"
        boss = Game.solarsystem[self.boss_number]
        bossrank = boss.rank()
        if bossrank == "sun":
            return "asteroid" if self.small else "planet"
        elif bossrank == "planet":
            return "satellite" if self.small else "moon"
        else:
            return "object"
 
    def __repr__(self):
        return self.name
 
    def __str__(self):
        satellites = len(self.satellites())
        return "#{:>2}: '{}', {} {}{}{}{}".format(
        self.number, 
        self.name, 
        "an" if self.rank()[0] in "aeiou" else "a",
        self.rank(), 
        " with a ring" if self.ring else "",
        "" if self.boss_number is None else " orbiting {}".format(
        Game.solarsystem[self.boss_number].name),
        "" if satellites == 0 else ", orbited by {} objects".format(
        satellites))
 
# ---- create some planets ----
CelestialBody("Sun", boss_number = None) # object 0
CelestialBody("Merury") # boss_number is 0 by default 
CelestialBody("Venus")
CelestialBody("Earth")
CelestialBody("Mars")
CelestialBody("Jupiter")
CelestialBody("Saturn", ring=True)
CelestialBody("Ceres", small=True) # asteroid
# ---- create some moons -------
CelestialBody("Moon", boss_number=3)   # 3=Earth
CelestialBody("Phobos", boss_number=4) # 4=Mars
CelestialBody("Deimos", boss_number=4)
CelestialBody("Io", boss_number=5)     # 5=Jupiter
CelestialBody("Europa", boss_number=5) 
CelestialBody("Callisto", boss_number=5) 
CelestialBody("Titan", boss_number=6)  # 6=Saturn
CelestialBody("Enceladus", boss_number=6)
# --- other objects ----
CelestialBody("International Space Station", boss_number=3, small=True)
CelestialBody("Lunar Orbiter", boss_number=8, small=True)
 
 
print("solarsystem:", Game.solarsystem)
for p in Game.solarsystem.values():
    print(p)

output:

# 0: 'Sun', a sun, orbited by 7 objects
# 1: 'Merury', a planet orbiting Sun
# 2: 'Venus', a planet orbiting Sun
# 3: 'Earth', a planet orbiting Sun, orbited by 2 objects
# 4: 'Mars', a planet orbiting Sun, orbited by 2 objects
# 5: 'Jupiter', a planet orbiting Sun, orbited by 3 objects
# 6: 'Saturn', a planet with a ring orbiting Sun, orbited by 2 objects
# 7: 'Ceres', an asteroid orbiting Sun
# 8: 'Moon', a moon orbiting Earth, orbited by 1 objects
# 9: 'Phobos', a moon orbiting Mars
#10: 'Deimos', a moon orbiting Mars
#11: 'Io', a moon orbiting Jupiter
#12: 'Europa', a moon orbiting Jupiter
#13: 'Callisto', a moon orbiting Jupiter
#14: 'Titan', a moon orbiting Saturn
#15: 'Enceladus', a moon orbiting Saturn
#16: 'International Space Station', a satellite orbiting Earth
#17: 'Lunar Orbiter', an object orbiting Moon
en/blog/2020/0325_class.txt · Last modified: 2020/03/26 09:27 by horstjens@gmail.com