Classes and Object-Oriented Programming in Python

Procedural and Object-Oriented Programming

Chapter 1010 of Starting Out with Python, Sixth Edition, provides a comprehensive overview of Classes and Object-Oriented Programming, with copyrights held by Pearson Education, Inc. in 20232023, 20212021, and 20182018. The material begins by distinguishing between procedural and object-oriented programming paradigms. Procedural programming is defined as the practice of writing programs consisting of functions that perform specific tasks. In this model, procedures typically operate on data items that are kept separate from the procedures themselves. Data items are commonly passed from one procedure to another, and the primary focus of the developer is to create procedures that operate effectively on the program’s data.

Object-oriented programming (OOP), by contrast, is focused on the creation of objects. An object is defined as an entity that contains both data and procedures. The data within an object is referred to as data attributes, while the procedures are known as methods. Methods are responsible for performing operations on the data attributes. A fundamental concept in OOP is encapsulation, which refers to the combining of data and code into a single, cohesive object. This structure is visualized in Figure 10−110-1, illustrating that an object serves as a container for both its data attributes and its methods.

Core Principles of Object-Oriented Programming

One of the primary advantages of object-oriented programming is data hiding. This concept ensures that an object’s data attributes are hidden from any code residing outside the object itself. Access to these attributes is strictly restricted to the object’s own methods. This restriction serves two main purposes: it protects the data from accidental corruption by external code, and it allows external code to interact with the object without needing to understand the object's complex internal structures. This interaction is depicted in Figure 10−210-2, which shows outside code communicating specifically with the object's methods rather than the data directly.

Another significant benefit is object reusability. A well-designed object can be used across different programs without modification. For instance, a 3D3D image object created for an architectural visualization program could be reused in a game programming context. In everyday terms, an object can be understood through the example of a clock. The state of the clock is defined by its data attributes, such as the current second, minute, and hour. The clock provides public methods, such as \text{set_time} and \text{set_alarm_time}, which allow external code to manipulate it, while also utilizing private methods for its internal mechanical or logical workings.

Classes and Instances

The relationship between a class and an object is foundational to OOP. A class is the code that specifies the data attributes and methods for a particular type of object; it acts as a blueprint, similar to the architectural plans for a house or a cookie cutter. An instance is a specific object created from that class, much like a physical house built from a blueprint or an individual cookie cut from a mold. As shown in Figure 10−310-3, many distinct houses can be built from a single blueprint. Similarly, Figure 10−510-5 demonstrates that different entities, such as a housefly and a mosquito, can both be instances of a single Insect\text{Insect} class.

Defining Classes and the Initializer Method in Python

A class definition is a set of statements that define the methods and data attributes for the class. In Python, this begins with the class\text{class} keyword followed by the \text{Class_name}, which by convention typically starts with an uppercase letter. Every method definition within a class is similar to a standard function definition but must include the self\text{self} parameter. This parameter is a reference to the specific object that the method is currently working on.

The initializer method, denoted as \text{__init__ (self)}, is a special method that executes automatically whenever a new instance of a class is created. Its primary purpose is to initialize the object's data attributes and assign the self\text{self} parameter to the newly created object. For example, a Coin\text{Coin} class might import the random\text{random} module and define an \text{__init__} method where self.sideup\text{self.sideup} is initially set to the string 'Heads'. The class could also include a toss(self)\text{toss(self)} method that uses random.randint(0, 1)\text{random.randint(0, 1)} to determine if self.sideup\text{self.sideup} should be 'Heads' (if the result is 00) or 'Tails' (otherwise). A \text{get_sideup(self)} method would then simply return the value currently stored in self.sideup\text{self.sideup}.

Working with Instances and Dot Notation

To create a new instance of a class, the format \text{variable_name = ClassName()} is used. For example, \text{my_coin = Coin()} creates an instance and triggers the \text{__init__} method. This process is detailed in Figure 10−610-6. Once the instance exists, class methods are called using dot notation in the format \text{My_instance.method()}. Because the self\text{self} parameter automatically references the specific instance, the method impact is limited to that instance. In a code sequence where \text{my_coin = Coin()} is followed by \text{my_coin.toss()} and \text{print(my_coin.get_sideup())}, Line 11 creates the object and assigns its reference to \text{my_coin}, Line 22 calls the toss method to change the state, and Line 33 retrieves and displays the current state.

Hiding Attributes and Accessor/Mutator Methods

To ensure data attributes remain private and protected from outside code, Python developers use two underscores as a prefix for attribute names, such as \text{__sideup}. Classes can be stored in external modules, which are files ending in .py\text{.py}, allowing them to be imported into any program. These classes can have complex methods, such as in a BankAccount\text{BankAccount} class where \text{__init__} might accept a starting balance and a deposit\text{deposit} method might accept an amount\text{amount} parameter to update the \text{__balance} attribute.

An object's state reflects the values of its attributes at a specific moment. The \text{__str__} method is a special function used to display this state; it is called automatically whenever an object is passed to the print\text{print} or str\text{str} functions. Furthermore, instance attributes belong to specific instances. If multiple instances of a class are created (such as coin1\text{coin1}, coin2\text{coin2}, and coin3\text{coin3} in Figure 10−810-8), each maintains its own unique set of attributes as seen in Figure 10−910-9. Accessor methods, or getters, are used to safely retrieve attribute values without changing them, while mutator methods, or setters, are used to store or modify these values.

Designing Classes and the Unified Modeling Language

When passing an object as an argument to a function or method, a reference to the object is passed, allowing the receiving code to access and modify the actual object through its methods. For designing these systems, the Unified Modeling Language (UML) provides a standard graphical format. As seen in Figure 10−1010-10, a UML diagram for a class is a box divided into three sections: the top section contains the class name, the middle section lists the data attributes, and the bottom section lists the methods. Figure 10−1110-11 shows this applied specifically to the Coin\text{Coin} class.

Identifying classes during development involves analyzing the problem domain—the written description of the real-world objects or processes the program will simulate. This domain description can include physical objects, roles played by people, business event results, or recordkeeping items. To identify potential classes, a developer identifying all nouns, noun phrases, and pronouns in the description and then refines the list. Refining involves removing synonyms, irrelevant items, objects that aren't classes, and simple values that can be served by standard variables. Finally, the developer identifies a class's responsibilities: what it must "know" (its data attributes) and what it must "do" (its methods), based on the requirements of the problem domain.