Classes#

So far, you’ve learned about Python’s core data types: strings, numbers, lists, tuples, and dictionaries. In this section, you will delve into the last major data structure: classes.

Unlike the data types you’ve encountered so far, classes are more flexible. They enable you to define both the information and the behavior that characterize any entity you want to model in your program. This notebook serves as a brief introduction to classes. The first part presents some generic examples, while the second part illustrates how classes can be applied in the context of a psychological experiment.

There’s a lot of new terminology associated with learning about classes. If you’re already familiar with object-oriented programming (OOP) from another language, this will be a concise overview of Python’s approach to OOP. If you’re new to programming, you’ll encounter many novel concepts. Start by reading through, experimenting with the examples on your own machine, and trust that the ideas will become clearer as you progress.

Why classes?#

You might be wondering if learning about classes is worth the effort. To understand their value, let’s imagine you’re approaching a coding problem and thinking about what solutions to use. For example, creating a rocket ship in a video game.

How might you approach this problem? One way you could create a rocketship is to create a list of variables, each representing its x or y coordingates and it’s possible behavior like moving up or down. However, managing these all of these variables separately can become inefficient and complex. Especially if you have multiple rockets!

Now suppose you used a more sophisticated data type like dictionaries. Now you can store each rocket’s x and y coordinates, acceleration, and remaining fuel. But here you’d still end up juggling several dictionaries for each rocket, and it would quickly become confusing. How would you track which dictionary corresponds to which rocket? What if you want to access all the data for a specific rocket? This approach can also lead to a tangled web of variables and a lot of headaches.

A more streamlined way to accomplish the same thing is to make a rocket ship class. This class will not only store information about the rocket’s current position but also include behaviors that change its position, such as move_up() or move_right(). By encapsulating both the rocket’s data (like its coordinates) and its actions (like moving), we get a clear demonstration of how classes function in Python. They serve as blueprints for creating objects that possess both characteristics and capabilities. This not only makes your code cleaner and more organized but also makes it easier to manage and scale as your program grows.

Here is what a simple rocket ship class looks like in code:

class Rocket():   
     
    def __init__(self):
        # Each rocket has an (x,y) position.
        self.x = 0
        self.y = 0

Now, we have defined a class named ‘Rocket’ and equipped it with two pieces of information (known as attributes) which are the x and y coordinates. Before we continue, let’s cover some basic terminology:

General terminology#

A class is a body of code that defines the attributes and behaviors required to accurately model something you need for your program. You can model something from the real world, such as a rocket ship, or you can model something from a virtual world such as a rocket in a game, or a set of physical laws for a game engine.

An object is a particular instance (a specific realization) of a class. An object has a certain set of values for all of the attributes (variables) in the class. You can have as many objects as you want for any one class. For example, Rect() is a class in PsychoPy. When you create a new rectangle by assigning Rect() to a variable, you are creating a new instance of the class – the specific Rectangle object that is assigned to your variable.

An attribute is a piece of information. In code, an attribute is just a variable that is part of a class. In our rocket example, the x and y coordinates are attributes.

A behavior is an action that is defined within a class. These are made up of methods. A method is just a function that is defined within a particular class. Initializing the rocket class, was a method. We are also going to add more methods to our rocket like the ability to move.

There is more to know, but this terminology is enough to get you started. It will make more sense as you see more examples, and start to use classes on your own. For now, let’s focus on the methods we can see in this example:

The __init__() method#

The first line of a class definition in Python starts with the keyword class. This signals to Python that you’re defining a class. The naming convention for classes follows the same basic rules as for naming variables, but with an important convention: classes should use CamelCase. CamelCase means starting each word with a capital letter without using underscores. For example, MyClass or RocketShip. The class name is followed by parentheses, which are empty for now but can later include a base class from which your new class inherits.

It’s a good practice to document your classes. Initially, a simple comment at the beginning of your class, summarizing its purpose, is sufficient. Though Python provides a more formal syntax for documentation, you can start with basic comments and evolve to more formal documentation as you become more comfortable.

The first step in creating a class is defining the init() method, where __ is a double underscore. This method (known as the constructor) initializes the state of an object when it is first created. The term “self” in the method signature is essential; it’s a reference to the instance of the class itself, allowing you to access and modify the object’s attributes and call its methods from within the class. In our Rocket example, this method sets the initial x and y coordinates of the rocket to 0.

The next important concept to understand is self. It refers to the instance on which a method is being called. This is how you access attributes and other methods from within the class. In essence, self allows you to work with individual instances of a class. Therefore, all methods in a class should include self as their first parameter, enabling them to modify or interact with the class’s attributes.

As of now, our Rocket class is pretty boring, it has attributes but it doesn’t do anything. Let’s imbue it with a fundamental capability of a rocket: moving upwards. This action is defined as a method within the class. Here’s how we can implement this behavior in code:

class Rocket():

    def __init__(self):
        # Each rocket has an (x,y) position.
        self.x = 0
        self.y = 0
        
    def move_up(self):
        # Increment the y-position of the rocket.
        self.y += 1

The Rocket class now has attributes (x and y coordinates) and methods (moving up). However, up to this point, we haven’t actually created a rocket. What we have is the blueprint for a rocket.

To bring our rocket to life, to have an actual rocket that we can interact with in our program, we need to create an instance of the Rocket class. Here’s how you can create a rocket in your code:

my_rocket = Rocket()

To actually use a class, you have to create an instance of it. You do this by assigning a variable to the class. Here we made an instance of the class Rocket by assigning the variable my_rocket. Now my_rocket becomes equipped with its own set of variables and the ability to perform the class’s defined actions.

When we create my_rocket, it’s more than just a variable; it’s a complete Rocket object, part of the main program scope, each residing in its unique location in the computer’s memory. Each instance of a class is independent. This means if decide to create multiple rockets, changes made to one rocket, such as altering its coordinates or moving it, won’t impact the others.

class Rocket():
    # Rocket simulates a rocket ship for a game (or a physics simulation!).
    
    def __init__(self):
        # Each rocket has an (x,y) position.
        self.x = 0
        self.y = 0
        
    def move_up(self,increment):
        # Increment the y-position of the rocket.
        self.y += 1

# Create a Rocket object, and have it start to move up.
my_rocket = Rocket()

print(type(my_rocket))

print("Rocket altitude:", my_rocket.y)

my_rocket.move_up()
print("Rocket altitude:", my_rocket.y)

my_rocket.move_up()
print("Rocket altitude:", my_rocket.y)
<class '__main__.Rocket'>
Rocket altitude: 0
Rocket altitude: 2
Rocket altitude: 5

To interact with an object’s attributes or invoke its methods, we use dot notation. For example, to access the y-coordinate of my_rocket, you would use my_rocket.y. To move the rocket up, you call the method with my_rocket.move_up().

Note

You’ll recognize this dot notation from things like random.shuffle() (shuffle is one of the methods possessed by objects instantiated by the Random class inside the random module contained in random.py). Whew..

Remember: each instance of a class is a separate entity with its own unique set of variables. Though all rockets share the same methods, like moving up, the actions of one rocket do not impact the others.

Now, let’s see how we can create a simple fleet of rockets:

class Rocket():
    # Rocket simulates a rocket ship for a game,
    #  or a physics simulation.
    
    def __init__(self):
        # Each rocket has an (x,y) position.
        self.x = 0
        self.y = 0
        
    def move_up(self):
        # Increment the y-position of the rocket.
        self.y += 1
        
# Create a fleet of 5 rockets, and store them in a list.

my_rockets = []
for _ in range(5):
    new_rocket = Rocket()
    my_rockets.append(new_rocket)

# Show that each rocket is a separate object.
for rocket in my_rockets:
    print(rocket)
<__main__.Rocket object at 0x7fa95e0d1d00>
<__main__.Rocket object at 0x7fa95e0d1ac0>
<__main__.Rocket object at 0x7fa95e213cd0>
<__main__.Rocket object at 0x7fa95e213550>
<__main__.Rocket object at 0x7fa95e213640>

When we print out each Rocket object, you’ll notice that it displays a unique place in memory. These distinct memory locations demonstrate that each rocket is a separate object in our program.

But what would happen if we assign one rocket object to another, like with my_rockets[0] = my_rockets[1]?

Try it! After making this assignment, print out the rocket objects again. Pay close attention to the memory addresses and observe any changes. This exercise will help you understand how Python handles object references and the implications of assigning one object to another. Reflect on what you see – why do you think the memory addresses change or stay the same? Why does this happen?

Tip

You can use list comprehension to create your rocket fleet in one line:

my_rockets = [Rocket() for _ in range(5)]

Verify that each rocket has its own x and y values by moving just one of the rockets:

class Rocket():
    # Rocket simulates a rocket ship for a game,
    #  or a physics simulation.
    
    def __init__(self):
        # Each rocket has an (x,y) position.
        self.x = 0
        self.y = 0
        
    def move_up(self):
        # Increment the y-position of the rocket.
        self.y += 1
        
# Create a fleet of 5 rockets, and store them in a list.
my_rockets = [Rocket() for x in range(0,5)]

# Move the first rocket up.
my_rockets[0].move_up()

# Show that only the first rocket has moved.
for rocket in my_rockets:
    print("Rocket altitude:", rocket.y)
Rocket altitude: 1
Rocket altitude: 0
Rocket altitude: 0
Rocket altitude: 0
Rocket altitude: 0

Review: Object-Oriented terminology#

Classes are a fundamental part of a programming paradigm known as object-oriented programming (OOP). OOP emphasizes creating reusable blocks of code, known as classes, which form the basis for building applications.

In OOP, when you want to use the functionality defined in a class, you create an object from that class – this is the essence of the ‘object-oriented’ approach. While Python is a versatile language that supports multiple programming paradigms, object-oriented programming plays a significant role in Python development.

You’ll likely use objects in most, if not all, of your Python projects. To fully grasp how classes work in Python, it’s important to understand the language and concepts specific to OOP, such as encapsulation, inheritance, and polymorphism. These concepts form the backbone of object-oriented programming and will be crucial as we delve deeper into Python classes.

A closer look at the Rocket class#

Now that you have seen a simple example of a class, and have learned some basic OOP terminology, it will be helpful to take a closer look at the Rocket class that we started with.

A simple method#

Here is the method that was defined for the Rocket class:

class Rocket():
    # Rocket simulates a rocket ship for a game,
    #  or a physics simulation.
    
    def __init__(self):
        # Each rocket has an (x,y) position.
        self.x = 0
        self.y = 0
        
    def move_up(self):
        # Increment the y-position of the rocket.
        self.y += 1

In our initial code, we have two methods: initalization and moving upwards.

To cement the concept, let’s look at what happens when a method is called:

class Rocket():
    
    def __init__(self):
        # Each rocket has an (x,y) position.
        self.x = 0
        self.y = 0
        
    def move_up(self):
        # Increment the y-position of the rocket.
        self.y += 1

# Create a Rocket object, and have it start to move up.
my_rocket = Rocket()
print("Rocket altitude:", my_rocket.y)

my_rocket.move_up()
print("Rocket altitude:", my_rocket.y)

my_rocket.move_up()
print("Rocket altitude:", my_rocket.y)
Rocket altitude: 0
Rocket altitude: 1
Rocket altitude: 2

To review the concepts: in our example, a Rocket object is created and stored in the variable my_rocket. After this object is created, its y value is printed. The value of the attribute y is accessed using dot notation. The phrase my_rocket.y asks Python to return “the value of the variable y attached to the object my_rocket”.

After the object my_rocket is created and its initial y-value is printed, the method move_up() is called. This tells Python to apply the method move_up() to the object my_rocket. Python finds the y-value associated with my_rocket and adds 1 to that value. This process is repeated several times, and you can see from the output that the y-value is in fact increasing.

Tip

Want to remind yourself what methods are defined/available for use with an object? Use help(your_object)

Making multiple objects from a class#

One of the main purposes and strengths of object oriented programming (OOP) is the ability to foster reusable code. Once you’ve crafted a class, like our Rocket class, you can create numerous objects from it as needed.

This brings us to an important practice in programming: often, classes are defined in separate files and then imported into the main program. This approach enables you to build a library of classes that can be used repeatedly across various programs. When you have a well-tested class, you can rely on it to function consistently in new projects, ensuring that the objects you create will behave as expected.

Think back to our section “Why Classes” and consider how complicated a video game would be if every action, enemy, location and object were created and stored seperately

This principle of ‘code reusability’ is evident with our Rocket class. We can use this single class definition to create a whole fleet of Rocket objects. Let’s give it a try:

class Rocket():
    # Rocket simulates a rocket ship for a game,
    #  or a physics simulation.
    
    def __init__(self):
        # Each rocket has an (x,y) position.
        self.x = 0
        self.y = 0
        
    def move_up(self):
        # Increment the y-position of the rocket.
        self.y += 1
        
# Create a fleet of 5 rockets, and store them in a list.
my_rockets = []
for x in range(0,5):
    new_rocket = Rocket()
    my_rockets.append(new_rocket)

# Show that each rocket is a separate object.
for rocket in my_rockets:
    print(rocket)
<__main__.Rocket object at 0x7fedd45e8610>
<__main__.Rocket object at 0x7fedd3b03b20>
<__main__.Rocket object at 0x7fedd45e8eb0>
<__main__.Rocket object at 0x7fedd45e8d60>
<__main__.Rocket object at 0x7fedd45e8820>

As you can see, with just one class definition, we’re able to instantiate multiple Rocket objects, each with their individual attributes and behaviors. This is a great example of how classes in Python enable efficient and modular code.

In fact, we could potentially make this code even more concise with lists. If you’re already familiar with list comprehensions in Python, feel free to use them to create multiple instances of a class efficiently. However, I understand that not everyone may be comfortable with comprehensions yet. So, let’s explore a more straightforward method that’s easier for beginners to grasp.

Instead of using list comprehensions, we’ll start by creating an empty list. Then, we’ll populate this list with instances of our class using a for loop. This method is slightly more verbose than a list comprehension but is often more accessible for those new to Python. It also allows us to skip using a temporary variable like new_rocket, making the code more concise

class Rocket():
    
    def __init__(self):
        # Each rocket has an (x,y) position.
        self.x = 0
        self.y = 0
        
    def move_up(self):
        # Increment the y-position of the rocket.
        self.y += 1
        
# Create a fleet of 5 rockets, and store them in a list.
my_rockets = []
for x in range(0,5):
    my_rockets.append(Rocket())

# Show that each rocket is a separate object.
for rocket in my_rockets:
    print(rocket)
<__main__.Rocket object at 0x7fedd45e8e50>
<__main__.Rocket object at 0x7fedd45e8eb0>
<__main__.Rocket object at 0x7fedd45e8d60>
<__main__.Rocket object at 0x7fedd45e8bb0>
<__main__.Rocket object at 0x7fedd45e8f10>

What exactly happens in this for loop? The line my_rockets.append(Rocket()) is executed 5 times. Each time, a new Rocket object is created and then added to the list my_rockets. The __init__() method is executed once for each of these objects, so each object gets its own x and y value. When a method is called on one of these objects, the self variable allows access to just that object’s attributes, and ensures that modifying one object does not affect any of the other objecs that have been created from the class.

Each of these objects can be worked with individually. At this point we are ready to move on and see how to add more functionality to the Rocket class. We will work slowly, and give you the chance to start writing your own simple classes.

A quick check-in#

Congratulations on reaching this point! Understanding classes is a significant milestone in your programming journey. As you progress, you’ll learn how classes can be used in flexible and powerful ways to improve your code. If everything so far makes sense, you’re well on your way. If not, don’t worry—this is a complex topic, and it’s normal for it to take some time to fully grasp. Here are a few strategies that might help:

  • Revisit Previous Sections: Sometimes, concepts become clearer upon a second or third reading. Go back through the previous sections and see if things start to click.

  • Hands-On Practice: There’s no substitute for practice. Type out the examples in your own editor and experiment with them. See what happens if you change parts of the code. This kind of exploration can be incredibly enlightening.

  • Attempt the Next Exercise: The upcoming exercise is designed to reinforce the concepts you’ve just learned. Even if you’re not feeling entirely confident, give it a try—it might help solidify your understanding.

  • Keep Reading: The next sections will add more functionality to the Rocket class and revisit some of the covered concepts in a new context. This repetition might help reinforce your understanding.

Classes are a foundational aspect of programming, and mastering them now as a beginner will serve you well throughout your career. If you’re new to this, it’s perfectly okay to take your time. Be patient with yourself and trust that with practice and persistence, these tools can be mastered.

Refining the Rocket class#

The Rocket class so far does what a rocket is supposed to do. But, it is arguably still very boring is very simple. Let’s explore some of the capabilities of classes by making our rockets a little more interesting. We can do this by making some refinements to the __init__() method, and by the addition of some new methods.

Accepting parameters for the __init__() method#

The __init__() method is run automatically one time when you create a new object from a class. The __init__() method for the Rocket class so far is pretty simple:

class Rocket():
    # Rocket simulates a rocket ship for a game,
    #  or a physics simulation.
    
    def __init__(self):
        # Each rocket has an (x,y) position.
        self.x = 0
        self.y = 0
        
    def move_up(self):
        # Increment the y-position of the rocket.
        self.y += 1

All the __init__() method does so far is set the x and y values for the rocket to 0. We can easily add a couple keyword arguments so that new rockets can be initialized at any position:

class Rocket():
    
    def __init__(self, x=0, y=0):
        # Each rocket has an (x,y) position.
        self.x = x
        self.y = y
        
    def move_up(self):
        # Increment the y-position of the rocket.
        self.y += 1

Now when you create a new Rocket object you have the choice of passing in arbitrary initial values for x and y:

class Rocket():
    # Rocket simulates a rocket ship for a game,
    #  or a physics simulation.
    
    def __init__(self, x=0, y=0):
        # Each rocket has an (x,y) position.
        self.x = x
        self.y = y
        
    def move_up(self):
        # Increment the y-position of the rocket.
        self.y += 1
        
# Make a series of rockets at different starting places.
rockets = []
rockets.append(Rocket())
rockets.append(Rocket(0,10))
rockets.append(Rocket(100,0))

# Show where each rocket is.
for index, rocket in enumerate(rockets):
    print(f"Rocket {index} is at {rocket.x}, {rocket.y}")
Rocket 0 is at 0, 0
Rocket 1 is at 0, 10
Rocket 2 is at 100, 0

Accepting parameters in a method#

Remember, the init method in a class has a special role: it’s specifically designed to initialize new objects created from the class. However, it’s important to remember that this is just one of many methods a class can have, and like any method, it can accept many different parameters.

Let’s consider enhancing the functionality of our Rocket class. Up to this point, we’ve had a move_up() method. However, methods in a class are not limited to fixed behaviors like moving in one specific direction. By accepting parameters, methods can be made much more versatile. For instance, we can transform the move_up() method for our rocket into a more general move_rocket() method. This improved method accepts parameters that specify not just how much to move, but also in which direction. Our move_rocket method would allow us to move the rocket in any direction by any desired amount.

Here’s a look at how the move_rocket() method can be implemented to provide this enhanced flexibility:

class Rocket():
    
    def __init__(self, x=0, y=0):
        # Each rocket has an (x,y) position.
        self.x = x
        self.y = y
        
    def move_rocket(self, x_increment=0, y_increment=1):
        # Move the rocket according to the paremeters given.
        #  Default behavior is to move the rocket up one unit.
        self.x += x_increment
        self.y += y_increment

With this revised method we have greatly enhanced the capabilities of our Rocket class! To make its functionality clear, the parameters are named x_increment and y_increment, emphasizing that they represent changes to the rocket’s current position, rather than setting new absolute coordinates. We want to move the rocket, not teleport it to a new location.

Choosing the right default values for these parameters is critical. When we establish correct default values we can also establish meaningful default behaviors for the move_rocket() method. For instance, if someone calls move_rocket() without any parameters, we can set it up so the rocket moves up one unit in the y-direction by default. This makes the method intuitive and easier for the user.

Additionally, this method’s design allows for a lot of flexibility in our code and by extension, to our rocket class. By passing negative values to x_increment or y_increment, the rocket can be moved left, right, or downward. This flexibility showcases the power of parameterized methods in classes, enabling complex and dynamic behavior with simple method calls.

Adding a new method#

One of the strenghts of object-oriented programming (OOP) is its ability to closely mimic real-world situations by adding attributes and behaviors to classes.

Consider a team piloting a fleet of rockets: one of their most important jobs would be to maintain safe distances between each rocket so they don’t crash into each other. If we want to model this scenario in our Rocket class, we can introduce a new method that calculates and reports the distance from one rocket to another.

For those who might be new to the concept of distance calculation in a two-dimensional space, there’s a straightforward formula used to determine the distance between two points, given their x and y coordinates. Our new method will incorporate this formula to calculate the distance. This not only adds a practical functionality to our Rocket class but also demonstrates how mathematical concepts can be integrated into programming to solve real-world problems.

The method will take the x and y coordinates of another rocket as inputs and will return the calculated distance. This is how we can continue to expand the capabilities of our class, making it more versatile and reflective of real-life situations.

from math import sqrt

class Rocket():
    # Rocket simulates a rocket ship for a game,
    #  or a physics simulation.
    
    def __init__(self, x=0, y=0):
        # Each rocket has an (x,y) position.
        self.x = x
        self.y = y
        
    def move_rocket(self, x_increment=0, y_increment=1):
        # Move the rocket according to the paremeters given.
        #  Default behavior is to move the rocket up one unit.
        self.x += x_increment
        self.y += y_increment
        
    def get_distance(self, other_rocket):
        # Calculates the distance from this rocket to another rocket,
        #  and returns that value.
        distance = sqrt((self.x-other_rocket.x)**2+(self.y-other_rocket.y)**2)
        return distance
    
# Make two rockets, at different places.
rocket_0 = Rocket()
rocket_1 = Rocket(10,5)

# Show the distance between them.
distance = rocket_0.get_distance(rocket_1)
print(f"The rockets are {round(distance,2)} units apart.")
The rockets are 11.18 units apart.

These enhancements to our Rocket class illustrate an advantage of object-oriented programming: the ability to tailor a class’s attributes and behaviors to precisely model the phenomena you’re interested in.

Imagine the possibilities with our Rocket class! Our rockets could have attributes like a name, crew capacity, payload capacity, a specific amount of fuel, and much more. The behaviors you define are limited only by your imagination. You could program the rocket to interact with other rockets, launch facilities, and even simulate the effects of gravitational fields—anything that your project requires.

While the idea of adding such complexity might seem daunting, what you’ve learned so far lays the groundwork for these advancements. You’ve already grasped the core concepts of object-oriented programming: defining classes, creating objects, and customizing methods and attributes.

Now, it’s a great time to put this knowledge into practice to cement these concepts. Try writing some classes on your own and model some behaviors of your own. Start simple, maybe a class for a car, a book, or even a plant. See how you can represent different real-world objects as Python classes. After you’ve had some practice, we’ll delve into object inheritance, a powerful feature that will further expand your capabilities in object-oriented programming. This is just the beginning, and you’re well on your way to mastering these concepts!

Inheritance#

One of the fundamental objectives in object-oriented programming (OOP) is to foster the creation of stable, reliable, and reusable code. In Python, as in other OOP-supportive languages, this is often done through the concept of inheritance, where one class (known as the child or subclass) can inherit from another class (referred to as the parent or superclass).

Inheritance allows a new class to adopt all the attributes and behaviors of an existing class. This means when you create a subclass, it automatically possesses the properties and methods of its superclass. However, the beauty of inheritance lies in its flexibility: a subclass can modify or override any aspects of the superclass that don’t fit its purpose. It can also introduce new attributes and behaviors that are specific to itself. While the subclass inherits everything from the parent class, it’s important to note that the reverse isn’t true: attributes and methods defined in the child class are exclusive to it and not accessible to the parent.

To illustrate this concept, let’s use the example of a Vehicle class. This class can have general attributes like a position in space, a weight, a color etc, and a method like move().

Our example of the Rocket class might easily fit into the parent class of Vehicle. But we might also have other subclasses like Car. The Car class will have all the attributes and methods of the superclass “Vehicle” like position and ability to move, but it can also have additional features like airbags and a unique method like park() or horn() that we wouldnt expect our rocket to have. Here, the Car class and our rocket class are the child or subclasses, while Vehicle is the parent or superclass. Notice that Car and Rocket share some properties that make them both vehicles. But they do not share all the same properties.

The SpaceShuttle class#

Let’s expand our space fleet with another Vehicle. We have a Rocket class, now let’s add a SpaceShuttle class.

When modeling a space shuttle in our program, we might be tempted to start from scratch and create an entirely new class. However, a space shuttle shares many characteristics with a generic rocket; it’s essentially a specialized type of rocket. This is a perfect scenario to leverage the power of inheritance in object-oriented programming.

Instead of writing a new class from scratch, we can inherit all the attributes and behaviors of the existing Rocket class and then extend it with specific features unique to a space shuttle. This approach not only saves time but also helps in maintaining consistency and reducing code redundancy.

One distinct characteristic of a space shuttle, as opposed to other rockets, is its reusability. While a rocket is typically used only once and can’t be relaunched, a space shuttle can be used over and over again. To represent this, we can add an attribute that tracks the number of flights completed by the shuttle. Other essential features of the shuttle, like its basic structure and movement capabilities, are already encapsulated in the Rocket class. Here’s how the SpaceShuttle class might look

from math import sqrt

class Rocket():
    
    def __init__(self, x=0, y=0):
        # Each rocket has an (x,y) position.
        self.x = x
        self.y = y
        
    def move_rocket(self, x_increment=0, y_increment=1):
        # Move the rocket according to the paremeters given.
        #  Default behavior is to move the rocket up one unit.
        self.x += x_increment
        self.y += y_increment
        
    def get_distance(self, other_rocket):
        # Calculates the distance from this rocket to another rocket,
        #  and returns that value.
        distance = sqrt((self.x-other_rocket.x)**2+(self.y-other_rocket.y)**2)
        return distance
    
class Shuttle(Rocket):
    # Shuttle simulates a space shuttle, which is really
    #  just a reusable rocket.
    
    def __init__(self, x=0, y=0, flights_completed=0):
        super().__init__(x, y)
        self.flights_completed = flights_completed
        
shuttle = Shuttle(10,0,3)
print(shuttle.flights_completed)
3

When a new class is based on an existing class, you write the name of the parent class in parentheses when you define the new class:

class NewClass(ParentClass):
    pass

When creating a new class through inheritance, it’s essential to properly initialize the inherited attributes from the parent class. This is where the __init__() method of the subclass (the newly created class) comes into play. In this method, we need to ensure that the initialization process of the parent class is also executed for the subclass. This step is crucial to maintain the integrity of the new object, ensuring that all attributes defined in the parent class are appropriately initialized in the subclass.

To achieve this, the __init__() method of the subclass should call the __init__() method of the parent class. This is done using the super() function. The super().__init__() call in the subclass’s __init__() method allows us to pass the necessary parameters to the parent class’s __init__() method. Here’s how it works:

  • The subclass’s __init__() method accepts all the parameters required by the parent class, possibly along with additional parameters specific to the subclass.

  • Inside the __init__() method of the subclass, we use super().__init__() to call the corresponding method of the parent class.

  • We pass the required parameters to super().__init__() so that the parent class can properly initialize its attributes.

For example, in the SpaceShuttle class inheriting from the Rocket class:

class NewClass(ParentClass):
    
    def __init__(self, arguments_new_class, arguments_parent_class):
        super().__init__(arguments_parent_class)
        # Code for initializing an object of the new class.

The super() function passes the self argument to the parent class automatically. You could also do this by explicitly naming the parent class when you call the __init__() function, but you then have to include the self argument manually:

class Shuttle(Rocket):
    # Shuttle simulates a space shuttle, which is really
    #  just a reusable rocket.
    
    def __init__(self, x=0, y=0, flights_completed=0):
        Rocket.__init__(self, x, y)
        self.flights_completed = flights_completed

The output above shows that a new Shuttle object was created. This new Shuttle object can store the number of flights completed, but it also has all of the functionality of the Rocket class: it has a position that can be changed, and it can calculate the distance between itself and other rockets or shuttles. This can be demonstrated by creating several rockets and shuttles, and then finding the distance between one shuttle and all the other shuttles and rockets. This example uses a simple function called randint, which generates a random integer between a lower and upper bound, to determine the position of each rocket and shuttle:

A quick note on syntax: while it might initially seem simpler to directly call the parent class’s init() method, using the super() syntax is almost always going to make things easier for you in the long run.

The advantage of super() is that it doesn’t require you to explicitly name the parent class, making your code more adaptable to modifications. As you delve deeper into Python classes, you might encounter scenarios where a child class inherits from multiple parent classes. In such cases, super() becomes particularly useful as it can seamlessly handle calling the init() methods of all parent classes in the correct order, all within a single line.

from math import sqrt
from random import randint

class Rocket():
    # Rocket simulates a rocket ship for a game,
    #  or a physics simulation.
    
    def __init__(self, x=0, y=0):
        # Each rocket has an (x,y) position.
        self.x = x
        self.y = y
        
    def move_rocket(self, x_increment=0, y_increment=1):
        # Move the rocket according to the paremeters given.
        #  Default behavior is to move the rocket up one unit.
        self.x += x_increment
        self.y += y_increment
        
    def get_distance(self, other_rocket):
        # Calculates the distance from this rocket to another rocket,
        #  and returns that value.
        distance = sqrt((self.x-other_rocket.x)**2+(self.y-other_rocket.y)**2)
        return distance
    
class Shuttle(Rocket):
    # Shuttle simulates a space shuttle, which is really
    #  just a reusable rocket.
    
    def __init__(self, x=0, y=0, flights_completed=0):
        super().__init__(x, y)
        self.flights_completed = flights_completed
        
        
# Create several shuttles and rockets, with random positions.
#  Shuttles have a random number of flights completed.
shuttles = []
for x in range(0,3):
    x = randint(0,100)
    y = randint(1,100)
    flights_completed = randint(0,10)
    shuttles.append(Shuttle(x, y, flights_completed))

rockets = []
for x in range(0,3):
    x = randint(0,100)
    y = randint(1,100)
    rockets.append(Rocket(x, y))
    
# Show the number of flights completed for each shuttle.
for index, shuttle in enumerate(shuttles):
    print(f"Shuttle {index} has completed {shuttle.flights_completed} flights.")
    
print("\n")    
# Show the distance from the first shuttle to all other shuttles.
first_shuttle = shuttles[0]
for index, shuttle in enumerate(shuttles):
    distance = first_shuttle.get_distance(shuttle)
    print(f"The first shuttle is {round(distance,2)} units away from shuttle {index}.")

print("\n")
# Show the distance from the first shuttle to all other rockets.
for index, rocket in enumerate(rockets):
    distance = first_shuttle.get_distance(rocket)
    print(f"The first shuttle is {round(distance,3)} units away from rocket {index}.")
Shuttle 0 has completed 4 flights.
Shuttle 1 has completed 7 flights.
Shuttle 2 has completed 5 flights.


The first shuttle is 0.0 units away from shuttle 0.
The first shuttle is 45.89 units away from shuttle 1.
The first shuttle is 52.8 units away from shuttle 2.


The first shuttle is 54.452 units away from rocket 0.
The first shuttle is 93.92 units away from rocket 1.
The first shuttle is 62.008 units away from rocket 2.

Inheritance is not just a feature but a cornerstone of object-oriented programming. It empowers you to model a wide array of real-world and virtual phenomena with remarkable precision and efficiency. The classes you create, with the principles of inheritance, can become stable and reusable components across various applications, enhancing the versatility of your code.

Imagine the possibilities - with the foundational understanding you now have, you’re equipped to tackle exciting projects. Why not try your hand at building a hierarchy of classes? Here are some ideas to get you started:

  • Animal Kingdom: Create a basic Animal class and then derive specific animal classes like Bird, Fish, or Mammal. Each subclass can have unique attributes and behaviors, like fly() for Bird or swim() for Fish.

  • Transport System: Start with a general Vehicle class and extend it to specific types like Car, Bicycle, and Boat. Each subclass could have specialized methods like drive() for Car or sail() for Boat.

  • Soccer Team: Start with a Player class and extend it to specify different roles like Goalie, Stiker or Wingback. Each subclass has different abilities and responsibilities in the game.

Remember, these examples are just starting points. The real joy and excitement of programming comes from creating solutions that are unique to your own personal vision and needs. Before we move on to models, don’t be afraid to experiment and build upon what you’ve learned in new ways.

Modules and classes#

As you continue working with classes, you’ll notice that your program files are becoming longer. Not surprising given that you are now able to add so much more! On the one hand, this is a positive sign, indicating that your programs are becoming more robust and capable of handling complex tasks. On the other hand, larger files can be difficult to keep organized

In Python, a common strategy to manage growing codebases is to modularize your code. This involves organizing related functions and classes into separate modules. Just like we previously modularized the Stroop experiment code by separating commonly used functions into a different file, the same principle applies to classes in object-oriented programming (OOP).

By placing a class and its various methods into a separate file, you create a module that can be easily reused across different programs. This modular approach has several advantages:

  • Reusability: Isolated classes in modules can be imported and used in numerous programs, reducing the need to duplicate code.

  • Maintainability: Smaller, focused files are easier to maintain and update compared to larger, monolithic files.

  • Collaboration: Modular code is easier for teams to work on, as different parts of the program can be developed and debugged independently.

  • Reliability: As you reuse and refine your classes across different projects, they become more robust and reliable.

However, effective and organizaed modulatization requires some planning. You’ll need to stop and consider how to logically group classes and functions into modules, ensuring that each module has a clear and distinct responsibility.

To put this into practice, let’s start by moving some of our existing classes into their own files. This will not only help in organizing our code but also give us a firsthand experience of working with modules in Python.

Storing a single class in a module#

When you save a class into a separate file, that file is called a module. You can have any number of classes in a single module. There are a number of ways you can then import the class you are interested in.

Start out by saving just the Rocket class into a file called rocket.py. Notice the naming convention being used here: the module is saved with a lowercase name, and the class starts with an uppercase letter. This convention is pretty important for a number of reasons, and it is a really good idea to follow the convention.

from math import sqrt

class Rocket():
    
    def __init__(self, x=0, y=0):
        # Each rocket has an (x,y) position.
        self.x = x
        self.y = y
        
    def move_rocket(self, x_increment=0, y_increment=1):
        # Move the rocket according to the paremeters given.
        #  Default behavior is to move the rocket up one unit.
        self.x += x_increment
        self.y += y_increment
        
    def get_distance(self, other_rocket):
        # Calculates the distance from this rocket to another rocket,
        #  and returns that value.
        distance = sqrt((self.x-other_rocket.x)**2+(self.y-other_rocket.y)**2)
        return distance

Make a separate file called rocket_game.py. Or, if you want it to sound more scienc-ey, call it rocket_simulation.py. The exact name doesn’t matter, but the convention does. Follow the convention by using a lowercase_underscore name for this file. This file will use the rocket class to do something.

# Save as rocket_game.py
from rocket import Rocket

rocket = Rocket()
print(f"The rocket is at {rocket.x}, {rocket.y}")
The rocket is at 0, 0

By moving our Rocket class into its own file, we’ve created a cleaner and more focused main program file. This approach offers several advantages:

  • Clarity and Focus: The main file now becomes less cluttered, as it no longer contains the detailed implementation of the Rocket class. This separation allows your main program to be more focused on its specific logic and functionalities.

  • Reusability: With the Rocket class in a separate file, you can easily use it across multiple programs without needing to duplicate the class code. This means that all the attributes and behaviors of a rocket are defined in one place (rocket.py), and this definition can be accessed and utilized in any other file within your project.

  • Simplified Code Management: Having a dedicated file for the Rocket class makes it easier to manage and update. Changes to the class can be made in one place, and these changes will automatically reflect in every part of your program that uses this class.

The first line of your main program almost always contains an import statement like “import rocket”. This tells Python to look for a file named rocket.py in the same directory as your main program file. If the class files are located in different directories, you can still access them, but you might need to modify the PYTHONPATH or use relative imports depending on your project structure.

When Python locates and reads rocket.py, it imports the Rocket class into your main file. This process is done behind the scenes; you won’t see the code from rocket.py in your main file, but you can use the Rocket class as if it were defined there. This is the essence of Python’s import system - it keeps your codebase organized and modular, allowing you to focus on building your application with well-structured and reusable components.

Storing multiple classes in a module#

A module is simply a file that contains one or more classes or functions, so the Shuttle class actually belongs in the rocket module as well:

# Save as rocket.py
from math import sqrt

class Rocket():
    # Rocket simulates a rocket ship for a game,
    #  or a physics simulation.
    
    def __init__(self, x=0, y=0):
        # Each rocket has an (x,y) position.
        self.x = x
        self.y = y
        
    def move_rocket(self, x_increment=0, y_increment=1):
        # Move the rocket according to the paremeters given.
        #  Default behavior is to move the rocket up one unit.
        self.x += x_increment
        self.y += y_increment
        
    def get_distance(self, other_rocket):
        # Calculates the distance from this rocket to another rocket,
        #  and returns that value.
        distance = sqrt((self.x-other_rocket.x)**2+(self.y-other_rocket.y)**2)
        return distance
    

class Shuttle(Rocket):
    # Shuttle simulates a space shuttle, which is really
    #  just a reusable rocket.
    
    def __init__(self, x=0, y=0, flights_completed=0):
        super().__init__(x, y)
        self.flights_completed = flights_completed

Now you can import the Rocket and the Shuttle class, and use them both in a clean uncluttered program file:

# Save as rocket_game.py
from rocket import Rocket, Shuttle

rocket = Rocket()
print(f"The rocket is at {rocket.x}, {rocket.y}")

shuttle = Shuttle()
print(f"\nThe shuttle is at {shuttle.x}, {shuttle.y}")

print(f"\nThe shuttle has completed {shuttle.flights_completed} flights.")
---------------------------------------------------------------------------
ImportError                               Traceback (most recent call last)
/Users/glupyan/gitRepos/psych750.github.io/notebooks/classes.ipynb Cell 77 in <cell line: 2>()
      <a href='vscode-notebook-cell:/Users/glupyan/gitRepos/psych750.github.io/notebooks/classes.ipynb#Y410sZmlsZQ%3D%3D?line=0'>1</a> # Save as rocket_game.py
----> <a href='vscode-notebook-cell:/Users/glupyan/gitRepos/psych750.github.io/notebooks/classes.ipynb#Y410sZmlsZQ%3D%3D?line=1'>2</a> from rocket import Rocket, Shuttle
      <a href='vscode-notebook-cell:/Users/glupyan/gitRepos/psych750.github.io/notebooks/classes.ipynb#Y410sZmlsZQ%3D%3D?line=3'>4</a> rocket = Rocket()
      <a href='vscode-notebook-cell:/Users/glupyan/gitRepos/psych750.github.io/notebooks/classes.ipynb#Y410sZmlsZQ%3D%3D?line=4'>5</a> print(f"The rocket is at {rocket.x}, {rocket.y}")

ImportError: cannot import name 'Shuttle' from 'rocket' (/Users/glupyan/gitRepos/psych750.github.io/notebooks/rocket.py)

The first line tells Python to import both the Rocket and the Shuttle classes from the rocket module. You don’t have to import every class in a module; you can pick and choose the classes you care to use, and Python will only spend time processing those particular classes.

Conclusion: The Power of Classes in Python#

Classes are a fundamental tool for programming. In addition to lists, variables and dictonaries, classes are a datatype that when used correctly, will save you time and effort in programming complex objects. To conclude our exploration of classes in Python, let’s review some the key concepts and terminology for understanding classes

Understanding Classes and Objects:

  • Classes are blueprints for creating objects, each with their own attributes (data) and methods (behaviors). Through classes, you learned how to encapsulate data and functionality, making your code more organized and efficient. This tells us what differentiates classes and why they are such useful tools.

  • Advantages of Using Classes: Classes allow for code reuse and reduction of redundancy. By defining attributes and methods once in a class, you can create multiple instances (objects) without repeating code, leading to cleaner and more manageable programs.

  • Inheritance - Extending Classes: Inheritance is a powerful feature in object-oriented programming. It allows a new class (subclass) to inherit attributes and methods from an existing class (superclass). This concept was exemplified in the transition from a generic Rocket class to a more specialized SpaceShuttle class, demonstrating how you can build upon existing code to create new functionalities.

  • Modularization - Organizing Code: Modularization involves separating code into distinct sections (modules), each with a specific responsibility. By placing your Rocket class in a separate file (rocket.py), you learned how to create reusable modules. This practice not only makes your code more readable and maintainable but also simplifies collaboration and debugging.

  • Importance of Practice and Exploration: Throughout these lessons, we emphasized hands-on practice and experimentation. Writing your own classes, experimenting with inheritance, and modularizing your code are crucial steps in internalizing these concepts.

Final Thoughts:

Understanding and eventually mastering this datastype will greatly improve your coding and serve as a valuable tool throughout your career. Classes, inheritance, and modularization are not just features of Python; they are tools that empower you to write effective, efficient, and scalable code. They are stepping stones towards becoming a proficient programmer, capable of modeling complex real-world problems and crafting elegant solutions.

To extend the use of classes, the next section shows an example of using classes in Psychopy.

Example of using a class in a Psychopy experiment#

This section goes through a more practical example of using a class in an experiment. Let’s say we want to make a moving circle that wiggles its way across a window. Here’s some basic code to make this happen.

A single moving circle#

import time
import random
import sys
import os
from math import *
from psychopy import visual, core, event

win = visual.Window([300,300],color="black", units='pix',allowGUI=True)
my_mouse = event.Mouse(win=win)

target = visual.Circle(win,size=20,lineColor="black",fillColor=[1,1,1])
min_angle=-30
max_angle=30
prev_angle_to_deviate=0
new_cur_angle=0.0
inter_step_interval = 3.0

while True:
	core.wait(.02)
	target.draw()
	win.flip()

	cur_angle_to_deviate = prev_angle_to_deviate + random.randint(min_angle,max_angle); #calculate new angle
	cur_angle = cur_angle_to_deviate*pi/180.0; #convert to radians
	
	new_x_pos = inter_step_interval*cos(cur_angle)	
	new_y_pos = inter_step_interval*sin(cur_angle)
	
	target.setPos((new_x_pos,new_y_pos),'+')
	hit_boundary=False
	if (abs(target.pos[0]) > 150 or abs(target.pos[1]) > 150):
		hit_boundary=True
		new_x_pos =  inter_step_interval*cos(cur_angle-pi)
		new_y_pos =  inter_step_interval*sin(cur_angle-pi)

	prev_angle_to_deviate = cur_angle_to_deviate
	if hit_boundary:
		prev_angle_to_deviate -= 180
		prev_angle_to_deviate %= 360

	if event.getKeys(['space']):
		sys.exit()

Let’s extend this code so that when we click on the circle, it gets dimmer and moves slower.

import time
import random
import sys
import os
from math import *
from psychopy import visual, core, event

win = visual.Window([350,350],color="black", units='pix',allowGUI=True)
my_mouse = event.Mouse(win=win)

target = visual.Circle(win,size=20,lineColor="black",fillColor=[1,1,1])
min_angle=-30
max_angle=30
prev_angle_to_deviate=0
new_cur_angle=0.0
inter_step_interval = 2.0

while True:
	core.wait(.02)
	target.draw()
	win.flip()

	cur_angle_to_deviate = prev_angle_to_deviate + random.randint(min_angle,max_angle); #calculate new angle
	cur_angle = cur_angle_to_deviate*pi/180.0; #convert to radians
	
	new_x_pos = inter_step_interval*cos(cur_angle)	
	new_y_pos = inter_step_interval*sin(cur_angle)

	if my_mouse.isPressedIn(target):
		inter_step_interval *= .8
		target.opacity *= .9
	
	target.setPos((new_x_pos,new_y_pos),'+')
	hit_boundary=False
	if (abs(target.pos[0]) > 150 or abs(target.pos[1]) > 150):
		hit_boundary=True
		new_x_pos =  inter_step_interval*cos(cur_angle-pi)
		new_y_pos =  inter_step_interval*sin(cur_angle-pi)

	prev_angle_to_deviate = cur_angle_to_deviate
	if hit_boundary:
		prev_angle_to_deviate -= 180
		prev_angle_to_deviate %= 360

	if event.getKeys(['space']):
		sys.exit()

More circles!#

But now suppose we want to have lots of moving circles that move independently of one another such that we can set some to move faster, have them be different colors, allow people to click on them independently, etc. This is the kind of situation where classes shine.

The code below implements a movingCircle class. We then use it to make a bunch of individual circles and allow the user to click on them individually. Notice that it’s not much longer than the code above. And if we want 10 circles, we just need to change numCircles = 4 to numCircles = 5. Wizardry!

import time
import random
import sys
import os
from math import sin, cos, pi
from psychopy import visual, core, event

win = visual.Window([300,300],color="black", units='pix',allowGUI=True)
my_mouse = event.Mouse(win=win)
num_cirlces = 4

class MovingCircle():
	
	def __init__(self,win):

		self.min_angle=-30
		self.max_angle=30
		self.prev_angle_to_deviate=0
		self.angle_moving_degrees=0.0
		self.inter_step_interval = 2.0
		self.target = visual.Circle(win,size=20,lineColor="black",fillColor=[1,1,1])

	def change_speed(self,delta_speed):
		self.inter_step_interval *= delta_speed

	def make_dimmer(self,percent):
		self.target.opacity *= percent

	def get_pos(self):
		return self.target.pos

	def target(self):
		return self.target

	def move_it(self):
		"gets new position and sets target to that position"
		cur_angle_to_deviate = self.prev_angle_to_deviate + random.randint(self.min_angle,self.max_angle); #calculate new angle
		cur_angle = cur_angle_to_deviate*pi/180.0; #convert to radians
		
		new_x_pos = self.inter_step_interval*cos(cur_angle)	
		new_y_pos = self.inter_step_interval*sin(cur_angle)
		
		self.target.setPos((new_x_pos,new_y_pos),'+')
		hit_boundary=False
		if (abs(self.target.pos[0]) > 150 or abs(self.target.pos[1]) > 150):
			hit_boundary=True
			new_x_pos =  self.inter_step_interval*cos(cur_angle-pi)
			new_y_pos =  self.inter_step_interval*sin(cur_angle-pi)

		self.prev_angle_to_deviate = cur_angle_to_deviate
		if hit_boundary:
			self.prev_angle_to_deviate -= 180
			self.prev_angle_to_deviate %= 360


circles = [MovingCircle(win) for _ in range(num_cirlces)]
while True:
	for cur_circle in circles:
		cur_circle.target.draw()
		if my_mouse.isPressedIn(cur_circle.target):
			print('clicked on a circle!' )
			cur_circle.change_speed(.8)
			cur_circle.make_dimmer(.9)

		cur_circle.move_it()
	core.wait(.05)
	win.flip()

	if event.getKeys(['space']):
		sys.exit()

Make sure you understand what’s happening here. Play around with the code. If you’re confused, message on Slack.

A number of ways to import modules and classes#

We’ve covered imports already, but just as a review: Recall that there are several ways to import modules and classes, and each has its own merits.

import module_name#

The syntax for importing classes that was just shown:

from module_name import ClassName

is straightforward, and is commonly used. It allows you to use the class names directly in your program, so you have very clean and readable code. This can be a problem, however, if the names of the classes you are importing conflict with names that have already been used in your program This is unlikely to happen in short programs like those in the examples, but becomes increasingly likely as your codebase grows. It’s also an issue if you are sharing your code. Maybe you take pains to ensure that a class name doesn’t conflict with names used in your code, but can you guarantee that it won’t happen when others use your code? So, you can also import the module itself:

# Save as rocket_game.py
import rocket

rocket_0 = rocket.Rocket()
print(f"The rocket is at {rocket_0.x}, {rocket_0.y}")
print(f"Shuttle {index} has completed {shuttle.flights_completed} flights.")

shuttle_0 = rocket.Shuttle()
print(f"The shuttle is at {shuttle_0.x}, {shuttle_0.y}")
print(f"Shuttle has completed {shuttle_0.flights_completed} flights.")
The rocket is at (0, 0).

The shuttle is at (0, 0).
The shuttle has completed 0 flights.

The general syntax for import is:

import module_name

After this, classes are accessed using the dot notation:

module_name.ClassName

This prevents some name conflicts. If you were reading carefully however, you might have noticed that the variable name rocket in the previous example had to be changed because it has the same name as the module itself. This is not good, because in a longer program that could mean a lot of renaming.

import module_name as local_module_name#

There is another syntax for imports that is quite useful:

import module_name as local_module_name

When you are importing a module into one of your projects, you are free to choose any name you want for the module in your project. So the last example could be rewritten in a way that the variable name rocket would not need to be changed:

# Save as rocket_game.py
import rocket as rocket_module

rocket_0 = rocket_module.Rocket()
print(f"The rocket is at {rocket_0.x}, {rocket_0.y}")
print(f"Shuttle {index} has completed {shuttle.flights_completed} flights.")

shuttle_0 = rocket_module.Shuttle()
print(f"The shuttle is at {shuttle_0.x}, {shuttle_0.y}")
print(f"Shuttle has completed {shuttle_0.flights_completed} flights.")
The rocket is at 0, 0
Shuttle 2 has completed 7 flights.
---------------------------------------------------------------------------
AttributeError                            Traceback (most recent call last)
/Users/glupyan/gitRepos/psych750.github.io/notebooks/classes.ipynb Cell 84 in <cell line: 8>()
      <a href='vscode-notebook-cell:/Users/glupyan/gitRepos/psych750.github.io/notebooks/classes.ipynb#Y212sZmlsZQ%3D%3D?line=4'>5</a> print(f"The rocket is at {rocket_0.x}, {rocket_0.y}")
      <a href='vscode-notebook-cell:/Users/glupyan/gitRepos/psych750.github.io/notebooks/classes.ipynb#Y212sZmlsZQ%3D%3D?line=5'>6</a> print(f"Shuttle {index} has completed {shuttle.flights_completed} flights.")
----> <a href='vscode-notebook-cell:/Users/glupyan/gitRepos/psych750.github.io/notebooks/classes.ipynb#Y212sZmlsZQ%3D%3D?line=7'>8</a> shuttle_0 = rocket_module.Shuttle()
      <a href='vscode-notebook-cell:/Users/glupyan/gitRepos/psych750.github.io/notebooks/classes.ipynb#Y212sZmlsZQ%3D%3D?line=8'>9</a> print(f"The shuttle is at {shuttle_0.x}, {shuttle_0.y}")
     <a href='vscode-notebook-cell:/Users/glupyan/gitRepos/psych750.github.io/notebooks/classes.ipynb#Y212sZmlsZQ%3D%3D?line=9'>10</a> print(f"Shuttle has completed {shuttle_0.flights_completed} flights.")

AttributeError: module 'rocket' has no attribute 'Shuttle'

This approach is often used to shorten the name of the module, so you don’t have to type a long module name before each class name that you want to use. But it is easy to shorten a name so much that you force people reading your code to scroll to the top of your file and see what the shortened name stands for. In this example,

import rocket as rocket_module

leads to much more readable code than something like:

import rocket as r

from module_name import *#

There is one more import syntax that you should be aware of, but Python gurus advise against using. This syntax imports all of the available classes and functions in a module:

from module_name import *

This isn’t recommended for a couple reasons. First, you don’t know what all the names of the classes and functions in a module are. If you accidentally give one of your variables the same name as a name from the module, you will have naming conflicts. Second, you may be importing way more code into your program than you need.

If you really need all the functions and classes from a module, just import the module and use the module_name.ClassName syntax in your program.

You will get a better sense of how to write imports as you read more Python code and as you write and share some of your own code.

A module of functions#

You can use modules to store a set of functions you want available in different programs as well, even if those functions are not attached to any one class. To do this, you save the functions into a file, and then import that file just as you saw in the last section. Here is a really simple example; save this is multiplying.py:

# Save as multiplying.py
def double(x):
    return 2*x

def triple(x):
    return 3*x

def quadruple(x):
    return 4*x

Now you can import the file multiplying.py, and use these functions. Using the from module_name import function_name syntax:

from multiplying import double, triple, quadruple

print(double(5))
print(triple(5))
print(quadruple(5))
10
15
20

Using the import module_name syntax:

import multiplying

print(multiplying.double(5))
print(multiplying.triple(5))
print(multiplying.quadruple(5))
10
15
20

Using the import module_name as local_module_name syntax:

import multiplying as m

print(m.double(5))
print(m.triple(5))
print(m.quadruple(5))
10
15
20

Using the from module_name import * syntax:

from multiplying import *

print(double(5))
print(triple(5))
print(quadruple(5))
10
15
20