1
Current Location:
>
Object-Oriented Programming
Python Object-Oriented Programming Basics
Release time:2024-10-24 10:34:37 Number of reads: 14
Copyright Statement: This article is an original work of the website and follows the CC 4.0 BY-SA copyright agreement. Please include the original source link and this statement when reprinting.

Article link: https://junyayun.com/en/content/aid/583?s=en%2Fcontent%2Faid%2F583

Here is a professional translation from the identified source language to American English:

Classes and Objects

Class Definition and Instantiation

Hey folks! Today we'll talk about the basics of object-oriented programming (OOP) in Python. You may have learned some theoretical knowledge already, but you can only truly understand the essence through practical examples. Let's start with the most fundamental concept: "classes and objects"!

In real life, we often encounter the concept of "objects". For example, a car is an object with attributes like color, brand, model, etc., and methods (functions) like start, accelerate, brake, etc. In programming, we can use "classes" to describe these objects and create corresponding "instances".

Let's look at a simple example where we define a Car class:

class Car:
    def __init__(self, make, model, year):
        self.make = make
        self.model = model
        self.year = year
        self.fuel = 100 # Assume the fuel tank is full

    def drive(self, distance):
        fuel_consumed = distance * 0.05 # Assume 0.05 liters of fuel consumed per km
        if self.fuel >= fuel_consumed:
            self.fuel -= fuel_consumed
            print(f"Drove {distance} km, remaining fuel {self.fuel:.2f} liters")
        else:
            print("Not enough fuel to drive!")

Here we defined a Car class with three attributes: manufacturer make, model model, and year year. The __init__ method is a special constructor that is automatically called when creating an object, used to initialize the object's attributes.

We also defined a drive method to simulate driving a car. The self parameter represents the current object instance, allowing us to access and modify the object's attributes.

So how do we create an instance of the Car object?

my_car = Car("Toyota", "Camry", 2022)
print(my_car.make, my_car.model, my_car.year) # Toyota Camry 2022
my_car.drive(100) # Drove 100 km, remaining fuel 95.00 liters

Here we created an instance my_car of the Car class, passing in the manufacturer, model, and year as initialization parameters. Then we can access its attributes and call its methods.

Isn't that cool? Through classes, we can create countless instances of objects, each with its own attributes and behaviors. This programming paradigm of encapsulating data (attributes) and behaviors (methods) together is the core idea of object-oriented programming.

However, talking about just theoretical knowledge may still be too dry. What if I told you that when you open your phone's camera to take a photo, the camera is an object; when you share photos on social media, those photos are objects; even the entire social network application itself can be seen as a large object composed of countless smaller objects. Doesn't it sound more interesting now? Object-oriented programming is ubiquitous in the various applications we use daily!

So go ahead and give it a try! Define some interesting classes, create instances of various objects, and experience the charm of classes and objects. I believe that through practice, you'll soon master the tricks of object-oriented programming!

Attributes and Methods

In the previous section, we learned how to define classes and create object instances. However, having an empty class is not enough; we need to add attributes and methods to give it life.

Still using the car example, in addition to basic attributes like brand, model, and year, a car should have other attributes like color, engine model, fuel type, etc. Moreover, cars have various functions corresponding to different methods, such as start, accelerate, brake, steer, etc.

Let's extend the Car class we defined previously:

class Car:
    def __init__(self, make, model, year, color, engine):
        self.make = make
        self.model = model
        self.year = year
        self.color = color
        self.engine = engine
        self.fuel = 100
        self.velocity = 0

    def accelerate(self, speed_increase):
        self.velocity += speed_increase
        fuel_consumed = speed_increase * 0.2
        self.fuel -= fuel_consumed
        print(f"Current speed {self.velocity} km/h, remaining fuel {self.fuel:.2f} liters")

    def brake(self):
        self.velocity = 0
        print("Stopped!")

    def turn(self, direction):
        print(f"Turning {direction}...")

We added two new attributes to the Car class: color and engine, and also initialized the fuel and velocity attributes. Then we defined three new methods:

  • The accelerate method simulates acceleration, taking a speed_increase parameter representing the speed increase. Accelerating consumes a certain amount of fuel.
  • The brake method simulates braking, setting the speed to zero.
  • The turn method simulates turning, taking a direction parameter representing the turning direction.

Now, let's create a Car instance and try calling its methods:

my_car = Car("Honda", "Civic", 2020, "Red", "2.0L")
print(my_car.color, my_car.engine) # Red 2.0L

my_car.accelerate(60)  # Current speed 60 km/h, remaining fuel 88.00 liters
my_car.turn("right")   # Turning right...
my_car.accelerate(40)  # Current speed 100 km/h, remaining fuel 80.00 liters
my_car.brake()         # Stopped!

By continuously instantiating objects and calling methods, do you have a deeper understanding of object-oriented programming? You can try adding more attributes and methods to the Car class, such as the number of seats, top speed, shifting gears, reversing, etc., and turn it into a more complete car simulator!

In actual development, we often need to continuously extend and modify classes to meet ever-changing requirements. So mastering the ability to define attributes and methods is very important. Only then can we create objects with complete behavioral logic and fully functional.

However, one point to note is that an object's attributes and methods should correspond to its real-world characteristics and behaviors, not assigned arbitrarily. Otherwise, the object loses its meaning, and the code becomes difficult to maintain. Therefore, when designing classes, be sure to think through the object's boundaries and responsibilities first.

Okay, keep going! With continuous practice, you'll soon master the skills of designing attributes and methods. Next, we'll learn how to protect an object's internal state, making it more robust and secure.

Encapsulation

Private Attributes and Methods

In the previous section, we learned how to define attributes and methods for classes. However, have you ever wondered whether we should allow external code to access and modify an object's internal state at will?

As the designer of an object, we certainly hope that the object can have a certain "privacy" to prevent external code from unintentionally or intentionally corrupting its internal state. This requires the concept of encapsulation.

In Python, we can use a double underscore prefix to set an attribute or method as "private". Although Python's private nature is not as thorough as Java's, it can still prevent most external code from accessing internal members.

Let's modify the previous Car class:

class Car:
    def __init__(self, make, model, year, color, engine):
        self.make = make
        self.model = model
        self.year = year
        self.color = color
        self.__engine = engine  # Set the engine attribute as private
        self.__fuel = 100  # Set the fuel attribute as private
        self.__velocity = 0

    def accelerate(self, speed_increase):
        self.__velocity += speed_increase
        fuel_consumed = speed_increase * 0.2
        self.__fuel -= fuel_consumed
        print(f"Current speed {self.__velocity} km/h, remaining fuel {self.__fuel:.2f} liters")

    def brake(self):
        self.__velocity = 0
        print("Stopped!")

    def turn(self, direction):
        print(f"Turning {direction}...")

    def get_fuel(self):
        return self.__fuel

my_car = Car("Honda", "Civic", 2020, "Red", "2.0L")
print(my_car.get_fuel())  # 100.0

We set the engine, fuel, and velocity attributes as private, and external code cannot directly access them. However, we can provide get and set methods within the class to allow external code to read and modify private attributes in a controlled manner.

For example, the code above provides a get_fuel method to get the current fuel level. If we need to modify the fuel level, we can also provide a set_fuel method.

Through this approach, we can protect the object's internal state and prevent external code from unintentionally or maliciously modifying it. This not only improves the code's robustness and security but also follows the best practices of object-oriented programming.

You may ask, if external code really wants to access private members, can't it still use some "unconventional" methods? Yes, that's true. But even though Python cannot achieve true privacy, marking internal members as private still sends a clear signal to external code: please do not access these members, as they may change in future versions.

Therefore, when designing classes, we should mark those attributes and methods used only internally as private, while keeping members that need to be exposed as public. This practice not only enhances code readability and maintainability but also follows the principle of least privilege, thereby improving code security.

Of course, in actual development, we should also be careful not to overuse private members, as it may reduce the code's flexibility and extensibility. Everything should be done on a case-by-case basis, following best practices.

Getter and Setter Methods

In the previous section, we learned how to use private attributes to protect an object's internal state. However, simply making attributes private is not enough; we also need to provide "getter" and "setter" methods to allow external code to read and modify private attributes in a controlled manner.

A "getter" method is used to get (retrieve) the value of an attribute, while a "setter" method is used to set (modify) the value of an attribute. Through this approach, we can not only protect the privacy of attributes but also add additional logic in the getter and setter methods, such as data validation, logging, etc.

Let's modify the previous Car class by adding getter and setter methods:

class Car:
    def __init__(self, make, model, year, color, engine):
        self.make = make
        self.model = model
        self.year = year
        self.color = color
        self.__engine = engine
        self.__fuel = 100
        self.__velocity = 0

    def accelerate(self, speed_increase):
        self.__velocity += speed_increase
        fuel_consumed = speed_increase * 0.2
        self.__fuel -= fuel_consumed
        print(f"Current speed {self.__velocity} km/h, remaining fuel {self.get_fuel():.2f} liters")

    def brake(self):
        self.__velocity = 0
        print("Stopped!")

    def turn(self, direction):
        print(f"Turning {direction}...")

    def get_fuel(self):
        return self.__fuel

    def set_fuel(self, new_fuel):
        if 0 <= new_fuel <= 100:
            self.__fuel = new_fuel
        else:
            print("Fuel level out of valid range (0-100)!")

my_car = Car("Honda", "Civic", 2020, "Red", "2.0L")
print(my_car.get_fuel())  # 100.0

my_car.set_fuel(80)
print(my_car.get_fuel())  # 80.0

my_car.set_fuel(120)  # Fuel level out of valid range (0-100)!

We added a get_fuel method and a set_fuel method to the Car class. The get_fuel method is used to get the current fuel level, while the set_fuel method is used to set a new fuel level.

However, we added a check in the set_fuel method to ensure that the new fuel level is within a reasonable range (0-100). If the input value is out of this range, an error message will be printed.

Through this approach, we can not only protect the privacy of the __fuel attribute but also perform necessary data validation when setting a new value, ensuring the consistency and integrity of the object's internal state.

You may have noticed that we also used the get_fuel method in the accelerate method to get the current fuel level. This is because, as the designer of a class, we should follow the best practice of "using getter/setter methods to access private members even within the class". This not only enhances code consistency but also facilitates future modifications to the getter/setter methods.

In addition to data validation, we can also add other logic to the getter/setter methods, such as logging, access control, etc. For example, we can modify the set_fuel method to only allow admin users to modify the fuel level:

def set_fuel(self, new_fuel, is_admin=False):
    if is_admin:
        if 0 <= new_fuel <= 100:
            self.__fuel = new_fuel
        else:
            print("Fuel level out of valid range (0-100)!")
    else:
        print("You do not have permission to modify the fuel level!")

With the above code, only when the is_admin parameter is True can the fuel level be modified. Otherwise, an "insufficient permission" error message will be printed.

As you can see, getter and setter methods not only protect an object's internal state but also allow us to add more functionality and flexibility to our code. Therefore, when designing classes, we should weigh the pros and cons and reasonably use getter and setter methods to ensure code robustness and maintainability.

Inheritance and Polymorphism

Single Inheritance

Parent and Child Class Relationships

In real life, things often have an "is-a" relationship. For example, a sedan "is-a" type of car, and a cat "is-a" type of animal. This relationship is called "inheritance" in object-oriented programming.

Through inheritance, we can create new classes based on existing ones, inheriting the attributes and methods of the existing class, while also being able to add new features to the new class. This not only improves code reusability but also follows the "Don't Repeat Yourself" (DRY) principle in programming.

Let's look at an example. Suppose we already have a Vehicle class representing a generic means of transportation:

class Vehicle:
    def __init__(self, make, model, year):
        self.make = make
        self.model = model
        self.year = year

    def start(self):
        print("Starting engine...")

    def stop(self):
        print("Shutting off...")

Now, we want to create a Car class representing a special type of vehicle. Since a car "is-a" type of vehicle, we can make Car inherit from the Vehicle class, automatically inheriting the make, model, year attributes, as well as the start and stop methods.

class Car(Vehicle):
    def __init__(self, make, model, year, fuel_type):
        super().__init__(make, model, year)
        self.fuel_type = fuel_type

    def refuel(self):
        print(f"Refueling with {self.fuel_type} fuel...")

In the code above, we use the super().__init__(make, model, year) syntax to call the __init__ method of the parent Vehicle class to initialize the make, model, and year attributes.

We then define a new refuel method in the Car class, which is specific to cars.

To create an instance of the Car class, we can do:

my_car = Car("Toyota", "Camry", 2022, "gasoline")
print(my_car.make, my_car.model, my_car.year)  # Output: Toyota Camry 2022
my_car.start()  # Output: Starting engine...
my_car.refuel()  # Output: Refueling with gasoline fuel...

Here, we create a Car instance my_car with the specified make, model, year, and fuel type. We can then access its attributes and methods, including those inherited from the Vehicle class, as well as the new refuel method specific to the Car class.

Isn't inheritance a powerful feature? By inheriting from an existing class, we can reuse its code and add new functionality to the derived class. This not only reduces code duplication but also promotes code organization and maintainability.

Method Overriding

Sometimes, we need to redefine a method from the parent class in the child class to implement different behavior logic. This operation is called method overriding.

class Vehicle:
    def start(self):
        print("Starting vehicle...")

class Car(Vehicle):
    def start(self):
        print("Starting car...")

my_car = Car()
my_car.start()  # Output: Starting car...

In the above example, the Car class overrides the start method from the parent Vehicle class. When we call my_car.start(), it will execute the overridden start method in the Car class.

Method overriding allows us to customize the behavior of methods in the derived class while still inheriting the common attributes and methods from the parent class.

Multiple Inheritance

Python supports multiple inheritance, which means a child class can inherit attributes and methods from multiple parent classes.

class Vehicle:
    def start(self):
        print("Starting vehicle...")

class Flyable:
    def fly(self):
        print("Flying in the sky!")

class Car(Vehicle, Flyable):
    def drive(self):
        print("Driving the car...")

my_car = Car()
my_car.start()  # Output: Starting vehicle...
my_car.fly()    # Output: Flying in the sky!
my_car.drive()  # Output: Driving the car...

In the above example, the Car class inherits from both the Vehicle and Flyable classes, so it has all the attributes and methods from both parent classes.

Python's Method Resolution Order (MRO)

When using multiple inheritance, it's possible for a child class to inherit the same attribute or method from multiple parent classes, resulting in a "diamond inheritance" problem. Python uses the Method Resolution Order (MRO) to resolve this ambiguity.

The MRO defines the order in which attributes and methods are searched when inheriting from multiple classes. It is an ordered list of classes from the child class to the parent classes. Python will search for a matching method from left to right in this order until it finds the first match.

class A:
    def do(self):
        print("A")

class B(A):
    pass

class C(A):
    def do(self):
        print("C")

class D(B, C):
    pass

d = D()
d.do()  # Output: C
print(D.__mro__) # Output: (<class '__main__.D'>, <class '__main__.B'>, <class '__main__.C'>, <class '__main__.A'>, <class 'object'>)

In the above example, the D class inherits from both B and C, which in turn inherit from A. When calling d.do(), Python searches for the do method in the MRO order of the D class, finding a match in the C class, so it outputs C.

The MRO search order is: the current class D, then B, then C, and finally A and object. This resolves the ambiguity that may arise from "diamond inheritance".

Using the super() Function

When we need to call a method from the parent class in the child class, we can use the super() function. super() is used to get the parent class of the current class and call the parent class's method using a temporary object.

class Vehicle:
    def __init__(self, make, model, year):
        self.make = make
        self.model = model
        self.year = year

    def start(self):
        print("Starting vehicle...")

class Car(Vehicle):
    def __init__(self, make, model, year):
        super().__init__(make, model, year)  # Call parent's __init__()
        self.fuel_type = "Gasoline"

    def start(self):
        super().start()  # Call parent's start()
        print("Starting car...")

my_car = Car("Toyota", "Camry", 2022)
my_car.start()

In the above example, the __init__ method of the Car class uses super().__init__(make, model, year) to call the __init__ method of the parent Vehicle class to initialize the make, model, and year attributes.

Similarly, the start method in the Car class calls the parent Vehicle class's start method using super().start().

Using super() allows us to reuse code effectively and reduce duplication. In the case of multiple inheritance, super() can also call the next parent class's method based on the MRO order.

Overall, Python's object-oriented programming is powerful and flexible. By mastering concepts like classes, objects, inheritance, and polymorphism, you can write programs with clear structures and strong extensibility. I hope this article has provided some insights for you. If you have any further questions, feel free to continue the discussion!

Review of Object-Oriented Basics
Previous
2024-10-24 10:34:37
Python Object-Oriented Programming: From Beginner to Expert
2024-10-24 10:34:37
Next
Related articles