Chuyển đến nội dung chính

Đóng gói (Encapsulation) trong Python: Hướng dẫn toàn diện

Tìm hiểu các nguyên lý cơ bản để triển khai đóng gói trong lập trình hướng đối tượng với Python.
Đã cập nhật 5 thg 10, 2026  · 11 phút đọc

Khám phá cùng AI

ChatGPTClaudePerplexity

Đóng gói là một nguyên tắc cốt lõi của lập trình hướng đối tượng trong Python. Nó bảo vệ lớp của bạn khỏi các thay đổi hoặc xoá nhầm và thúc đẩy khả năng tái sử dụng cũng như bảo trì mã. Hãy xem định nghĩa lớp đơn giản sau:

class Smartphone:
   def __init__(self, brand, os):
       self.brand = brand
       self.os = os

iphone = Smartphone("Apple", "iOS 17")

Nhiều lập trình viên Python định nghĩa lớp như thế này. Tuy nhiên, đây còn xa mới là thực hành tốt nhất mà những Pythonista chuyên nghiệp tuân theo. Vấn đề của lớp này lộ rõ khi bạn thử sửa dữ liệu của nó:

iphone.os = "Android"
print(iphone.os)
Android

Hãy tưởng tượng một chiếc iPhone chạy Android — thật gây sốc! Rõ ràng, chúng ta cần đặt ra một số ranh giới trong lớp để người dùng không thể thay đổi thuộc tính theo ý thích.

Nếu họ quyết định thay đổi, các thay đổi đó phải theo điều kiện của chúng ta, tuân theo quy tắc của chúng ta. Nhưng chúng ta vẫn muốn các thuộc tính .os hoặc .brand giữ nguyên bề ngoài.

Tất cả điều này có thể được thúc đẩy nhờ đóng gói và trong hướng dẫn này, chúng ta sẽ học tất cả về nó. Bắt đầu nhé!

Đóng gói được thực hiện trong Python như thế nào?

Python hỗ trợ đóng gói thông qua quy ước và thực hành lập trình hơn là các bộ định danh truy cập bắt buộc. Bạn thấy đấy, nguyên lý nền tảng đằng sau phần lớn thiết kế mã Python là “Chúng ta đều là người lớn ở đây.” Chúng ta chỉ có thể triển khai đóng gói như một quy ước và kỳ vọng các nhà phát triển Python khác tin tưởng và tôn trọng mã của chúng ta.

Trong các ngôn ngữ OOP khác như Java và C++, đóng gói được buộc chặt bằng các bộ định danh truy cập như public, private hoặc protected, còn Python thì không có:

Tính năng Python Java C++
Bộ định danh truy cập Không có bộ định danh bắt buộc; dùng quy ước đặt tên (_protected, __private) Bắt buộc bằng từ khóa public, protected, private Bắt buộc bằng từ khóa public, protected, private
Phương thức getter/setter Tùy chọn, thường dùng với bộ trang trí @property để kiểm soát truy cập Thực hành phổ biến, thường được triển khai dưới dạng phương thức Thực hành phổ biến, thường được triển khai dưới dạng phương thức
Truy cập dữ liệu Truy cập thông qua quy ước đặt tên; dựa vào kỷ luật của lập trình viên Được kiểm soát bởi bộ định danh truy cập; được trình biên dịch bắt buộc Được kiểm soát bởi bộ định danh truy cập; được trình biên dịch bắt buộc
Triết lý "Chúng ta đều là người lớn ở đây" - dựa vào quy ước và sự tin cậy Thực thi nghiêm ngặt kiểm soát truy cập Thực thi nghiêm ngặt kiểm soát truy cập

Vì vậy, hầu hết, nếu không muốn nói là tất cả, các kỹ thuật đóng gói tôi sắp chỉ cho bạn đều là quy ước của Python. Chúng có thể dễ dàng bị phá vỡ nếu bạn muốn. Nhưng tôi tin bạn sẽ tôn trọng và tuân theo chúng trong các dự án phát triển của mình.

Bộ định danh truy cập trong Python

Giả sử chúng ta có lớp đơn giản này:

class Tree:
   def __init__(self, height):
       self.height = height

pine = Tree(20)
print(pine.height)
20

Nó có một thuộc tính height có thể in ra. Vấn đề là chúng ta cũng có thể đổi nó thành bất cứ thứ gì mình muốn:

pine.height = 50
pine.height
50
pine.height = "Grandma"
pine.height
'Grandma'

Vậy làm sao để nói với người dùng rằng việc thay đổi height là không được phép? Chúng ta có thể biến nó thành một thành viên được bảo vệ (protected) bằng cách thêm một dấu gạch dưới ở trước:

class Tree:
   def __init__(self, height):
       self._height = height

pine = Tree(20)
pine._height
20

Giờ đây, những người hiểu quy ước này sẽ biết rằng họ chỉ nên truy cập thuộc tính và chúng ta đang mạnh mẽ không khuyến khích họ sử dụng và sửa đổi nó. Nhưng nếu họ muốn, họ vẫn có thể sửa, đúng vậy.

Vậy làm sao để ngăn điều đó? Bằng cách dùng một quy ước khác — biến thuộc tính thành thành viên riêng tư (private) bằng cách thêm hai dấu gạch dưới ở trước:

class Tree:
   def __init__(self, height):
       self.__height = height


pine = Tree(20)
pine.__height
AttributeError: 'Tree' object has no attribute '__height'

Bây giờ, Python sẽ báo lỗi nếu ai đó cố truy cập thuộc tính này, chứ đừng nói đến sửa đổi nó.

Nhưng bạn có nhận ra điều chúng ta vừa làm không? Chúng ta đã ẩn đi thông tin duy nhất liên quan đến đối tượng khỏi người dùng. Lớp của chúng ta vừa trở nên vô dụng vì nó không có thuộc tính công khai.

Vậy làm sao để công khai chiều cao cây cho người dùng nhưng vẫn kiểm soát cách họ truy cập và sửa đổi? Ví dụ, chúng ta muốn chiều cao cây nằm trong một khoảng nhất định và chỉ nhận giá trị số nguyên. Làm sao buộc điều đó?

Lúc này, người bạn dùng Java của bạn có thể góp ý rằng hãy dùng phương thức getter và setter. Vậy hãy thử trước:

class Tree:
   def __init__(self, height):
       self.__height = height

   def get_height(self):
       return self.__height

   def set_height(self, new_height):
       if not isinstance(new_height, int):
           raise TypeError("Tree height must be an integer")
       if 0 < new_height <= 40:
           self.__height = new_height
       else:
           raise ValueError("Invalid height for a pine tree")


pine = Tree(20)
pine.get_height()
20

Theo cách này, bạn tạo một thuộc tính riêng tư __height nhưng cho phép người dùng truy cập và sửa nó một cách có kiểm soát bằng các phương thức get_height và set_height.

pine.set_height(25)

pine.get_height()
25

Trước khi đặt giá trị mới, set_height đảm bảo chiều cao mới nằm trong khoảng cho phép và là số.

pine.set_height("Password")
TypeError: Tree height must be an integer

Nhưng các phương thức này có vẻ quá nặng cho một thao tác đơn giản. Hơn nữa, viết mã như thế này cũng không đẹp:

# Tăng chiều cao thêm 5
pine.set_height(pine.get_height() + 5)

Chẳng phải sẽ đẹp và dễ đọc hơn nếu ta có thể viết thế này:

pine.height += 5

mà vẫn buộc đúng kiểu dữ liệu và khoảng giá trị cho height? Câu trả lời là có và chúng ta sẽ học cách làm điều đó trong phần tiếp theo.

Sử dụng bộ trang trí @property trong các lớp Python

Chúng ta giới thiệu một kỹ thuật mới — tạo property cho các thuộc tính:

class Tree:
   def __init__(self, height):
       # Trước tiên, tạo một thuộc tính riêng tư hoặc được bảo vệ
       self.__height = height

   @property
   def height(self):
       return self.__height

pine = Tree(17)
pine.height
17

Chúng ta muốn người dùng truy cập một thuộc tính ẩn tên __height như thể đó là thuộc tính bình thường tên height. Để làm vậy, ta định nghĩa một phương thức tên height trả về self.__height và trang trí nó với @property.

Bây giờ, ta có thể gọi height và truy cập thuộc tính riêng tư:

pine.height
17

Nhưng phần hay nhất là người dùng không thể sửa nó:

pine.height = 15
AttributeError: can't set attribute 'height'

Vì vậy, ta thêm một phương thức khác tên height(self, new_height) được bọc bởi bộ trang trí height.setter. Bên trong phương thức này, ta triển khai logic buộc kiểu dữ liệu và khoảng giá trị mong muốn cho height:

class Tree:
   def __init__(self, height):
       self.__height = height

   @property
   def height(self):
       return self.__height

   @height.setter
   def height(self, new_height):
       if not isinstance(new_height, int):
           raise TypeError("Tree height must be an integer")
       if 0 < new_height <= 40:
           self.__height = new_height
       else:
           raise ValueError("Invalid height for a pine tree")

Giờ đây, khi người dùng cố sửa thuộc tính height, @height.setter sẽ được gọi, đảm bảo giá trị truyền vào là hợp lệ:

pine = Tree(10)

pine.height = 33  # Gọi @height.setter
pine.height = 45  # Sẽ phát sinh lỗi
ValueError: Invalid height for a pine tree

Chúng ta cũng có thể tuỳ biến cách thuộc tính height được truy cập qua dấu chấm với @height.getter:

class Tree:
   def __init__(self, height):
       self.__height = height

   @property
   def height(self):
       return self.__height

   @height.getter
   def height(self):
       # Bạn có thể trả về một phiên bản tuỳ biến của height
       return f"This tree is {self.__height} meters"


pine = Tree(33)

pine.height
'This tree is 33 meters'

Mặc dù chúng ta tạo pine với chiều cao kiểu số nguyên, ta vẫn có thể biến đổi giá trị trả về nhờ @height.getter.

Đó là các ví dụ cho cách chúng ta có thể thúc đẩy đóng gói trong một lớp Python. Hãy nhớ, đóng gói vẫn chỉ là quy ước vì chúng ta vẫn có thể phá vỡ thành viên riêng tư __height bên trong:

pine._Tree__height = "Gotcha!"

pine.height
'This tree is Gotcha! meters'

Mọi thứ trong lớp Python đều là công khai, các phương thức riêng tư cũng vậy. Đây không phải là lỗi thiết kế mà là minh chứng cho cách tiếp cận “Chúng ta đều là người lớn ở đây”.

Dưới đây là tóm lược các bộ định danh truy cập trong Python mà chúng ta vừa xem:

Bộ định danh/Quy ước Mô tả Ví dụ
Công khai (Public) Mức truy cập mặc định; thuộc tính và phương thức có thể truy cập từ bên ngoài lớp self.attribute
Được bảo vệ (Protected) Ký hiệu bằng một dấu gạch dưới; quy ước cho biết quyền truy cập bị hạn chế một phần self._attribute
Riêng tư (Private) Ký hiệu bằng hai dấu gạch dưới; đổi tên (name mangling) cung cấp hạn chế truy cập ở mức nhất định self.__attribute
Property Cung cấp truy cập có kiểm soát tới thuộc tính riêng tư bằng @property decorator @property def attribute(self):
Property chỉ đọc Không có @attribute.setter; cho phép truy cập chỉ đọc một thuộc tính @property def attribute(self):

Thực hành tốt khi triển khai đóng gói

Có một số thực hành tốt bạn có thể làm theo để đảm bảo mã của mình tương thích với mã do các OOPista giàu kinh nghiệm khác viết:

  1. Tạo các thuộc tính hoặc phương thức được bảo vệ hoặc riêng tư nếu chúng chỉ được bạn sử dụng. Các thành viên được bảo vệ hoặc riêng tư sẽ bị loại khỏi tài liệu và báo hiệu cho người khác rằng chúng có thể được bạn, với vai trò nhà phát triển, thay đổi mà không cần thông báo, do đó không khuyến khích họ sử dụng.
  2. Bạn không phải lúc nào cũng cần tạo property cho mọi thuộc tính của lớp. Với các lớp lớn có nhiều thuộc tính, việc viết các phương thức attr.getter và attr.setter có thể trở thành cực hình.
  3. Cân nhắc đưa ra cảnh báo mỗi khi người dùng truy cập một thành viên được bảo vệ (_).
  4. Hạn chế dùng thành viên riêng tư, vì chúng có thể khiến mã khó đọc với những người không quen quy ước.
  5. Ưu tiên sự rõ ràng hơn mập mờ. Vì đóng gói nhằm cải thiện khả năng bảo trì mã và bảo vệ dữ liệu, đừng hoàn toàn che giấu các chi tiết triển khai quan trọng của lớp.
  6. Nếu bạn muốn tạo property chỉ đọc, đừng triển khai phương thức @attr.setter. Người dùng có thể truy cập property nhưng không thể sửa nó.
  7. Luôn nhớ rằng đóng gói là một quy ước, không phải phần cú pháp bị bắt buộc trong Python.
  8. Với các lớp đơn giản, cân nhắc dùng dataclass cho phép bật đóng gói lớp chỉ với một dòng mã. Tuy nhiên, dataclass phù hợp với các lớp đơn giản hơn có thuộc tính và phương thức dự đoán được. Xem hướng dẫn về dataclass này để tìm hiểu thêm.

Kết luận và Tài nguyên tham khảo thêm

Trong hướng dẫn này, chúng ta đã học một trong những trụ cột của lập trình hướng đối tượng trong Python: đóng gói.

Đóng gói cho phép bạn định nghĩa quyền truy cập có kiểm soát đến dữ liệu được lưu bên trong các đối tượng của lớp. Điều này giúp bạn viết mã sạch, dễ đọc và hiệu quả, đồng thời ngăn thay đổi hoặc xoá nhầm dữ liệu của lớp.

Dưới đây là một số tài nguyên liên quan giúp nâng cao kiến thức OOP của bạn:

Câu hỏi thường gặp

Đóng gói khác gì với trừu tượng hóa trong Python?

Đóng gói là việc gom dữ liệu và các phương thức thao tác trên dữ liệu trong cùng một đơn vị, chẳng hạn như một lớp, và hạn chế truy cập đến một số thành phần. Ngược lại, trừu tượng hóa nhằm che giấu sự phức tạp bên trong trong khi chỉ phơi bày các phần thiết yếu. Đóng gói tập trung vào việc hạn chế truy cập, còn trừu tượng hóa tập trung vào việc đơn giản hóa tương tác.

Đóng gói có dùng trong lập trình hàm không, hay nó chỉ dành cho OOP?

Đóng gói là một khái niệm gắn chặt với lập trình hướng đối tượng (OOP) và thường không được dùng trong lập trình hàm. Lập trình hàm nhấn mạnh tính bất biến và các hàm không trạng thái, khác với OOP tập trung vào đối tượng và việc đóng gói dữ liệu trong các đối tượng đó.

Những sai lầm thường gặp khi triển khai đóng gói trong Python là gì?

Những sai lầm phổ biến gồm lạm dụng biến riêng tư, khiến mã khó bảo trì, và không cung cấp đủ phương thức truy cập (getter/setter), dẫn đến mã mong manh. Ngoài ra, lập trình viên có thể quên rằng đóng gói trong Python chỉ là quy ước nên có thể bị vượt qua.

Đóng gói có thể cải thiện bảo mật mã như thế nào?

Đóng gói có thể cải thiện bảo mật mã bằng cách hạn chế truy cập vào trạng thái nội bộ của một đối tượng và ngăn sửa đổi trái phép. Bằng cách kiểm soát cách dữ liệu được truy cập và sửa đổi, nhà phát triển có thể đảm bảo dữ liệu luôn hợp lệ và nhất quán.

Việc dùng bộ trang trí @property ảnh hưởng đến hiệu năng trong Python như thế nào?

Việc dùng bộ trang trí @property có thể tạo ra một chút chi phí do phải gọi phương thức bổ sung, nhưng trong hầu hết trường hợp, tác động này không đáng kể. Lợi ích về khả năng đọc và bảo trì mã thường vượt trội so với chi phí hiệu năng nhỏ này.

Có thể thực thi chặt chẽ đóng gói trong Python như trong Java hoặc C++ không?

Python không thực thi chặt chẽ đóng gói như Java hoặc C++. Triết lý của nó dựa trên các quy ước và sự thấu hiểu rằng lập trình viên sẽ tôn trọng cách dùng dự kiến của các thành viên riêng tư và được bảo vệ. Tuy nhiên, dùng quy ước đặt tên bằng dấu gạch dưới có thể giúp ngăn việc lạm dụng.

Một số ứng dụng thực tế của đóng gói trong các dự án Python là gì?

Đóng gói được dùng để bảo vệ dữ liệu nhạy cảm trong các ứng dụng như phần mềm ngân hàng, nơi chi tiết tài khoản được ẩn đi. Nó cũng được dùng trong thư viện và framework để che giấu các chi tiết triển khai phức tạp, cho phép lập trình viên tương tác thông qua giao diện đơn giản.

Làm sao tôi có thể kiểm thử hiệu quả mã được đóng gói trong Python?

Kiểm thử mã được đóng gói liên quan đến việc viết unit test cho giao diện công khai của lớp. Bằng cách kiểm thử các phương thức và property được phơi bày với người dùng, bạn đảm bảo các thay đổi trạng thái bên trong được quản lý đúng và logic đóng gói được giữ vững.

Đóng gói có thể áp dụng cho các module Python không, hay chỉ giới hạn trong các lớp?

Mặc dù đóng gói chủ yếu gắn với lớp, khái niệm này có thể mở rộng sang module bằng cách dùng các hàm hoặc biến có tiền tố gạch dưới để báo hiệu dùng riêng trong module.

Đóng gói liên hệ với đa hình trong Python như thế nào?

Đóng gói và đa hình đều là các nguyên tắc của OOP nhưng phục vụ những mục đích khác nhau. Đóng gói hạn chế truy cập vào một số phần của đối tượng, còn đa hình cho phép các đối tượng được xử lý như thể chúng là thể hiện của lớp cha, giúp mở rộng và bảo trì mã dễ dàng hơn. Chúng có thể được dùng cùng nhau để tạo cấu trúc mã linh hoạt và an toàn.


Bexruz (Bex) Tuychiev's photo
Author
Bexruz (Bex) Tuychiev
LinkedIn

Tôi là người sáng tạo nội dung về khoa học dữ liệu với hơn 2 năm kinh nghiệm và là một trong những tài khoản có lượng người theo dõi lớn nhất trên Medium. Tôi thích viết các bài chuyên sâu về AI và ML với chút giọng điệu mỉa mai, vì bạn cũng phải làm gì đó để chúng bớt khô khan. Tôi đã xuất bản hơn 130 bài viết và một khóa học trên DataCamp, cùng với một khóa nữa đang được thực hiện. Nội dung của tôi đã được hơn 5 triệu lượt xem, trong đó 20 nghìn người đã trở thành người theo dõi trên cả Medium và LinkedIn. 

Chủ đề
Python

Tiếp tục hành trình Python của bạn ngay hôm nay!

Khóa học

Lập trình hướng đối tượng trong Python

4 giờ
105.8K
Tìm hiểu cách tạo lớp và tận dụng kế thừa cùng đa hình để tái sử dụng và tối ưu hóa mã.
Xem chi tiếtRight Arrow
Bắt Đầu Khóa Học
Xem thêmRight Arrow
Liên quan

blog

Claude Opus 4.6: Tính năng, Điểm chuẩn, Bài kiểm tra thực hành và hơn thế nữa

Mô hình mới nhất của Anthropic dẫn đầu ở mã hóa tác tử và lập luận phức tạp. Thêm vào đó, nó có cửa sổ ngữ cảnh 1M.
Matt Crabtree's photo

Matt Crabtree

10 phút

Xem ThêmXem Thêm