Chuyển đến nội dung chính

Toán tử Walrus của Python: Biểu thức gán (Hướng dẫn)

Toán tử walrus := của Python cho phép bạn gán giá trị cho biến ngay trong một biểu thức, kết hợp thao tác gán và sử dụng trong một bước duy nhất.
· 8 phút đọc

Khám phá cùng AI

Python 3.8 đã giới thiệu một toán tử mới cho ngôn ngữ. Tên chính thức của nó là biểu thức gán, và nó dùng toán tử :=.

Tuy nhiên, nó thường được gọi là toán tử walrus vì trông giống như chiếc ngà của hải mã!

Đã có tranh luận trong cộng đồng Python vốn dĩ khá yên ả khi toán tử này được thêm vào ngôn ngữ, và mức độ sử dụng của nó vẫn còn tương đối hạn chế. Dù vậy, biểu thức gán giờ là một phần của Python và có thể hữu ích trong một số tình huống.

Hướng dẫn này sẽ khám phá cách toán tử walrus của Python hoạt động và thảo luận những tình huống có thể sử dụng nó.

Toán tử Walrus của Python là gì

biểu thức gán sử dụng toán tử :=, trông giống với câu lệnh gán phổ biến hơn, vốn dùng toán tử =. Tuy nhiên, có một khác biệt quan trọng giữa hai thứ này.

Một câu lệnh (statement) là một đơn vị mã thực hiện hành động nào đó. Một biểu thức (expression) là một câu lệnh có thể được đánh giá ra một giá trị. Do đó, một biểu thức trả về giá trị, còn những câu lệnh không phải biểu thức thì không trả về giá trị nào.

REPL của Python (Read, Evaluate, Print, and Loop) hiển thị giá trị của một biểu thức ngay khi nó được đánh giá. Tuy nhiên, không có giá trị nào được hiển thị khi thực thi một câu lệnh không phải biểu thức:

>>> import random  # Not an expression
>>> 10 * 2  # An expression
20

Câu lệnh import không được đánh giá ra bất kỳ giá trị nào. Trong khi đó, 10 * 2 là một biểu thức được đánh giá ra một giá trị.

Câu lệnh gán dùng toán tử = gán một đối tượng cho một biến hoặc một tham chiếu khác, nhưng không trả về giá trị đó. Biểu thức gán, hay toán tử walrus, thực hiện cùng hành động như câu lệnh gán nhưng đồng thời cũng trả về đối tượng:

>>> value = 10  # Assignment statement
>>> value
10
>>> (value := 20)  # Assignment expression
20

Lưu ý rằng câu lệnh gán không trả về giá trị, trái ngược với biểu thức gán. Vì vậy, một biểu thức gán kết hợp vài thao tác:

  1. Đánh giá biểu thức ở phía bên phải của toán tử :=.
  2. Gán giá trị này cho một tên biến.
  3. Trả về giá trị đó.

Dấu ngoặc tròn là cú pháp bắt buộc khi dùng biểu thức gán thay cho câu lệnh gán, như trong ví dụ này, để giảm mơ hồ. Phần sau của hướng dẫn sẽ nói thêm về yêu cầu cú pháp cho toán tử walrus.

Một biểu thức gán có thể dùng ở bất kỳ nơi nào biểu thức có thể dùng trong Python. Hãy xem ví dụ sau:

print(f"The number is {(value := 50)}")
print(value)
The number is 50
50

Tên biến value được định nghĩa bên trong f-string ở dòng đầu tiên. Tuy nhiên, số nguyên cũng được trả về trực tiếp vào cặp ngoặc nhọn trong f-string. Các ngoặc nhọn trong f-string phải chứa một biểu thức, đó là lý do vì sao không thể dùng câu lệnh gán ở đây.

Các trường hợp sử dụng Toán tử Walrus của Python

Hai trường hợp sử dụng phổ biến của toán tử walrus là đơn giản hóa và tối ưu hóa mã.

Đơn giản hóa mã

Một trường hợp thường gặp là đơn giản hóa đoạn mã mà bình thường một biến cần được định nghĩa trước khi dùng, chẳng hạn trong một số câu lệnh điều kiện. Hãy xem vòng lặp while sau:

import random
value = random.randint(1, 20)
while value < 18:
    print(value)
    value = random.randint(1, 20)
7
5
1
11
12
1
9

Đoạn mã này chạy khối while miễn là giá trị ngẫu nhiên tạo ra nhỏ hơn 18. Biến value cần được định nghĩa trước vòng lặp while để có thể dùng trong câu lệnh while. Một số ngẫu nhiên mới được gán cho value ở cuối khối while.

Biểu thức gán := có thể dùng để giảm số dòng mã và chỉ gọi hàm random.randint() tại một nơi trong chương trình:

import random
while (value := random.randint(1, 20)) < 18:
    print(value)
1
9
2
4
9
15
11

Một số ngẫu nhiên mới được tạo ra và gán cho value mỗi khi vòng lặp while bắt đầu một vòng lặp mới.

Tối ưu hóa mã

Hãy xem một ví dụ khác dùng list comprehension. Xét đoạn mã sau, lọc một danh sách chuỗi chứa ngày tháng và tạo một danh sách các datetime.datetime object chỉ chứa các ngày trong một năm cho trước:

from datetime import datetime
def format_date(date_str):
    return datetime.strptime(date_str, "%Y-%m-%d")
dates = ["2024-01-01", "2022-12-31", "2024-06-15", "2023-08-23", "2024-11-30"]
formatted_dates = [
    format_date(date)
    for date in dates
    if format_date(date).year == 2024
]
print(formatted_dates)
[datetime.datetime(2024, 1, 1, 0, 0), datetime.datetime(2024, 6, 15, 0, 0), datetime.datetime(2024, 11, 30, 0, 0)]

Hàm format_date() nhận một chuỗi ngày theo định dạng yyyy-mm-dd và trả về một đối tượng datetime.datetime. Một trong các thuộc tính của datetime.datetime là .year, chứa thành phần năm của ngày.

Chỉ ba ngày trong năm 2024 được đưa vào danh sách cuối, và danh sách này chứa các đối tượng datetime.datetime thay vì chuỗi. List comprehension có một mệnh đề if, trong đó gọi format_date(). Tuy nhiên, format_date() cũng được gọi ở biểu thức đầu tiên trong list comprehension, vốn tạo ra giá trị để thêm vào danh sách. Gọi hàm với cùng tham số hai lần là kém hiệu quả, đặc biệt nếu hàm là nút thắt cổ chai về hiệu năng.

Một cách để tránh gọi cùng một hàm hai lần là gọi hàm và gán nó cho một biến trước khi dùng. Tuy nhiên, điều này không thể làm với câu lệnh gán nếu vẫn muốn dùng list comprehension. Giải pháp này cần một vòng lặp for thông thường:

from datetime import datetime
def format_date(date_str):
    return datetime.strptime(date_str, "%Y-%m-%d")
dates = ["2024-01-01", "2022-12-31", "2024-06-15", "2023-08-23", "2024-11-30"]
formatted_dates = []
for date in dates:
    formatted_date = format_date(date)
    if formatted_date.year == 2024:
        formatted_dates.append(formatted_date)
print(formatted_dates)
[datetime.datetime(2024, 1, 1, 0, 0), datetime.datetime(2024, 6, 15, 0, 0), datetime.datetime(2024, 11, 30, 0, 0)]

Toán tử walrus cung cấp một lựa chọn thay thế tương thích với list comprehension, vì ngày đã được định dạng mà hàm trả về có thể được gán cho một tên biến và đồng thời trả về trong cùng một biểu thức:

from datetime import datetime
def format_date(date_str):
    return datetime.strptime(date_str, "%Y-%m-%d")
dates = ["2024-01-01", "2022-12-31", "2024-06-15", "2023-08-23", "2024-11-30"]
formatted_dates = [
    formatted_date
    for date in dates
    if (formatted_date := format_date(date)).year == 2024
]
print(formatted_dates)
[datetime.datetime(2024, 1, 1, 0, 0), datetime.datetime(2024, 6, 15, 0, 0), datetime.datetime(2024, 11, 30, 0, 0)]

Biểu thức gán trong list comprehension là biểu thức nằm trong ngoặc, (formatted_date := format_date(date)). Biểu thức này gọi hàm format_date(), gán giá trị của nó cho biến formatted_date, và trả về giá trị đó. Vì biểu thức gán được đánh giá ra một đối tượng datetime.datetime, nên mã truy cập thuộc tính .year của nó và so sánh với 2024 bằng toán tử bằng.

Mệnh đề if trong list comprehension được thực thi trước biểu thức tạo ra giá trị để lưu vào danh sách. Do đó, toán tử walrus được dùng trong mệnh đề if, và biến được định nghĩa trong biểu thức gán này được dùng làm biểu thức đầu tiên trong list comprehension.

Tranh luận quanh Toán tử Walrus của Python

Biểu thức gán không được tất cả lập trình viên Python yêu thích. Một số xem đây là cách đơn giản hóa mã, nhưng số khác cho rằng nó làm mã kém dễ đọc và thích các lựa chọn dài dòng hơn. Tính dễ đọc có thể mang tính chủ quan, nên ý kiến về việc toán tử walrus làm mã dễ đọc hơn hay kém đi là khác nhau.

Ví dụ cuối ở phần trước cho thấy các đánh đổi cần thiết. Không có toán tử walrus, đoạn mã này cần khởi tạo trước một danh sách rỗng, rồi thêm giá trị vào đó trong một vòng lặp for. Đây là giải pháp dài hơn và kém hiệu quả hơn so với việc dùng list comprehension, vốn được tối ưu cho công việc này.

Tuy nhiên, giải pháp vòng lặp for tiêu chuẩn có thể dễ đọc hơn với một số lập trình viên thích viết nhiều mã thay vì dùng toán tử walrus, đặc biệt khi hiệu năng không phải mối bận tâm.

Nhiều hướng dẫn phong cách mã hóa vẫn khuyến nghị tránh dùng biểu thức gán hoặc chỉ dùng một cách dè dặt.

Toán tử Walrus của Python: Quy tắc cú pháp

Bạn thường sẽ gặp lỗi cú pháp khi lần đầu tìm hiểu toán tử walrus do các quy tắc cú pháp của nó.

Để tránh nhầm lẫn giữa câu lệnh gán = và biểu thức gán :=, không có tình huống nào mà cả hai đều hợp lệ về mặt cú pháp. Vì vậy, thường cần đặt biểu thức gán trong dấu ngoặc:

(value := 20)  # Valid
value := 20  # Invalid (SyntaxError)

Yêu cầu đặt biểu thức gán trong ngoặc ở tình huống này là ngược lại với yêu cầu của câu lệnh gán, vốn không thể đặt trong ngoặc.

Tuy nhiên, biểu thức gán không phải lúc nào cũng cần ngoặc:

import random
if value := random.randint(0, 3):
    print(f"{value} is greater than 0")
1 is greater than 0

Biểu thức gán trong câu lệnh if tạo ra một số ngẫu nhiên từ 0 đến 3 và gán nó cho biến value. Các số nguyên 1, 2 và 3 là truthy, và mã trong khối if sẽ chạy khi các giá trị này được tạo. Tuy nhiên, khi random.randint() trả về 0, vốn là falsy, khối if không được thực thi.

Câu lệnh gán không hợp lệ sau từ khóa if. Do đó, không có sự nhầm lẫn trong tình huống này, và không cần dấu ngoặc.

Ví dụ này cũng minh họa một điểm quan trọng khác về biểu thức gán. Phạm vi của biến được gán tuân theo các quy tắc LEGB thông thường. Do đó, biến value khả dụng ở mọi nơi trong phạm vi toàn cục trong ví dụ này:

import random
if value := random.randint(0, 3):
    print(f"{value} is greater than 0")
   
print(value)
2 is greater than 0
2

Nếu một biểu thức gán được dùng bên trong định nghĩa hàm, biến đó chỉ khả dụng cục bộ trong hàm. Hành vi này giống với biến được tạo bằng câu lệnh gán.

Ví dụ này có thể được sửa đổi để cho thấy một cạm bẫy khác khi dùng toán tử walrus. Hãy thay đổi mã để thêm một toán tử so sánh trong câu lệnh if:

import random
if value := random.randint(0, 3) < 2:
    print(f"{value} is less than 2")
True is less than 2

Kết quả hiển thị gợi ý vấn đề của đoạn mã này vì value là Boolean True chứ không phải số nguyên. Toán tử walrus có độ ưu tiên thấp nhất trong số các toán tử, và chỉ dấu phẩy có độ ưu tiên thấp hơn. Vì vậy, toán tử nhỏ hơn < được đánh giá trước, và kết quả của nó được gán cho value. Cần dùng ngoặc để xử lý tình huống này:

import random
if (value := random.randint(0, 3)) < 2:
    print(f"{value} is less than 2")
0 is less than 2

Giờ đây, biểu thức gán được đánh giá trước vì ngoặc có độ ưu tiên cao nhất. Số được trả về bởi random.randint() được gán cho value và được dùng làm toán hạng thứ nhất cho toán tử nhỏ hơn.

Không giống câu lệnh gán, toán tử walrus chỉ có thể gán giá trị cho tên biến và không thể dùng để gán cho chỉ số (subscript) hoặc thuộc tính:

(name := "James")  # Valid
points = {"James": 10, "Mary": 20}
(points["Sarah"] := 30)  # Invalid
class Article:
    pass
article = Article()
(article.title := "The Walrus Operator")  # Invalid

Không thể gán giá trị bằng chỉ số hoặc cho thuộc tính khi dùng toán tử walrus.

Kết luận

Biểu thức gán, dùng cú pháp := và thường được gọi là toán tử walrus, là một bổ sung tương đối mới cho Python. Câu lệnh gán, dùng toán tử =, vẫn là cách phổ biến hơn để gán giá trị cho tên biến.

Tuy nhiên, toán tử walrus mở rộng thao tác gán này bằng cách cũng trả về giá trị được gán, biến thao tác gán thành một biểu thức. Do đó, bạn có thể dùng biểu thức gán ở bất cứ nơi nào có thể dùng các biểu thức khác trong Python.

Dẫu vậy, cộng đồng Python vẫn chia rẽ về tính hữu dụng và khả năng đọc của toán tử walrus. Vì lý do này, biểu thức gán không được dùng thường xuyên như các bổ sung mới khác của Python. Biết cách dùng toán tử này là rất tốt, nhưng đừng lạm dụng nó!

Stephen Gruppetta

Tác giả

Stephen Gruppetta

Tôi học Vật lý và Toán ở bậc đại học tại Đại học Malta. Sau đó, tôi chuyển đến London và lấy bằng Tiến sĩ Vật lý tại Imperial College. Tôi nghiên cứu các kỹ thuật quang học mới để chụp ảnh võng mạc người. Hiện nay, tôi tập trung vào việc viết về Python, truyền đạt về Python và giảng dạy Python.

Chủ đề

Liên quan