Diferències
Ací es mostren les diferències entre la revisió seleccionada i la versió actual de la pàgina.
| Ambdós costats versió prèvia Revisió prèvia | |||
| info:cursos:pue:python-pcpp1:m1:2.2 [05/11/2023 21:32] – suprimit - edició externa (Unknown date) 127.0.0.1 | info:cursos:pue:python-pcpp1:m1:2.2 [05/11/2023 21:32] (actual) – ↷ Page moved from info:cursos:pue:python-pcpp1:2.2 to info:cursos:pue:python-pcpp1:m1:2.2 mate | ||
|---|---|---|---|
| Línia 1: | Línia 1: | ||
| + | = 2.2 Inheritance and polymorphism — Inheritance as a pillar of OOP | ||
| + | Inheritance is one of the fundamental concepts of object oriented programming, | ||
| + | |||
| + | This means that inheritance is a way of building a new class, not from scratch, but by using an already defined repertoire of traits. The new class inherits (and this is the key) all the already existing equipment, but is able to add some new features if needed. | ||
| + | |||
| + | Each subclass is more specialized (or more specific) than its superclass. Conversely, each superclass is more general (more abstract) than any of its subclasses. Note that we've presumed that a class may only have one superclass — this is not always true, but we'll discuss this issue more a bit later. | ||
| + | |||
| + | {{ : | ||
| + | |||
| + | A very simple example of two-level inheritance is presented here: | ||
| + | |||
| + | <code python> | ||
| + | pass | ||
| + | |||
| + | class LandVehicle(Vehicle): | ||
| + | pass | ||
| + | |||
| + | class TrackedVehicle(LandVehicle): | ||
| + | pass</ | ||
| + | |||
| + | All the presented classes are empty for now, as we're going to show you how the mutual relations between the super- and subclasses work. | ||
| + | |||
| + | We can say that: | ||
| + | |||
| + | * the **Vehicle** class is the superclass for both the **LandVehicle** and **TrackedVehicle** classes; | ||
| + | * the **LandVehicle** class is a subclass of **Vehicle** and a superclass of **TrackedVehicle** at the same time; | ||
| + | * the **TrackedVehicle** class is a subclass of both the **Vehicle** and **LandVehicle** classes. | ||
| + | |||
| + | == Inheritance and polymorphism — Single inheritance vs. multiple inheritance | ||
| + | There are no obstacles to using multiple inheritance in Python. You can derive any new class from more than one previously defined class. | ||
| + | |||
| + | But multiple inheritance should be used with more prudence than single inheritance because: | ||
| + | |||
| + | * a single inheritance class is always simpler, safer, and easier to understand and maintain; | ||
| + | * multiple inheritance may make method overriding somewhat tricky; moreover, using the super() function can lead to ambiguity; | ||
| + | * it is highly probable that by implementing multiple inheritance you are violating the single responsibility principle; | ||
| + | If your solution tends to require multiple inheritance, | ||
| + | |||
| + | == MRO - Method Resolution Order | ||
| + | The spectrum of issues possibly coming from multiple inheritance is illustrated by a classical problem named the diamond problem, or even the deadly diamond of death. The name reflects the shape of the inheritance diagram — take a look at the picture. | ||
| + | |||
| + | There is the top-most superclass named A; | ||
| + | there are two subclasses derived from A — B and C; | ||
| + | and there is also the bottom-most subclass named D, derived from B and C (or C and B, as these two variants mean different things in Python) | ||
| + | Can you see the diamond there? | ||
| + | {{ : | ||
| + | |||
| + | The ambiguity that arises here is caused by the fact that class B and class C are inherited from superclass A, and class D is inherited from both classes B and C. If you want to call the method info(), which part of the code would be executed then? | ||
| + | |||
| + | Python lets you implement such a class hierarchy. Can you guess the output of the code? | ||
| + | <code python> | ||
| + | def info(self): | ||
| + | print(' | ||
| + | |||
| + | class B(A): | ||
| + | def info(self): | ||
| + | print(' | ||
| + | |||
| + | class C(A): | ||
| + | def info(self): | ||
| + | print(' | ||
| + | |||
| + | class D(B, C): | ||
| + | pass | ||
| + | |||
| + | D().info() # Class B | ||
| + | </ | ||
| + | |||
| + | In the multiple inheritance scenario, any specified attribute is searched for first in the current class. If it is not found, the search continues into the direct parent classes in depth-first level (the first level above), from the left to the right, according to the class definition. This is the result of the MRO algorithm. | ||
| + | |||
| + | In our case: | ||
| + | |||
| + | * **class D** does not define the method '' | ||
| + | * **class D** is constructed in this order: | ||
| + | * the definition of **class B** is fetched; | ||
| + | * the definition of **class C** is fetched; | ||
| + | * Python finds the requested method in the **class B** definition and stops searching; | ||
| + | * Python executes the method. | ||
| + | | ||
| + | == Possible pitfalls - MRO inconsistency | ||
| + | **MRO** can report definition inconsistencies when a subtle change in the **class D** definition is introduced, which is possible when you work with complex class hierarchies. | ||
| + | |||
| + | Imagine that you have changed the **class D** definition from: | ||
| + | <code python> | ||
| + | def info(self): | ||
| + | print(' | ||
| + | |||
| + | class B(A): | ||
| + | def info(self): | ||
| + | print(' | ||
| + | |||
| + | class C(A): | ||
| + | def info(self): | ||
| + | print(' | ||
| + | |||
| + | class D(B, C): | ||
| + | pass | ||
| + | |||
| + | D().info() | ||
| + | pass</ | ||
| + | <code python> | ||
| + | pass</ | ||
| + | <code ; output> | ||
| + | File " | ||
| + | class D(A, C): | ||
| + | TypeError: Cannot create a consistent method resolution order (MRO) for bases A, C</ | ||
| + | |||
| + | This message informs us that the MRO algorithm had problems determining which method (originating from the A or C classes) should be called. | ||
| + | |||
| + | Due to MRO, you should knowingly list the superclasses in the subclass definition. In the following example, class D is based on classes B and C, whereas class E is based on classes C and B (the order matters!). | ||
| + | |||
| + | <code python> | ||
| + | def info(self): | ||
| + | print(' | ||
| + | |||
| + | class B(A): | ||
| + | def info(self): | ||
| + | print(' | ||
| + | |||
| + | class C(A): | ||
| + | def info(self): | ||
| + | print(' | ||
| + | |||
| + | class D(B, C): | ||
| + | pass | ||
| + | |||
| + | class E(C, B): | ||
| + | pass | ||
| + | |||
| + | D().info() | ||
| + | E().info() | ||
| + | </ | ||
| + | <code ; output> | ||
| + | Class C</ | ||
| + | |||
| + | == LAB | ||
| + | === Objectives | ||
| + | * improving the student' | ||
| + | * pointing out the nature of multiple inheritance problems. | ||
| + | === Scenario | ||
| + | Your task is to build a multifunction device (MFD) class consisting of methods responsible for document scanning, printing, and sending via fax. | ||
| + | The methods are delivered by the following classes: | ||
| + | * scan(), delivered by the Scanner class; | ||
| + | * print(), delivered by the Printer class; | ||
| + | * send() and print(), delivered by the Fax class. | ||
| + | Each method should print a message indicating its purpose and origin, like: | ||
| + | * ' | ||
| + | * ' | ||
| + | |||
| + | * create an MFD_SPF class (' | ||
| + | * create an MFD_SFP class (' | ||
| + | * on each object call the methods: scan(), print(), send(); | ||
| + | * observe the output differences. Was the Printer class utilized each time? | ||
| + | |||
| + | === reposta | ||
| + | <code python> | ||
| + | # | ||
| + | # -*- coding: utf-8 -* | ||
| + | |||
| + | |||
| + | class Scanner: | ||
| + | def scan(self): | ||
| + | print(" | ||
| + | | ||
| + | class Printer: | ||
| + | def print(self): | ||
| + | print(" | ||
| + | |||
| + | class Fax: | ||
| + | def send(self): | ||
| + | print(" | ||
| + | def print(self): | ||
| + | print(" | ||
| + | | ||
| + | class MFD_SPF(Scanner, | ||
| + | pass | ||
| + | class MFD_SFP(Scanner, | ||
| + | pass | ||
| + | |||
| + | oUn=MFD_SFP() | ||
| + | oDos=MFD_SPF() | ||
| + | |||
| + | oUn.scan() | ||
| + | oUn.print() | ||
| + | oUn.send() | ||
| + | |||
| + | oDos.scan() | ||
| + | oDos.print() | ||
| + | oDos.send() | ||
| + | </ | ||
| + | |||
| + | == Inheritance and polymorphism — Inheritance as a pillar of OOP | ||
| + | In Python, polymorphism is the provision of a single interface to objects of different types. In other words, it is the ability to create abstract methods from specific types in order to treat those types in a uniform way. | ||
| + | |||
| + | Imagine that you have to print a string or an integer — it is more convenient when a function is called simply **print**, not **print_string** or **print_integer**. | ||
| + | |||
| + | However, the string must be handled differently than the integer, so there will be two implementations of the function that lead to printing, but naming them with a common name creates a convenient abstract interface independent of the type of value to be printed. | ||
| + | |||
| + | The same rule applies to the operation of addition. We know that addition is expressed with the ' | ||
| + | |||
| + | Let's see what methods are present for both built-in types (string and integer) responsible for handling the ' | ||
| + | |||
| + | < | ||
| + | [' | ||
| + | >>> | ||
| + | [' | ||
| + | |||
| + | As you can see, there are many attributes available for the string and integer types, many of them carrying the same names. The first name common to both lists is %%__add__%%, | ||
| + | |||
| + | To briefly demonstrate polymorphism on integers and strings, execute the following code in the Python interpreter: | ||
| + | |||
| + | <code python> | ||
| + | a = 10 | ||
| + | print(a.__add__(20)) | ||
| + | b = ' | ||
| + | print(b.__add__(' | ||
| + | </ | ||
| + | <code ; output> | ||
| + | 30 | ||
| + | abcdef | ||
| + | </ | ||
| + | By the way, if you look for a method that is used when you print a value associated with an object, the **%%__str__%%** method is called to prepare a string that is used in turn for printing. | ||
| + | |||
| + | One way to carry out polymorphism is inheritance, | ||
| + | |||
| + | * most of the code could be reused and only specific methods are implemented, | ||
| + | * the code is clearly structured; | ||
| + | * there is a uniform way of calling methods responsible for the same operations, implemented accordingly for the types. | ||
| + | |||
| + | **Remember** | ||
| + | You can use inheritance to create polymorphic behavior, and usually that's what you do, but that's not what polymorphism is about. | ||
| + | |||
| + | In the right pane, there is a code implementing both inheritance and polymorphism: | ||
| + | * **inheritance**: | ||
| + | * **polymorphism**: | ||
| + | |||
| + | <code python> | ||
| + | class Device: | ||
| + | def turn_on(self): | ||
| + | print(' | ||
| + | |||
| + | class Radio(Device): | ||
| + | pass | ||
| + | |||
| + | class PortableRadio(Device): | ||
| + | def turn_on(self): | ||
| + | print(' | ||
| + | |||
| + | class TvSet(Device): | ||
| + | def turn_on(self): | ||
| + | print(' | ||
| + | |||
| + | device = Device() | ||
| + | radio = Radio() | ||
| + | portableRadio = PortableRadio() | ||
| + | tvset = TvSet() | ||
| + | |||
| + | for element in (device, radio, portableRadio, | ||
| + | element.turn_on() | ||
| + | </ | ||
| + | <code ; output> | ||
| + | The device was turned on | ||
| + | The device was turned on | ||
| + | PortableRadio type object was turned on | ||
| + | TvSet type object was turned on | ||
| + | </ | ||
| + | |||
| + | Duck typing is a fancy name for the term describing an application of the duck test: "If it walks like a duck and it quacks like a duck, then it must be a duck", which determines whether an object can be used for a particular purpose. An object' | ||
| + | |||
| + | Duck typing is another way of achieving polymorphism, | ||
| + | |||
| + | In duck typing, we believe that objects own the methods that are called. If they do not own them, then we should be prepared to handle exceptions. | ||
| + | |||
| + | Let's talk about two things that share conceptually similar methods, but represent totally different things, like cheese and wax. Both can melt, and we use the melted forms for different purposes. | ||
| + | |||
| + | When you run the code you should receive the following output: | ||
| + | <code python> | ||
| + | class Wax: | ||
| + | def melt(self): | ||
| + | print(" | ||
| + | |||
| + | class Cheese: | ||
| + | def melt(self): | ||
| + | print(" | ||
| + | |||
| + | class Wood: | ||
| + | def fire(self): | ||
| + | print(" | ||
| + | |||
| + | for element in Wax(), Cheese(), Wood(): | ||
| + | try: | ||
| + | element.melt() | ||
| + | except AttributeError: | ||
| + | print(' | ||
| + | </ | ||
| + | <code ; output> | ||
| + | Wax can be used to form a tool | ||
| + | Cheese can be eaten | ||
| + | No melt() method | ||
| + | </ | ||
| + | |||
| + | Both the Wax and Cheese classes own melt() methods, but there is no relation between the two. Thanks to duck typing, we can call the melt() method. Unfortunatelly, | ||
| + | |||
| + | == Summary: | ||
| + | |||
| + | * polymorphism is used when different class objects share conceptually similar methods (but are not always inherited) | ||
| + | * polymorphism leverages clarity and expressiveness of the application design and development; | ||
| + | * when polymorphism is assumed, it is wise to handle exceptions that could pop up. | ||