Một trong những yêu cầu cơ bản của mọi ngôn ngữ lập trình là truyền các tham số cho hàm (function) trong các ngôn ngữ lập trình hướng thủ tục hoặc phương thức (method) trong các ngôn ngữ lập trình hướng đối tượng. Python cũng không ngoại lệ.
Cách khai báo và truyền các tham số vào các phương thức (để bảo đảm tính nhất quán, chúng ta tạm sử dụng thuật ngữ phương thức cho cả phương thức và hàm trong Python) trong Python cũng tương tự như các ngôn ngữ lập trình phổ biến khác. Ví dụ như sau:
Để khai báo một phương thức có sử dụng tham số:
1 2 3 4 |
def single_param_method_1(param): ''' Khai báo trên sẽ tạo ra một phương thức có tên là single_param_method_1 và chấp nhận một tham số là param với kiểu bất kỳ. Tham số cũng có thể trả về một giá trị với kiểu bất kỳ ''' |
Hoặc để khai báo kiểu của tham số và kiểu của giá trị trả về từ phương thức, chúng ta có thể sử dụng cách khai báo mới hơn có dùng gợi ý kiểu (type hint). Kiểu khai báo này được đặc tả trong các PEP 483 và PEP 484 như trong ví dụ sau:
1 2 3 4 |
def single_param_method_t2(param: str) -> str: ''' Khai báo trên cũng tạo ra một phương thức có tên là single_param_method_2, phương thức này sẽ chấp nhận một tham số là param có kiểu chuỗi (str) và trả về một giá trị cũng kiểu chuỗi. Khai báo này khác với khai báo đầu tiên ở chỗ nếu tham số hoặc giá trị trả về của tham số không phải kiểu chuỗi sẽ tạo ra một thông báo lỗi khi chạy chương trình. ''' |
Khi thực hiện chương trình, phương thức sẽ nhận các giá trị được truyền cho tham số như trong ví dụ sau:
1 2 3 4 5 6 |
def greeting(name: str) -> str: greeting_string = "Hello, {name}" return greeting_string greeting_a_friend = greeting("Hải") print(greeting_a_friend) |
Chương trình này sẽ in ra dòng chữ “Hello, Hải” khi được thực hiện.
Khi muốn truyền nhiều tham số cho phương thức và biết trước số lượng tham số, chúng ta có thể sử dụng cú pháp dưới đây để khai báo phương thức:
1 2 3 4 |
def multi_params_method_1(param_1, param_2, param_3): ''' Khai báo trên sẽ tạo ra phương thức gọi là multi_params_method_1 và chấp nhận 3 tham số lần lượt là param_1, param_2 và param_3 với kiểu bất kỳ (Chúng ta cũng có thể áp dụng type hint tương tự như khi khai báo phương thức single_param_method_2. ''' |
Cách truyền tham số cho các phương thức với nhiều tham số như trên cũng tương tự như với phương thức chỉ có một tham số như trong ví dụ sau:
1 2 3 4 5 |
def greetings(name_1, name_2, name_3): return f"Hello, {name_1}, {name_2} and {name_3}" greeting_friends = greetings("Hải", "Vân", "An") print(greeting_friends) |
Chương trình này sẽ in ra dòng chữ “Hello, Hải, Vân và An” khi được thực hiện.
Đến đây, chúng ta đã biết cách để truyền một hoặc nhiều tham số cho phương thức và sử dụng chúng. Tuy nhiên, cách này chỉ áp dụng được nếu chúng ta biết được số lượng tham số cần truyền khi khai báo phương thức. Trong nhiều tình huống thực tế, chúng ta không biết trước được số lượng tham số cần truyền cho đến khi thực hiện chương trình. Ví dụ như trong phương thức greetings ở ví dụ trên, nếu lúc thực hiện chương trình chúng ta phát hiện ra rằng chúng ta có tên của hơn 3 người như giả định trong khai báo của phương thức, và cách duy nhất để khắc phục là phải sửa lại khai báo để phương thức có thể nhận vào nhiều tham số hơn 3, nhưng đây chỉ là giải pháp tạm thời vì nếu số tham số luôn thay đổi mỗi lần gọi phương thức thì chúng ta vẫn gặp rắc rối. Làm cách nào để giải quyết triệt để tình huống này?
Câu trả lời là khai báo các tham số đặc biệt *args và **kwargs trong Python. Đây là một trong những điểm mạnh của Python vì nó là một ngôn ngữ lập trình rất linh hoạt và luôn có một giải pháp dễ dàng cho nhiều trường hợp khác nhau.
Chúng ta hãy bắt đầu với *args. Để khai báo một số không xác định các tham số cho một phương thức, chúng ta có thể sử dụng cú pháp như sau:
1 |
def multi_param_method_2(*args): |
Lưu ý là *args chỉ là quy ước (args là viết tắt của từ arguments cũng có nghĩa là tham số). Trong thực tế, ngoại trừ dấu * là bắt buộc, args có thể được thay đổi thành một từ bất kỳ. Ví dụ: cách khai báo như sau vẫn được xem là hợp lệ:
1 |
def multi_param_method_2(*tham_so): |
Tuy nhiên, cách khai báo với *args là quy ước được sử dụng rộng rãi trong cộng đồng lập trình viên Python, vì vậy, chúng ta nên làm quen với cách khai báo này.
Với cách khai báo này, *args sẽ hoạt động như là một danh sách (list) bên trong phương thức. Vì vậy, chúng ta có thể sử dụng các phương thức để truy cập danh sách với *args như trong ví dụ sau:
1 2 3 4 5 6 7 |
def test_args(*args): print(args[0]) # Truy cập theo chỉ mục (index) print(args[:2]) # Truy cập sử dụng phương pháp slide cho list for name in args: # Truy cập tuần tự nhờ vòng lặp print(name) test_args("Hải", "Vân", "An", "Tâm") |
Khi thực hiện, chương trình sẽ in ra kết quả như sau:
Hải # print(args[0])
(‘Hải’, ‘Vân’) # print(args[:2])
Hải
Vân
An
Tâm # print(name) trong vòng lặp for
Rất hữu ích, phải không các bạn? Tuy nhiên, vẫn còn một hạn chế với *args là chúng ta phải truy cập các giá trị tham số qua vị trí (index) hoặc vòng lặp tuần tự vì bản chất của *args là một danh sách. Nếu chúng ta cần truy cập giá trị của tham số theo từ khóa (keyword) they vì vị trí, chúng ta sẽ cần đến **kwargs.
Tương tự như với *args, chỉ có hai dấu * mở đầu trong cú pháp **kwargs là bắt buộc, còn lại là tùy chọn (kwargs là viết tắt của “key word arguments” – tham số theo từ khóa). Tuy nhiên, đây cũng là một quy ước quen thuộc trong cộng đồng Python và chúng ta nên làm quen với cách đặt tên này.
Cách khai báo **kwargs cũng tương tự như *args và chúng cũng giống nhau là đều cho phép phương thức nhận một số lượng tham số bất kỳ. Tuy nhiên, khác với *args, các thành phần của **kwargs có thể được truy cập thông qua từ khóa thay vì chỉ mục. Chúng ta có thể thấy rõ hơn qua ví dụ sau:
1 2 3 4 5 6 7 |
def test_kwargs(**kwargs): print(kwargs["Name"]) # Truy cập theo từ khóa print(kwargs["Age"]) for key, value in kwargs.items(): # Liệt kê các thành phần của kwargs print("{0} = {1}".format(key, value)) test_kwargs(Name="Hải", Age=20, Job="Engineer") |
Khi thực hiện, chương trình sẽ in ra kết quả như sau:
Hải # print(kwargs[“Name”])
20 # print(kwargs[“Age”])
Name = Hải # for key, value in kwargs.items():
Age = 20
Job = Engineer
Ứng dụng:
Một trong những ứng dụng thường gặp của *args và **kwargs là trong các phương thức wrapper (dùng để tạo ra các decorator cho các phương thức khác). Ví dụ như time wrapper dùng để đo thời gian thực hiện của các phương thức như trong ví dụ dưới đây:
Định nghĩa phương thức wrapper:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 |
import time def timer(func): def wrapper(*args, **kwargs): # Bắt đầu tính giờ start_time = time.time() # Gọi phương thức cần đo thời gian result = func(*args, **kwargs) # Chấm dứt bộ đếm giờ end_time = time.time() # Tính thời gian thực hiện phương thức được gọi và hiển thị execution_time = end_time - start_time print(f"Thời gian thực hiện: {execution_time} seconds") # Trả về kết quả của phương thức đã được gọi return result # Trả về tham chiếu đến phương thức wrapper return wrapper |
Sau đó, để đo thời gian thực hiện của một phương thức bất kỳ, chúng ta có thể gọi wrapper như sau:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
@timer def test(): print("Bắt đầu thực hiện phương thức test...") # Mô phỏng thời gian thực hiện phương thức trong 5 giây time.sleep(5) print("Phương thức test hoàn tất!") test() |
Đây là kết quả khi thực hiện chương trình:
Bắt đầu thực hiện phương thức test…”
Phương thức test hoàn tất!”
Thời gian thực hiện: 5.006425619125366 seconds
Rất hay, phải không các bạn?
TLDR: Sử dụng *args và **kwargs cho các phương thức với số lượng tham số không xác định. *args thích hợp cho các trường hợp truy cập tuần tự hoặc theo chỉ mục (index), **kwargs thích hợp cho các trường hợp truy cập ngẫu nhiên đến các giá trị của tham số bằng từ khóa (keyword).
Hy vọng bài viết này sẽ giúp cho các bạn hiểu rõ hơn về khái niệm *args và **kwargs cũng như ứng dụng trong thực tiễn