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

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

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 với Class trong Python

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:

  • self là chính đối tượng đang được tạo. self.pin = pin nghĩa là “lưu chân GPIO vào đối tượng này”.
  • self.pin, self.color, self.state là thuộc tính đối tượng, mỗi LED có giá trị riêng.
  • count khai 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à RgbLed kế thừa từ Led.
  • super().__init__(...) gọi hàm khởi tạo của lớp cha, để các thuộc tính pin, color, state vẫn được tạo đầy đủ.
  • Hàm off() viết lại trong lớp con gọi là ghi đè (override). Gọi rgb.off() sẽ chạy bản của RgbLed chứ không chạy bản của Led.

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!

Rate this post

Để lại một bình luận

Email của bạn sẽ không được hiển thị công khai. Các trường bắt buộc được đánh dấu *