From The Well-Grounded Python Developer by Doug Farrell

This article delves into using the OOP coding paradigm in the Python language.


Take 40% off The Well-Grounded Python Developer by entering fccfarrell2 into the discount code box at checkout at manning.com.


Object-oriented

The ability to place functions into modules provides many opportunities for how you structure an API. The type and order of the parameters passed to the functions which make up an API offer possibilities to make your API more apparent and useful.

Breaking up functions to stay with the concept of single responsibility and manageable function length also makes it more likely your API consists of multiple functions. Users of the API pass data structures and state information into those functions, getting the updated structures and state modified returned by those functions, and pass them on to other functions to arrive at the final result.

Often the data structures passed between functions are collection objects, lists, sets, or dictionaries, for example. These objects are powerful, and taking advantage of what they offer is important in Python development.

By themselves, data structures don’t do anything, the functions they’re passed to know what to do with the data structures they receive as input.

Because everything in Python is an object, creating objects of your own is available to you using object-oriented programming. One of the goals of creating objects is to encapsulate data and the methods that act on that data into one thing. Conceptually you’re creating something having functionality you provided. Doing this creates something you can think about as an object or thing, having behavior. Creating classes is how you design these objects, assigning data, and functionality to them.

Class Definition

Python provides object-oriented programming by defining classes which can be instantiated into objects when needed. Instantiation is the act of taking something from a definition (the class) to reality. You could say the blueprint for a house is the class definition, and building the house instantiates it.

Here’s a simple class definition for a Person class from the CH_05/example_01.py example code:

 
 class Person:
     """Defines a person by name
     """
  
     def __init__(self, fname: str, mname: str = None, lname: str = None):
         self.fname = fname
         self.mname = mname
         self.lname = lname
  
     def full_name(self) -> str:
         """This method returns the person's full name
         """
         full_name = self.fname
         if self.mname is not None:
             full_name = f"{full_name} {self.mname}"
         if self.lname is not None:
             full_name = f"{full_name} {self.lname}"
         return full_name
  

This class definition creates a template to create a Person object instance containing a person’s first, middle, and last names. It also provides the full_name() method to get the person’s full name based on the information passed to the object by its constructor, the __init__() method.

This class can be represented visually in UML as well. UML (Unified Modeling Language) is a standardized way to visually present the design of systems. It’s unnecessary to use UML diagrams when designing and building a system, but they can be useful to introduce abstract concepts which are difficult to present as text. Here’s the UML diagram for the Person class:



The UML diagram for the Person class shows the name of the class, the attributes it contains, and the methods it provides.

The plus sign ‘+’ character in front of the attribute and method names indicates they’re public. In Python, attributes and methods of a class have no notion of public, protected, or private access.

Python’s class design is based on the idea “we’re all adults here,” and the developers who use your classes behave accordingly. Using plain attributes should be the default when designing your classes. You’ll see later how class properties can be used to gain control of how attributes are accessed and used.

A simple use case for the Person class is presented in the CH_05/example_01.py application:

 
 # Create some people
 people = [
     Person("John", "George", "Smith"),
     Person("Bill", lname="Thompson"),
     Person("Sam", mname="Watson"),
     Person("Tom"),
 ]
  
 # Print out the full names of the people
 for person in people:
     print(person.full_name())
  

This code creates four instances of the Person class, each representing a different person and exercising all the variations of the constructor. The for loop iterates through the list of Person object instances and calls the full_name() method of each.

Drawing With Class

The rest of the examples you’re going to create are object-oriented applications which animate some shapes on screen. Each example expands on the one previous to present the following concepts:

  • Inheritance – parent/child relationships between classes
  • Polymorphism – using an object as if it had multiple forms
  • Composition – using composition instead of inheritance to give attributes and behavior to a class

To create the drawing application, you’ll be using the arcade module available in the Python Package Index (pypi.org). This module provides the framework to create a drawing surface on the computer screen and draw and animate those objects.



The first thing to do is to define a class for a rectangle to draw on the screen. Starting with the UML diagram for the Rectangle class:

The UML diagram shows the attributes encapsulated in the class necessary to render a rectangle on-screen. All of these attributes are initialized during the construction of a Rectangle object:

  • x, y, width, height – define the position of the Rectangle on screen and the dimensions to use when drawing it
  • pen_color, fill_color – define the colors used to outline the Rectangle and fill it
  • dir_x, dir_y – the direction of movement relative to the screen x and y axes, these are either 1 or -1
  • speed_x, speed_y – the speed at which the Rectangle is moving in pixels per update
  • The diagram also includes the definition of three methods the class supports:
  • set_pen_color() – provides a mechanism to set the pen color used to draw the rectangle instance object
  • set_fill_color() – provides a mechanism to set the fill color used to fill a rectangle instance object
  • draw() – draws a Rectangle object instance on the screen

This UML diagram can be turned into a Python class definition in code. Here’s the Rectangle class based on the above diagram from CH_05/example_02.py:

 
 class Rectangle:
     """This class defines a simple rectangle object
     """
  
     def __init__(
         self,
         x: int,
         y: int,
         width: int,
         height: int,
         pen_color: tuple = COLOR_PALETTE[0],
         fill_color: tuple = COLOR_PALETTE[1],
         dir_x: int = 1,
         dir_y: int = 1,
         speed_x: int = 1,
         speed_y: int = 1
     ):
         self.x = x
         self.y = y
         self.width = width
         self.height = height
         self.pen_color = pen_color
         self.fill_color = fill_color
         self.dir_x = 1 if dir_x > 0 else -1
         self.dir_y = 1 if dir_y > 0 else -1
         self.speed_x = speed_x
         self.speed_y = speed_y
  
     def set_pen_color(self, color: tuple) -> Rectangle:
         """Set the pen color of the rectangle
        
         Arguments:
             color {tuple} -- the color tuple to set the rectangle pen to
        
         Returns:
             Rectangle -- returns self for chaining
         """
         self.pen_color = color
         return self
  
     def set_fill_color(self, color: tuple) -> Rectangle:
         """Set the fill color of the rectangle
        
         Arguments:
             color {tuple} -- the color tuple to set the rectangle fill to
        
         Returns:
             Rectangle -- returns self for chaining
         """
         self.fill_color = color
         return self
  
     def draw(self):
         """Draw the rectangle based on the current state
         """
         arcade.draw_xywh_rectangle_filled(
             self.x, self.y, self.width, self.height, self.fill_color
         )
         arcade.draw_xywh_rectangle_outline(
             self.x, self.y, self.width, self.height, self.pen_color, 3
         )
  

This class defines a simple Rectangle object. The object is initialized with the lower-left corner of the object x, and y coordinates, the width and height, pen and fill colors the direction and speed of motion of the Rectangle. In the arcade module, the screen origin is in the lower-left corner, which is how most of us think about x and y axes on paper but are different than other screen rendering tools.

Modifying the values of the x and y attributes moves the Rectangle around the screen maintained by arcade and the instance of the Window class in the application. The Window class has two methods used to animate the objects on the screen; on_update() and on_draw(). The first updates the position of all the objects to present on the screen and the second draws those updated objects on the screen. The on_update() method is called every refresh iteration and it’s where the application modifies the position of the rectangles that were appended to the self.rectangles collection. The on_update() method looks like this:

 
 def on_update(self, delta_time):
     """Update the position of the rectangles in the display
     """
     for Rectangle in self.rectangles:
         rectangle.x += rectangle.speed_x
         rectangle.y += rectangle.speed_y
  

This code iterates through the collection of rectangles and updates the position of each one by its x and y speed values, changing its position on the screen.

The updated rectangles are drawn on the screen by the Window instance method on_draw(), which looks like this:

 
 def on_draw(self):
     """Called whenever you need to draw your window
     """
  
     # Clear the screen and start drawing
     arcade.start_render()
  
     # Draw the rectangles
     for Rectangle in self.rectangles:
         rectangle.draw()
  

Every time the on_draw() method is called, the screen is cleared and the self.rectangles collection is iterated through, and each Rectangle has its draw() method called.

The Rectangle class has behavior defined by the methods set_pen_color(), set_fill_color() and draw(). These methods use and alter the data encapsulated by the class definition. They provide the API you interact with when using the class.

Look at the set_pen_color() and set_fill_color() methods and you’ll see they return self. Returning self can be useful to chain methods of the class together into a series of operations. Here’s an example from CH_05/example_02.py using the Rectangle class. This shows the pen and fill colors being changed when the arcade schedule functionality calls this code every second:

 
 def change_colors(self, interval):
     """This function is called once a second to
     change the colors of all the rectangles to
     a random selection from COLOR_PALETTE
        
     Arguments:
         interval {int} – interval passed in from
         the arcade schedule function
     """
     for Rectangle in self.rectangles:
         rectangle.set_pen_color(choice(COLOR_PALETTE)).set_fill_color(
             choice(COLOR_PALETTE)
         )
  

The change_colors() method of the Window instance is called by an arcade schedule function every second. It iterates through the collection of rectangles and calls the set_pen_color() and set_fill_color() in a chained manner to set random colors picked from the globally defined COLOR_PALETTE list.

When the CH_05/example02.py application is run, it creates a window on the screen and animates a vertically aligned rectangle upwards at a 45-degree angle. It also changes the pen and fill colors of the Rectangle every second the application runs. Here’s a screenshot of the running application:



Properties

As mentioned above, direct access to the attributes of a class should often be the default. The Rectangle example above follows this practice. In some situations you’ll want more control over how the attributes of a class are used or changed.

The definition of the Rectangle class includes attributes for the x and y origin of the Rectangle, which helps draw it in the window. That window has dimensions, and if you ran the CH_05/example_02.py application long enough, you saw the Rectangle move off the screen.

Currently, the origin of a Rectangle instance is set to any integer value. No known screen has a resolution as large as the range of integer values, and none at all deal with negative numbers directly. The window declared in the application has a width of 600 and a height of 800 in pixels.

Where rectangle objects can be drawn should be constrained to within the boundaries of the screen dimensions. Constraining the values of x and y means having functionality in place to limit the values they can be assigned. Your goal is to make the rectangle bounce around within the screen window.

If you’ve come from other languages supporting object-oriented programming, you might be familiar with getters and setters. These are methods provided by the developer to control access to attributes of a class instance. Those methods also give the developer a place to insert behavior when the attributes are retrieved or modified.

Adding getter and setter methods to the Rectangle x and y attributes could be done by defining methods like this:

 
 def get_x(self):
 def set_x(self, value):
 def get_y(self):
 def set_y(self, value):
  

Using these getter and setter functions also means changing the example code from this:

 
 rectangle.x += 1
 rectangle.y += 1
  

to this:

 
 rectangle.set_x(rectangle.get_x() + 1)
 rectangle.set_y(rectangle.get_y() + 1)
  

In my opinion, the changes to use the getters and setters work but reduces the readability of the code significantly from the direct attribute access version.

By using Python property decorators, you can control how class attributes are accessed and modified while still using the direct attribute access syntax. The Rectangle class can be modified to use property decorators offering this behavior. The updated portion of the Rectangle class from example program CH_05/example_03.py is shown below:

 
 class Rectangle:
     """This class defines a simple rectangle object
     """
     def __init__(
         self,
         x: int,
         y: int,
         width: int,
         height: int,
         pen_color: str = "BLACK",
         fill_color: str = "BLUE",
     ):
         self._x = x
         self._y = y
         self.width = width
         self.height = height
         self.pen_color = pen_color
         self.fill_color = fill_color
  
     @property
     def x(self):
         return self._x
  
     @x.setter
     def x(self, value: int):
         """Limit the self._x to within the screen dimensions
        
         Arguments:
             value {int} -- the value to set x to
         """
         if self._x + value < 0:
             self._x = 0
         elif self._x + self._width + value > Screen.max_x:
             self._x = Screen.max_x - self._width
         else:
             self._x = value
  
     @property
     def y(self):
         return self._y
  
     @y.setter
     def y(self, value):
         """Limit the self._y to within the screen dimensions
        
         Arguments:
             value {int} -- the value to set y to
         """
         if self._y + value < 0:
             self._y = 0
         elif self._y + self._height + value > Screen.max_y:
             self._y = Screen.max_y - self._height
         else:
             self._y = value
  

The first thing to notice is the attributes x and y are prefixed with a single underbar ‘_’ character. Using the underbar this way is a convention to indicate the attribute should be considered private and not accessed directly, but it doesn’t enforce any notion of a private attribute.

The second thing to notice is the new decorated methods in the class. For example the two new methods for accessing the self._x attribute are:

 
     @property
     def x(self):
         return self._x
  
     @x.setter
     def x(self, value):
         """Limit the self._x to within the screen dimensions
        
         Arguments:
             value {int} -- the value to set x to
         """
         if not (0 < value < SCREEN_WIDTH - self.width):
             self.dir_x = -self.dir_x
         self._x += abs(self._x - value) * self.dir_x
  

The @property decorator over the first def x(self) function defines the getter functionality, in this case, returning the value of self._x.

The @x.setter decorator over the second def x(self, value) function defines the setter functionality. Inside the function self._x is constrained to within the screen x-axis maximum dimension. If setting the value of self._x places any part of the Rectangle outside the screen area, the direction of travel is negated to start it moving in the opposite direction.

Having the above-decorated methods in the Rectangle class means code like this works again:

 
 rectangle.x += 1

The program statement appears to be setting the rectangle instance x attribute directly, but the decorated methods above are called instead. The += operation calls the getter method to retrieve the current value of self._x, adds one to that value, and uses the setter method to set self._x to that new value. If the resulting change places the Rectangle outside of the screen dimensions, the direction of travel along the x-axis is reversed.

The beautiful part of this is you can define your classes using direct attribute access initially. If it becomes necessary to constrain access to an attribute, you can define getter and setter property methods. Existing code using your class doesn’t have to change at all. From the point of view of the caller, the API of the class is the same.

Take note of another feature of using setter and getter decorated methods. You don’t need to create both setter and getter decorated functions on attributes. You can create only a getter, which creates a read-only attribute. Likewise, you can create only a setter creating a write-only attribute.

The rarely used @deleter decorator can be used to delete an attribute.

Composition

In the inheritance section, you saw the relationships between the Rectangle, Square, Circle, and Shape classes. These relationships allowed the child classes to inherit attributes and behavior from their parent class. This creates the idea that a Rectangle IS-A Shape, and a Square IS-A Rectangle, which also means it IS-A Shape as well.

These relationships also imply a certain similarity between attributes and behaviors of the parent classes and the child classes which inherit from them, but this isn’t the only way to include attributes and behavior into classes.

Look at the Shape class, it has two attributes for pen and fill color. These two attributes provide color to the shape and are distinguished from each other by their names, but they offer the same thing, a color, most likely from a palette of colors the system can create. This means the color is a common attribute within the Shape itself and expressed twice.

It’s possible with inheritance to handle this and add to the hierarchy in the examples by creating a Color class having pen and fill color attributes and having the Shape class inherit from it.

Doing this works, but the inheritance feels awkward. You can make a Shape have an IS-A relationship to a Color class in code, but logically it doesn’t make sense. A shape is not a color, and it doesn’t fit the IS-A model of an inheritance structure.

Instead of trying to force inheritance to provide the desired behavior, you can use composition. You’ve already been using composition when giving class attributes which are integers and strings. You can take this further and create custom classes to be used as attributes, composing behavior into your classes.

Creating a new class Color provides a consistent abstraction for color in the application. It has a class-level definition for the colors supported and has a mechanism only to allow defined colors to be set. The UML diagram showing the addition of a Color class to the hierarchy structure looks like this:



The Color class is connected to the Shape class as a composite, indicated in the diagram above by the connecting line with the filled black diamond symbol. Here’s what the Color class looks like from the CH_05/example_06.py application program:

 
 @dataclass
 class Color:
     """This class defines a color and it's methods
     """
  
     PALETTE = [
         arcade.color.BLACK,
         arcade.color.LIGHT_GRAY,
         arcade.color.LIGHT_CRIMSON,
         arcade.color.LIGHT_BLUE,
         arcade.color.LIGHT_CORAL,
         arcade.color.LIGHT_CYAN,
         arcade.color.LIGHT_GREEN,
         arcade.color.LIGHT_YELLOW,
         arcade.color.LIGHT_PASTEL_PURPLE,
         arcade.color.LIGHT_SALMON,
         arcade.color.LIGHT_TAUPE,
         arcade.color.LIGHT_SLATE_GRAY,
     ]
     color: tuple = PALETTE[0]
     _color: tuple = field(init=False)
  
     @property
     def color(self) -> tuple:
         return self._color
  
     @color.setter
     def color(self, value: tuple) -> None:
         """Sets the color in the class
        
         Arguments:
             value {tuple} -- the color tuple from COLOR_PALETTE to set
         """
         if value in Color.PALETTE:
             self._color = value
  

The Color class moves the allowable color list within the scope of the class and out of the global namespace. It’s also a Python dataclass, which can make defining simple classes that are mostly data easier to implement. The class provides getter and setter property decorators to make using the color within the class more straightforward.

In order to use the Color class the Shape class is modified to use it for the pen and fill color attributes. The __init__() constructor for the class is shown below:

 
 class Shape:
     """This class defines generic shape object
     """
     def __init__(
         self,
         x: int,
         y: int,
         width: int,
         height: int,
         pen: Color = Color(),
         fill: Color = Color(),
         dir_x: int = 1,
         dir_y: int = 1,
         speed_x: int = 1,
         speed_y: int = 1,
     ):
         self._x = x
         self._y = y
         self.width = width
         self.height = height
         self.pen = Color(Color.PALETTE[0])
         self.fill = Color(Color.PALETTE[1])
         self.dir_x = 1 if dir_x > 0 else -1
         self.dir_y = 1 if dir_y > 0 else -1
         self.speed_x = speed_x
         self.speed_y = speed_y
  

The attribute names for pen and fill color have been simplified to pen and color because they’re both Color class instances. The initial default values have been set to black for the pen and light gray for the fill colors. Adding the Color class to the Shape class this way creates a HAS-A relationship; a Shape has Color attributes but it isn’t a Color.

The set_pen_color() and set_fill_color() methods have also been modified to use the new pen and fill attributes. Setting a color for the pen now looks like this:

 
 def set_pen_color(self, color: tuple) -> Rectangle:
     """Set the pen color of the Rectangle
        
     Arguments:
         color {tuple} -- the color tuple to set the rectangle pen to
        
     Returns:
         Rectangle -- returns self for chaining
     """
     self.pen.color = color
         return self
  

Running the CH_05/example_06.py application produces a screen exactly like you’ve seen before, three shapes bouncing around the window and changing colors every second.

The use of composition gives you a way to add attributes and behavior to a class without having to create contrived hierarchies of inheritance.

That’s all for this article. If you want to learn more about the book, check it out on our browser-based liveBook platform here.