Inheritance (kế thừa) trong Python
Kế thừa (Inheritance) trong lập trình Python hướng đối tượng cho phép chúng ta xác định một lớp kế thừa tất cả các phương thức và thuộc tính từ một lớp khác.
Kế thừa là một trong những nguyên lý cơ bản của lập trình hướng đối tượng. Kế thừa là cơ chế tái sử dụng code mà tất cả các ngôn ngữ lập trình hướng đối tượng đều thực thi.
Python cũng hỗ trợ kế thừa. Cơ chế thực hiện kế thừa trong Python có điểm hơi khác so với một số ngôn ngữ khác.
Ví dụ về Inheritance (kế thừa) trong Python
Kế thừa(inheritance) là một công cụ rất mạnh trong lập trình hướng đối tượng cho phép tạo ra cácclassmới từ một class sẵn có. Qua đó có thể tái sử dụng code của class đã có. Kế thừa giúp giảm thiểu việc lặp code giữa các class.
Hãy cùng thực hiện ví dụ sau đây.
Ví dụ
class Person:
count = 0
def __init__(self, fname='', lname='', age=18):
self.fname = fname
self.lname = lname
self.age = age
Person.count += 1
def print(self):
print(f'{self.fname} {self.lname} ({self.age} years old)')
@property
def full_name(self):
return f'{self.fname} {self.lname}'
@classmethod
def print_count(cls):
print(f'{cls.count} objects created')
@staticmethod
def birth_year(age: int) -> int:
from datetime import datetime as dt
year = dt.now().year
return year - age
class Student(Person):
def __init__(self, fname='', lname='', age=18, group='', specialization=''):
super().__init__(fname, lname, age)
self.group = group
self.specialization = specialization
def print(self):
super().print()
print(f'Group {self.group}/{self.specialization}')
@property
def academic_info(self):
return f'Group {self.group}, Specialization of {self.specialization}'
if __name__ == '__main__':
trump = Student('Donald', 'Trump', 22, '051311', 'Computer science')
trump.print()
print(trump.full_name)
print(Student.count)
Student.print_count()
print(Student.birth_year(37))
print(trump.academic_info)
Kết quả chạy chương trình như sau:
Ví dụ
Donald Trump (22 years old)
Group 051311/Computer science
Donald Trump
1
1 objects created
1983
Group 051311, Specialization of Computer science
Trong ví dụ trên chúng ta đã xây dựng hai class: Person và Student.
Trong class Person chúng ta tạo các instance attribute (fname
, lname
, age
) trong hàm tạo, một class attribute (count
), một class method (print_count
), một static method (birth_year
) và một property (full_name
).
Chúng ta xây dựng class thứ hai Student kế thừa từ Person. Để ý rằng trong lớp Student chúng ta chỉ định nghĩa hàm tạo và phương thức print().
Biến trump được tạo ra từ lớp Student. Tuy nhiên, khi sử dụng biến trump chúng ta lại truy xuất được tới các thành viên của lớp Peson.
Chúng ta gọi guan hệ giữa Student và Person là quan hệ kế thừa. Trong đó, Student kế thừa Person, hoặc Person sinh ra Student. Lớp Person gọi là lớp cha hoặc lớp cơ sở. Lớp Student gọi là lớp con hoặc lớp dẫn xuất.
Kế thừa trong Python
Cú pháp khai báo kế thừa trong Python như sau: class <lớp con>(<lớp cha>):
. Như trong ví dụ ở phần trên, để khai báo lớp Student kế thừa lớp Person, chúng ta viết phần header như sau: class Student(Person):
.
Trong Python, lớp con kế thừa tất cả mọi thứ từ lớp cha. “Kế thừa” ở đây cần hiểu là tất cả những gì được định nghĩa ở lớp cha sẽ đều có thể sử dụng qua tên class con hoặc qua object của lớp con.
Trong ví dụ trên, ở lớp cha chúng ta xây dựng đầy đủ những thành phần thường gặp ở class Python: constructor (__init__
), instance attribute (fname
, lname
, age
), class attribute (count
), property (full_name
), class method (print_count
), static method (birth_year
).
Bạn có thể sử dụng tất cả các thành phần trên của Person qua object của Student hoặc qua chính class Student:
Ví dụ
trump = Student('Donald', 'Trump', 22, '051311', 'Computer science')
trump.print()
print(trump.full_name)
print(Student.count)
Student.print_count()
print(Student.birth_year(37))
print(trump.academic_info)
Python cho phép một class con kế thừa nhiều class cha cùng lúc. Hiện tượng này được gọi là đa kế thừa (multiple inheritance).
Đa kế thừa cũng được hỗ trợ trong một số ngôn ngữ như C++. Tuy nhiên, nhìn chung người ta không khuyến khích sử dụng đa kế thừa vì nó có thể dẫn đến những kết quả khó dự đoán. Vì vậy, các ngôn ngữ như C# hay Java không hỗ trợ đa kế thừa.
Mọi class trong Python đều kế thừa từ một class “tổ tông” chung – lớp object
. Tuy nhiên, khi xây dựng class mới bạn không cần chỉ định lớp cha object. Python sẽ làm việc này tự động.
Ghi đè trong Python
Class con khi kế thừa từ class cha sẽ có tất cả những gì định nghĩa trong class cha. Tuy nhiên, đôi khi những gì định nghĩa trong class lại không hoàn toàn phù hợp với class con.
Ví dụ, trong lớp Person ở ví dụ trên có định nghĩa phương thức print()
. Lớp Student thừa kế Person cũng sẽ thừa kế print()
. Tuy nhiên, print()
của Person chỉ in ra các thông tin riêng của Person, bao gồm fname, lname và age. Trong khi đó Student tự định nghĩa thêm hai attribute group và specialization. Nghĩa là print()
mà Student kế thừa từ Person không hoàn toàn phù hợp.
Từ những tình huống tương tự dẫn đến nhu cầu viết lại một phương thức vốn đã được định nghĩa sẵn ở lớp cha. Hoặc cũng có thể diễn đạt theo cách khác: định nghĩa một phương thức mới trong lớp con có cùng tên và tham số với phương thức được kế thừa từ lớp cha (dĩ nhiên, phần thân khác nhau). Trong kế thừa, hiện tượng này được gọi là ghi đè phương thức (method overriding).
Trong ví dụ của chúng ta, lớp con Student ghi đè phương thức print() của lớp cha Person. Trong lớp con Student định nghĩa một phương thức print() có cùng header như phương thức print() của Person nhưng với phần suite khác biệt:
Ví dụ
def print(self):
super().print()
print(f'Group {self.group}/{self.specialization}')
Khi gặp lệnh gọi print() từ một object của Student, Python sẽ gọi phương thức print() mới của Student.
Python sử dụng cơ chế gọi là Method Resolution Order (MRO) để xác định xem cần phải thực hiện phương thức nào trong kế thừa.
Để ý trong phương thức print() mới có lời gọi super().print()
. Đây là cách Python cho phép gọi tới hàm print() cũ của Person. Ở đây chúng ta tận dụng hàm print() của lớp Person để in ra các thông tin vốn có sẵn ở lớp Person. Sau đó in bổ sung những thông tin riêng của Student.
Một tình huống ghi đè thường gặp khác liên quan đến constructor.
Do constructor __init__ được lớp con thừa kế, nếu bạn không có nhu cầu định nghĩa thêm attribute riêng cho class con thì bạn thậm chí không cần xây dựng constructor ở lớp con nữa. Python tự động gọi constructor của lớp cha khi tạo object của lớp con.
Tuy vậy, thông thường lớp con thường định nghĩa thêm attribute của riêng mình. Do vậy, trong lớp con cũng thường định nghĩa constructor của riêng mình. Hiện tượng này được gọi là ghi đè hàm tạo (constructor overriding).
Khi ghi đè hàm tạo, bạn sẽ có nhu cầu gọi tới hàm tạo của lớp cha trước khi thêm attribute riêng của lớp con. Chính xác hơn, khi ghi đè hàm tạo, bạn bắt buộc phải gọi hàm tạo của lớp cha trong hàm tạo của lớp con qua phương thức super()
:
Ví dụ
class Student(Person):
def __init__(self, fname='', lname='', age=18, group='', specialization=''):
super().__init__(fname, lname, age)
self.group = group
self.specialization = specialization
Hàm super() trả lại một proxy object – một object tạm của class cha – giúp truy xuất phương thức của class cha từ class con. Hàm super() giúp tránh sử dụng trực tiếp tên class cha và làm việc với đa kế thừa.
Khi gặp hàm tạo ghi đè ở lớp con, Python sẽ không tự động gọi hàm tạo của lớp cha nữa mà sẽ chỉ gọi hàm tạo của lớp con. Nếu bạn không gọi hàm tạo của lớp cha trong hàm tạo mới của lớp con, Python sẽ không thể tạo các attribute cần thiết cho lớp cha và sẽ dẫn đến lỗi.
Name mangling và kế thừa
Trên thực tế không phải tất cả mọi thự trong lớp cha đều được lớp con kế thừa. Nếu bạn sử dụng name mangling, các thành viên private (trong tên gọi có hai dấu gạch chân) sẽ không được kế thừa.
Name mangling là cơ chế của Python để mô phỏng việc kiểm soát truy cập các thành viên của class. Python quy ước:(1) nếu thành viên cần giới hạn truy cập trong phạm vi class thì tên gọi cần bắt đầu bằng hai dấu gạch chân. Ví dụ __private
.(2) nếu thành viên cần truy cập giới hạn trong phạm vi của class hoặc ở class con thì tên gọi cần bắt đầu bằng một dấu gạch chân. Ví dụ _protected
.
Python hoặc IDE sẽ sinh lỗi hoặc cảnh báo nếu khi sử dụng attribute bạn vi phạm các quy ước trên.
Để thử nghiệm, hãy bổ sung thêm hai attribute sau vào hàm tạo của Person:
Ví dụ
self._protected = True
self.__private = True
Giờ hãy thử nghiệm các lệnh gọi sau:
Ví dụ
trump = Student('Donald', 'Trump', 22, '051311', 'Computer science')
print(trump._protected) # True
print(trump.__private) # lỗi
Trong nhóm 3 lệnh trên, lệnh thứ 3 sẽ gây lỗi: AttributeError: 'Student' object has no attribute '__private'
. Nghĩa là attribute __private không được Student kế thừa.
Trong khi đó truy xuất trump._protected
không gây lỗi. Nghĩa là _protected
được Student kế thừa.
Lưu ý, việc truy xuất _protected như trên sẽ bị IDE như PyCharm cảnh báo ‘Access to a protected member of a class‘. Bạn không nên truy xuất protected attribute ở ngoài class cha hoặc ngoài class con.
Như vậy name mangling có khả năng kiểm soát truy cập trong kế thừa.
Kế thừa và đa hình trong Python
Hãy cùng thực hiện ví dụ sau:
Ví dụ
class Person:
def __init__(self, firstname: str, lastname: str, birthyear: int):
self.first_name = firstname
self.last_name = lastname
self.birth_year = birthyear
def display(self):
print(f"{self.first_name} {self.last_name}, born {self.birth_year}")
class Student(Person):
def __init__(self, firstname: str, lastname: str, birthyear: int, group: str):
super().__init__(firstname, lastname, birthyear)
self.group = group
def display(self):
print(f"{self.first_name} {self.last_name}, born {self.birth_year}, group {self.group}")
class Teacher:
def __init__(self, firstname: str, lastname: str, birthyear: int, specialization: str):
self.first_name = firstname
self.last_name = lastname
self.birth_year = birthyear
self.specialization = specialization
def display(self):
print(f"{self.first_name} {self.last_name}, born {self.birth_year}, mastered in {self.specialization}")
def show_info(p):
p.display()
if __name__ == '__main__':
putin = Person('Vladimir', 'Putin', 1955)
trump = Student('Donald', 'Trump', 1965, 'TWH_2017_2021')
obama = Teacher('Barack', 'Obama', 1975, 'Computer Science')
show_info(putin)
show_info(trump)
show_info(obama)
Trong ví dụ này chúng ta xây dựng 3 class, Person, Student, Teacher. Trong đó Student kế thừa Person. Cả 3 class này đều có phương thức display(self).
Hãy để ý phương thức show_info(p). Bạn không cần quan tâm p có kiểu gì, chỉ cần p chứa phương thức display() là có thể sử dụng được trong show_info().
Việc gọi p.display() như vậy liên quan đến cơ chế đa hình (polymorphism).
Trong lập trình hướng đối tượng, kế thừa và đa hình là hai nguyên lý khác nhau.
Đa hìnhthiết lập mối quan hệ là (tiếng Anh gọi là is-a) giữa kiểu cơ sở và kiểu dẫn xuất. Ví dụ, nếu chúng ta có lớp cơ sở Person và lớp dẫn xuất Student thì một object của Student cũng là object của Person, kiểu Student cũng là kiểu Person. Nói theo ngôn ngữ thông thường thì sinh viên cũng là người. Một anh sinh viên chắc chắn là một người.
Trong khi đó,kế thừaliên quan đếntái sử dụng code. Lớp con thừa hưởng code của lớp cha.
Một cách nói khác, đa hình liên quan tới quan hệ về ngữ nghĩa, còn kế thừa liên quan tới cú pháp.
Trong các ngôn ngữ như C++, C#, Java, hai khái niệm này hầu như được đồng nhất, thể hiện ở chỗ:
- class con thừa hưởng các thành viên của class cha (kế thừa, tái sử dụng code);
- một object thuộc kiểu con có thể gán cho biến thuộc kiểu cha, tức là kiểu cơ sở có thể dùng để thay thế cho kiểu dẫn xuất (đa hình);
- một phương thức xử lý được object kiểu cha thì sẽ xử lý được object kiểu con.
Tổ hợp kế thừa + đa hình cho phép các ngôn ngữ lập trình hướng đối tượng xử lý object ở dạng tổng quát.
Trong Python điều này vẫn đúng. Tuy nhiên, Python còn mềm dẻo hơn nữa. Python sử dụng nguyên lý có tên gọi là duct typing để thực hiện cơ chế đa hình.
Nguyên lý duck typing (nguyên lý con vịt): nếu một con vật nhìn giống như con vịt, có thể đi và bơi như con vịt, thì nó nhất định là một con vịt. Như vậy, theo nguyên lý này, một con thiên nga nhỏ cũng có thể xem là một con vịt!
Mặc dù nghe khá buồn cười nhưng nó thể hiển đặc điểm nhận dạng object của Python: đa hình trong Python đạt được không cần liên quan đến kế thừa. Đa hình trong Python liên quan đến các thành viên của class. Nếu hai object có các thành viên tương tự nhau thì chúng được xem là thuộc cùng một kiểu. Do vậy chúng có thể sử dụng trong cùng một hàm/phương thức.
Như vậy, theo nguyên lý trên, nếu bạn xây dựng class Person, Student, Teacher với cùng các thành viên, object của hai class này được xem là tương tự nhau và có thể truyền cho cùng một phương thức xử lý. Student và Teacher không cần có bất kỳ quan hệ gì về kế thừa.
Kết luận
Trong bài học này chúng ta đã xem xét chi tiết về kế thừa trong Python, bao gồm các vấn đề sau:
- Trong Python, class con có thể kế thừa tất cả mọi thành viên của class cha, ngoại trừ thành viên private (sử dụng name mangling).
- Python hỗ trợ đa kế thừa. Đây là điều khác biệt với C# hay Java nhưng tương tự với C++.
- Python cũng hỗ trợ ghi đè phương thức, bao gồm cả hàm tạo.
- Trong class con có thể truy xuất phương thức của lớp cha qua một proxy tạo ra bởi hàm super().