Object-oriented Programming trong Python
Giới thiệu về Lập trình hướng đối tượng (Object-oriented Programming) trong Python
Object-oriented Programming (hay OOP) là một mô hình lập trình phụ thuộc vào kiến trúc của object và class.
Các khái niệm cơ bản trong OOP
Dưới đây là những khái niệm cơ bản trong lập trình hướng đối tượng:
- Đặc tính (properties)
- Phương thức (method)
- Lớp (class)
- Đối tượng (object)
- Thực thể (instance)
Trong thực tế, chúng ta có thể thấy rất nhiều objects xung quanh chúng ta như các tòa nhà, con người, etc.
Hay khi đi làm, mỗi người trong công ty đều có tên (name), tuổi (age), mức lương (salary) và phòng ban (department) khác nhau nhưng họ đều là nhân viên (employee).
Vậy để dễ quản lý, chúng ta coi employee là 1 class – tập hợp các employee có đặc điểm chung về properties (đều có name, age, salary, department) nhưng khác giá trị. Ví dụ employee A, employee B là hai objects thuộc class employee.
Method có thể được coi là các hành vi (behaviors) của object. Method thể truy cập vào các đặc tính (properties), và các methods khác của class mà nó thuộc về. Methods có thể nhận và trả về giá trị, hoặc chỉ thực hiện các hành động nào đó trên object của class.
Ví dụ object Employee
sẽ có methods như tính thuế thu nhập dựa trên lương (tax()
), hay cung cấp thông tin của employee (employee_info()
).
Một Instance là một biến (variable) mà giữ địa chỉ bộ nhớ (memory address) của object đó. Ví dụ cho dễ hiểu thì:
- Class là một bản thiết kế chi tiết cho ngôi nhà
- Object là tất cả những ngôi nhà được xây dựng dựa trên bản thiết kế đó
- Instance là một căn nhà cụ thể (nhà của ông A, của ông B)
Khởi tạo object trong Python
Trong Python, ta có initializer, như tên gọi của nó, là một method được dùng để khởi tạo một object của một class. Method này cũng tương tự các method khác nhưng khác ở chỗ initializer thì đã được đặt tên là: __init__
.
Initializer được gọi tự động khi một object của class được tạo. Đây là một method đặc biệt giúp chúng ta xác định và gán giá trị cho các variables của instance. Tương tự các method khác, initializer cũng có thể có optional parameters (tham số mặc định).
Dưới đây là ví dụ khi objects của Employee
class được tạo, một với tham số mặc định, một với tham số của chúng ta truyền vào.
classEmployee:# Khai báo properties và gán giá trị cho chúngdef__init__(self, ID =None, salary =0, department =None):
self.ID = ID
self.salary = salary
self.department = department
# Tạo một object của class Employee với các tham số mặc định
steve = Employee()# Tạo một object của class Employee với tham số của chúng ta
mark = Employee("3789",2500,"Human Resources")# In ra properties của mark và steveprint("steve")print("ID :", steve.ID)print("Salary :", steve.salary)print("Department :", steve.department)print("mark")print("ID :", mark.ID)print("Salary :", mark.salary)print("Department :", mark.department)
Output
steve
ID : None
Salary : 0
Department : None
mark
ID : 3789
Salary : 2500
Department : Human Resources
Class variables và instance variables
Trong Python, properties được chia làm 2 loại:
- Class variables
- Instance variables
Class variables trong Python
Tất cả các objects của class đều được phép truy cập và thay đổi giá trị của class variable. Khi thay đổi giá trị của class variable thì giá trị của property này sẽ thay đổi trong tất cả các object của class.
Instance variables
Instance variables là của riêng với mỗi objects. Sự thay đổi ở instance variables của object nào thì chỉ ảnh hưởng đến object đó.
Khai báo class variable và instance variable
Class variables được khai báo ngoài initializer
và instance variables được khai báo trong scope của initializer
classPlayer:
team_name ='Manchester City'# class variabledef__init__(self, name):
self.name = name # instance variable
Chúng ta phải sử dụng class variable đúng cách vì như đã nói, chúng được chia sẻ cho tất cả các objects thuộc class và có thể thay đổi giá trị của class variable bằng cách sử dụng bất kỳ objects nào.
Dưới đây là ví dụ về sử dụng class variable ***sai
***.
classPlayer:
team_name ='Manchester City'# class variables
former_teams =[]# class variablesdef__init__(self, name):
self.name = name # instance variables# Tạo 2 objects thuộc class Player
p1 = Player("David Silva")
p2 = Player("Yaya Toure")# Dùng built-in method `append` để thêm giá trị vào variable `former_teams`
p1.former_teams.append('Celta Vigo')# dùng sai class variable
p2.former_teams.append('Barcelona')# dùng sai class variable# In properties của các objectsprint("Name:", p1.name)print("Team Name:", p1.team_name)print(p1.former_teams)print("Name:", p2.name)print("Team Name:", p2.team_name)print(p2.former_teams)
Output
Name: David Silva
Team Name: Manchester City
['Celta Vigo', 'Barcelona']
Name: Yaya Toure
Team Name: Manchester City
['Celta Vigo', 'Barcelona']
Ở ví dụ trên, trong khi instance variablename
là riêng biệt cho mỗi object thuộc Player
class. Thì class variableformer_teams
, có thể truy cập bởi tất cả các object thuộc class nên nó đã được cập nhật giá trị 2 lần.
Chúng ta đang lưu trữ tất cả các cầu thủ hiện đang chơi cho cùng một đội, nhưng mỗi cầu thủ trong đội phần lớn sẽ không cùng đội bóng cũ. Để tránh vấn đề này, triển khai chính xác cho ví dụ trên sẽ như sau:
classPlayer:
team_name ='Manchester City'# class variablesdef__init__(self, name):
self.name = name # instance variables
self.former_teams =[]# instance variables# Tạo 2 objects thuộc class Player
p1 = Player("David Silva")
p2 = Player("Yaya Toure")# Dùng built-in method `append` để thêm giá trị vào variable `former_teams`
p1.former_teams.append('Celta Vigo')# dùng sai class variable
p2.former_teams.append('Barcelona')# dùng sai class variable# In properties của các objectsprint("Name:", p1.name)print("Team Name:", p1.team_name)print(p1.former_teams)print("Name:", p2.name)print("Team Name:", p2.team_name)print(p2.former_teams)
Output
Name: David Silva
Team Name: Manchester City
['Celta Vigo']
Name: Yaya Toure
Team Name: Manchester City
['Barcelona']
Bây giờ, former_teams
đã là của riêng của mỗi object Player
, và sẽ được truy cập bởi object đó mà thôi.
Một ví dụ khác về sử dụng class variables
classPlayer:
team_name ='Manchester City'# class variables
team_members =[]# class variablesdef__init__(self, name):
self.name = name # instance variables
self.former_teams =[]# instance variables# sử dụng method append để mỗi lần tạo ra object Player # thì sẽ tự thêm `name` của player đó vào class variable team_members
self.team_members.append(self.name)
p1 = Player('David Silva')
p2 = Player('Yaya Toure')print("Team Name:", p1.team_name)print("Team Members:")print(p1.team_members)print("")print("Name:", p2.name)print("Team Members:")print(p2.team_members)
Output
Team Name: Manchester City
Team Members:
['David Silva', 'Yaya Toure']
Name: Yaya Toure
Team Members:
['David Silva', 'Yaya Toure']
Ở ví dụ trên, chúng ta đã khai báo class variable team_members
, một list được chia sẻ với tất cả các object thuộc Player
class. Mỗi object Player
được tạo ra, name
của object sẽ được thêm vào list team_members
, chúng ta có thể thấy p1
và p2
đều có thể truy cập vào team_members
.
Methods trong Python
Trong phần này, chúng ta sẽ xem về chuyện tương tác giữa properties
và các objects
. Đây là lúc methods
xuất hiện, methods
là một nhóm các statements (câu lệnh) thực hiện một số hoạt động (operations) và có thể trả về (return) hoặc không trả về một kết quả.
Có 3 loại method
trong Python:
- Instance methods
- Class methods
- Static methods
Chúng ta sẽ gọi method cho instance method vì nó thường được sử dụng nhất.
Instance Method
self
Ở những phần code ở trên, chúng ta thấy từ khóa self
hay được xuất hiện, đây là lúc giải thích về nó. Đầu tiên, chúng ta cùng xem cách self
argument hoạt động thông qua phần code bên dưới. Chúng ta có một class Employee
như sau:
classEmployee:def__init__(self, ID=None, department=None):
self.ID = ID
self.department = department
Khi chúng ta tạo object employee1
employee1 = Employee("Furaudorin","FAA")
Thì Python sẽ convert giúp chúng ta thành:
Employee.__init__(employee1,"Furaudorin","FAA")
Và bên trong initializer
sẽ thực thi như sau:
employee1.ID = "Furaudorin"
employee1.department = "FAA"
self
parameter là một tham chiếu đến object hiện tại của class, và được sử dụng để truy cập các variables và methods thuộc về class.
Nó không nhất thiết phải được đặt tên là self, nó không phải là Python keyword, bạn có thể đặt nó là gì tùy thích, nhưng nó phải là parameter đầu tiên của initializer và method. Nếu không sẽ xảy ra lỗi:
classEmployee:def__init__(ID=None, department=None):
ID = ID
department = department
steve = Employee(3789,"Human Resources")print(steve.ID)print(steve.department)
Output
Traceback (most recent call last):
File "/Users/abre/Desktop/object-oriented-trong-python/test.py", line 7, in <module>
steve = Employee(3789, "Human Resources")
TypeError: __init__() takes from 0 to 2 positional arguments but 3 were given
Class Method trong Python
Class methods làm việc với class variables và có thể truy cập bằng cách sử dụng Class name
thay vì object. Class method có thể được truy cập bằng cách sử dụng tên class mà không cần tạo class object.
Syntax của class method
Để khai báo một class method, chúng ta sử dụng @classmethod
decorator. cls
parameter được sử để refer tới class cũng như chúng ta sử dụng self
để refer tới object của class. Bạn cũng có thể sử dụng bất cứ tên nào để thay thế cls
, nhưng vì convention, chúng ta sẽ sử dụng cls
.
Tất cả class methods phải có ít nhất 1 parameter, là
cls
.
Ví dụ class method
classPlayer:
team_name ='Manchester City'# class variabledef__init__(self, name):
self.name = name # instance variable# define class method get_team_name sử dụng @classmethod decorator@classmethoddefget_team_name(cls):return cls.team_name
print(Player.get_team_name())
Output
Manchester City
Static methods trong Python
Static method là method được dùng chỉ giới hạn ở phạm vi class. Chúng không tương tác với class variable hay instance variable. Chúng được sử dụng như các utility functions bên trong class.
Static methods có thể được truy cập bằng cách sử dụng class name hoặc object name
Syntax của static method
Để khai báo static method, chúng ta sử dụng @staticmethod
decorator. Vì nó không được sử dụng để tham chiếu đến object hay class nên chúng ta không sử dụng self
hay cls
argument. Static methods không biết bất cứ thứ gì về state của class.
classPlayer:
team_name ='Manchester City'# class variabledef__init__(self, name):
self.name = name # instance variable@staticmethoddefdemo():print("I am a static method.")
p1 = Player('lol')
p1.demo()
Player.demo()
Output
I am a static method.
I am a static method.
Ví dụ static method
Giả sử chúng ta có 1 class BodyInfo
chứa thông tin về cân nặng và chiều cao của một người. Chúng ta có thể tạo một static method để tính BMI cho bất kỳ cân nặng
và chiều cao
nào được truyền vào, ví dụ:
classBodyInfo:@staticmethoddefbmi(weight, height):return weight /(height**2)
weight =75
height =1.8print(BodyInfo.bmi(weight, height))
Output
23.148148148148145
Vì sự đặc biệt của static method, nó được dùng rất hạn chế, khi cần sử dụng utility function mà không cần tham chiếu tới object hay class thì chúng ta có thể tạo ra chúng, việc gọi chúng thông qua class name
hay class object
giúp chúng ta hiểu rõ về bối cảnh sử dụng cũng như chức năng của chúng.
Access Modifiers trong Python
Chúng ta cùng tìm hiểu về private
, public
attributes trong Python
Public attributes trong Python
Trong Python, tất cả attributes mặc định là public. Nếu chúng ta muốn chỉ định một method hay variables nào đó không nên được coi là public
, chúng ta phải khai báo nó là private
.
Ví dụ về public attributes
classEmployee:def__init__(self, ID, salary):# các properties này đều là public
self.ID = ID
self.salary = salary
# method này là publicdefdisplay_id(self):print("ID:", self.ID)
steve = Employee(3789,2500)
steve.display_id()print(steve.salary)
Ouput
ID: 3789
2500
Ở phần code trên, properties ID
, salary
và method display_id()
là public nên chúng ta có thể truy cập ở trong cũng như ở ngoài class.
Private attributes trong Python
Mục đích sử dụng private attributes là để ẩn nó khỏi người dùng và các class khác.
Nhưng trong Python, không có sự tồn tại của “private”. Tuy nhiên, một quy ước đang được hầu hết các developer sử dụng là chúng ta có thể tạo private attributes bằng cách sử dụng tiền tố (prefix) __
.
Phân biệt giữa single leading underscore _
và double leading underscores __
trong Python
Hãy xem đoạn code dưới đây:
classMyClass():def__init__(self):
self.__superprivate ="Hello"
self._semiprivate =", world!"
mc = MyClass()
print(mc._semiprivate)
, world!
print(mc.__superprivate)
Traceback (most recent call last):
File "/Users/abre/Desktop/object-oriented-trong-python/test.py", line 10, in <module>
print(mc.__superprivate)
AttributeError: 'MyClass' object has no attribute '__superprivate'
Single leading underscore _
đơn giản chỉ là quy ước, một cách để lập trình viên chỉ ra rằng attributes này là private, không nên truy cập ngoài class này.
Double leading underscores __
thì có ý nghĩa thật sự, trình thông dịch Python sẽ thay thế __
bằng tên class mà gọi nó để đảm bảo attributes này sẽ không trùng với attributes khác có cùng tên trong class khác.
Tại sao ở trên mình nói trong Python, không có private, đoạn dưới sẽ giải thích.
Private properties trong Python
Đầu tiên chúng ta xem khi truy cập vào một private property bằng cách thông thường sẽ như thế nào nhé:
classEmployee:def__init__(self, ID, salary):
self.ID = ID
self.__salary = salary # salary là một private property
steve = Employee(3789,2500)print("ID:", steve.ID)print("Salary:", steve.__salary)# dòng này sẽ gây lỗi
Output
ID: 3789
Traceback (most recent call last):
File "main.py", line 9, in <module>
print("Salary:", steve.__salary) # this will cause an error
AttributeError: 'Employee' object has no attribute '__salary'
ID
là public property nhưng __salary
là private property nên không thể truy cập bên ngoài class.
Private methods trong Python
Truy cập vào private properties một cách thông thường sẽ gây lỗi, vậy với private methods thì sao?
classEmployee:def__init__(self, ID, salary):
self.ID = ID
self.__salary = salary # salary là một private propertydefdisplay_salary(self):# display_salary là một public methodprint("Salary:", self.__salary)def__display_id(self):# display_id là một private methodprint("ID:", self.ID)
steve = Employee(3789,2500)
steve.display_salary()
steve.__display_id()# dòng này sẽ gây lỗi
Output
Salary: 2500
Traceback (most recent call last):
File "main.py", line 15, in <module>
steve.__display_id() # this will generate an error
AttributeError: 'Employee' object has no attribute '__display_id'
Tương tự như trên, __display_id
là private method, không thể truy cập được từ bên ngoài class.
Vậy làm sao để truy cập private attributes trong main code
Vậy tại sao ở trên nói trong Python không có sự tồn tại của “private”?
Nếu cảm thấy thật sự cần thiết sử dụng private
property hoặc method, chúng ta… vẫn có thể. Ta sử dụng prefix _<ClassName>
để truy cập, ví dụ như:
classEmployee:def__init__(self, ID, salary):
self.ID = ID
self.__salary = salary # salary là một private property
steve = Employee(3789,2500)print(steve._Employee__salary)# truy cập private property
Output
2500
Không có lỗi gì, chúng ta đã truy cập được salary
của steve
và in giá trị của nó ra, tương tự ta có thể dùng cách trên để truy cập private methods.
Information Hiding trong Python
Information hiding là một khái niệm thiết yếu trong OOP. Nói một cách đơn giản thì để đảm bảo rằng dữ liệu được truy cập và sử dụng đúng mục đích và an toàn (bảo mật) thì chúng ta sẽ giấu đi các hoạt động bên trong class và chỉ cung cấp một giao diện (interface) mà qua đó thế giới bên ngoài có thể tương tác với class mà không cần biết điều gì đang xảy ra bên trong.
Data hiding có thể chia làm 2 phần chính:
- Encapsulation (Tính đóng gói)
- Abstraction (Tính trừu tượng)
Encapsulation (Tính đóng gói) trong Python
Encapsulation (tính đóng gói) là một kỹ thuật lập trình cơ bản được sử dụng để ẩn dữ liệu trong OOP. Có thể hiểu, tính đóng gói là việc giấu tất cả dữ liệu, methods vào bên trong và chúng ta sẽ thao tác với dữ liệu, methods đó bên trong class. Những thứ bên trong sẽ không thể bị sửa đổi hay truy cập bởi codes bên ngoài từ các nơi khác của chương trình.
Getter and Setters trong Python
Như vài ngôn ngữ lập trình khác, để có thể truy cập vào các private properties từ bên ngoài class, chúng ta dùng
getter
vàsetter
getter
method cho phép đọc giá trị của propertysetter
method cho phép chỉnh sửa giá trị của property
Trong Python cũng có thể sử dụng getter, và setter.
classUser:def__init__(self, username=None):
self.__username = username
defset_username(self, x):
self.__username = x
defget_username(self):return(self.__username)
steve = User('steve1')print('Before setting:', steve.get_username())
steve.set_username('steve2')print('After setting:', steve.get_username())
Output
Before setting: steve1
After setting: steve2
Ở đoạn code trên, chúng ta đã tạo một class User
, 1 private property username
và getter và setter method cho property đó. Sau đó chúng ta tạo object steve
và gán giá trị của username
là “steve1”, sau đó in ra giá trị đó. Tiếp theo đó chúng ta sử dụng set_username
method để thay đổi giá trị của username
thành “steve2” sau đó chúng ta thấy giá trị của username
đã được sửa thành “steve2” khi in ra.
@property
decorator
Không, quên những gì bạn vừa đọc đi, đã có nhiều thảo luận về việc dùng getter, setter
trong Python và đa số người sử dụng Python đều không sử dụng nó. Tóm tắt là nó rối, nó tốn tài nguyên, và Python được sinh ra để trở thành ngôn ngữ lập trình dễ sử dụng cho con người.
Thay vào đó họ sử dụng @property
decorator nếu như muốn “làm gì đó private
” với attributes trong class. Trong bài học của khóa học CS50P họ cũng sử dụng decorator này.
@property
decorator:
classStudent:def__init__(self, name, house):ifnot name:raise ValueError("Invalid name")
self.__name = name
self.__house = house
def__str__(self):returnf"{self.__name} from {self.__house}"# Getter cho house# Hãy chắc chắn rằng tên của getter và setter trùng với tên của property@propertydefhouse(self):return self.__house
# Setter cho house# Hãy chắc chắn rằng tên của getter và setter trùng với tên của property@house.setterdefhouse(self, house):if house notin["Gryffindor","Hufflepuff","Ravenclaw","Slytherin"]:raise ValueError("Invalid house")
self.__house = house
defmain():# Ở đây mình sẽ nhập name là Abre, house là: Hufflepuff
student = get_student()print(f"Student info: {student}")print(student)# Truy cập house của studentprint(f"House of student: {student.house}")# Thay đổi giá trị của house của student thành Ravenclaw
student.house ="Ravenclaw"# In ra thông tin student sau khi đổiprint(f"Student info after change house: {student}")defget_student():
name =input("Name: ")
house =input("House: ")return Student(name, house)if __name__ =="__main__":
main()
Output
Name: Abre
House: Hufflepuff
Student info: Abre from Hufflepuff
Phu from Hufflepuff
House of student: Hufflepuff
Student info after change house: Abre from Ravenclaw
Như ở trên, ta có thể thấy @property
được sử dụng để lấy giá trị của thuộc tính private mà không cần sử dụng bất kỳ phương thức getter nào. Chúng ta thêm một dòng @property
trước method mà chúng ta trả về private variable. Còn để set giá trị cho private variable, chúng ta thêm dòng @method_name.setter
trước method đó rồi sử dụng nó như một setter.
Bạn có thể tìm hiểu thêm tại đây và oop lecture from CS50P.
Lợi ích của Encapsulation trong Python
- Dễ bảo trì: Code được đóng gói trong những phần riêng biệt, như là class, method, interface, etc. Vì vậy khi thay đổi hay cập nhật thì chúng không ảnh hưởng đến phần còn lại.
- Testing khỏe hơn: Chúng ta dễ test hơn vì sẽ chỉ phải tập trung ở 1 nơi chứ không phải lo nó còn ảnh hưởng đến nơi nào khác không. Tiết kiệm thời gian.
- Che giấu dữ liệu: Khi sử dụng người dùng chỉ sẽ nhận được kết quả mà không biết hay truy cập được chi tiết bên trong của object.
Inheritance (tính kế thừa) trong Python
Inheritance giúp tăng khả năng tái sử dụng code bằng cách cung cấp cho chúng ta cách để tạo một class mới từ một class hiện có. Class mới (class con/child class) sẽ kế thừa tất cả các non-private attributes (chỉ cần không phải private thì đều được kế thừa) từ class hiện có (class cha/parent class).
Vậy, khi nào ta sử dụng inheritance?
Bất cứ khi nào mối quan hệ giữa 2 objects là IS A thì chúng ta có thể. Ví dụ như Car
IS AVehicle
, Bicycle
IS A Vehicle, etc. Vehicle
sẽ có các properties như make
, color
, model
.
Car
hay Bicycle
hay Train
đều có các properties trên, và chúng đều IS AVehicle
.
Giả sử phần mềm chúng ta được tạo ra để quản lý Vehicle
. Chúng ta sẽ tạo ra class Vehicle
và nó sẽ là parent class (super class), Car
hay Bicycle
hay Train
sẽ là child class (sub class) và chúng sẽ thừa kế từ Vehicle
các non-private attributes.
Triển khai inheritance trong Python
classVehicle:def__init__(self, make, color, model):
self.make = make
self.color = color
self.model = model
defprint_details(self):print("Manufacturer:", self.make)print("Color:", self.color)print("Model:", self.model)classCar(Vehicle):def__init__(self, make, color, model, doors):# gọi constructor (initializer) từ parent class
Vehicle.__init__(self, make, color, model)
self.doors = doors
defprint_car_details(self):
self.print_details()print("Doors:", self.doors)
obj1 = Car("Suzuki","Grey","2015",4)
obj1.print_car_details()
Output
Manufacturer: Suzuki
Color: Grey
Model: 2015
Doors: 4
Ở đoạn code trên, chúng ta tạo ra 2 class Vehicle
– parent class và Car(Vehicle)
– child class thừa kế từ Vehicle
.
Để ý hàm __init__
của Car
, chúng ta có gọi lại hàm __init__
của Vehicle
, hàm print_car_details
cũng gọi hàm print_details
của Vehicle
.
Như đã nói, child class sẽ thừa kế lại các non-private methods và variables từ parent class. Sau đó chúng ta tạo ra object Car obj1
và truyền vào giá trị cho nó. Sau đó chúng ta gọi hàm print_car_details
và hàm này đã in ra toàn bộ thông tin của obj1
mà trong đó có sử dụng hàm print_details
của Vehicle
.
super()
function
Việc sử dụng super()
phát huy tác dụng khi chúng ta triển khai inheritance. Nó được sử dụng trong một child class để tham chiếu đến parent class mà không cần đặt tên rõ ràng cho các methods, variables.
Nó làm cho codes dễ quản lý hơn và không cần dùng tên của parent class để truy cập các attributes của parent class.
Sử dụng super() function truy cập vào property của parent class
classVehicle:
fuel_cap =90classCar(Vehicle):
fuel_cap =50defdisplay(self):# truy cập fuel_cap từ class Vehicle dùng super()print("Fuel cap from the Vehicle Class:",super().fuel_cap)# truy cập fuel_cap từ class Car dùng selfprint("Fuel cap from the Car Class:", self.fuel_cap)
obj1 = Car()# tạo một car object
obj1.display()# gọi method display() của class Car
Output
Fuel cap from the Vehicle Class: 90
Fuel cap from the Car Class: 50
Chúng ta đã truy cập vào giá trị của property fuel_cap
của parent class bằng cách sử dụng super().
parent_class_property.
Gọi method của parent class
classVehicle:defdisplay(self):print("I am from the Vehicle Class")classCar(Vehicle):defdisplay(self):super().display()print("I am from the Car Class")
obj1 = Car()
obj1.display()# gọi method display() của class Car
Output
I am from the Vehicle Class
I am from the Car Class
Ta có thể thấy, super()
đã tham chiếu đến parent class và truy cập vào display()
method ở parent class.
Sử dụng super()
với initializer
classParentClass():def__init__(self, a, b):
self.a = a
self.b = b
classChildClass(ParentClass):def__init__(self, a, b, c):super().__init__(a, b)
self.c = c
obj = ChildClass(1,2,3)print(obj.a)print(obj.b)print(obj.c)
Ở phần code đầu tiên giới thiệu về inheritance, chúng ta gọi __init__
bằng tên của parent class
ParentClass().__init__(a, b)
Ở đây lại gọi __init__
bằng super()
, đó là 2 cách chúng ta có thể.
Như đã nói, child class thừa kế tất cả non-private attributes của parent class. Chúng ta có thể dùng self
để truy cập chúng. Và chúng ta đã gọi self.print_details
ở phần triển khai inheritance trong Python
thay vì super().print_details
là tại vì ở ví dụ đó, chúng ta có 2 methods khác tên (print_car_details
và print_details
).
Nếu chúng ta dùng self
mà tên 2 methods ở parent class và child class là giống nhau thì sẽ bị lỗi. Còn ở ví dụ gọi method của parent class
chúng ta có thể đặt tên method của parent class và child class giống nhau vì chúng ta dùng super()
để gọi method từ parent class, tiện thể nhắc luôn parent class còn được gọi là super class.
Lợi ích của Inheritance
Reusability (Tái sử dụng)
Ví dụ như sau, ta có một ứng dụng dành cho ngân hàng, ta thiết kế class BankAccount
và cho class SavingsAccount
và class CheckingAccount
thừa kế BankAccount
, và cả 2 loại account này đều có behaviors như deposit()
và withdraw()
nên ta đưa 2 methods này vào class BankAccount
luôn.
Trong ví dụ trên chúng ta không cần viết lại code cho 2 methods deposit()
và withdraw()
bên trong 2 child classes SavingsAccount
và CheckingAccount
.
Code modification (Dễ bảo trì/ sửa đổi)
Giả sử bạn đặt cùng một đoạn code vào các class khác nhau, khi chúng ta muốn sửa đổi đoạn code đó, và nó ở trong rất nhiều class, sẽ có khả năng chúng ta quên sửa ở 1 class nào đó và sẽ dẫn đến lỗi.
Chúng ta có thể tránh việc này bằng cách sử dụng inheritance, thứ sẽ đảm bảo việc sửa đổi ở parent class sẽ được diễn ra ở child class.
Extensibility (Khả năng mở rộng)
Ví dụ sau này chúng ta muốn tạo 1 class mới cho ứng dụng ngân hàng này, gọi nó là MoneyMarketAccount
thì chúng ta có thể thừa kế lại từ BankAccount
class thay vì implement một class từ đầu vì chúng ta có thể sử dụng các attributes phổ biến của BankAccount
cho MoneyMarketAccount
.
Data hiding (Che giấu dữ liệu)
Parent class giữ một số dữ liệu private vì vậy child class không thể thay đổi nó. Như hình trên, người dùng sử dụng các methods trên mà không cần biết bên trong nó là gì, và cũng không thể sửa đổi các private variables của object.
Polymorphism (Tính đa hình) trong Python
Dẫn nhập về tính đa hình trong Python
Dẫn nhập 1
Trong lập trình, polymorphism để cập đến việc cùng 1 object thể hiện các hành vi khác nhau.
Ví dụ, class Shape
, có thể là rectangle, circle, polygon hay diamond. Tất cả đều là shape nhưng properties của chúng là khác nhau. Đa hình là vậy.
Giả sử ứng dụng của chúng ta cần method để tính diện tích của từng shape cụ thể. Công thức tính diện tích của mỗi hình là khác nhau, nên chúng ta không thể implement 1 method duy nhất mà sử dụng cho 4 shapes trên được. Chúng ta có thể tạo ra các methods riêng biệt trong từng class (ví dụ get_square_area()
, get_diamond_area()
, etc.) Nhưng làm vậy sẽ khó để nhớ methods name vì có rất nhiều shapes.
Sẽ tốt hơn nếu methods tính diện tích của các shapes đều là get_area()
. Chúng ta sẽ dễ dàng sử dụng ứng dụng hơn. Có thể làm được điều này bằng cách sử dụng polymorphism trong OOP. Parent class sẽ define một methods mà không implement gì trong đó. Mỗi child class sẽ thừa kế methods đó và tự implement phù hợp cho riêng mình.
Class Shape
sẽ có method get_area()
, các child class như Rectangle, Circle sẽ thừa kế lại method này. Khi Rectangle gọi method get_area()
thì sẽ phản hồi bằng cách gọi methods get_area()
của Rectangle, tương tự với class Circle
, etc.
Dẫn nhập 2
Sư tử, chó, mèo, gà, gấu, tất cả đều là động vật (animal), nhưng properties của chúng thì không giống nhau hoàn toàn. Lấy ví dụ theo trên, ta có class Animal
và các class Lion
, Dog
, Chicken
sẽ kế thừa class Animal
.
Class Animal
sẽ có các methods, ví dụ chúng ta có method make_noise
, các class kế thừa từ Animal
cũng sẽ có method này, việc triển khai method này trên các con vật khác nhau sẽ khác nhau. Ví dụ con chó, con gà, và sư tử, chúng đều là Animal nhưng tiếng động chúng phát ra là khác nhau.
Triển khai tính đa hình (polymorphism) sử dụng tính kế thừa (inheritance) trong Python
classShape:def__init__(self):passdefget_area(self):pass# class Rectangle thừa kế từ class ShapeclassRectangle(Shape):def__init__(self, width=0, height=0):
self.width = width
self.height = height
# method get_area() của Rectangledefget_area(self):return(self.width * self.height)# class Circle thừa kế từ class ShapeclassCircle(Shape):def__init__(self, radius=0):
self.radius = radius
# method get_area() của Circledefget_area(self):return(self.radius * self.radius *3.142)
shapes =[Rectangle(6,10), Circle(7)]print("Area of rectangle is:",str(shapes[0].get_area()))print("Area of circle is:",str(shapes[1].get_area()))
Output
Area of rectangle is: 60
Area of circle is: 153.958
Chúng ta tạo ra class Shape
với một public method get_area()
, class Rectangle
và class Circle
thừa kế lại từ class Shape
. Chúng thừa kế lại method get_area()
của class Shape
nhưng get_area()
của mỗi class sẽ trả về giá trị diện tích của mỗi class, không giống nhau.
Đây gọi là polymorphism: các methods giống nhau nhưng sẽ triển khai cụ thể cho từng class.
Method overriding (ghi đè phương thức) trong Python
Trong OOP, nếu một child class implement lại một method đã được define ở parent class, thì nó được gọi là method overriding.
Như ví dụ ở trên, là method overriding.
Giả sử ta có một parent class Animal
và child class Lion
. Cả hai đều có method print_animal
, method này có cùng tên, cùng parameters, và cùng return type. Nhưng nội dung được triển khai (implementation) bên trong thì khác nhau.
Ví dụ về method overriding
classAnimal:def__init__(self):passdefprint_animal(self):print("I am from the Animal class")defprint_animal_two(self):print("I am from the Animal class")classLion(Animal):def__init__(self):super()defprint_animal(self):# method overridingprint("I am from the Lion class")
lion = Lion()
lion.print_animal()
lion.print_animal_two()
Khi chạy chương trình thì chúng ta sẽ nhận được:
I am from the Lion class
I am from the Animal class
Lợi ích của Method overriding
- Child class có thể tự triển khai method của riêng chúng với các method đã thừa kế từ parent class mà không cần sửa đổi method ở parent class.
- Child class có thể sử dụng cách triển khai method của parent class hoặc định nghĩa lại cách triển khai của riêng nó.
Method overloading (nạp chồng phương thức) trong Python
Khái niệm method overloading là hiện tượng nhiều method có cùng tên, tuy nhiên số lượng parameters hoặc type của parameters trong các methods này là khác nhau. Overloading đề cập đến việc làm cho một method thực hiện các hoạt động (operations) khác nhau dựa trên các arguments của nó. Tùy vào số lượng và kiểu của parameters truyền vào thì class sẽ biết mà xử lý để trả về các kết quả tương ứng.
Các method có cùng tên, cùng danh sách parameters, nhưng kiểu trả về khác nhau không được xem là overloading method.
Không như các ngôn ngữ lập trình khác, ở Python thì methods không thể explicitly overloaded (nạp chồng rõ ràng), chỉ có thể implicitly overloaded (nạp chồng ngầm).
Như ở Java
thì có thể tạo nhiều constructor trong class với số lượng parameters khác nhau để khi tạo object tùy số lượng parameters truyền vào thì Class sẽ biết mà sử dụng constructor nào.
Có 2 cách để tạo ra hiện tượng overload:
- Thay đổi số lượng tham số
- Thay đổi kiểu dữ liệu của tham số
classSum:defaddition(self, a, b, c =0):return a + b + c
Để hiểu rõ cái này mình sẽ thêm 1 ví dụ về Java ở đây:
classSum{// addition 1publicintaddition(int a,int b){return a + b;}// addition 2publicintaddition(int a,int b,int c){return a + b + c;}}
Ta thấy ví dụ ở Java, ta có 2 methods cùng tên addition
, khi ta gọi method này với số lượng parameter là 2 thì sẽ nhận được kết quả từ method addition 1
, còn số lượng parameter truyền vào là 3 thì sẽ nhận được kết quả từ method addition 2
.
Thêm một ví dụ rõ hơn trong Python
classEmployee:def__init__(self, ID=None, salary=None, department=None):
self.ID = ID
self.salary = salary
self.department = department
# method overloadingdefdemo(self, a, b, c, d=5, e=None):print("a =", a)print("b =", b)print("c =", c)print("d =", d)print("e =", e)defdemo(self, a, b, c):print("a = ", a)print("b = ", b)print("c = ", c)# Tạo object steve của class Employee
steve = Employee()# In ra properties của object steveprint("Demo 1")
steve.demo(1,2,3)print("n")print("Demo 2")
steve.demo(1,2,3,4,5)
Output
Demo 1
a = 1
b = 2
c = 3
Demo 2
Traceback (most recent call last):
File "main.py", line 30, in <module>
steve.demo(1, 2, 3, 4, 5)
TypeError: demo() takes 4 positional arguments but 6 were given
Nhận được TypeError
, điều này là khi có nhiều method trùng tên, Python sẽ coi method được khai báo cuối cùng là method sẽ được sử dụng khi chúng ta gọi. Ở đây là method demo
với 4 parameters nên khi ta truyền vào 5 parameter, tính cả instance là 6 nên nó đã báo lỗi.
Vậy phải làm sao? Chúng ta có vài cách để thực hiện method overloading trong Python, ở đây chúng ta sẽ dùng multiple dispatch
Multipledispatch trong Python
from multipledispatch import dispatch
classEmployee:def__init__(self, ID=None, salary=None, department=None):
self.ID = ID
self.salary = salary
self.department = department
@dispatch(int,int,int,int,int)defdemo(self, a, b, c, d=5, e=None):print("a =", a)print("b =", b)print("c =", c)print("d =", d)print("e =", e)@dispatch(int,int,int)defdemo(self, a, b, c):print("a = ", a)print("b = ", b)print("c = ", c)# Tạo object steve của class Employee
steve = Employee()# In ra properties của object steveprint("Demo 1")
steve.demo(1,2,3)print("n")print("Demo 2")
steve.demo(1,2,3,4,5)
Output
Demo 1
a = 1
b = 2
c = 3
Demo 2
a = 1
b = 2
c = 3
d = 4
e = 5
Dựa vào multipledispatch
chúng ta đã overloading method một cách rõ ràng, như cách nó xảy ra ở Java.
Operator overloading (nạp chồng toán tử) trong Python
Toán tử (operator) trong Python có thể được overloaded để hoạt động theo một cách nhất định dựa trên người dùng.
Java và JavaScript không hỗ trợ operator overloading.
Bất cứ khi nào một toán tử được sử dụng trong Python, method tương ứng sẽ được gọi để thực function đã define trước đó của nó.
Ví dụ khi sử dụng toán tử +
, nó sẽ gọi hàm đặc biệt __add__
.
Trong Python, toán tử +
nếu được sử dụng giữa hai int
data types chúng sẽ cộng hai số lại với nhau. Còn khi toán tử này được sử dụng với hai string
data types chúng sẽ merge hai chuỗi đó lại với nhau.
Overloading operators cho một class do người dùng định nghĩa
classCharacter:def__init__(self, name, strength=0, agility=0, intellect=0):ifnot name:raise ValueError("Invalid name")
self.name = name
self.strength = strength
self.agility = agility
self.intellect = intellect
def__str__(self):returnf"{self.name} has {self.agility} strength, {self.intellect} agility and {self.intellect} intellect"def__add__(self, other):
name = self.name + other.name
strength = self.strength + other.strength
agility = self.agility + other.agility
intellect = self.intellect + other.intellect
return Character(name, strength, agility, intellect)
hammer = Character("Hammer",10,50,25)print(hammer)
wolf = Character("Wolf",50,10,10)print(wolf)
polymerization = hammer + wolf
print(polymerization)
Output
Hammer has 50 strength, 25 agility and 25 intellect
Wolf has 10 strength, 10 agility and 10 intellect
HammerWolf has 60 strength, 35 agility and 35 intellect
Ở trên, mình đã overloaded lại operator +
, và sử dụng trong class Character với 2 character hammer
và wolf
, và đã dung hợp (polymerization) chúng nó lại để thành 1 character khác.
Bạn có thể đặt tên argument thứ hai là bất cứ gì, nhưng theo quy ước thì chúng ta sẽ sử dụng
other
để nói tới other object.
Triển khai tính đa hình (polymorphism) sử dụng duck typing
trong Python
Duck Typing là 1 đặc trưng của các ngôn ngữ động như Python, nó được coi là một trong những concepts hữu ích nhất trong OOP trong Python, nó được dùng khi type hoặc class của object không quan trọng bằng method mà nó triển khai.
Tên gọi bắt nguồn từ duck test với tư tưởng: “Nếu ta thấy 1 con vật đi 2 chân và biết bơi như con vịt thì đấy hẳn là con vịt”. Duck typing kế thừa và phát triển lại từ khái niệm dynamic typing trong Python, dynamic typing có nghĩa là chúng ta có thể thay đổi type của object sau khi chúng được tạo ra.
Doạn code sau biểu hiện dynamic typing trong Python
x =5# type of x là integerprint(type(x))
x ="Educative"# type of x giờ là stringprint(type(x))
Output
<class 'int'>
<class 'str'>
Giờ ta triển khai duck typing
classDog:defSpeak(self):print("Woof woof")classCat:defSpeak(self):print("Meow meow")classAnimalSound:defSound(self, animal):
animal.Speak()
sound = AnimalSound()
dog = Dog()
cat = Cat()
sound.Sound(dog)
sound.Sound(cat)
Output
Woof woof
Meow meow
Type của Animal
sẽ được xác định khi method được gọi, nên không quan trọng là Dog
hay là Cat
miễn là method Speak()
phải được định nghĩa trong 2 class đó.
Đây là cách chúng ta triển khai polymorphism mà không cần inheritance. Vì animal
dù là Dog
hay Cat
cũng không quan trọng, miễn là chúng có methods liên quan Speak()
.
Abstract Base Class trong Python
Đầu tiên mình muốn nói về interface
trong ngôn ngữ lập trình Java để chúng ta dễ hình dung về abstract base class.
Một interface
sẽ khai báo ra các methods của nó, các methods này không có nội dung. Class mà implements interface này phải có tất cả các methods được khai báo trong interface và phải định nghĩa nội dung của methods.
Như vậy, interface
và class
là hai khái niệm khác nhau. Interface định nghĩa ra 1 tiêu chuẩn nào đó mà các class implement nó phải tuân thủ.
Tính trừu tượng trong Python được triển khai nhờ vào Abstract Base Class. Abstract Base Class (ABC) định nghĩa một tập hợp các methods và các properties mà 1 class phải implement để có thể được coi là 1 thể hiện duck-type của class đó.
Tại sao phải dùng ABC?
Xem ví dụ sau đây:
classShape:defarea(self):passdefperimeter(self):passclassSquare(Shape):def__init__(self, length):
self.length = length
defarea(self):return(self.length * self.length)defperimeter(self):return(4* self.length)
shape = Shape()
square = Square(4)
Ta có thể thấy object shape
có thể được tạo ra mặc dù object này không có gì cả. Để ngăn cản user tạo object từ class Shape
, chúng ta dùng ABC
.
Syntax của Abstract Base Class trong Python
Để định nghĩa một ABC, chúng ta sử dụng module abc
. Abstract base class được kế thừa từ built-in class ABC
. Chúng ta phải sử dụng decorator @abstractmethod
phía trên method mà chúng ta muốn khai báo là một phương thức trừu tượng (abstract method).
Lưu ý rằng các abstractmethod
trong parent class thì ta không được triển khai nó, mà chỉ được định nghĩa nó để child class kế thừa lại và triển khai.
from abc import ABC, abstractmethod
classParentClass(ABC):@abstractmethoddefmethod(self)
Ví dụ 1 về abstract base class
from abc import ABC, abstractmethod
classShape(ABC):# Shape là child class của class ABC@abstractmethoddefarea(self):pass@abstractmethoddefperimeter(self):passclassSquare(Shape):def__init__(self, length):
self.length = length
shape = Shape()
Output
Traceback (most recent call last):
File "main.py", line 19, in <module>
shape = Shape()
TypeError: Can't instantiate abstract class Shape with abstract methods area, perimeter
Đoạn code trên không compile được vì khi ta tạo ra object shape
, ở class Shape
, 2 abstract methods area()
và perimeter()
chưa được triển khai gì cả.
Ví dụ 2 về abstract base class
from abc import ABC, abstractmethod
classShape(ABC):# Shape là child class của class ABC@abstractmethoddefarea(self):pass@abstractmethoddefperimeter(self):passclassSquare(Shape):def__init__(self, length):
self.length = length
square = Square(4)
Output
Traceback (most recent call last):
File "main.py", line 19, in <module>
square = Square(4)
TypeError: Can't instantiate abstract class Square with abstract methods area, perimeter
Như có thể thấy, code cũng không compile được khi ta tạo object từ class Square
vì chúng ta chưa định nghĩa cho hai abstract methods area()
và perimeter()
trong class Square
. Giờ ta sẽ làm điều đó.
Ví dụ 3 về abstract base class
from abc import ABC, abstractmethod
classShape(ABC):# Shape là child class của class ABC@abstractmethoddefarea(self):pass@abstractmethoddefperimeter(self):passclassSquare(Shape):def__init__(self, length):
self.length = length
defarea(self):return(self.length * self.length)defperimeter(self):return(4* self.length)
shape = Shape()
Output
Traceback (most recent call last):
File "main.py", line 25, in <module>
shape = Shape()
TypeError: Can't instantiate abstract class Shape with abstract methods area, perimeter
Giờ ta đã định nghĩa cho hai abstract methods area()
và perimeter()
trong class Square
. Nhưng ở class Shape
thì 2 abstract methods đó vẫn chưa có gì, nên khi ta tạo object từ class Shape
thì chương trình vẫn sẽ bị lỗi (như ví dụ 1), không compile được.
Ví dụ 4 về abstract base class
from abc import ABC, abstractmethod
classShape(ABC):# Shape là child class của class ABC@abstractmethoddefarea(self):pass@abstractmethoddefperimeter(self):passclassSquare(Shape):def__init__(self, length):
self.length = length
defarea(self):return(self.length * self.length)defperimeter(self):return(4* self.length)
square = Square(4)print("Square's area: {}".format(square.area()))
Output
Square's area: 16
Giờ đã compile được, vì chúng ta đã implement ở 2 abstract methods của class Square
. Như bạn có thể thấy, vẫn đoạn code ở ví dụ 3 về abstract base class
nhưng ta không thể tạo ra object của class Shape
nhưng Square
thì có thể.
Methods với
@abstractmethod
decorators ở parent class phải được triển khai ở child class
Như lúc đầu, ta có thể tạo ra object từ class Shape mà không để làm gì cả. Việc dùng ABC, chúng ta có thể quản lý objects nào được tạo và không được tạo.
Vậy là sau bài viết này, chúng ta đã cùng tìm hiểu về OOP trong Python, hiểu các khái niệm về class, object, method cũng như bốn tính chất: encapsulation, inheritance, polymorphism, và abstraction.
Cảm ơn bạn đã xem đến đây, đây là bài viết đầu tay của mình, nếu bạn thấy có gì sai xót mong bạn góp ý để mình chỉnh sửa bài viết để người đọc sau có một bài viết trọn vẹn hơn để tham khảo nhé!
Nguồn: viblo.asia