Hướng dẫn comprehension python

Nhiều năm về trước, khi tôi còn đang là một lập trình viên .NET & Oracle DBA, do yếu tố công việc nên thi thoảng cũng có lúc dùng Python để viết một số script chạy trên server hoặc crawl dữ liệu từ các máy chủ khác. Lúc đó, tôi chỉ học các syntax và làm một số các ví dụ cơ bản bằng ngôn ngữ Python (tất nhiên là không dám điền vào CV là thành thạo Python rồi).

Đến một ngày, khi "học thử" buổi 01 lớp học offline của nhóm pymi.vn (cộng đồng người học và lập trình Python khá lớn ở Hà Nội & TP HCM), tôi thực sự bất ngờ và mê mẩn trước một "vũ khí" đặc biệt được mentor thực hiện demo với lớp, một cú pháp mà chỉ ngôn ngữ lập trình Python mới có: List comprehension. Cú pháp list-comprehension cho phép lập trình viên có thể rút gọn các dòng code của mình khi xử lý dữ liệu trong các mảng/array/list xuống rất nhiều, thậm chí chỉ còn 01 dòng (so với 3-5 dòng code thông thường).

List-comprehension là cái gì ?

List-comprehension là một cú pháp cho phép lập trình viên nhanh chóng tạo ra một biến dữ liệu list mới từ một list cũ hoặc vòng lặp dạng in-line, kết hợp với các điều kiện cho trước.
Về bản chất, lập trình viên hoàn toàn có thể tạo ra một list mới bằng cách sử dụng vòng lặp for/while thông thường.

Cú pháp cơ bản:

# basic looping 
for item in iterable:
    expression

# list comprehension
[expression for item in iterable]


Ví dụ: Tạo ra một list danh sách các số từ 0 đến 99

# basic looping 
my_list = []
for number in range(100):
    my_list.append(number)

# list comprehension
my_comprehension_list = [number for number in range(100)]


Ví dụ: Cho một list gồm các số nguyên, hãy tạo ra một list mới với các phần tử là bình phương của phần tử trong list cũ

old_list = [1, 2, 3, 4, 5]

# basic looping 
new_list_1 = []
for number in old_list:
    new_list_1.append(number * number)

# list comprehension
new_list_2 = [number * number for number in old_list]

Ví dụ: Cho một list gồm các string khác nhau, tạo ra một list mới với các string ở định dạng UPPER (viết hoa)

pets = ['dog', 'cat', 'bird', 'fish', 'mouse']

# basic looping 
new_pets_1 = []
for pet in pets:
    new_pets_1.append(pet.upper())

# list comprehension
new_pets_2 = [pet.upper() for pet in pets]


Nhìn qua thì thấy hơi hơi... khó hiểu đúng không ? Thực ra với các python-beginner thì dùng list-comprehension hay không cũng không thành vấn đề lắm, nhưng cứ thử áp dụng list-comprehension vào các bài toán xử lý list cần đến vòng lặp, lâu dần sẽ quen và "nghiện" lúc nào không biết.

Một vài ứng dụng của list-comprehension.

1. Lọc dữ liệu trong list (Filter)

# basic looping with a conditional statement
for item in iterable:
    if some_condition:
        expression



# list comprehension with a conditional statement
[expression for item in iterable if some_condition]


Ví dụ: Cho một list các số nguyên, tạo ra một list mới chỉ chứa các số chia hết cho 3.

old_list = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15]
# basic looping 

new_list_1 = []
for number in old_list:
    if number % 3 == 0:
        new_list_1.append(number)

# list comprehension
new_list_2 = [number for number in old_list if number % 3 == 0]

Ngoài cách đặt filter ở cuối, chúng ta cũng có thể đặt filter ở phía trước.

Ví dụ: Cho một list các số nguyên, tạo ra một list mới chỉ chứa các số chia hết cho 2.

old_list = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15]

# list comprehension
new_list = [(number if number % 2 == 0  else 0) for number in old_list]
print(new_list)

[0, 2, 0, 4, 0, 6, 0, 8, 0, 10, 0, 12, 0, 14, 0]

2. Filter với biểu thức so sánh bên ngoài

Phần điều kiện filter ở some_condition ở thể không phải là so sánh trực tiếp mà có gọi đến một function bên ngoài và trả về kết quả so sánh.

Ví dụ: Cho danh sách tên các con vật, tạo ra một danh sách mới chứa tên các con vật có 4 chân.

pets = ['dog', 'cat', 'bird', 'fish', 'mouse', 'chicken']

def has_four_legs(pet):
    return pet in ['dog', 'cat', 'mouse']

new_pets = [pet for pet in pets if has_four_legs(pet)]
print(new_pets)

['dog', 'cat', 'mouse']

Ví dụ: Cho một danh sách các số, hãy tạo ra một list mới là hiệu của 100 và các số trong list.

old_list = [1, 2, 3, 4, 0, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15]

def get_sub_result(number):
    return 100 - number

new_list = [get_sub_result(number) for number in old_list]
print(new_list)

[99, 98, 97, 96, 100, 95, 94, 93, 92, 91, 90, 89, 88, 87, 86, 85]

3. Exception handles khi dùng list-comprehension

Việc gọi ra function thực hiện filter bên ngoài sẽ giúp chúng ta có thể handles được các exception.

Ví dụ: Thay vì tính hiệu của 100 và số trong list thì yêu cầu là tính thương (chia), nếu không thực hiện handles exception thì chúng ta sẽ bị lỗi tại phép chia cho 0

old_list = [1, 2, 3, 4, 0, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15]

def get_div_result(number):
    return 100 / number

new_list = [get_div_result(number) for number in old_list]
print(new_list)



---------------------------------------------------------------------------
ZeroDivisionError                         Traceback (most recent call last)
 in 
      4     return 100 / number
      5 
----> 6 new_list = [get_div_result(number) for number in old_list]
      7 print(new_list)

 in (.0)
      4     return 100 / number
      5 
----> 6 new_list = [get_div_result(number) for number in old_list]
      7 print(new_list)

 in get_div_result(number)
      2 
      3 def get_div_result(number):
----> 4     return 100 / number
      5 
      6 new_list = [get_div_result(number) for number in old_list]

ZeroDivisionError: division by zero

Thực hiện handles exception ở function get_div_result

old_list = [1, 2, 3, 4, 0, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15]

def get_div_result(number):
    try:
        return 100 / number
    except ZeroDivisionError:
        return 0

new_list = [get_div_result(number) for number in old_list]
print(new_list)

[100.0, 50.0, 33.333333333333336, 25.0, 0, 20.0, 16.666666666666668, 14.285714285714286, 12.5, 11.11111111111111, 10.0, 9.090909090909092, 8.333333333333334, 7.6923076923076925, 7.142857142857143, 6.666666666666667]

4. Sử dụng kết hợp với Walrus Operator

Từ bản Python 3.8 trở lên, Python có cú pháp khá thú vị là Walrus Operator (:= ) dùng để gán chung dữ liệu ngay trong các vòng lặp, câu lệnh so sánh, ...
Việc sử dụng Walrus Operator kết hợp với list-comprehension cho ra những solution khá thú vị.

Ví dụ: Cho 1 chuỗi, lấy ra ngẫu nhiên 10 ký tự trong chuỗi, sau đó tạo ra một list, nếu ký tự lấy ra không phải là nguyên âm thì thực hiện viết hoa ký tự đó và đưa vào list.

Nếu làm theo cách thông thường, mất khoảng chừng này dòng code để làm:

import random

letters = list('this is a sample about list-comprehension and walrus operator')

consonants = []

for _ in range(10):
    letter = random.choice(letters)
    if letter not in 'aeoui':
        consonants.append(letter.upper())

print(consonants)

['-', 'L', 'D', 'N', 'S']

Nếu sử dụng Walrus Operator kết hợp list-comprehension

import random
letters = list('this is a sample about list-comprehension and walrus operator')
consonants = [letter.upper() for _ in range(0, 10) if (letter := random.choice(letters)) not in 'aeoui']
print(consonants)

[' ', 'M', 'S', 'T', 'L', ' ', 'S']

So sánh tốc độ xử lý list-comprehension và vòng lặp thông thường.

Sau một vài ví dụ ở trên, chúng ta có thể thấy list-comprehension được sử dụng với 2 mục đích chính:
- Tạo ra một list mới từ vòng lặp.
- Thực hiện filter dữ liệu trong list để tạo ra một list mới.

Bài test này sẽ thực hiện test tốc độ xử lý của 2 trường hợp trên.

import time 

MAX_RANGE = 20000000  # 20 triệu

def double_number(number):
    return number * 2

def create_basic_list():
    basic_list = []
    for number in range(MAX_RANGE):
        basic_list.append(double_number(number))
    

def create_comprehension_list():
    comprehension_list = [double_number(number) for number in range(MAX_RANGE)]
    

def create_basic_list_filtered():
    basic_list = []
    for number in range(MAX_RANGE):
        if number % 2 == 0:
            basic_list.append(double_number(number))

            
def create_comprehension_list_filtered():
    comprehension_list = [double_number(number) for number in range(MAX_RANGE) if number % 2 == 0]


    
def benchmark(function, function_name):
    start = time.time()
    function()
    end = time.time()
    print("{0} seconds for {1}".format((end - start), function_name))

benchmark(create_basic_list, "create_basic_list")
benchmark(create_comprehension_list, "create_comprehension_list")
benchmark(create_basic_list_filtered, "create_basic_list_filtered")
benchmark(create_comprehension_list_filtered, "create_comprehension_list_filtered")

Thời gian chạy lần 1:
3.2794528007507324 seconds for create_basic_list
2.5837883949279785 seconds for create_comprehension_list
2.3607914447784424 seconds for create_basic_list_filtered
2.052985668182373 seconds for create_comprehension_list_filtered

Thời gian chạy lần 2:
3.187282085418701 seconds for create_basic_list
2.589780569076538 seconds for create_comprehension_list
2.3507118225097656 seconds for create_basic_list_filtered
2.1287717819213867 seconds for create_comprehension_list_filtered

Thời gian chạy lần 3:
3.20554518699646 seconds for create_basic_list
2.5390865802764893 seconds for create_comprehension_list
2.3841211795806885 seconds for create_basic_list_filtered
2.0037364959716797 seconds for create_comprehension_list_filtered

Nhìn kết quả trên, chúng ta đã thấy list-comprehension đang nhanh hơn cách làm thông thường, vậy có lý gì mà các lập trình Python lại không dùng list-comprehension nhỉ.

Kết luận

Ngoài list-comprehension, Python còn cung cấp set-comprehension, dict-comprehension với những ưu điểm không kém gì list-comprehension. Các bạn hãy khám phá thêm nhé.
Cảm ơn các bạn đã theo dõi bài viết.