Learning notes of Python 3 - Object Oriented Advanced Programming (Part 1)

Posted by staggman on Wed, 29 Dec 2021 20:17:33 +0100

Learning notes of Python 3 - Object Oriented Advanced Programming (Part 1)

catalogue

Learning notes of Python 3 - Object Oriented Advanced Programming (Part 1)

preface

1, Use__ slots__

1. Demand

2. Use__ slots__

2, Use @ property

3, Multiple inheritance

1. Multiple inheritance

2.Mixln

preface

Data encapsulation, inheritance and polymorphism are only three basic concepts in object-oriented programming. In Python, object orientation has many advanced features that allow us to write very powerful functions.

We will discuss the concepts of multiple inheritance, custom classes, metaclasses, and so on.

1, Use__ slots__

1. Demand

Normally, when we define a class and create an instance of class, we can bind any properties and methods to the instance, which is the flexibility of dynamic language.

#Define class first
class Student(object):
    pass

#Then, try to bind an attribute to the instance:
s = Student()
s.name = 'Michael' #Dynamically bind a property to an instance
print(s.name)
#Result: Michael

#Then try to bind a method to the instance:
def set_age(self,age): #Define a function as an instance method
    self.age = age

from types import MethodType
s.set_age = MethodType(set_age,s) #Bind a method to the instance
s.set_age(25) #Call instance method
s.age #test result
25

#However, a method bound to one instance does not work on another instance:
s2 = Student() #Create a new instance
s2.set_age(25) #An error is reported when trying to call a method
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AttributeError: 'Student' object has no attribute 'set_age'

#To bind methods to all instances, you can bind methods to class:
def set_score(self,score):
    self.score = score
Student.set_score = set_score

#After binding methods to class, all instances can call:

s.set_score(100)
s.score
100
s2.set_score(99)
s2.score
99

Normally, the above set_score method can be directly defined in class, but dynamic binding allows us to dynamically add functions to class during program running, which is difficult to implement in static language.

2. Use__ slots__

But what if we want to limit the properties of the instance? For example, only the name and age attributes are allowed to be added to the Student instance.

For the purpose of limitation, python allows you to define a special class when defining a class__ slots__ Variable to limit the attributes that can be added to the class instance:

class Student(object):
    __slots__ = ('name', 'age') # Use tuple to define the attribute names that are allowed to bind

Then, let's try:

s = Student() # Create a new instance
s.name = 'Michael' # Binding attribute 'name'
s.age = 25 # Binding property 'age'
s.score = 99 # Binding attribute 'score'
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AttributeError: 'Student' object has no attribute 'score'

Because 'score' is not placed in__ slots__ Therefore, the score attribute cannot be bound. If you try to bind the score, you will get an error of AttributeError.

Use__ slots__ Notice__ slots__ The defined properties only work for the current class instance, but not for the inherited subclasses:

class GraduateStudent(Student):
    pass

g = GraduateStudent()
g.score = 9999

Unless also defined in a subclass__ slots__, In this way, the properties that subclass instances allow to define are their own__ slots__ Add parent__ slots__.

2, Use @ property

When Binding attributes, if we directly expose the attributes, although it is very simple to write, there is no way to check the parameters, so we can change the scores casually:

s = Student()
s.score = 9999

This is obviously illogical. To limit the scope of the score, you can use a set_score() method to set the score, and then through a get_score() to get the score. In this way, set_ In the score () method, you can check the parameters:

class Student(object):

    def get_score(self):
         return self._score

    def set_score(self, value):
        if not isinstance(value, int):
            raise ValueError('score must be an integer!')
        if value < 0 or value > 100:
            raise ValueError('score must between 0 ~ 100!')
        self._score = value

Now, you can't set score arbitrarily by operating on any Student instance:

s = Student()
s.set_score(60) # ok!
s.get_score()
60
s.set_score(9999)
Traceback (most recent call last):
  ...
ValueError: score must between 0 ~ 100!

However, the above calling method is slightly more complex than using properties directly. At this time, we need to use the decorator we mentioned earlier. The @ property decorator built in python is responsible for turning a method into a property call:

class Student(object):
    @property
    def score(self):
        return self._score
    @score.setter
    def score(self,value):
        if not isinstance(value,int):
            raise ValueError('score must be an integer!')
        if value < 0 or value > 100:
            raise ValueError('score must between 0 ~ 100!')
        self.score = value

@The implementation of property is complex. Let's first investigate how to use it. To turn a getter method into a property, just add @ property. At this time, @ property itself creates another decorator @ score Setter is responsible for changing a setter method into property assignment, so we have a controllable property operation:

s = Student()
s.score = 60 # OK, actually converted to s.set_score(60)
s.score # OK, actually converted to s.get_score()
60
s.score = 9999
Traceback (most recent call last):
  ...
ValueError: score must between 0 ~ 100!

Notice the magic @ property. When we operate on the instance property, we know that the property is probably not directly exposed, but implemented through getter and setter methods.

You can also define a read-only attribute. Only getter methods are defined. If setter methods are not defined, it is a read-only attribute,

class Student(object):

    # Method name and instance variable are birth:
    @property
    def birth(self):
        return self.birth

This is because when calling s.birth, it is first converted to a method call, and then return self Birth is regarded as accessing the property of self, so it is converted to method call, resulting in infinite recursion, and finally lead to stack overflow and error RecursionError.

Summary: @ property is widely used in the definition of classes. It allows the caller to write short code and ensure necessary checks on parameters. In this way, the possibility of errors is reduced when the program runs.

3, Multiple inheritance

1. Multiple inheritance

We have talked about the principle of inheritance. What about multiple inheritance?

There are four kinds of animals in animals, dog, bat, parrot and ostrich. If we classify them according to mammals and birds, we can design such a class level:

                ┌───────────────┐
                │    Animal     │
                └───────────────┘
                        │
           ┌────────────┴────────────┐
           │                         │
           ▼                         ▼
    ┌─────────────┐           ┌─────────────┐
    │   Mammal    │           │    Bird     │
    └─────────────┘           └─────────────┘
           │                         │
     ┌─────┴──────┐            ┌─────┴──────┐
     │            │            │            │
     ▼            ▼            ▼            ▼
┌─────────┐  ┌─────────┐  ┌─────────┐  ┌─────────┐
│   Dog   │  │   Bat   │  │ Parrot  │  │ Ostrich │
└─────────┘  └─────────┘  └─────────┘  └─────────┘

However, if we classify it according to "can run" and "can fly", we should design such a class level:

                ┌───────────────┐
                │    Animal     │
                └───────────────┘
                        │
           ┌────────────┴────────────┐
           │                         │
           ▼                         ▼
    ┌─────────────┐           ┌─────────────┐
    │  Runnable   │           │   Flyable   │
    └─────────────┘           └─────────────┘
           │                         │
     ┌─────┴──────┐            ┌─────┴──────┐
     │            │            │            │
     ▼            ▼            ▼            ▼
┌─────────┐  ┌─────────┐  ┌─────────┐  ┌─────────┐
│   Dog   │  │ Ostrich │  │ Parrot  │  │   Bat   │
└─────────┘  └─────────┘  └─────────┘  └─────────┘

If we want to include the above two categories, we have to design more levels:

  • Mammals: mammals that can run and fly;
  • Birds: birds that can run and fly.

In this way, the hierarchy of classes is complex:

                ┌───────────────┐
                │    Animal     │
                └───────────────┘
                        │
           ┌────────────┴────────────┐
           │                         │
           ▼                         ▼
    ┌─────────────┐           ┌─────────────┐
    │   Mammal    │           │    Bird     │
    └─────────────┘           └─────────────┘
           │                         │
     ┌─────┴──────┐            ┌─────┴──────┐
     │            │            │            │
     ▼            ▼            ▼            ▼
┌─────────┐  ┌─────────┐  ┌─────────┐  ┌─────────┐
│  MRun   │  │  MFly   │  │  BRun   │  │  BFly   │
└─────────┘  └─────────┘  └─────────┘  └─────────┘
     │            │            │            │
     │            │            │            │
     ▼            ▼            ▼            ▼
┌─────────┐  ┌─────────┐  ┌─────────┐  ┌─────────┐
│   Dog   │  │   Bat   │  │ Ostrich │  │ Parrot  │
└─────────┘  └─────────┘  └─────────┘  └─────────┘

If we want to add "pet" and "non pet", the number of classes will increase exponentially. Obviously, this design will not work.

The correct approach is to adopt multiple inheritance. First, the main class levels are still designed according to mammals and birds:

class Animal(object):
    pass

# Category:
class Mammal(Animal):
    pass

class Bird(Animal):
    pass

# Various animals:
class Dog(Mammal):
    pass

class Bat(Mammal):
    pass

class Parrot(Bird):
    pass

class Ostrich(Bird):
    pass

Now, to add Runnable and Flyable functions to animals, we only need to define Runnable and Flyable classes:

class Runnable(object):
    def run(self):
        print('Running...')

class Flyable(object):
    def fly(self):
        print('Flying...')

For animals that need Runnable function, inherit one more Runnable, such as Dog:

class Dog(Mammal, Runnable):
    pass

For animals that need Flyable function, inherit one more Flyable, such as Bat:

class Bat(Mammal, Flyable):
    pass

Through multiple inheritance, a subclass can obtain all the functions of multiple parent classes at the same time.

2.Mixln

When designing the inheritance relationship of classes, the main line is usually inherited from a single line. For example, Ostrich inherits from Bird. However, if additional functions are required, they can be realized through multiple inheritance. For example, if ostriches inherit birds and run at the same time, this design is usually called Mixln.

The purpose of MixIn is to add multiple functions to a class. In this way, when designing a class, we give priority to combining multiple MixIn functions through multiple inheritance, rather than designing multi-level and complex inheritance relationships.

Many of Python's own libraries also use MixIn. For example, python comes with two types of network services, TCPServer and UDPServer. To serve multiple users at the same time, you must use multi process or multi-threaded models, which are provided by ForkingMixIn and ThreadingMixIn. Through composition, we can create appropriate services.

For example, write a multi process TCP service, which is defined as follows:

class MyTCPServer(TCPServer, ForkingMixIn):
    pass

Write a multi-threaded UDP service, which is defined as follows:

class MyUDPServer(UDPServer, ThreadingMixIn):
    pass

If you plan to develop a more advanced collaborative process model, you can write a coroutine mixin:

class MyTCPServer(TCPServer, CoroutineMixIn):
    pass

In this way, we do not need a complex and huge inheritance chain. As long as we choose to combine the functions of different classes, we can quickly construct the required subclasses.

Summary:

Because Python allows multiple inheritance, MixIn is a common design.

Languages that only allow single inheritance (such as Java) cannot use MixIn's design.

Topics: Python