Python Advanced Guide (Easy Advanced Programming): Sixteen, Object-Oriented Programming and Inheritance

Original: http://inventwithpython.com/beyond/chapter16.html

Define a function, and call it from several places, saving you the hassle of copying and pasting source code. Not duplicating code is a good practice, because if you need to modify it (whether to fix a bug or add a new feature), you only need to modify it in one place. There is no repeated code, and programs are shorter and easier to read.

Similar to functions, inheritance is a code reuse technique that you can apply to classes. This is the act of placing classes in a parent-child relationship, where the child class inherits a copy of the parent's method, saving you from having to duplicate a method in multiple classes.

Many programmers consider inheritance to be overrated, even dangerous, because a large number of inherited classes increases program complexity. A blog post with the title "Inheritance is Evil" isn't entirely rambling; inheritance is certainly prone to overuse. But limited use of this technique can save a lot of time when organizing code.

How inheritance works

To create a new subclass, put the name of an existing parent class in classparentheses in the statement. To practice subclassing, open a new file editor window and enter the following code; save as inheritanceExample.py:

class ParentClass: # 1
    def printHello(self): # 2
        print('Hello, world!')

class ChildClass(ParentClass): # 3
    def someNewMethod(self):
        print('ParentClass objects don't have this method.')

class GrandchildClass(ChildClass): # 4
    def anotherNewMethod(self):
        print('Only GrandchildClass objects have this method.')

print('Create a ParentClass object and call its methods:')
parent = ParentClass()
parent.printHello()

print('Create a ChildClass object and call its methods:')
child = ChildClass()
child.printHello()
child.someNewMethod()

print('Create a GrandchildClass object and call its methods:')
grandchild = GrandchildClass()
grandchild.printHello()
grandchild.someNewMethod()
grandchild.anotherNewMethod()

print('An error:')
parent.someNewMethod()

When running the program, the output should look like this:

Create a ParentClass object and call its methods:
Hello, world!
Create a ChildClass object and call its methods:
Hello, world!
ParentClass objects don't have this method.
Create a GrandchildClass object and call its methods:
Hello, world!
ParentClass objects don't have this method.
Only GrandchildClass objects have this method.
An error:
Traceback (most recent call last):
  File "inheritanceExample.py", line 35, in <module>
    parent.someNewMethod() # ParentClass objects don't have this method.
AttributeError: 'ParentClass' object has no attribute 'someNewMethod'

We created three classes named ParentClass1, ChildClass3 and 4. A subclass means it will have all the same methods as . We say inherited methods . In addition, the subclass inherits from , so it has all the same methods as its parent class .GrandchildClassChildClass ParentClassChildClassParentClassChildClass ParentClassGrandchildClassChildClassChildClassParentClass

Using this technique, we've effectively printHello()copied and pasted the code for method 2 into the ChildClassand GrandchildClassclass. Any changes we make to printHello()the code will not only update ParentClass, but also update ChildClassand GrandchildClass. This is the same as changing the code in a function will update all its function calls. You can see this relationship in Figure 16-1. Note that in the class diagram, the arrows point from subclasses to base classes. This reflects the fact that a class is always aware of its base classes, but not its subclasses.

f16001

Figure 16-1: A hierarchy diagram (left) and a Venn diagram (right) showing the relationship between the three classes and the methods they have

It is often said that parent-child classes represent an "is-a" relationship. An ChildClassobject is an ParentClassobject because it has ParentClassall the same methods as an object, including some additional methods it defines. This relationship is one-way: ParentClassobjects are not ChildClassobjects. If an ParentClassobject tries to call someNewMethod(), which only exists on ChildClassobjects (and ChildClasssubclasses of ), Python throws one AttributeError.

Programmers often think that related classes must fit into a real-world "is" hierarchy. OOP tutorials generally have Vehicle``FourWheelVehicle▶``Car``Animal``Bird``Sparrow, or Shape``Rectangle``Squarethe parent class, child class, grandson class. But remember, the main purpose of inheritance is code reuse. If your program requires a class with a set of methods that are an exact superset of other classes' methods, inheritance allows you to avoid copying and pasting code.

We also sometimes call subclasses subclasses or derived classes , and parent classes superclasses or base classes .

override method

A subclass inherits all the methods of its parent class. But subclasses can override inherited methods by providing their own methods with their own code. The overridden method of the subclass will have the same name as the method of the superclass.

To illustrate this concept, let's go back to the tic-tac-toe game we created in the previous chapter. This time, we'll create a new class MiniBoardthat inherits TTTBoardand overrides getBoardStr()to provide a smaller tic-tac-toe board. The program will ask the player which style of board to use. We don't need to copy and paste the rest of TTTBoardthe methods as MiniBoardthey will be inherited.

Add the following to tictactoe_oop.pythe end of your file to subclass the original TTTBoardclass and then override getBoardStr()the methods:

class MiniBoard(TTTBoard):
    def getBoardStr(self):
        """Return a tiny text-representation of the board."""
        # Change blank spaces to a '.'
        for space in ALL_SPACES:
            if self._spaces[space] == BLANK:
                self._spaces[space] = '.'

        boardStr = f'''
          {
      
      self._spaces['1']}{
      
      self._spaces['2']}{
      
      self._spaces['3']} 123
          {
      
      self._spaces['4']}{
      
      self._spaces['5']}{
      
      self._spaces['6']} 456
          {
      
      self._spaces['7']}{
      
      self._spaces['8']}{
      
      self._spaces['9']} 789'''

        # Change '.' back to blank spaces.
        for space in ALL_SPACES:
            if self._spaces[space] == '.':
                self._spaces[space] = BLANK
        return boardStr

As with the methods TTTBoardof the class , the method creates a multi-line string of a tic-tac-toe board that is displayed when passed to the function. But this string is much smaller, dropping the line between the X and O tokens, and using dots to represent spaces.getBoardStr()MiniBoardgetBoardStr()print()

Change main()the line in so that it instantiates an MiniBoardobject instead of an TTTBoardobject:

 if input('Use mini board? Y/N: ').lower().startswith('y'):
        gameBoard = MiniBoard() # Create a MiniBoard object.
    else:
        gameBoard = TTTBoard() # Create a TTTBoard object.

Except for main()this one line modification, the rest of the program is the same as before. When you run the program now, the output will look like this:

Welcome to Tic-Tac-Toe!
Use mini board? Y/N: y

          ... 123
          ... 456
          ... 789
What is X's move? (1-9)
1

          X.. 123
          ... 456
          ... 789
What is O's move? (1-9)
`--snip--`
          XXX 123
          .OO 456
          O.X 789
X has won the game!
Thanks for playing!

Your program can now easily have implementations of these two tic-tac-toe board classes. Of course, if you only want the mini version of the board, you can simply replace the code TTTBoardin getBoardStr()the method. But if you need and , inheritance allows you to easily create two classes by reusing their common code.

If we weren't using inheritance, we could TTTBoardadd a useMiniBoardnew property called , and getBoardStr()put a if-elsestatement in it to decide when to show the regular panel or the mini-panel. For such a simple change, this will work fine. But what if MiniBoardthe subclass needs to override 2, 3, or even 100 methods? What if we wanted to create several different TTTBoardsubclasses? Not using inheritance would cause the statement explosion in our methods if-elseand greatly increase the complexity of the code. By using subclasses and overriding methods, we can better organize our code into separate classes to handle these different use cases.

super()function

Subclass overridden methods are usually similar to superclass methods. Although inheritance is a code reuse technique, overriding a method can cause you to override the same code in a parent class method as part of a subclass method. To prevent this duplication of code, built-in super()functions allow an overriding method to call the original method in the parent class.

For example, let's create a HintBoardnew class called , which is TTTBoarda subclass of . The new class overrides getBoardStr(), so after drawing the tic-tac-toe board, it also adds a hint if X or O wins on their next move. This means that the methods HintBoardof the class getBoardStr()must do all the same tasks as the methods TTTBoardof the class that draw the tic-tac-toe board. Instead of duplicating code, getBoardStr()we can do this by calling methods of a class super()from methods HintBoardof a class . Add the following to the end of your file:getBoardStr()TTTBoardgetBoardStr()tictactoe_oop.py

class HintBoard(TTTBoard):
    def getBoardStr(self):
        """Return a text-representation of the board with hints."""
        boardStr = super().getBoardStr() # Call getBoardStr() in TTTBoard. # 1

        xCanWin = False
        oCanWin = False
        originalSpaces = self._spaces # Backup _spaces. # 2
        for space in ALL_SPACES: # Check each space:
            # Simulate X moving on this space:
            self._spaces = copy.copy(originalSpaces)
            if self._spaces[space] == BLANK:
                self._spaces[space] = X
            if self.isWinner(X):
                xCanWin = True
            # Simulate O moving on this space:
            self._spaces = copy.copy(originalSpaces) # 3
            if self._spaces[space] == BLANK:
                self._spaces[space] = O
            if self.isWinner(O):
                oCanWin = True
        if xCanWin:
            boardStr += '\nX can win in one more move.'
        if oCanWin:
            boardStr += '\nO can win in one more move.'
        self._spaces = originalSpaces
        return boardStr

First, super().getBoardStr()1 runs the code inside the parent TTTBoardclass getBoardStr(), returning the string for the tic-tac-toe board. We temporarily save this string in a boardStrvariable called . The rest of the code in this method handles the generation of hints using the checkerboard string created by reusing TTTBoardthe class . The method getBoardStr()then sets the and variable to and backs up the dictionary as variable 2. Then a loop loops over all the board spaces from to . Inside the loop, the attribute is set to a copy of the dictionary, and if the current space being looped is empty, an X is placed there. This simulates the next move of X on this empty space. A call to will determine if this is a winning move, and if so, will set it to . Then repeat these steps for O to see if O can move 3 on this space to win. This method uses the module to copy the dictionary in, so add the following line at the top of the :getBoardStr()xCanWinoCanWinFalseself._spacesoriginalSpacesfor'1''9'self._spacesoriginalSpacesself.isWinner()xCanWinTruecopyself._spacestictactoe.py

import copy

Next, change main()the line in so that it instantiates an HintBoardobject instead of an TTTBoardobject:

 gameBoard = HintBoard() # Create a TTT board object.

Except for main()this one line modification, the rest of the program is exactly the same as before. When you run the program now, the output will look like this:

Welcome to Tic-Tac-Toe!
`--snip--`
      X| |   1 2 3
      -+-+-
       | |O  4 5 6
      -+-+-
       | |X  7 8 9
X can win in one more move.
What is O's move? (1-9)
5

      X| |   1 2 3
      -+-+-
       |O|O  4 5 6
      -+-+-
       | |X  7 8 9
O can win in one more move.
`--snip--`
The game is a tie!
Thanks for playing!

At the end of the method, if truexCanWin , an additional statement message is added to the string. Finally, back again.oCanWinTrueboardStrboardStr

Not every overridden method needs to be used super()! If the overriding method of a class does something completely different from the overridden method in the parent class, there is no need to use super()call the overridden method. Functions are especially useful when a class has more than one parent method super(), as explained in "Multiple Inheritance" later in this chapter.

Prefer composition over inheritance

Inheritance is a great technique for code reuse, and you probably want to start using it in all your classes right away. But you may not always want base and subclasses to be so tightly coupled. Creating multiple levels of inheritance doesn't add organization to your code, it adds bureaucracy.

While you can use inheritance with classes that have an "is" relationship (in other words, when the subclass is a kind of parent class), it is often advantageous to use a technique called composition with classes that have a "is " relationship . Composition is a class design technique in which objects are contained within classes rather than classes that inherit those objects. This is what we do when we add properties to a class. When designing your classes using inheritance, favor composition over inheritance. This is what we did in all the examples in this and the previous chapter, as described in the table below:

  • An object "has" a certain number of Galleon, Sickle, and Canute coins.
  • An object "has" a set of nine spaces.
  • An MiniBoardobject is an TTTBoardobject, so it also "has" a set of nine spaces.
  • An HintBoardobject is an TTTBoardobject, so it also "has" a set of nine spaces.

Let's go back to WizCointhe classes from the previous chapter. If we create a new WizardCustomerclass to represent customers in the wizarding world, these customers will carry a certain amount of money, we can WizCoinrepresent this money through the class. But there is no "is-one" relationship between the two classes; an WizardCustomerobject is not an WizCoinobject. If we use inheritance, it may result in some clumsy code:

import wizcoin

class WizardCustomer(wizcoin.WizCoin): # 1
    def __init__(self, name):
        self.name = name
        super().__init__(0, 0, 0)

wizard = WizardCustomer('Alice')
print(f'{
      
      wizard.name} has {
      
      wizard.value()} knuts worth of money.')
print(f'{
      
      wizard.name}\'s coins weigh {
      
      wizard.weightInGrams()} grams.')

In this example, WizardCustomerinheriting WizCoinmethods of a 1 object, such as value()and weightInGrams(). Technically, inheriting from WizCoincan WizardCustomeraccomplish the same task as including WizCoinan object as a property . WizardCustomerBut the names of the wizard.value()and wizard.weightInGrams()methods are misleading: they seem to return the value and weight of the wizard, not the value and weight of the wizard coin. Also, if we later wanted to add a method to the wizard's weight weightInGrams(), that method name would already be taken.

It's better to have an WizCoinobject as a property, since a wizard customer "has" a certain amount of wizard coins:

import wizcoin

class WizardCustomer:
    def __init__(self, name):
        self.name = name
        self.purse = wizcoin.WizCoin(0, 0, 0) # 1

wizard = WizardCustomer('Alice')
print(f'{
      
      wizard.name} has {
      
      wizard.purse.value()} knuts worth of money.')
print(f'{
      
      wizard.name}\'s coins weigh {
      
      wizard.purse.weightInGrams()} grams.')

WizardCustomerInstead of having the class inherit methods from , we WizCoingave WizardCustomerthe class an purseattribute 1 , which contains an WizCoinobject. When using composition, WizCoinany changes to the methods of the class will not change WizardCustomerthe methods of the class. This technique provides more flexibility for future design changes of the two classes and makes the code more maintainable.

Inherited Disadvantages

The main disadvantage of inheritance is that any future changes you make to the parent class are bound to be inherited by all its subclasses. In most cases, this tight coupling is exactly what you want. But in some cases, your code needs don't quite fit your inheritance model.

CarFor example, suppose we have , Motorcycleand classes in a vehicle simulation program LunarRover. They all require similar methods such as startIgnition()and changeTire(). Instead of copying and pasting this code into each class, we can create a parent class Vehicleand have Car, , Motorcycleand inherit from it. LunarRoverNow, if we need to fix a bug in, say, a changeTire()method, we only need to modify it in one place. This is especially useful if we have dozens of Vehicledifferent vehicle-related classes that we inherit from. The code for these classes looks like this:

class Vehicle:
    def __init__(self):
        print('Vehicle created.')
    def startIgnition(self):
        pass  # Ignition starting code goes here.
    def changeTire(self):
        pass  # Tire changing code goes here.

class Car(Vehicle):
    def __init__(self):
        print('Car created.')

class Motorcycle(Vehicle):
    def __init__(self):
        print('Motorcycle created.')

class LunarRover(Vehicle):
    def __init__(self):
        print('LunarRover created.')

But all future Vehiclechanges to will affect these subclasses as well. What if we need a changeSparkPlug()method? Cars and motorcycles have internal combustion engines with spark plugs, but lunar rovers do not. By favoring composition over inheritance, we can create individual CombustionEngineand ElectricEngineclasses. We then designed Vehiclethe class so that it "has a" engine attribute, either a CombustionEngine ElectricEngine对象,使用适当的方法:

class CombustionEngine:
   def __init__(self):
       print('Combustion engine created.')
   def changeSparkPlug(self):
       pass  # Spark plug changing code goes here.

class ElectricEngine:
   def __init__(self):
       print('Electric engine created.')

class Vehicle:
   def __init__(self):
       print('Vehicle created.')
       self.engine = CombustionEngine()  # Use this engine by default.
`--snip--`

class LunarRover(Vehicle):
   def __init__(self):
       print('LunarRover created.')
       self.engine = ElectricEngine()

This can require a lot of code rewriting, especially if you have several classes that inherit from pre-existing Vehicleclasses: for Vehicleeach object of the class or its subclasses, all vehicleObj.changeSparkPlug()calls need to be turned into vehicleObj.engine.changeSparkPlug(). Because such large changes can introduce errors, you may wish to simply make LunarVehiclethe changeSparkPlug()method do nothing. In this case, the Python-style approach is to set it LunarVehiclewithin the class to :changeSparkPlugNone

class LunarRover(Vehicle):
    changeSparkPlug = None
    def __init__(self):
        print('LunarRover created.')

changeSparkPlug = Noneline follows the syntax described in "Class Attributes" later in this chapter. This overrides the method Vehicleinherited from changeSparkPlug(), so LunarRovercalling it with an object will result in an error:

>>> myVehicle = LunarRover()
LunarRover created.
>>> myVehicle.changeSparkPlug()
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: 'NoneType' object is not callable

This error allows us to fail fast and spot the problem immediately if we try to LunarRovercall this inappropriate method with an object. Any LunarRoversubclasses also changeSparkPlug()inherit this Nonevalue. TypeError: 'NoneType' object is not callableThe error message tells us that LunarRoverthe programmer of the class intentionally changeSparkPlug()set the method to None. If there wasn't such a method in the first place, we'd get an NameError: name 'changeSparkPlug' is not definederror message.

Inheritance can create complex and contradictory classes. It is often advantageous to use text instead.

isinstance()and issubclass()functions

When we need to know the type of an object, we can pass the object to a built-in type()function, as described in the previous chapter. But if we're doing type checking on an object, isinstance()it's a better idea to use the more flexible built-in functions. The function will return if the object belongs to the given class or a subclass of the given class . Enter the following in the interactive shell:isinstance()True

>>> class ParentClass:
...    pass
...
>>> class ChildClass(ParentClass):
...    pass
...
>>> parent = ParentClass() # Create a ParentClass object.
>>> child = ChildClass() # Create a ChildClass object.
>>> isinstance(parent, ParentClass)
True
>>> isinstance(parent, ChildClass)
False
>>> isinstance(child, ChildClass) # 1
True
>>> isinstance(child, ParentClass) # 2
True

Note that the objects in isinstance()the representation are instances of 1 as well as instances of 2. This makes sense because an object is a kind of object.childChildClassChildClassParentClassChildClassParentClass

You can also pass a tuple of class objects as the second argument to see if the first argument is any of the classes in the tuple:

>>> isinstance(42, (int, str, bool)) # True if 42 is an int, str, or bool.
True

A less commonly used issubclass()built-in function recognizes whether the class object passed as the first argument is a subclass (or the same class) of the class object passed as the second argument:

>>> issubclass(ChildClass, ParentClass) # ChildClass subclasses ParentClass.
True
>>> issubclass(ChildClass, str) # ChildClass doesn't subclass str.
False
>>> issubclass(ChildClass, ChildClass) # ChildClass is ChildClass.
True

Just like with use isinstance(), you can pass a tuple of class objects as the second argument issubclass()to see if the first argument is a subclass of any of the classes in the tuple. isinstance()The key difference between and issubclass()is that issubclass()two class objects are passed, whereas isinstance()an object and a class object are passed.

class method

Class methods are associated with a class, not with a single object like regular methods. You can recognize a class method in your code when you see two tokens: the decorator defbefore the method's statement and the use as first parameter, as shown in the example below.@classmethodcls

class ExampleClass:
    def exampleRegularMethod(self):
        print('This is a regular method.')

    @classmethod
    def exampleClassMethod(cls):
        print('This is a class method.')

# Call the class method without instantiating an object:
ExampleClass.exampleClassMethod()

obj = ExampleClass()
# Given the above line, these two lines are equivalent:
obj.exampleClassMethod()
obj.__class__.exampleClassMethod()

Parameters behave similarly , except that selfthey refer to an object clsinstead of an object's class . This means that code within a class method cannot access individual object properties or call regular methods of the object. Class methods can only call other class methods or access class attributes. We use name because it is a Python keyword, and like other keywords, such as , or , we cannot use it for parameter names. We often call class attributes through class objects, as in . But we can also call them through any object of the class, just like in .clsselfclsclassifwhileimportExampleClass.exampleClassMethod()obj.exampleClassMethod()

Class methods are not commonly used. The most common use case is to provide __init__()optional constructors other than . For example, what if the constructor could accept both a string of data required by the new object and a string of a filename containing the data required by the new object? We don't want __init__()long and confusing parameter lists for methods. Instead, let's use a class method that returns a new object.

For example, let's create a AsciiArtclass. As you saw in Chapter 14, ASCII art uses text characters to form images.

class AsciiArt:
   def __init__(self, characters):
       self._characters = characters

   @classmethod
   def fromFile(cls, filename):
       with open(filename) as fileObj:
           characters = fileObj.read()
           return cls(characters)

   def display(self):
       print(self._characters)

   # Other AsciiArt methods would go here...

face1 = AsciiArt(' _______\n' +
                '|  . .  |\n' +
                '| \\___/ |\n' +
                '|_______|')
face1.display()

face2 = AsciiArt.fromFile('face.txt')
face2.display()

AsciiArtclass has a __init__()method to which the image's text characters can be passed as a string. It also has a fromFile()class method that can be passed the filename string of the text file containing the ASCII art. Both methods create AsciiArtobjects.

When you run this program and have a file containing ASCII art surfaces face.txt, the output will look like this:

 _______
|  . .  |
| \___/ |
|_______|
 _______
|  . .  |
| \___/ |
|_______|

Class methods make your code easier to read than having __init__()to do everything .fromFile()

Another benefit of class methods is that AsciiArtsubclasses of a class can inherit its fromFile()method (and override it if necessary). That's why we call it in the method AsciiArtof the class instead of . Calling also works in subclasses of , without modification, since the class is not hardcoded into the methods. But a call always calls the class's and not the subclass's . You can understand it as "an object representing this class"fromFile()cls(characters)AsciiArt(characters)cls()AsciiArtAsciiArtAsciiArt()AsciiArt__init__()__init__()cls

Remember that just as regular methods should always use their parameters somewhere in the code self, a class method should always use its clsparameters. If your class method's code never uses clsparameters, this is a sign that your class method is probably just a function.

class attribute

Class attributes are variables that belong to a class rather than an object. We create class attributes inside a class but outside all methods, just like we .pycreate global variables inside a file but outside all functions. Here's an countexample of a class attribute named , which keeps track of how many CreateCounterobjects have been created:

class CreateCounter:
    count = 0 # This is a class attribute.

    def __init__(self):
        CreateCounter.count += 1

print('Objects created:', CreateCounter.count)  # Prints 0.
a = CreateCounter()
b = CreateCounter()
c = CreateCounter()
print('Objects created:', CreateCounter.count)  # Prints 3.

CreateCounterClasses have a countclass attribute named . All CreateCounterobjects share this property instead of having their own individual countproperties. That's why the lines in the constructor CreateCounter.count += 1can record each object being created CreateCounter. When you run the program, the output will look like this:

Objects created: 0
Objects created: 3

We rarely use class attributes. Even this "count how many objects have been created CreateCounter" example can be done more simply by using a global variable instead of a class property.

static method

A static method does not have an selfor clsparameter. Static methods are really just functions in that they cannot access properties or methods of the class or its objects. In Python, static methods are rarely needed. If you decide to use a function, then you should consider creating a regular function.

We define a static method by defplacing @staticmethoda decorator before the static method's statement. Below is an example of a static method.

class ExampleClassWithStaticMethod:
   @staticmethod
   def sayHello():
       print('Hello!')

# Note that no object is created, the class name precedes sayHello():
ExampleClassWithStaticMethod.sayHello()

There is little difference between static methods and functions in ExampleClassWithStaticMethoda class . In fact, you might prefer to use a function, since you can call it without having to type the class name beforehand.sayHello()sayHello()

Static methods are more common in other languages ​​that don't have Python's flexible language features. Python includes static methods that mimic features of other languages, but provide little practical value.

When to Use Object-Oriented Classes and Static Attributes

You rarely need class methods, class properties and static methods. They are also prone to overuse. If you're thinking, "Why can't I use a function or a global variable instead?" that's a hint that you probably don't need to use class methods, class properties, or static methods. The only reason this intermediate-level book introduces them is so you can recognize them when you encounter them in code, but I discourage you from using them. They are useful if you are creating your own framework with a series of carefully crafted classes that are subclassed by programmers using the framework. But when you're writing simple Python applications, you probably don't need them.

For more discussion of these features and why you might or might not need them, read Phillip J. Eby's dirtsimple.org/2004/12/python-is-not-java.htmlarticle "Python is not Java" and Ryan Tomayko tomayko.com/blog/2004/the-static-method-thing's article "Static methods."

object oriented buzzword

Explanations of OOP usually start with a number of terms, such as inheritance, encapsulation, and polymorphism. The importance of knowing these terms is overrated, but you should have at least a basic understanding of them. I've already covered inheritance, so I'll describe the other concepts here.

encapsulation

The word encapsulation has two common but related definitions. The first definition is that encapsulation is the bundling of related data and code into a single unit. To encapsulate means to box . That's basically what classes do: they group related properties and methods. For example, our WizCoinclass encapsulates three integers knuts, sicklesand , galleonsinto a single WizCoinobject.

The second definition is that encapsulation is an information hiding technique that lets an object hide complex implementation details about how the object works. You can see this in "Private Properties and Methods" on page 282, where BankAccountobjects provide deposit()and withdraw()methods to hide the details of how their _balanceproperties are handled. Functions serve a similar black-box purpose: math.sqrt()how the function computes the square root of a number is hidden. You just need to know that the function returns the square root of the number you pass it.

polymorphism

Polymorphism allows objects of one type to be treated as objects of another type. For example, len()the function returns the length of the argument passed to it. You can pass a string len()to see how many characters it has, but you can also pass a list or dictionary len()to see how many entries or key-value pairs it has, respectively. This form of polymorphism is known as generic function or parametric polymorphism because it can handle many different types of objects.

Polymorphism also refers to ad hoc polymorphism or operator overloading , where operators (like +or *) can behave differently depending on the type of object they operate on. For example, +the operator does mathematical addition when dealing with two integer values ​​or floating-point values, but does string concatenation when dealing with two strings. Operator overloading is covered in Chapter 17.

When Not to Use Inheritance

It's easy to overengineer your classes using inheritance. As Luciano Ramaljo said, "Having things in order inspires our sense of order; programmers do it for fun." When several functions in a class or a module can achieve the same , we'll create classes, subclasses, and subclasses. But recall the Python mantra from Chapter 6: Simple is better than complex.

Using OOP allows you to organize your code into smaller units (classes in this case) that are pyeasier to reason about than one large file containing hundreds of functions defined in no particular order. Inheritance is useful if you have several functions that all operate on the same dictionary or list data structure. In this case, it is beneficial to organize them into a class.

But here are some examples that don't require creating classes or using inheritance:

  • If your class consists of methods that never use selfor clsparameters, delete the class and use functions instead of methods.
  • If you've created a parent class with only one subclass, but never created objects of the parent class, you can combine them into one class.
  • If you create more than three or four levels of subclasses, you may be using inheritance unnecessarily. Merge these subclasses into fewer classes.

As the non-object-oriented and object-oriented versions of the tic-tac-toe program demonstrated in the previous chapter, it is of course possible to not use classes and still have a working, error-free program. Don't feel like you have to design your program into some complex network of classes. Simple solutions that work are better than complex solutions that don't work. Joel Spolsky writes in his blog post "Don't Let Astronaut Architects Scare You".

You should know how object-oriented concepts like inheritance work because they help you organize your code and make development and debugging easier. Because of Python's flexibility, the language not only provides OOP features, but also doesn't require you to use them when they don't fit your program's needs.

multiple inheritance

Many programming languages ​​limit classes to at most one parent class. Python supports multiple parent classes by providing a feature called multiple inheritance . For example, we can have a class with flyInTheAir()a method Airplaneand a class with floatOnWater()a method Ship. We can then create a class that inherits from Airplaneand Shipby FlyingBoatlisting classthe two in the statement, separated by a comma. Open a new file editor window and save the following as flyingboat.py:

class Airplane:
    def flyInTheAir(self):
        print('Flying...')

class Ship:
    def floatOnWater(self):
        print('Floating...')

class FlyingBoat(Airplane, Ship):
    pass

The objects we create FlyingBoatwill inherit flyInTheAir()and floatOnWater()methods, as you can see in the interactive shell:

>>> from flyingboat import *
>>> seaDuck = FlyingBoat()
>>> seaDuck.flyInTheAir()
Flying...
>>> seaDuck.floatOnWater()
Floating...

Multiple inheritance is simple as long as the method names of the parent classes are different and do not overlap. These kinds of classes are called mixins . (That's just a generic term for a class; Python doesn't have mixinkeywords.) But what happens when we inherit from multiple complex classes that share method names?

MiniBoardFor example, consider the Tic Tac Toe board class from earlier in this chapter HintTTTBoard. What if we wanted a class that displayed a miniature tic-tac-toe board and gave hints? Through multiple inheritance, we can reuse these existing classes. Add the following to tictactoe_oop.pythe end of your file, but before the statement that calls main()the function if:

class HybridBoard(HintBoard, MiniBoard):
    pass

This class has nothing. It reuses code through inheritance HintBoardand . MiniBoardNext, change main()the code in the function so that it creates an HybridBoardobject:

gameBoard = HybridBoard() # Create a TTT board object.

Both parent classes MiniBoardand HintBoardboth have a getBoardStr()method called , so HybridBoardwhich one to inherit from? When you run this program, the output shows a miniature tic-tac-toe board, but also provides hints:

`--snip--`
          X.. 123
          .O. 456
          X.. 789
X can win in one more move.

Python seems to magically merge MiniBoardclass getBoardStr()methods and HintBoardclass getBoardStr()methods to achieve both! But that's because I've written them to work with each other. In fact, if you change the order of the classes in HybridBoardthe class classstatement, it looks like this:

`class HybridBoard(MiniBoard, HintBoard):` 

You completely lose the hint:

`--snip--`
          X.. 123
          .O. 456
          X.. 789

To understand why this is the case, you need to understand Python's Method Resolution Order ( MRO ) and super()how functions actually work.

method resolution order

Our tic-tac-toe program now has four classes to represent the board, three with defined getBoardStr()methods and one with inherited getBoardStr()methods, as shown in Figure 16-2.

f16002

Figure 16-2: The four classes in our tic-tac-toe program

When we call it on an HybridBoardobject getBoardStr(), Python knows that HybridBoardthe class doesn't have a method with that name, so it checks its parent class. But the class has two parents, and they both have a getBoardStr()method. Which will be called?

You can HybridBoardfind out by examining the MRO of a class, which is super()an ordered list of classes that Python checks when inheriting a method or when a method calls a function. You can see the MRO of HybridBoarda class by calling its method in an interactive shell :mro()

>>> from tictactoe_oop import *
>>> HybridBoard.mro()
[<class 'tictactoe_oop.HybridBoard'>, <class 'tictactoe_oop.HintBoard'>, <class 'tictactoe_oop.MiniBoard'>, <class 'tictactoe_oop.TTTBoard'>, <class 'object'>]

As you can see from this return value, when HybridBoardcalling a method on , Python first HybridBoardchecks for it in the class. If not present, Python checks HintBoardfor class, then MiniBoardclass, and finally TTTBoardclass. At the end of each MRO list is the built-in objectclass, which is the parent class of all classes in Python.

With single inheritance, determining the MRO is easy: just create a chain of parent classes. With multiple inheritance, it's trickier. Python's MRO follows the C3 algorithm, the details of which are beyond the scope of this book. But you can determine MRO by remembering two rules:

  • Python checks subclasses before checking superclasses.
  • Python examines classthe inherited classes listed from left to right in the statement.

If we HybridBoardcall on an object getBoardStr(), Python first checks HybridBoardthe class. Then, since the parent of the class is sum from left to HintBoardright MiniBoard, Python checks HintBoard. This parent class has a getBoardStr()method, so HybridBoardinherit and call it.

But it doesn't end there: next, the method call super().getBoardStr(). is a somewhat misleading name for a function in superPython , since instead of returning the parent class, it returns the next class in the MRO. super()This means that when we HybridBoardcall on an object getBoardStr(), the next class in its MRO, HintBoardafter, is MiniBoard, not the parent class TTTBoard. So super().getBoardStr()the call to the MiniBoardclass getBoardStr()method that returns the miniature tic tac toe string. The rest of the code in the class after this super()call appends the prompt text to this string.HintBoardgetBoardStr()

If we change the HybridBoardclass classstatement so that it is listed first MiniBoard, HintBoardits MRO will be MiniBoardlisted HintBoardfirst. This means inheriting HybridBoardfrom , without calling to. This ordering causes the miniature tic-tac-toe board to display silent errors: no calls , methods of classes never call methods of classes .MiniBoardgetBoardStr()MiniBoardsuper()super()MiniBoardgetBoardStr()HintBoardgetBoardStr()

Multiple inheritance allows you to create a lot of functionality with a small amount of code, but can easily lead to over-engineered, hard-to-understand code. Supports single inheritance, mixin classes, or no inheritance. These techniques are often better able to accomplish the task of the program.

Summarize

Inheritance is a code reuse technique. It allows you to create subclasses that inherit the methods of their parent class. You can override these methods to give them new code, but you can also use super()functions to call the original methods in the parent class. A subclass has an "is" relationship with its parent class because an object of the subclass is a type of object of the parent class.

In Python, the use of classes and inheritance is optional. Some programmers don't think the complexity of heavy use of inheritance is worth it. Using composition rather than inheritance is often more flexible because it implements a "has" relationship with objects of one class and objects of other classes, rather than inheriting methods directly from those other classes. This means that objects of one class can own objects of another class. For example, Customeran object may have a property that is assigned to Datethe object birthdaterather than to Customera subclass of the class Date.

Just as type()it can return the type of the object passed to it, isinstance()and issubclass()functions return the type and inheritance information of the object passed to them.

Classes can have object methods and properties, but they can also have class methods, class properties, and static methods. Although these are rarely used, they can support other object-oriented techniques that global variables and functions cannot.

Python allows classes to inherit from multiple parent classes, although this can lead to difficult-to-understand code. super()Functions and methods of a class determine how methods are inherited based on the MRO. You can mro()view a class's MRO in the interactive shell by invoking the class's methods.

This chapter and the previous one cover general OOP concepts. In the next chapter, we'll explore Python-specific OOP techniques. `

Guess you like

Origin blog.csdn.net/wizardforcel/article/details/130030640