Class inheritance and polymorphism details for Python

  • 2020-06-12 09:48:48
  • OfStack

The definition of a class

Suppose you want to define a class Point to represent a two-dimensional coordinate point:


# point.py
class Point:
  def __init__(self, x=0, y=0):
    self.x, self.y = x, y

The bottom line is the method of SUCCinit__, equivalent to the constructor of C++ / Java. The ways with double underline are special, with many more per ___, and will be described later.

The parameter self is equivalent to this of C++ and represents the current instance. All methods have this parameter, but it is not specified when invoked.


>>> from point import *
>>> p = Point(10, 10) # __init__  Is called 
>>> type(p)
<class 'point.Point'>
>>> p.x, p.y
(10, 10)

Almost all special methods (including ___, 19en__) are implicitly called (not directly called).

For Python, where 1 is an object, the class itself is of course an object:


>>> type(Point)
<class 'type'>
>>> dir(Point)
['__class__', '__delattr__', '__dict__', ..., '__init__', ...]
>>> Point.__class__
<class 'type'>

Point is an instance of type, which is one instance of p.

Now add method set:


class Point:
  ...
  def set(self, x, y):
    self.x, self.y = x, y


>>> p = Point(10, 10)
>>> p.set(0, 0)
>>> p.x, p.y
(0, 0)

p. set (...). It's just 1 grammar sugar, which you can also write as Point. set(p...) , so it is obvious that p is the self parameter:


>>> Point.set(p, 0, 0)
>>> p.x, p.y
(0, 0)

It is worth noting that self is not a keyword and can even be replaced by another name, such as this:


class Point:
  ...
  def set(this, x, y):
    this.x, this.y = x, y

Unlike C++, the "member variable" must be prefixed with self. Otherwise it becomes an attribute of the class (equivalent to C++ static members) rather than an object attribute.

Access control

Python does not have access controls like public/protected/private, and if you must mean "private", it is customary to prefix it with a double underscore.


class Point:
  def __init__(self, x=0, y=0):
    self.__x, self.__y = x, y

  def set(self, x, y):
    self.__x, self.__y = x, y

  def __f(self):
    pass

___, 67en and f are private:


>>> p = Point(10, 10)
>>> p.__x
...
AttributeError: 'Point' object has no attribute '__x'
>>> p.__f()
...
AttributeError: 'Point' object has no attribute '__f'

_repr_

Try printing an instance of Point:


>>> p = Point(10, 10)
>>> p
<point.Point object at 0x000000000272AA20>

Often, this is not the output we want, we want:


>>> from point import *
>>> p = Point(10, 10) # __init__  Is called 
>>> type(p)
<class 'point.Point'>
>>> p.x, p.y
(10, 10)

0

___ by adding a special method:


class Point:
  def __repr__(self):
    return 'Point({}, {})'.format(self.__x, self.__y)

It is not hard to see that the interaction mode actually calls repr(p) when printing p:

> > > repr(p)
'Point(10, 10)'

_str_

If no ___ is provided, str() USES the result of repr() by default.
Both of these are string representations of objects, but a little different. To put it simply, the results of repr() are interpreter oriented and are usually legitimate Python code, such as Point(10, 10); str(), on the other hand, has user-oriented results that are more concise, such as (10, 10).

Per this principle, we have provided ___ of Point per hundred thirteen en__, as follows:


class Point:
  def __str__(self):
    return '({}, {})'.format(self.__x, self.__y)

_add_

The addition of two coordinate points is a reasonable requirement.


>>> from point import *
>>> p = Point(10, 10) # __init__  Is called 
>>> type(p)
<class 'point.Point'>
>>> p.x, p.y
(10, 10)

3

By adding a special method with successie ___ :


>>> from point import *
>>> p = Point(10, 10) # __init__  Is called 
>>> type(p)
<class 'point.Point'>
>>> p.x, p.y
(10, 10)

4

>>> from point import *
>>> p = Point(10, 10) # __init__  Is called 
>>> type(p)
<class 'point.Point'>
>>> p.x, p.y
(10, 10)

5

This is like the operator overload 1 in C++.
Python's built-in types, such as strings and lists, "overload" the + operator.

There are many other special methods, but I won't introduce them here.

inheritance

Take the most common example in a textbook. Circle and Rectangle inherit from Shape, different graphics, area (area) calculation method is different.


>>> from point import *
>>> p = Point(10, 10) # __init__  Is called 
>>> type(p)
<class 'point.Point'>
>>> p.x, p.y
(10, 10)

6

The usage is more direct:


>>> from shape import *
>>> circle = Circle(3.0)
>>> circle.area()
28.274333882308138
>>> rectangle = Rectangle(2.0, 3.0)
>>> rectangle.area()
6.0
 

If Circle does not define its own area:


class Circle(Shape):
  pass 
 

Then it will inherit from its parent class Shape area:


>>> from point import *
>>> p = Point(10, 10) # __init__  Is called 
>>> type(p)
<class 'point.Point'>
>>> p.x, p.y
(10, 10)

9

1 Once Circle defined its own area, the area inherited from Shape was rewritten (overwrite) :


>>> from shape import *
>>> Shape.area is Circle.area
False
 

This is more clearly seen through the dictionary of classes:


>>> Shape.__dict__['area']
<function Shape.area at 0x0000000001FDB9D8>
>>> Circle.__dict__['area']
<function Circle.area at 0x0000000001FDBB70>
 

So, a subclass overrides a superclass method by binding the same property name to a different function object. So Python has no concept of override overridden.

Similarly, even if Shape does not define area, Shape as an "interface" is not guaranteed by the syntax.

You can even add methods dynamically:


class Circle(Shape):
  ...
  # def area(self):
    # return math.pi * self.r * self.r

#  for  Circle  add  area  Methods. 
Circle.area = lambda self: math.pi * self.r * self.r
 

Dynamic languages 1 are generally this flexible, and Python is no exception.

9. Classes 9. Classes

Compared with other programming languages, Python's class mechanism adds classes with a minimum of new syntax and semantics.

Python implements the class mechanism with minimal new syntax and semantics, which is amazing, but it also makes C++ / Java programmers uncomfortable.

polymorphism

As mentioned earlier, Python does not overwrite the concept (override). Strictly speaking, Python does not support polymorphism.

To solve the interface and implementation problems in the inheritance structure, or to better use Python for interface programming (as advocated by the design pattern), we need to set some specifications artificially.

Consider Shape.area (). Is there a better implementation than simply returning 0.0?

The built-in module asyncio for example, AbstractEventLoop is in principle an interface, similar to the interface in Java or the pure virtual class in C++, but Python does not have the syntax to guarantee this point. To try to show that AbstractEventLoop is an interface, first mark it abstract in name (Abstract) and then let each method throw an exception NotImplementedError.


class AbstractEventLoop:
  def run_forever(self):
    raise NotImplementedError
  ...
 

Even so, you cannot forbid users from instantiating AbstractEventLoop:


loop = asyncio.AbstractEventLoop()
try:
  loop.run_forever()
except NotImplementedError:
  pass
 

C++ can be avoided by using a pure virtual function or setting the constructor to protected, not to mention Java, which is an interface with full syntax support.

You can't force subclasses to implement every method defined in "interface" either; the pure virtual function of C++ can force this point (Java, not to mention).

Even if a subclass "thinks" it implements a method in "interface," there is no guarantee that the name of the method is spelled correctly, as the C++ override keyword does (not to mention Java).

The lack of static typing makes it difficult for Python to implement the strict polymorphic checking mechanism of C++ / Java. So interface oriented programming, for Python, depends more on programmer literacy.

Going back to the Shape example, as with asyncio, let's change the "interface" to look like this:


class AbstractShape:
  def area(self):
    raise NotImplementedError
 

That way, it's more like an interface.

super

Sometimes, you need to call a superclass method in a subclass.

Each ___ has a colour property, so add 1 argument from color to init__ :


class AbstractShape:
  def __init__(self, color):
    self.color = color
 

S261EN__() will have to be changed too:


class Circle(AbstractShape):
  def __init__(self, color, r=0.0):
    super().__init__(color)
    self.r = r
 

___, pass color to the parent class with init__(). You don't need super:


class Circle(AbstractShape):
  def __init__(self, color, r=0.0):
    AbstractShape.__init__(self, color)
    self.r = r
 

But super is recommended because it avoids hard coding and can handle multiple inheritance cases.


Related articles: