A comprehensive guide to exploring modern Python through data structures, design patterns, and effective object-oriented techniques
Key FeaturesBuild an intuitive understanding of object-oriented design, from introductory to mature programsLearn the ins and outs of Python syntax, libraries, and best practicesExamine a machine-learning case study at the end of each chapterBook DescriptionObject-oriented programming (OOP) is a popular design paradigm in which data and behaviors are encapsulated in such a way that they can be manipulated together. Python Object-Oriented Programming, Fourth Edition dives deep into the various aspects of OOP, Python as an OOP language, common and advanced design patterns, and hands-on data manipulation and testing of more complex OOP systems. These concepts are consolidated by open-ended exercises, as well as a real-world case study at the end of every chapter, newly written for this edition. All example code is now compatible with Python 3.9+ syntax and has been updated with type hints for ease of learning.
Steven and Dusty provide a comprehensive, illustrative tour of important OOP concepts, such as inheritance, composition, and polymorphism, and explain how they work together with Python's classes and data structures to facilitate good design. In addition, the book also features an in-depth look at Python's exception handling and how functional programming intersects with OOP. Two very powerful automated testing systems, unittest and pytest, are introduced. The final chapter provides a detailed discussion of Python's concurrent programming ecosystem.
By the end of the book, you will have a thorough understanding of how to think about and apply object-oriented principles using Python syntax and be able to confidently create robust and reliable programs.
What you will learnImplement objects in Python by creating classes and defining methodsExtend class functionality using inheritanceUse exceptions to handle unusual situations cleanlyUnderstand when to use object-oriented features, and more importantly, when not to use themDiscover several widely used design patterns and how they are implemented in PythonUncover the simplicity of unit and integration testing and understand why they are so importantLearn to statically type check your dynamic codeUnderstand concurrency with asyncio and how it speeds up programsWho this book is forIf you are new to object-oriented programming techniques, or if you have basic Python skills and wish to learn how and when to correctly apply OOP principles in Python, this is the book for you. Moreover, if you are an object-oriented programmer coming from other languages or seeking a leg up in the new world of Python, you will find this book a useful introduction to Python. Minimal previous experience with Python is necessary.
Table of ContentsObject-Oriented DesignObjects in PythonWhen Objects Are AlikeExpecting the UnexpectedWhen to Use Object-Oriented ProgrammingAbstract Base Classes and Operator OverloadingPython Data StructuresThe Intersection of Object-Oriented and Functional ProgrammingStrings, Serialization, and File PathsThe Iterator PatternCommon Design PatternsAdvanced Design PatternsTesti
Like all books by Steven F. Lott, this book is no exception. I read all books by Steven, and I always learn something from them. Besides teaching how to write OOP and use it, this book explains how to write OOP code using modern data types like NamedTuple, TypedDict, tuples, etc. As always, I learned excellent tips and techniques on how to write nice tests. There are also very interesting chapters on Concurrency and Design Patterns. My only complaint is the type of examples provided in this book. Examples are complicated, and they are based on machine learning data, sailing data, etc. So to understand an example, sometimes you need to understand the domain knowledge of the example. I wish examples were simpler to get a concept and then maybe provide more complex examples. Steven is not biased to OOP in this book, but he also explains the difference between OOP and functional programming and teaches where OOP would be the best fit. In any way, it's an excellent book, and I learned a lot from it, and I use it as a reference if I need to find an answer to my questions.
2/21/2022 This is the second time I have read this book. It has so much deep knowledge that I had to read it a second time. I also use it now as a reference book for cases like: Design patterns Data Structures (NamedTuples vs List vs Dict) Testing and much much more.
Great book about OOP: the first part is very accurate and well written whereas common/advanced patterns and concurrency sections are a bit too advanced and - in my opinion - possibly out of scope.
NOTES An object is a collection of data and associated behaviours. Classes describe related objects, they are like blueprints for creating an object. Objects are instances of classes that can be associated with each other. Methods are like functions, but they have access to the attributes, in particular the instance variables with the data associated with this object. They accept parameters and return values. ? A class attribute is a variable that belongs to a certain class, and not a particular object. Every instance of this class shares the same variable. These attributes are usually defined outside the __init__ constructor. ? An instance/object attribute is a variable that belongs to one (and only one) object. Every instance of a class points to its own attributes variables. These attributes are defined within the __init__ constructor. Encapsulation means hiding the internal details or mechanics of how an object does something. Abstraction is outer layout in terms of design, giving only essential things and hiding unnecessary details.
Inheritance is a mechanism that allows us to inherit all the properties, attributes and methods from another class. The class from which the properties and functionalities are utilized is called the parent class (also called as Base Class). The class which uses the properties from another class is called as Child Class (also known as Derived class). Inheritance is also called an Is-A Relation. Composition: by using the class name or by creating the object we can access the members of one class inside another class. It enables creating complex types by combining objects of different classes. It means that a class Composite can contain an object of another class Component. This type of relationship is known as Has-A Relation. The following special forms using leading or trailing underscores are recognized (these can generally be combined with any case convention): - _single_leading_underscore: weak "internal use" indicator. E.g. from M import * does not import objects whose name starts with an underscore. - single_trailing_underscore_: used by convention to avoid conflicts with Python keyword, e.g.Tkinter.Toplevel(master, class_='ClassName') - __double_leading_underscore: when naming a class attribute, invokes name mangling (inside class FooBar, __boo becomes _FooBar__boo; see below). - __double_leading_and_trailing_underscore__: "magic" objects or attributes that live in user-controlled namespaces. E.g. __init__, __import__ or __file__. Never invent such names; only use them as documented. In Python, the use of an underscore in a function name indicates that the function is intended for internal use and should not be called directly by users. It is a convention used to indicate that the function is "private" and not part of the public API of the module. However, it is not enforced by the language and can still be called by the user if they choose to do so.
Inheritance is used where a class wants to derive the nature of parent class and then modify or extend the functionality of it – it provides abstraction. Inheritance will extend the functionality with extra features allows overriding of methods, but in the case of Composition, we can only use that class we can not modify or extend the functionality of it. It will not provide extra features. Thus, when one needs to use the class as it without any modification, the composition is recommended and when one needs to change the behavior of the method in another class, then inheritance is recommended.
In Python, not only can we derive a class from the superclass but you can also derive a class from the derived class. This form of inheritance is known as multilevel inheritance. If two superclasses have the same method name and the derived class calls that method, Python uses the Method Resolution Order (MRO) to search for the right method to call.
In Python, a method is formatted identically to a function. It starts with the def keyword followed by a space and the name of that method. This is followed by a set of parentheses containing the parameter list. The only difference is that methods of classes require the self argument. __init__ initialization method tells the required parms the class need to be instantiated.
Modules are python files that can contain useful code, that we can just import (both classes and/or functions). Package is a collection of modules in the same folder; to tell Python that a folder is a pkg we normally place an empty file in the folder named __init__.py. Packages are a collection of related modules that aim to achieve a common goal. Finally, the Python standard library is a collection of packages and modules that can be used to access built-in functionality.
We can set default values for parameters, so that their values are optional when calling the function. They cannot be set dynamically unless they are set as None.
All module-level code is executed immediately at the time it is imported. The class and def statements create code objects to be executed later when the function is called. This can be a tricky thing for scripts that perform execution, such as the main script in our e-commerce example. Sometimes, we write a program that does something useful, and then later find that we want to import a function or class from that module into a different program. However, as soon as we import it, any code at the module level is immediately executed. If we are not careful, we can end up running the first program when we really only meant to access a couple of functions inside that module.To solve this, we should always put our startup code in a function (conventionally, called main()) and only execute that function when we know we are running the module as a script, but not when our code is being imported from a different script. We can do this by guarding the call to main inside a conditional statement.
Python includes the special variable called __name__ that contains the scope of the code being executed as a string. __main__ is the name of the top-level scope in which top-level code executes. For example, the scope of the code executed in the interpreter shell will be __main__
[it is common to create a different virtual environment for each Python project; virtual envs are essential for keeping the third-party dependencies separate from Python’s standard library.
Inheritance Technically every class that we create uses inheritance. All Python classes are subclasses of the special built-in class named object. It provides a little bit of metadata and a few built in behaviours. Class variables are part of the class definition and is shared by all instances of the class. A subclass can overwrite the __init__ method by creating a new one; we can also use super() function to return the object as if it was actually an instance of the parent class and on top of that build extra code: all methods can be modified via overriding and calls to super().
Dataclasses, as the name clearly suggests, are classes that are meant to hold data. The motivation behind this module is that we sometimes define classes that only act as data containers and when we do that, we spend a consequent amount of time writing boilerplate code with tons of arguments, an ugly __init__ method and many overridden functions.
Dataclasses alleviates this problem while providing additional useful methods under the hood supporting inheritance. Moreover, since Dataclasses is relatively new in the Python ecosystem, it enforces modern practices such as type annotations. dataclasses doesn’t just allow you to write more compact code. The dataclass decorator is actually a code generator that automatically adds other methods under the hood. If we use the inspectmodule to check what methods have been added to the Person class, we can see the __init__ , __eq__ and __repr__ methods: these methods are responsible for setting the attribute values, testing for equality and representing objects in a nice string format.
Type hints are optional, but useful: : = Examples: vector: b: float = 2.4; f: set = {"a", "b", "c"}; List[float]; Dict[str, str] **kwargs collects all additional keyword arguments passed into the method, that we are not explicitly listed in the parameter list, into a dictionary. This allows to have any number of arguments when we cannot know in advance what they will be. “->” symbol annotates what the function returns.
Polymorphism Polymorphism (the condition of occurrence in different forms) defines methods in the child class that have the same name as the methods in the parent class: different behaviours happen, depending on which subclass is being used, without having to explicitly know what the subclass actually is. It occurs when we have many classes that are related to each other by inheritance. Like we specified in the previous chapter; Inheritance lets us inherit attributes and methods from another class. Polymorphism uses those methods to perform different tasks. In inheritance, the child class inherits the methods from the parent class. Also, it is possible to modify a method in a child class that it has inherited from the parent class.
Exceptions A Python program terminates as soon as it encounters an error. In Python, an error can be a syntax error or an exception. An exception error occurs whenever syntactically correct Python code results in an error. The last line of the message indicated what type of exception error you ran into.
Instead of showing the message exception error, Python details what type of exception error was encountered. In this case, it was a ZeroDivisionError. Python comes with various built-in exceptions as well as the possibility to create self-defined exceptions.
Instead of waiting for a program to crash midway, you can also start by making an assertion in Python. We assert that a certain condition is met. If this condition turns out to be True, then that is excellent! The program can continue. If the condition turns out to be False, you can have the program throw an AssertionError exception. We can define new exceptions, for instance to add more info.
Properties: With Python’s property(), you can create managed attributes in your classes. You can use managed attributes, also known as properties, when you need to modify their internal implementation without changing the public API of the class. Providing stable APIs can help you avoid breaking your users’ code when they rely on your classes and objects.
Properties are arguably the most popular way to create managed attributes quickly and in the purest Pythonic style, to avoid getter and setters methods. This function allows you to turn class attributes into properties or managed attributes. Since property() is a built-in function, you can use it without importing anything. The @property is a built-in decorator for the property() function in Python. It is used to give "special" functionality to certain methods to make them act as getters, setters, or deleters when we define properties in a class.
Decorators are functions that take another function as an argument and return a new function with added functionality. With a decorator, you can attach pre- and post-processing operations to an existing function.
When Python 2.2 introduced property(), the decorator syntax wasn’t available. The only way to define properties was to pass getter, setter, and deleter methods, as you learned before. The decorator syntax was added in Python 2.4, and nowadays, using property() as a decorator is the most popular practice in the Python community.
The decorator syntax consists of placing the name of the decorator function with a leading @ symbol right before the definition of the function you want to decorate:
- Use methods to represent actions; things that can be done to, or performed by, the object. When you call a method, even with only one argument, it should do something. Method names are generally verbs. - Use attributes or properties to represent the state of the object. These are the nouns, adjectives, and prepositions that describe an object. o Default to ordinary (non-property) attributes, initialized in the _init__() method. These must be computed eagerly, which is a good starting point for any design. o Use properties for attributes in the exceptional case when there's a computation involved with setting or getting (or deleting) an attribute. Examples include data validation, logging, and access controls. We'll look at cache management in a moment. We can also use properties for lazy attributes, where we want to defer the computation because it's costly and rarely needed.
Decorators in Python are a way to modify or enhance the behavior of functions or classes without directly modifying their source code. They allow you to wrap a function or a class with another function, which is commonly referred to as a decorator function. Decorators provide a clean and efficient approach to adding functionality to existing code.
When you call the decorated function or instantiate the decorated class, the decorator function is invoked, and it can perform some actions before or after the decorated code executes.
Here's a simple example of a decorator that adds a timer functionality to a function:
@timer_decorator def my_function(): # Function code goes here pass
my_function()
In the example above, we define a decorator function called timer_decorator, which takes a function as an argument (func). It defines an inner function called wrapper that wraps around the original function. Inside wrapper, we measure the execution time before and after calling the original function, and then print the elapsed time.
Abstract Base Classes (ABC) An abstract base class is “Abstract” because it is non-existent – it’s just an ideal way for some other class to exist, but it cannot exist on its own. It is used as a blueprint for other classes; they help you define a blueprint for other classes that may have something in common.
Python, we have two approaches to defining similar things: - Duck typing: When two class definitions have the same attributes and methods, then instances of the two classes have the same protocol and can be used interchangeably. We often say, "When I see a bird that walks like a duck and swims like a duck and quacks like a duck, I call that bird a duck." - Inheritance: When two class definitions have common aspects, a subclass can share common features of a superclass. The implementation details of the two classes may vary, but the classes should be interchangeable when we use the common features defined by the superclass. We can take inheritance one step further. We can have superclass definitions that are abstract: this means they aren't directly useable by themselves, but can be used through inheritance to create concrete classes. - A class is another object with two very limited jobs: it has the special methods used to create and manage instances of the class, and it also acts as a container for the method definitions for objects of the class. We think of building class objects with the class statement, which leaves open the question of how the class statement builds the class object. - The type class is the internal object that builds our application classes. When we enter the code for a class, the details of construction are actually the responsibility of methods of the type class. After type has created our application class, our class then creates the application objects that solve our problem.
The type object is called the metaclass, the class used to build classes. This means every class object is an instance of type. Most of the time, we're perfectly happy with letting a class statement be handled by the type class so our application code can run. There's one place, however, where we might want to change how type works. Because type is itself a class, it can be extended. A class abc.ABCMeta extends the type class to check for methods decorated with @abstractmethod. When we extend abc.ABC, we're creating a new class that uses the ABCMeta metaclass. We can see this in the value of the special mro_attribute of the ABCMeta class; this attribute lists the classes used for resolving method names (MRO is Method Resolution Order). This special attribute lists the following classes to be searched for a given attribute: the abc.ABCMeta class, the type class, and finally the object class.
Generic classes and abstract base classes are not the same thing. The two concepts overlap, but are distinct: - Generic classes have an implicit relationship with Any. This often needs to be narrowed using type parameters, like list[int]. The list class is concrete, and when we want to extend it, we'll need to plug in a class name to replace the Any type. The Python interpreter does not use generic class hints in any way; they are only checked by static analysis tools such as mypy. - Abstract classes have placeholders instead of one or more methods. These placeholder methods require a design decision that supplies a concrete implementation. These classes are not completely defined. When we extend it, we'll need to provide a concrete method implementation. This is checked by mypy. That's not all. If we don't provide the missing methods, the interpreter will raise a runtime exception when we try to create an instance of an abstract class
Summary: - Using abstract base class definitions is a way to create class definitions with placeholders. This is a handy technique and can be somewhat clearer than using raise NotImplementedError in unimplemented methods. - ABCs and type hints provide ways to create class definitions. An ABC is a type hint that can help to clarify the essential features we need from an object. It's common, for example, to use Iterable[X] to emphasize that we need one aspect of a class implementation. - The collections.abc module defines abstract base classes for Python's built- in collections. When we want to make our own unique collect class that can integrate seamlessly with Python, we need to start with the definitions from this module.
There is a lovely Python OOP design book trapped inside of this one, but alas this book as been cursed with a kind of comprehensive giantism by the forces of the programming book market.
It starts very promisingly with OOP principles, no Python to be seen until the second chapter, and the second chapter is pleasantly basic, but well-structured from the beginning, introducing type hinting. If at this point they had introduced test-driven development and focused on developing the case-study example, it could have gone very well. Unfortunately, it really put language features over object-oriented design, frequently introducing new ideas with seemingly whatever domain seemed handy and striking their interests, resulting in little cohesion.
If it had done the opposite with each chapter, introducing the new concepts by developing the case study, and then including an additional appendix to act as a reference, I think they really would have had something remarkable. As it is, it's a nice reference with some real highlights for OOP-style.
Looking at it's organization again, the beginning four chapters are a very pleasant OOP and Python introduction, while the last four (10-14) have nice coverage of patterns, testing, and concurrency. It's really the middle chapters that really get into the mud of covering syntax, special cases, and particular features without a lot of motivation, with the exception of chapter 7, which really has nice coverage of data structures. Chapter 10, on iterators as a pattern, is a particularly nice showcase of the streaming features in Python allowed by various forms of iteration.
Overall, I like this book. It's a fine reference and if you skip around you'll find a rather nice OOP book inside.
I took a third edition from the library and since I found it interesting, decided to purchase one to read further. Why not go with a fourth edition? Big mistake, always go third edition if you have a choice!
Forth edition is a disaster. First of all, it's 200 (two hundreds!) pages more than the third one and mostly because of mypy and type annotations clutter. It's not about whether one should use type hints or not, rather about saving space and making concepts easier to understand, in this case hints are cluttering both example code and the book text. Yes, it's OOP book which is half length of Lutz Python primer which also covers OOP, I kid you not.
Second issue is inherited (pun intended) from the first one. What it takes author to describe in 120+ pages are covered in 45 minutes free videos by Corey Schafer with accessible and easy to read code. KNN Example is not really helping either, it's hard to grasp and it drags on instead of easier to digest examples from the third edition.
Last but not least, it's editing. Some phrasing is incomprehensible, some code snippets are forward -referencing stuff from the next chapters and in general it's not an easy read. Third edition wasn't perfect either, but fourth bloated already big chapters and reads very bad.
I do not recommend upgrading your third edition, unless you're interested in type hints and how to use hinting with classes or want to build KNN classifier using OOP. General styling of the book is better though and it looks more pleasant, but it's not a step forward rather two backward.
Frankly, I'm just giving up on this after much wading. This seems to suffer from the same issues as other Pakt programming books I have read. In trying to cater for the complete novice programmer, the experienced programmer in other languages coming fresh to Python, and the intermediate Python programmer looking to go deeper into Python's object-oriented idioms, it manages to do none of these things particularly well. It also attempts to be a sort of idiot's guide to object-oriented patterns and doesn't do that very well either. Trivial points are belaboured exhaustively while subtleties are glossed so fast you could miss them. I'm saying this as someone who started the book a second time after completing David Beasley's transparently clear and concise 'Python Distilled' in the hope of having a better grounding to carry me through the infelicities of this book, but it didn't. So, at second reading, page 543, I am throwing in the towel, and getting on with the infinitely more informative 'Fluent Python', O'Reilly's fully up to date meta-tome by the estimable Luciano Romalho. It's a darn shame because I imagine the authors are formidable programmers, but they don't have the knack of putting themselves in their audience's shoes, or even just running their text by a suitable beginner. I won't be buying Pakt titles again.