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 aspeed_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 adirection
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!