Reportar esta app

Description

Kế thuwaaf 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ề 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ác class mớ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:

inheritance.py


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
class Person:
    count =

    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:

Donald Trump (22 years old)
Group 051311/Computer science
Donald Trump
1
1 objects created
1984
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 Person.

Chúng ta gọi quan 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):

Tham khảo thêm ngay  Kiểu danh sách list trong Python

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:


1
2
3
4
5
6
7
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ẽ kế thừa 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, age. Trong khi đó Student tự định nghĩa thêm hai attribute group và speccialization. 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ứ 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.


1
2
3
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.

Tham khảo thêm ngay  Kiểu dữ liệu tuple trong Python

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():


1
2
3
4
5
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ảu 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ứu 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:


1
2
self._protected = True
self.__private = True

Giờ hãy thử nghiệm các lệnh gọi sau:


1
2
3
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’. Ngihax là attribute__private không được Student kế thừa.

Trong khi đso 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:

polymorphism.py


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
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).

Tham khảo thêm ngay  Funtion (hàm) trong Python

Hãy để ý phương thức show_infor(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ình thiế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ừa liên quan đến tá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 đến quan hệ về ngữ nghĩa, còn kế thừa liên quan tới cú pháp.

Trong ngôn ngữ C++, C#, Java, hai khái niệm này hầu như được đồng nhất, thể hiện ở chỗ.

  1. 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)
  2. 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)
  3. 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 viejt, 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 bất kỳ quan hệ gì về kế thừa.

Leave a Reply

Your email address will not be published. Required fields are marked *