Class Attributes và Instance Attributes
Trong hướng dẫn này chúng ta sẽ đi tìm hiểu về 2 loại thành phần chứa dữ liệu trong lập trình Python hướng đối tượng đó là Class Attributes và Instance Attributes
Mỗi class thường chứa hai loại thành viên quan trọng: thành phần chứa dữ liệu và thành phần xử lý dữ liệu. Trong Python, thành phần chứa dữ liệu được gọi là attribute. Có thể xem attribute của Python tương tự như biến của class trong các ngôn ngữ như C++/Java hay C#.
Python phân biệt hai loại attribute: instance attribute gắn với object và class attribute gắn với chính class.
Cách sử dụng attribute trong Python
Chúng ta bắt đầu với một ví dụ.
Tạo module book.py và viết code như sau:
Ví dụ
class Book:
"""A class for e-book"""
b = Book()
b.title = 'Python programming'
b.authors = 'Donald Trump'
b.year = 2020
print(b.title, b.authors, b.year) # kết quả là 'Python programming Donald Trumo 2020'
b2 = Book()
print(b2.title) # lỗi, không có attribute title trong object b2
Bạn có thể thấy rất nhiều điều lạ ở đây. Dễ thấy nhất là class Book hoàn toàn trống trơn. Trong class này chỉ có mỗi docstring. Tuy nhiên sau khi tạo object b, bạn lại có thể dùng phép toán truy xuất phần tử (dot notation) b.title
, b.authors
, b.year
, gán giá trị cho chúng và sau sử dụng lại chúng trong hàm print(). Rõ ràng bạn không hề tạo title, authors hay year trong khai báo Book.
Khi bạn tạo object b2 và thử truy xuất giá trị title (b2.title) thì lại gặp lỗi “không tìm thấy attribute title trong object b2”.
title, authors, year được gọi là những attribute của object b (nhưng không phải là attribute của b2).
Như vậy, trong Python, attribute là những biến có thể chứa giá trị đặc trưng cho một object. Nó được tạo hoàn toàn độc lập với khai báo class (không cần chỉ định trong khai báo class). Một cách chính xác hơn, biến này được gọi là instance attribute (do liên quan đến object).
Giờ hãy cập nhật class Book
như sau:
Ví dụ
class Book:
"""A class for e-book"""
def __init__(self,
title: str,
authors: str = '',
publisher: str = '',
year: int = 2020,
edition: int = 1):
"""Hàm tạo của class"""
self.title = title
self.authors = authors
self.publisher = publisher
self.year = year
self.edition = edition
def to_string(self, brief = True):
"""Get the infor and make a formated string"""
if brief:
return f"{self.title} by {self.authors}"
else:
return f"{self.title} by {self.authors}, {self.edition} edition, {self.publisher}, {self.year}"
def print(this, brief = False):
"""Print the book infor"""
print(this.to_string(brief))
# sử dụng class Book
b1 = Book('Tự học lập trình Python')
b1.authors = 'Nhật Linh'
b1.publisher = 'Tự học ICT'
b1.year = 2021
b1.print()
b2 = Book('Python programming', 'Trump D.', 'The White house', 2020)
print(b2.title, b2.authors)
Trong trường hợp này chúng ta tạo attribute bên trong hàm tạo của class. Bạn có thể thấy rằng giờ cả b1 và b2 đều có chung tổ hợp attribute title, authors, publisher, year và edition. Giờ những biến này được gọi là những instance attribute của class book.
Instance attribute
Trong Python, biến thành viên của class được gọi là instance attribute. Đây là các giá trị đặc trưng cho từng object.
Ví dụ, class book xác định rằng, tất cả sách được đặc trưng bởi tổ hợp giá trị của tiêu đề, tác giả, nhà xuất bản, năm xuất bản và lần tái bản. Vậy, cuốn sách thứ nhất có giá trị tổ hợp là ‘Tự học lập trình Python’ của tác giả ‘Nhật Linh’, do ‘Tự học ICT’ xuất bản năm 2020 và là lần xuất bản đầu tiên. Tổ hợp giá trị này đặc trưng cho riêng 1 cuốn sách (object). Cuốn sách khác sẽ có tổ hợp giá trị khác.
Khai báo instance attribute
Trong class book, các lệnh tạo (và gán giá trị) biến thành viên của class phải viết trong hàm tạo như sau:
Ví dụ
def __init__(self,
title: str,
authors: str = '',
publisher: str = '',
year: int = 2020,
edition: int = 1):
"""Hàm tạo của class"""
self.title = title
self.authors = authors
self.publisher = publisher
self.year = year
self.edition = edition
Nếu bạn xuất phát từ C++, Java hay C# sẽ thấy cách tạo biến thành viên trong Python hơi khác biệt:
- Biến thành viên trong Python được tạo ra trong hàm tạo chứ không viết trong thân class. Biến được khai báo trong thân class lại thuộc về nhóm class instance (chúng ta sẽ học sau).
- Biến thành viên được khai báo cùng với tham số self sử dụng phép toán truy xuất thành viên, giống như là các biến này đã có sẵn và bạn chỉ việc gán dữ liệu.
Trong Python, biến self (hay bất kỳ tên tham số nào) đứng đầu trong danh sách tham số của __init__ sẽ trỏ tới object vừa tạo (bởi magic method __new__()). Phép toán truy xuất thành viên (dấu chấm) trên object sử dụng một định danh mới và phép gán sẽ tự động tạo ra một biến thành viên.
Tên biến thành viên được đặt theo quy tắc đặt định danh chung của Python, cũng như theo quy ước đặt tên của biến (cục bộ và toàn cục).
Như vậy, self.title = 'A new book'
trong __init__() sẽ tạo ra biến thành viên title với giá trị ‘A new book’.
Tất cả các tham số còn lại của __init__() chính là để cung cấp giá trị ban đầu cho các biến thành viên.
Như trong ví dụ 1 bạn đã thấy, attribute không nhất thiết phải khai báo trong hàm tạo. Bạn có thể khai báo attribute sau khi tạo object. Chỉ có điều, khi này attribute đó chỉ tồn tại trên object cụ thể đó. Nếu bạn tạo ra object mới, nó sẽ không có attribute như object trước đó.Nếu bạn tạo attribute trong constructor, tất cả object tạo ra sẽ có chung tổ hợp attribute. Điều này phủ hợp với khái niệm class/object trong lập trình hướng đối tượng. Do vậy, bạn luôn nên tạo instance attribute trong constructor.
Truy xuất instance attribute
Với các instance attribute tạo ra như trên, bạn có thể truy xuất nó qua tên object trong code ở ngoài class như sau:
Ví dụ
b3 = Book('')
b3.title = 'Tự học lập trình Python'
b3.authors = 'Nhật Linh'
b3.publisher = 'Tự học ICT'
b3.year = 2021
b3.edition = 2
Việc truy xuất này là hai chiều, nghĩa là có thể gán giá trị hoặc đọc giá trị.
Đối với code ở bên trong class, cách truy cập là tương tự. Hãy xem phương thức to_string():
Ví dụ
def to_string(self, brief = True):
"""Get the infor and make a formated string"""
if brief:
return f"{self.title} by {self.authors}"
else:
return f"{self.title} by {self.authors}, {self.edition} edition, {self.publisher}, {self.year}"
Tạm thời chúng ta chưa trình bày kỹ về phương thức này.
Hãy nhìn cách sử dụng biến title, authors, edtion, publisher và year thông qua tham số self: self.title
, self.authors
, self.edition
, self.publisher
, self.year
.
Bạn không được viết trực tiếp tên attribute mà bắt buộc phải thông qua biến self. Nó không có gì khác biệt so với khi truy xuất từ tên object bên ngoài class, cả về hình thức và bản chất. Tham số self
thực chất là một object kiểu Book được Python tự động truyền khi gọi phương thức to_string().
Class attribute
Hãy hình dung yêu cầu sau: khi xây dựng class Book, làm thế nào để theo dõi số lượng object đã được tạo ra? Logic đơn giản nhất là tạo ra một biến đếm. Mỗi khi tạo một object mới thì tăng giá trị của biến đếm. Nếu biến đếm và việc tăng giá trị của biến đếm nằm ngoài class thì rất đơn giản. Nhưng nếu chúng ta cần tích hợp logic này vào chính class thì làm như thế nào?
Để ý thấy rằng, một biến đếm cho mục đích trên không thể phụ thuộc vào từng object. Nói cách khác, tất cả các object đều phải sử dụng chung một biến đếm. Để tăng giá trị khi tạo object, phép cộng phải được thực hiện trong constructor.
Python cung cấp một công cụ cho những mục đích tương tự: class attribute. Hãy thay đổi class Book như sau:
Ví dụ
class Book:
"""A class for e-book"""
count = 0
def __init__(self,
title: str,
authors: str = '',
publisher: str = '',
year: int = 2020,
edition: int = 1):
"""Hàm tạo của class"""
self.title = title
self.authors = authors
self.publisher = publisher
self.year = year
self.edition = edition
self.__private = True
Book.count += 1
Để ý dòng 4 có lệnh khởi tạo biến count = 0
, và dòng 18 có phép cộng Book.count += 1
. Trong class Python, count
là một class attribute.
Class attribute là một biến gắn liền với chính class, có thể được truy xuất từ các object và có giá trị chung cho tất cả các object.
Sự khác biệt giữa class attribute và instance attribute nằm ở chỗ: instance attribute gắn với từng object cụ thể và thể hiện trạng thái riêng của từng object; class attribute gắn với chính class và đặc trưng cho class, hoặc đặc trưng chung cho mọi object.
Với biến count xây dựng như trên bạn có thể sử dụng nó thông qua tên class: Book.count
. Bạn sử dụng lối viết này dù ở trong thân class hay bên ngoài class.
Ví dụ
b1 = Book('Lập trình hướng đối tượng với Python', 'Nhật Linh', 'Tự học ict', 2022, 2)
print(Book.count)
b2 = Book(title = 'Nhập môn lập trình Python', authors= 'Nhật linh', publisher= 'Tự học ICT')
print(Book.count)
b3 = Book('')
print(b3.count)
Do đặc điểm của class attribute là sử dụng chung trong các object khác nhau, bạn cũng có thể truy xuất giá trị của count thông qua tên object: b3.count
. Tuy nhiên, khi truy xuất qua tên object bạn không thay đổi được giá trị của class attribute. Nói chính xác hơn, thay đổi giá trị của class attribute qua tên object sẽ không lưu lại được.
Public, protected, private trong Python
Trong các ngôn ngữ hướng đối tượng truyền thống như C++, Java hay C#, mỗi thành viên thuộc về một trong các mức truy cập:(1) public: tự do truy cập, không có giới hạn gì(2) protected: chỉ class con và trong nội bộ class mới có thể truy cập(3) private: giới hạn truy cập trong nội bộ class.
Trong Python không có khái niệm về kiểm soát truy cập các thành viên như vậy. Hoặc cũng có thể nói rằng mặc định mọi thành viên của class trong Python đều là public. Nghĩa là code ngoài class có thể tự do truy cập các thành viên này.
Để mô phỏng lại hiệu quả của việc kiểm soát truy cập, Python sử dụng loại kỹ thuật có tên gọi là xáo trộn tên (name mangling).
Kỹ thuật này quy ước rằng:
- Nếu muốn biến chỉ được sử dụng trong nội bộ class và các class con (mức truy cập là protected), tên biến cần bắt đầu là _ (một dấu gạch chân);
- Nếu muốn biến chỉ được sử dụng trong nội bộ class (mức truy cập private), tên biến cần bắt đầu là __ (hai dấu gạch chân).
Ví dụ, bạn có thể khai báo biến protected và private trong constructor của lớp Book như sau:
Ví dụ
self.__private = True # private instance attribute
self._protected = False # protected instance attribute
Thực ra, loại kỹ thuật này không làm thay đổi được việc truy cập thành viên của class. Nó đơn thuần là chỉ báo để lập trình viên và IDE biết ý định sử dụng của thành viên đó.
Ví dụ, trong class Book như trên, thực tế biến __private
không hề trở thành private. IDE sẽ trợ giúp che đi biến này trong phần nhắc code. Tuy nhiên bạn vẫn có thể truy xuất nó như bình thường: b3.__private = False
.
Kết luận
Trong bài học này chúng ta đã xem xét cách sử dụng attribute – thành phần lưu trữ dữ liệu trong class Python.
Python phân biệt hai loại attribute: instance attribute gắn với object, class attribute gắn với chính class. Instance attribute tương tự như biến thành viên của C++/Java/C#, còn class attribut tương tự như biến static của các ngôn ngữ này.
Điểm khác biệt lớn trong Python là instance attribute khai báo trong hàm tạo, còn class attribute khai báo trực tiếp trong class.
Đồng thời, các attribute trong Python không phân biệt mức truy cập (public, protected, private) mà sử dụng kỹ thuật name mangling với vai trò chỉ báo.