We've been focused on objects and their attributes and methods. Now, we'll take a look at designing higher-level objects; the kind of objects that manage other objects – the objects that tie everything together.
The difference between these objects and most of the previous examples is that the latter usually represent concrete ideas. Management objects are more like office managers; they don't do the actual visible work out on the floor, but without them, there would be no communication between departments and nobody would know what they are supposed to do (although, this can be true anyway if the organization is badly managed!). Analogously, the attributes on a management class tend to refer to other objects that do the visible work; the behaviors on such a class delegate to those other classes at the right time, and pass messages between them.
As an example, we'll write a program that does a find-and-replace action for text files stored in a compressed ZIP file. We'll need objects to represent the ZIP file and each individual text file (luckily, we don't have to write these classes, as they're available in the Python standard library). The manager object will be responsible for ensuring the following three steps occur in order:
- Unzipping the compressed file
- Performing the find-and-replace action
- Zipping up the new files
The class is initialized with the .zip filename, and search and replace strings. We create a temporary directory to store the unzipped files in, so that the folder stays clean. The pathlib library helps out with file and directory manipulation. We'll learn more about it in Chapter 8, Strings and Serialization, but the interface should be pretty clear in the following example:
import sys import shutil import zipfile from pathlib import Path class ZipReplace: def __init__(self, filename, search_string, replace_string): self.filename = filename self.search_string = search_string self.replace_string = replace_string self.temp_directory = Path(f"unzipped-{filename}")
Then, we create an overall manager method for each of the three steps. This method delegates responsibility to other objects:
def zip_find_replace(self):
self.unzip_files()
self.find_replace()
self.zip_files()
Obviously, we could do all three steps in one method, or indeed in one script, without ever creating an object. There are several advantages to separating the three steps:
- Readability: The code for each step is in a self-contained unit that is easy to read and understand. The method name describes what the method does, and less additional documentation is required to understand what is going on.
- Extensibility: If a subclass wanted to use compressed TAR files instead of ZIP files, it could override the zip and unzip methods without having to duplicate the find_replace method.
- Partitioning: An external class could create an instance of this class and call the find_replace method directly on some folder without having to zip the content.
The delegation method is the first in the following code; the rest of the methods are included for completeness:
def unzip_files(self):
self.temp_directory.mkdir()
with zipfile.ZipFile(self.filename) as zip:
zip.extractall(self.temp_directory)
def find_replace(self):
for filename in self.temp_directory.iterdir():
with filename.open() as file:
contents = file.read()
contents = contents.replace(self.search_string, self.replace_string)
with filename.open("w") as file:
file.write(contents)
def zip_files(self):
with zipfile.ZipFile(self.filename, "w") as file:
for filename in self.temp_directory.iterdir():
file.write(filename, filename.name)
shutil.rmtree(self.temp_directory)
if __name__ == "__main__":
ZipReplace(*sys.argv[1:4]).zip_find_replace()
For brevity, the code for zipping and unzipping files is sparsely documented. Our current focus is on object-oriented design; if you are interested in the inner details of the zipfile module, refer to the documentation in the standard library, either online or by typing import zipfile ; help(zipfile) into your interactive interpreter. Note that this toy example only searches the top-level files in a ZIP file; if there are any folders in the unzipped content, they will not be scanned, nor will any files inside those folders.
The last two lines in the example allow us to run the program from the command line by passing the zip filename, the search string, and the replace string as arguments, as follows:
$python zipsearch.py hello.zip hello hi
Of course, this object does not have to be created from the command line; it could be imported from another module (to perform batch ZIP file processing), or accessed as part of a GUI interface or even a higher-level management object that knows where to get ZIP files (for example, to retrieve them from an FTP server or back them up to an external disk).
As programs become more and more complex, the objects being modeled become less and less like physical objects. Properties are other abstract objects, and methods are actions that change the state of those abstract objects. But at the heart of every object, no matter how complex, is a set of concrete data and well-defined behaviors.