Trong bài này chúng ta sẽ cùng học lập trình hướng đối tượng với Class trong Python: cách tạo lớp, đối tượng, hàm khởi tạo __init__, kế thừa, ghi đè phương thức và đóng gói dữ liệu. Khi chương trình bắt đầu lớn, Class chính là công cụ giúp code gọn gàng, dễ mở rộng, đặc biệt khi các bạn viết tool điều khiển nhiều thiết bị cùng lúc.
Bài 12 trong Serie Lập trình Python từ A tới Z
Lập trình hướng đối tượng là gì?
Lập trình hướng đối tượng (Object Oriented Programming, viết tắt OOP) là cách tổ chức chương trình xoay quanh các đối tượng. Mỗi đối tượng gom cả dữ liệu (thuộc tính) lẫn hành động (phương thức) của nó vào một chỗ.
Lấy ví dụ đời thường cho dễ hình dung nhé. Class (lớp) giống bản vẽ thiết kế một chiếc xe máy: có bánh xe, màu sơn, biển số, biết chạy, biết phanh. Object (đối tượng) là từng chiếc xe cụ thể sản xuất ra từ bản vẽ đó. Chiếc màu đỏ biển 29A, chiếc màu đen biển 30B, cùng một bản vẽ nhưng mỗi chiếc mang dữ liệu riêng.
Tại sao nên dùng Class?
Giả sử các bạn viết tool điều khiển 10 con LED trên board. Không dùng Class thì phải tạo 10 biến lưu chân GPIO, 10 biến lưu trạng thái, rồi viết hàm nhận đủ thứ tham số. Code rất nhanh rối.
Dùng Class thì mỗi LED là một đối tượng, tự biết chân của mình, tự nhớ trạng thái của mình, gọi led.on() là xong. Thêm con LED thứ 11 chỉ tốn đúng một dòng.
Ai đã học lập trình hướng đối tượng trong C++ trên blog sẽ thấy Python có đủ 4 tính chất đóng gói, kế thừa, đa hình, trừu tượng, chỉ là cú pháp gọn hơn rất nhiều.
Khai báo Class trong Python và tạo đối tượng
Khai báo Class
Class được khai báo bằng từ khóa class. Hàm đặc biệt __init__ là hàm khởi tạo (constructor), tự động chạy mỗi khi tạo một đối tượng mới.
class Led:
"""Mô phỏng một con LED gắn vào chân GPIO"""
count = 0 # thuộc tính của lớp, dùng chung cho mọi LED
def __init__(self, pin, color="red"):
self.pin = pin # thuộc tính của từng đối tượng
self.color = color
self.state = False
Led.count += 1
Giải thích từng phần:
selflà chính đối tượng đang được tạo.self.pin = pinnghĩa là “lưu chân GPIO vào đối tượng này”.self.pin,self.color,self.statelà thuộc tính đối tượng, mỗi LED có giá trị riêng.countkhai báo ngay trong thân Class là thuộc tính lớp, mọi LED dùng chung. Ở đây mình dùng nó để đếm tổng số LED đã tạo.
Thêm phương thức
Phương thức là hàm viết bên trong Class, tham số đầu tiên luôn là self.
def on(self):
self.state = True
print(f"LED {self.color} (GPIO{self.pin}) ON")
def off(self):
self.state = False
print(f"LED {self.color} (GPIO{self.pin}) OFF")
def toggle(self):
if self.state:
self.off()
else:
self.on()
def __str__(self):
return f"Led(pin={self.pin}, color={self.color}, state={'ON' if self.state else 'OFF'})"
Hàm __str__ cũng là hàm đặc biệt, quyết định đối tượng hiển thị thế nào khi gọi print(). Không có hàm này thì Python chỉ in ra một chuỗi khó hiểu kiểu <__main__.Led object at 0x7f...>.
Tạo đối tượng và sử dụng
led1 = Led(2) led2 = Led(4, "green") led1.on() led2.toggle() led2.toggle() print(led1) print(led2)
Khi gọi Led(2), Python tạo một đối tượng mới rồi gọi __init__(self, 2). Lúc gọi led1.on() thì self chính là led1, chúng ta không cần truyền self vào bằng tay.
Lưu ý: Quên tham số self khi khai báo phương thức là lỗi người mới gặp nhiều nhất. Viết def on(): thay vì def on(self): thì lúc gọi led1.on() sẽ báo TypeError: Led.on() takes 0 positional arguments but 1 was given, vì Python luôn tự truyền đối tượng vào tham số đầu tiên.
Kế thừa trong Python
Kế thừa (inheritance) cho phép tạo một Class mới dựa trên Class có sẵn, dùng lại toàn bộ thuộc tính và phương thức của lớp cha, chỉ viết thêm phần khác biệt.
Ví dụ LED RGB cũng là một con LED, nhưng có 3 chân và chỉnh được màu:
class RgbLed(Led):
"""LED RGB kế thừa từ Led, có thêm 3 kênh màu"""
def __init__(self, pin_r, pin_g, pin_b):
super().__init__(pin_r, "rgb")
self.pins = (pin_r, pin_g, pin_b)
self.rgb = (0, 0, 0)
def set_color(self, r, g, b):
self.rgb = (r, g, b)
self.state = (r, g, b) != (0, 0, 0)
print(f"RGB LED -> ({r}, {g}, {b})")
def off(self): # ghi đè (override) phương thức của lớp cha
self.set_color(0, 0, 0)
class RgbLed(Led)nghĩa làRgbLedkế thừa từLed.super().__init__(...)gọi hàm khởi tạo của lớp cha, để các thuộc tínhpin,color,statevẫn được tạo đầy đủ.- Hàm
off()viết lại trong lớp con gọi là ghi đè (override). Gọirgb.off()sẽ chạy bản củaRgbLedchứ không chạy bản củaLed.
Các phương thức không ghi đè như on(), toggle(), __str__() thì RgbLed dùng lại nguyên của lớp cha.
Lưu ý: Viết hàm __init__ ở lớp con mà quên gọi super().__init__() thì các thuộc tính của lớp cha sẽ không được tạo. Lúc gọi print(rgb) sẽ báo AttributeError: 'RgbLed' object has no attribute 'pin'.
Đóng gói dữ liệu với property
Đóng gói (encapsulation) là che giấu dữ liệu bên trong đối tượng, chỉ cho bên ngoài đọc hoặc sửa qua những cách mình cho phép.
Python không có từ khóa private như C++. Thay vào đó, người ta quy ước tên thuộc tính bắt đầu bằng dấu _ là thuộc tính nội bộ, bên ngoài không nên đụng vào. Kết hợp với @property để chỉ cho phép đọc:
class Sensor:
"""Đóng gói (encapsulation): giá trị đo chỉ đọc qua property"""
def __init__(self, name):
self.name = name
self._value = 0.0 # dấu _ : quy ước là thuộc tính nội bộ
@property
def value(self):
return self._value
def update(self, raw):
if raw < -40 or raw > 125:
print(f"{self.name}: giá trị {raw} ngoài dải đo, bỏ qua")
return
self._value = raw
Bên ngoài đọc giá trị bằng ds18b20.value giống như đọc thuộc tính bình thường. Nhưng muốn đổi giá trị thì bắt buộc phải qua update(), nơi có kiểm tra dải đo từ -40 tới 125 độ C của cảm biến DS18B20. Giá trị rác 300 độ sẽ bị loại ngay từ đầu.
ds18b20 = Sensor("DS18B20")
ds18b20.update(28.75)
ds18b20.update(300)
print(f"{ds18b20.name} = {ds18b20.value} độ C")
Đa hình trong Python
Đa hình (polymorphism) là cùng một lời gọi hàm nhưng mỗi loại đối tượng xử lý theo cách riêng của nó.
for led in [led1, led2, rgb]:
led.off()
Vòng lặp gọi off() trên cả 3 đối tượng mà không cần quan tâm đó là LED thường hay LED RGB. led1, led2 chạy bản off() của Led, còn rgb chạy bản đã ghi đè trong RgbLed. Thêm loại LED mới sau này cũng không phải sửa vòng lặp này, đúng không nào.
Kết quả
Code đầy đủ nằm trong file main.py trên GitHub. Chạy python main.py sẽ được:
LED red (GPIO2) ON LED green (GPIO4) ON LED green (GPIO4) OFF Led(pin=2, color=red, state=ON) Led(pin=4, color=green, state=OFF) RGB LED -> (255, 128, 0) RGB LED -> (0, 0, 0) Led(pin=25, color=rgb, state=OFF) Tổng số LED đã tạo: 3 DS18B20: giá trị 300 ngoài dải đo, bỏ qua DS18B20 = 28.75 độ C LED red (GPIO2) OFF LED green (GPIO4) OFF RGB LED -> (0, 0, 0)
Để ý dòng “Tổng số LED đã tạo: 3”: thuộc tính lớp count được cộng ở cả Led(2), Led(4) lẫn RgbLed(...), vì RgbLed gọi super().__init__() của lớp cha.
Kết
Class giúp gom dữ liệu và hành động của từng thiết bị vào một chỗ, rất hợp khi viết tool làm việc với nhiều board, nhiều cảm biến. Ở Phần 4 của serie, thư viện PySerial cũng cho các bạn làm việc với cổng COM qua một đối tượng serial.Serial, hiểu Class rồi sẽ thấy rất quen.
Hãy cùng chuyển sang bài tiếp theo về xử lý ngoại lệ với Try, Except trong Serie này nhé. Có thắc mắc gì các bạn để lại bình luận bên dưới!

