Thực Hành Tốt Nhất Khi Unpacking Nhiều Giá Trị Trả Về Trong Python

Cú pháp unpacking của Python là một tính năng mạnh mẽ cho phép các hàm trả về nhiều giá trị một cách rõ ràng và ngắn gọn. Tuy nhiên, như bất kỳ công cụ mạnh mẽ nào, nó cần được sử dụng cẩn thận để tránh gây ra lỗi hoặc làm giảm tính dễ đọc của mã. Bài viết này sẽ khám phá các lưu ý quan trọng khi unpacking nhiều giá trị trả về, dựa trên các thực hành tốt nhất được nêu trong Effective Python (Mục 19: Không Bao Giờ Unpack Quá Ba Biến Khi Hàm Trả Về Nhiều Giá Trị).

Hiểu Về Unpacking Trong Python

Python cho phép các hàm trả về nhiều giá trị bằng cách đóng gói chúng vào một tuple, sau đó người gọi có thể unpack tuple này. Dưới đây là một ví dụ đơn giản tính toán độ dài tối thiểu và tối đa của một quần thể cá sấu:

def get_stats(numbers):
    minimum = min(numbers)
    maximum = max(numbers)
    return minimum, maximum

lengths = [63, 73, 72, 60, 67, 66, 71, 61, 72, 70]
minimum, maximum = get_stats(lengths)
print(f'Min: {minimum}, Max: {maximum}')
# Kết quả: Min: 60, Max: 73

Trong trường hợp này, hàm get_stats trả về một tuple (minimum, maximum), được unpack thành hai biến. Cách tiếp cận này thanh lịch và tận dụng cú pháp unpacking của Python để giữ mã ngắn gọn.

Unpacking cũng có thể hoạt động với các starred expressions để xử lý các trường hợp linh hoạt. Ví dụ, để tính kích thước tương đối của mỗi con cá sấu so với trung bình của quần thể:

def get_avg_ratio(numbers):
    average = sum(numbers) / len(numbers)
    scaled = [x / average for x in numbers]
    scaled.sort(reverse=True)
    return scaled

longest, *middle, shortest = get_avg_ratio(lengths)
print(f'Dài nhất: {longest:>4.0%}')
print(f'Ngắn nhất: {shortest:>4.0%}')
# Kết quả:
# Dài nhất: 108%
# Ngắn nhất: 89%

Ở đây, starred expression *middle thu thập tất cả các giá trị giữa phần tử đầu tiên và cuối cùng, giúp xử lý linh hoạt các danh sách có độ dài khác nhau.

Vấn Đề Khi Unpacking Quá Nhiều Giá Trị

Mặc dù unpacking rất tiện lợi, nhưng vấn đề phát sinh khi hàm trả về quá nhiều giá trị. Hãy xem xét một phiên bản mở rộng của hàm get_stats trả về năm thống kê:

def get_stats(numbers):
    minimum = min(numbers)
    maximum = max(numbers)
    count = len(numbers)
    average = sum(numbers) / count
    sorted_numbers = sorted(numbers)
    middle = count // 2
    if count % 2 == 0:
        lower = sorted_numbers[middle - 1]
        upper = sorted_numbers[middle]
        median = (lower + upper) / 2
    else:
        median = sorted_numbers[middle]
    return minimum, maximum, average, median, count

minimum, maximum, average, median, count = get_stats(lengths)
print(f'Min: {minimum}, Max: {maximum}')
print(f'Trung bình: {average}, Trung vị: {median}, Số lượng: {count}')
# Kết quả:
# Min: 60, Max: 73
# Trung bình: 67.5, Trung vị: 68.5, Số lượng: 10

Mã này có hai vấn đề lớn:

  1. Dễ Gây Lỗi Khi Unpacking: Khi unpack nhiều giá trị (đặc biệt là các giá trị số), việc vô tình sắp xếp sai thứ tự là rất dễ xảy ra, dẫn đến các lỗi khó phát hiện. Ví dụ:

    # Đúng:
    minimum, maximum, average, median, count = get_stats(lengths)
    # Sai (median và average bị hoán đổi):
    minimum, maximum, median, average, count = get_stats(lengths)
    

    Những lỗi như vậy khó phát hiện vì các giá trị đều là số và mã sẽ chạy mà không báo lỗi.

  2. Giảm Tính Dễ Đọc: Việc unpack nhiều biến trong một dòng có thể làm mã khó đọc, đặc biệt khi cần ngắt dòng để tuân theo hướng dẫn phong cách PEP 8:

    minimum, maximum, average, median, count = get_stats(lengths)
    # Hoặc ngắt dòng:
    minimum, maximum, average, median, count = \
        get_stats(lengths)
    # Hoặc:
    (minimum, maximum, average,
     median, count) = get_stats(lengths)
    

    Các biến thể này làm giảm tính dễ đọc và khiến mã khó bảo trì hơn.

Thực Hành Tốt Nhất: Giới Hạn Unpacking Ở Ba Biến

Để tránh các vấn đề trên, một nguyên tắc quan trọng là không bao giờ unpack quá ba biến từ giá trị trả về của hàm. Giới hạn này áp dụng cho các biến riêng lẻ, kết hợp với starred expressions, hoặc bất kỳ sự kết hợp nào tương tự. Ví dụ:

  • Hai biến: minimum, maximum = get_stats(lengths)
  • Ba biến: longest, *middle, shortest = get_avg_ratio(lengths)

Nếu hàm cần trả về hơn ba giá trị, hãy cân nhắc sử dụng một cấu trúc dữ liệu nhẹ như namedtuple hoặc một lớp tùy chỉnh. Cách tiếp cận này cải thiện tính rõ ràng và giảm nguy cơ lỗi.

Ví dụ, thay vì trả về năm giá trị từ get_stats, bạn có thể định nghĩa một namedtuple:

from collections import namedtuple

Stats = namedtuple('Stats', ['minimum', 'maximum', 'average', 'median', 'count'])

def get_stats(numbers):
    minimum = min(numbers)
    maximum = max(numbers)
    count = len(numbers)
    average = sum(numbers) / count
    sorted_numbers = sorted(numbers)
    middle = count // 2
    if count % 2 == 0:
        lower = sorted_numbers[middle - 1]
        upper = sorted_numbers[middle]
        median = (lower + upper) / 2
    else:
        median = sorted_numbers[middle]
    return Stats(minimum, maximum, average, median, count)

stats = get_stats(lengths)
print(f'Min: {stats.minimum}, Max: {stats.maximum}')
print(f'Trung bình: {stats.average}, Trung vị: {stats.median}, Số lượng: {stats.count}')
# Kết quả:
# Min: 60, Max: 73
# Trung bình: 67.5, Trung vị: 68.5, Số lượng: 10

Sử dụng namedtuple mang lại nhiều lợi ích:

  • Tính rõ ràng: Mỗi giá trị được truy cập bằng tên (ví dụ: stats.minimum), giúp mã tự giải thích.
  • An toàn: Không có nguy cơ hoán đổi giá trị do nhầm lẫn khi unpacking.
  • Dễ đọc: Mã vẫn ngắn gọn và dễ theo dõi, ngay cả với nhiều giá trị.

Những Điều Cần Nhớ

  • Cú pháp unpacking của Python cho phép hàm trả về nhiều giá trị thông qua tuple, có thể được unpack thành các biến hoặc sử dụng với starred expressions.
  • Việc unpack hơn ba biến dễ gây lỗi do sắp xếp sai thứ tự và làm giảm tính dễ đọc do các dòng dài hoặc ngắt dòng.
  • Đối với các hàm trả về hơn ba giá trị, hãy sử dụng namedtuple hoặc một lớp nhẹ để tăng tính rõ ràng và dễ bảo trì.
  • Tuân thủ thực hành này đảm bảo mã Python của bạn chắc chắn, dễ đọc và dễ gỡ lỗi.

Bằng cách giới hạn unpacking ở ba biến và sử dụng các cấu trúc dữ liệu có tổ chức cho các trường hợp phức tạp hơn, bạn có thể viết mã Python vừa thanh lịch vừa đáng tin cậy.