Hướng dẫn python *args - python * args

Bài viết gốc https://manhhomienbienthuy.github.io/2019/09/20/python-args-kwargs.html

Nội dung chính

  • Sử dụng trong định nghĩa hàm
  • Sử dụng *args
  • Sử dụng **kwargs
  • Sử dụng để unpack
  • Unpack khi gọi hàm
  • Unpack khi gán biến
  • Các trường hợp unpack khác
  • Kết luận

Thỉnh thoảng, khi nhìn vào định nghĩa hàm, bạn có thể sẽ bắt gặp những hàm sử dụng cú pháp rất đặc biệt, đó là

>>> def foo(*args):
...     result = 0
...     for x in args:
...         result += x
...     return result
...
>>> foo(1, 2)
3
>>> foo(1, 2, 3)
6
4 và
>>> def foo(*args):
...     result = 0
...     for x in args:
...         result += x
...     return result
...
>>> foo(1, 2)
3
>>> foo(1, 2, 3)
6
5. Những người lần đầu tiên nhìn thấy cú pháp đó có thể sẽ cảm thấy ngỡ ngàng, không hiểu chúng có ý nghĩa gì và phải sử dụng như thế nào.

Có một điều chắc chắn là dù rất nhiều lập trình viên sử dụng cú pháp này, thì việc sử dụng tên

>>> def foo(*args):
...     result = 0
...     for x in args:
...         result += x
...     return result
...
>>> foo(1, 2)
3
>>> foo(1, 2, 3)
6
4 và
>>> def foo(*args):
...     result = 0
...     for x in args:
...         result += x
...     return result
...
>>> foo(1, 2)
3
>>> foo(1, 2, 3)
6
5 là hoàn toàn không bắt buộc. Chỉ có cú pháp với dấu
>>> def foo(*args):
...     result = 0
...     for x in args:
...         result += x
...     return result
...
>>> foo(1, 2)
3
>>> foo(1, 2, 3)
6
8 là bắt buộc mà thôi. Nếu muốn chúng ta hoàn toàn có thể viết là
>>> def foo(*args):
...     result = 0
...     for x in args:
...         result += x
...     return result
...
>>> foo(1, 2)
3
>>> foo(1, 2, 3)
6
9 và
>>> def foo(a, b, *args):
...     print('normal arguments', a, b)
...     for x in args:
...         print('another argument through *args', x)
...
>>> foo(1, 2, 3, 4)
normal arguments 1 2
another argument through *args 3
another argument through *args 4
0 cũng không gặp bất cứ vấn đề gì cả. Tuy nhiên,
>>> def foo(*args):
...     result = 0
...     for x in args:
...         result += x
...     return result
...
>>> foo(1, 2)
3
>>> foo(1, 2, 3)
6
4 và
>>> def foo(*args):
...     result = 0
...     for x in args:
...         result += x
...     return result
...
>>> foo(1, 2)
3
>>> foo(1, 2, 3)
6
5 được sử dụng phổ biến như một quy tắc ngầm vậy, do đó, hầu như mọi người đều sử dụng cách viết đó.

Trong bài viết này, chúng ta sẽ tìm hiểu xem chúng có ý nghĩa gì và cách sử dụng chúng như thế nào.

Sử dụng trong định nghĩa hàm

Sử dụng *args

Sử dụng **kwargs

>>> def foo(x, y):
...     return x + y
...
>>> foo(1, 2)
3

Sử dụng để unpack

Unpack khi gọi hàm

Unpack khi gán biến

Sử dụng *args

Sử dụng **kwargs

>>> def foo(numbers):
...     result = 0
...     for n in numbers:
...         result += n
...     return result
...
>>> foo([1, 2])
3
>>> foo([1, 2, 3])
6

Sử dụng để unpack

Unpack khi gọi hàm

>>> def foo(*args):
...     result = 0
...     for x in args:
...         result += x
...     return result
...
>>> foo(1, 2)
3
>>> foo(1, 2, 3)
6

Unpack khi gán biến

Các trường hợp unpack khác

Kết luận

>>> def foo(a, b, *args):
...     print('normal arguments', a, b)
...     for x in args:
...         print('another argument through *args', x)
...
>>> foo(1, 2, 3, 4)
normal arguments 1 2
another argument through *args 3
another argument through *args 4

Thỉnh thoảng, khi nhìn vào định nghĩa hàm, bạn có thể sẽ bắt gặp những hàm sử dụng cú pháp rất đặc biệt, đó là

>>> def foo(*args):
...     result = 0
...     for x in args:
...         result += x
...     return result
...
>>> foo(1, 2)
3
>>> foo(1, 2, 3)
6
4 và
>>> def foo(*args):
...     result = 0
...     for x in args:
...         result += x
...     return result
...
>>> foo(1, 2)
3
>>> foo(1, 2, 3)
6
5. Những người lần đầu tiên nhìn thấy cú pháp đó có thể sẽ cảm thấy ngỡ ngàng, không hiểu chúng có ý nghĩa gì và phải sử dụng như thế nào.

Có một điều chắc chắn là dù rất nhiều lập trình viên sử dụng cú pháp này, thì việc sử dụng tên

>>> def foo(*args):
...     result = 0
...     for x in args:
...         result += x
...     return result
...
>>> foo(1, 2)
3
>>> foo(1, 2, 3)
6
4 và
>>> def foo(*args):
...     result = 0
...     for x in args:
...         result += x
...     return result
...
>>> foo(1, 2)
3
>>> foo(1, 2, 3)
6
5 là hoàn toàn không bắt buộc. Chỉ có cú pháp với dấu
>>> def foo(*args):
...     result = 0
...     for x in args:
...         result += x
...     return result
...
>>> foo(1, 2)
3
>>> foo(1, 2, 3)
6
8 là bắt buộc mà thôi. Nếu muốn chúng ta hoàn toàn có thể viết là
>>> def foo(*args):
...     result = 0
...     for x in args:
...         result += x
...     return result
...
>>> foo(1, 2)
3
>>> foo(1, 2, 3)
6
9 và
>>> def foo(a, b, *args):
...     print('normal arguments', a, b)
...     for x in args:
...         print('another argument through *args', x)
...
>>> foo(1, 2, 3, 4)
normal arguments 1 2
another argument through *args 3
another argument through *args 4
0 cũng không gặp bất cứ vấn đề gì cả. Tuy nhiên,
>>> def foo(*args):
...     result = 0
...     for x in args:
...         result += x
...     return result
...
>>> foo(1, 2)
3
>>> foo(1, 2, 3)
6
4 và
>>> def foo(*args):
...     result = 0
...     for x in args:
...         result += x
...     return result
...
>>> foo(1, 2)
3
>>> foo(1, 2, 3)
6
5 được sử dụng phổ biến như một quy tắc ngầm vậy, do đó, hầu như mọi người đều sử dụng cách viết đó.

>>> def foo(a, *args, b):
...     print(a, b, args)
...
>>> foo(1, 2, 3, 4)
Traceback (most recent call last):
  File "", line 1, in <module>
TypeError: foo() missing 1 required keyword-only argument: 'b'

Sử dụng **kwargs

Sử dụng để unpack

Unpack khi gọi hàm

Unpack khi gán biến

Các trường hợp unpack khác

>>> def foo(a=0, b=1):
...     return a + b
...
>>> foo()
1
>>> foo(1, 2)
3
>>> foo(b=3, a=4)
7

Kết luận

>>> def foo(a):
...     for key, value in a.items():
...         print(key, value)
...
>>> foo({'a': 1, 'b': 2})
a 1
b 2

Cách làm này có nhiều bất tiện, thậm chí còn phức tạp hơn cả việc truyền vào một list cho hàm. Và trong trường hợp này,

>>> def foo(*args):
...     result = 0
...     for x in args:
...         result += x
...     return result
...
>>> foo(1, 2)
3
>>> foo(1, 2, 3)
6
5 vô cùng cần thiết.

>>> def foo(**kwargs):
...     for key, value in kwargs.items():
...         print(key, value)
...
>>> foo(a=1, b=2)
a 1
b 2

Như ví dụ ở trên, có thể thấy rằng việc sử dụng

>>> def foo(*args):
...     result = 0
...     for x in args:
...         result += x
...     return result
...
>>> foo(1, 2)
3
>>> foo(1, 2, 3)
6
5 đơn giản hơn rất nhiều. Thậm chí việc gọi hàm cũng dễ dàng hơn sử dụng dict.

Lưu ý rằng, với cách sử dụng

>>> def foo(*args):
...     result = 0
...     for x in args:
...         result += x
...     return result
...
>>> foo(1, 2)
3
>>> foo(1, 2, 3)
6
5 thì
>>> def foo(a=0, b=1):
...     return a + b
...
>>> foo()
1
>>> foo(1, 2)
3
>>> foo(b=3, a=4)
7
1 trong hàm sẽ nhận giá trị là một dict với key là các tham số được truyền kèm giá trị tương ứng của chúng.

Ngoài ra, cũng tương tự như

>>> def foo(*args):
...     result = 0
...     for x in args:
...         result += x
...     return result
...
>>> foo(1, 2)
3
>>> foo(1, 2, 3)
6
4,
>>> def foo(*args):
...     result = 0
...     for x in args:
...         result += x
...     return result
...
>>> foo(1, 2)
3
>>> foo(1, 2, 3)
6
5 cũng hoàn toàn có thể kết hợp được với các tham số thông thường khác, và kết hợp với cả
>>> def foo(*args):
...     result = 0
...     for x in args:
...         result += x
...     return result
...
>>> foo(1, 2)
3
>>> foo(1, 2, 3)
6
4 luôn. Nhưng thứ tự khi khai báo các tham số này rất quan trọng và không thể thay đổi được. Thứ tự đúng sẽ là:

  1. Các tham số bình thường
  2. >>> def foo(*args):
    ...     result = 0
    ...     for x in args:
    ...         result += x
    ...     return result
    ...
    >>> foo(1, 2)
    3
    >>> foo(1, 2, 3)
    6
    
    4
  3. >>> def foo(*args):
    ...     result = 0
    ...     for x in args:
    ...         result += x
    ...     return result
    ...
    >>> foo(1, 2)
    3
    >>> foo(1, 2, 3)
    6
    
    5

Việc kết hợp này rất phổ biến trong thực tế, nhưng một điều trớ trêu là các trường hợp mà tôi hay gặp lại thường dùng

>>> def foo(*args):
...     result = 0
...     for x in args:
...         result += x
...     return result
...
>>> foo(1, 2)
3
>>> foo(1, 2, 3)
6
4 và
>>> def foo(*args):
...     result = 0
...     for x in args:
...         result += x
...     return result
...
>>> foo(1, 2)
3
>>> foo(1, 2, 3)
6
5 để bỏ qua các tham số không cần xử lý (các tham số quan trọng được khai báo là tham số như thông thường).

Điều này cực kỳ phổ biến với các hàm nhận đầu vào từ form GUI hay lập trình web, vì dữ liệu đầu vào dạng này thường rất đa dạng, mà không phải dữ liệu nào nhận được chúng ta cũng cần xử lý.

>>> def foo(a, b, *args, **kwargs):
...     return a + b
...

Việc thay đổi thứ tự của

>>> def foo(*args):
...     result = 0
...     for x in args:
...         result += x
...     return result
...
>>> foo(1, 2)
3
>>> foo(1, 2, 3)
6
5 là không thể, nếu khai báo hàm với
>>> def foo(*args):
...     result = 0
...     for x in args:
...         result += x
...     return result
...
>>> foo(1, 2)
3
>>> foo(1, 2, 3)
6
5 trước bất kỳ một tham số nào, chúng ta sẽ gặp lỗi ngay:

>>> def foo(a, **kwargs, b):
  File "", line 1
    def foo(a, **kwargs, b):
                         ^
SyntaxError: invalid syntax

Sử dụng để unpack

Thực ra unpack không phải chính xác là sử dụng

>>> def foo(*args):
...     result = 0
...     for x in args:
...         result += x
...     return result
...
>>> foo(1, 2)
3
>>> foo(1, 2, 3)
6
4 và
>>> def foo(*args):
...     result = 0
...     for x in args:
...         result += x
...     return result
...
>>> foo(1, 2)
3
>>> foo(1, 2, 3)
6
5, nhưng cú pháp thì hoàn toàn giống nhau.

Unpack thực ra đã có từ Python 2, nhưng kể từ phiên bản 3.5, năng lực của nó đã được tăng lên hẳn vài bậc.

Unpack khi gọi hàm

Trong phần trước, chúng ta đã thấy cách sử dụng

>>> def foo(*args):
...     result = 0
...     for x in args:
...         result += x
...     return result
...
>>> foo(1, 2)
3
>>> foo(1, 2, 3)
6
4 và
>>> def foo(*args):
...     result = 0
...     for x in args:
...         result += x
...     return result
...
>>> foo(1, 2)
3
>>> foo(1, 2, 3)
6
5 để định nghĩa hàm. Không chỉ định nghĩa, nó còn có thể được sử dụng để gọi hàm.

Để minh hoạ, hãy xem xét hai cách gọi hàm như sau:

>>> def foo(numbers):
...     result = 0
...     for n in numbers:
...         result += n
...     return result
...
>>> foo([1, 2])
3
>>> foo([1, 2, 3])
6
0

Tôi nghĩ là kết quả trên đã phản ánh rất rõ cách hoạt động của unpack khi gọi hàm. Nếu cần một ví dụ khác, tôi nghĩ nên sử dụng một hàm tự định nghĩa, hãy xem xét một hàm đơn giản như sau:

>>> def foo(numbers):
...     result = 0
...     for n in numbers:
...         result += n
...     return result
...
>>> foo([1, 2])
3
>>> foo([1, 2, 3])
6
1

Chúng ta có thể sử dụng unpack để truyền tham số vào cho hàm. Nói một cách đơn giản thì cú pháp

>>> def foo(*args):
...     result = 0
...     for x in args:
...         result += x
...     return result
...
>>> foo(1, 2)
3
>>> foo(1, 2, 3)
6
8 được xử dụng với một đối tượng iterable, còn
>>> def foo(a):
...     for key, value in a.items():
...         print(key, value)
...
>>> foo({'a': 1, 'b': 2})
a 1
b 2
6 chỉ có thể dùng được với dict mà thôi.

>>> def foo(numbers):
...     result = 0
...     for n in numbers:
...         result += n
...     return result
...
>>> foo([1, 2])
3
>>> foo([1, 2, 3])
6
2

Cú pháp

>>> def foo(*args):
...     result = 0
...     for x in args:
...         result += x
...     return result
...
>>> foo(1, 2)
3
>>> foo(1, 2, 3)
6
8 và
>>> def foo(a):
...     for key, value in a.items():
...         print(key, value)
...
>>> foo({'a': 1, 'b': 2})
a 1
b 2
6 khi gọi hàm sẽ yêu cầu unpack giá trị được truyền vào trước khi thực hiện hàm đó. Và khi unpack, hàm sẽ nhận các tham số đơn lẻ như các tham số riêng biệt vậy.

Một lưu ý nhỏ là khi gọi hàm, số lượng các tham số của hàm và số lượng giá trị unpack được phải khớp nhau, nếu không sẽ có lỗi xảy ra:

>>> def foo(numbers):
...     result = 0
...     for n in numbers:
...         result += n
...     return result
...
>>> foo([1, 2])
3
>>> foo([1, 2, 3])
6
3

Như trong ví dụ trên, hàm chỉ nhận 3 tham số nhưng lại được truyền vào 4 giá trị nên chúng ta thấy lỗi đã xảy ra.

Trong các ví dụ trên, chúng ta thấy, việc unpack chủ yếu sử dụng cú pháp

>>> def foo(*args):
...     result = 0
...     for x in args:
...         result += x
...     return result
...
>>> foo(1, 2)
3
>>> foo(1, 2, 3)
6
8 mà ít khi sử dụng đến
>>> def foo(a):
...     for key, value in a.items():
...         print(key, value)
...
>>> foo({'a': 1, 'b': 2})
a 1
b 2
6. Nguyên nhân cũng là vì
>>> def foo(a):
...     for key, value in a.items():
...         print(key, value)
...
>>> foo({'a': 1, 'b': 2})
a 1
b 2
6 chỉ áp dụng được với dict. Và thực tế thì
>>> def foo(a):
...     for key, value in a.items():
...         print(key, value)
...
>>> foo({'a': 1, 'b': 2})
a 1
b 2
6 thường được dùng với dict trong trường hợp hàm có sử dụng keyword arguments:

>>> def foo(numbers):
...     result = 0
...     for n in numbers:
...         result += n
...     return result
...
>>> foo([1, 2])
3
>>> foo([1, 2, 3])
6
4

Lưu ý rằng, dict cũng là một iterable nên nó hoàn toàn có thể sử dụng

>>> def foo(*args):
...     result = 0
...     for x in args:
...         result += x
...     return result
...
>>> foo(1, 2)
3
>>> foo(1, 2, 3)
6
8 để unpack khi truyền hàm. Tuy nhiên, dùng
>>> def foo(*args):
...     result = 0
...     for x in args:
...         result += x
...     return result
...
>>> foo(1, 2)
3
>>> foo(1, 2, 3)
6
8 thì chúng ta sẽ chỉ truyền được key của dict vào cho hàm mà thôi:

>>> def foo(numbers):
...     result = 0
...     for n in numbers:
...         result += n
...     return result
...
>>> foo([1, 2])
3
>>> foo([1, 2, 3])
6
5

Ngoài ra, chúng ta hoàn toàn có thể unpack nhiều đối tượng khác nhau trong cùng một lời gọi hàm mà không gặp phải khó khăn gì (lưu ý duy nhất là số lượng giá trị sau khi unpack phải phù hợp với tham số của hàm):

>>> def foo(numbers):
...     result = 0
...     for n in numbers:
...         result += n
...     return result
...
>>> foo([1, 2])
3
>>> foo([1, 2, 3])
6
6

Bằng cách unpack nhiều giá trị như thế này, chúng ta đã làm "phẳng" các list này và truyền chúng như những giá trị riêng biệt vào hàm như trong ví dụ trên.

Unpack khi gán biến

Có nhiều trường hợp khác mà unpack cực kỳ cần thiết. Một nhu cầu khá thường xuyên của lập trình viên đó là chia giá trị một list (hoặc tuple) vào các biến riêng biệt. Như trong ví dụ dưới đây, chúng ta cần lấy ra giá trị đầu tiên, giá trị cuối cùng và các giá trị khác.

Sử dụng unpack cực kỳ nhanh chóng

>>> def foo(numbers):
...     result = 0
...     for n in numbers:
...         result += n
...     return result
...
>>> foo([1, 2])
3
>>> foo([1, 2, 3])
6
7

Nếu không có unpack, chúng ta sẽ phải làm một việc khá lòng vòng kiểu như thế này:

>>> def foo(numbers):
...     result = 0
...     for n in numbers:
...         result += n
...     return result
...
>>> foo([1, 2])
3
>>> foo([1, 2, 3])
6
8

Một vấn đề nho nhỏ là cú pháp

>>> def foo(a):
...     for key, value in a.items():
...         print(key, value)
...
>>> foo({'a': 1, 'b': 2})
a 1
b 2
6 không áp dụng được khi gán biến để unpack một dict được. Riêng điểm này thì ngôn ngữ JavaScript lại làm tốt hơn khi cho phép unpack cả một object với các thuộc tính cực kỳ phức tạp (hơi ngoài lề tí).

>>> def foo(numbers):
...     result = 0
...     for n in numbers:
...         result += n
...     return result
...
>>> foo([1, 2])
3
>>> foo([1, 2, 3])
6
9

Cú pháp unpack này có thể áp dụng với mọi đối tượng iterable, nhưng lưu ý rằng, đã gọi là unpack thì chúng ta phải chia giá trị ban đầu cho nhiều hơn một biến mới là unpack. Python cũng yêu cầu chúng ta, khi unpack thì phải gán giá trị cho một list hoặc tuple.

Ngoài ra cũng không thể sử dụng nhiều lần dấu

>>> def foo(*args):
...     result = 0
...     for x in args:
...         result += x
...     return result
...
>>> foo(1, 2)
3
>>> foo(1, 2, 3)
6
8 để unpack trong cùng một phép gán. Điều này cũng dễ hiểu thôi, vì nếu dùng nhiều dấu
>>> def foo(*args):
...     result = 0
...     for x in args:
...         result += x
...     return result
...
>>> foo(1, 2)
3
>>> foo(1, 2, 3)
6
8 thì biết các giá trị được phân chia như thế nào mà gán.

>>> def foo(*args):
...     result = 0
...     for x in args:
...         result += x
...     return result
...
>>> foo(1, 2)
3
>>> foo(1, 2, 3)
6
0

Có một trick nhỏ để giúp chúng ta unpack rồi gán cho một biến duy nhất, tuy nhiên chắc không ai dùng trick này làm gì cả vì trông nó không được thông minh cho lắm (không ai lại phải dùng unpack để gán biến này thành biến kia cả):

>>> def foo(*args):
...     result = 0
...     for x in args:
...         result += x
...     return result
...
>>> foo(1, 2)
3
>>> foo(1, 2, 3)
6
1

Các trường hợp unpack khác

Một điều thú vị là unpack có thể áp dụng với mọi đối tượng iterable, nó sẽ rất cần thiết nếu chúng ta cần làm "phẳng" 2 hay nhiều list, ví dụ:

>>> def foo(*args):
...     result = 0
...     for x in args:
...         result += x
...     return result
...
>>> foo(1, 2)
3
>>> foo(1, 2, 3)
6
2

Với dict, chúng ta cần đến cú pháp

>>> def foo(a):
...     for key, value in a.items():
...         print(key, value)
...
>>> foo({'a': 1, 'b': 2})
a 1
b 2
6 nếu muốn gộp hai dict với nhau:

>>> def foo(*args):
...     result = 0
...     for x in args:
...         result += x
...     return result
...
>>> foo(1, 2)
3
>>> foo(1, 2, 3)
6
3

Kết luận

Cú pháp

>>> def foo(*args):
...     result = 0
...     for x in args:
...         result += x
...     return result
...
>>> foo(1, 2)
3
>>> foo(1, 2, 3)
6
4 và
>>> def foo(*args):
...     result = 0
...     for x in args:
...         result += x
...     return result
...
>>> foo(1, 2)
3
>>> foo(1, 2, 3)
6
5 cho phép chúng ta định nghĩa hàm có thể nhận số lượng tham số tuỳ ý. Ngoài ra, cú pháp này còn có thể được sử dụng để unpack khi gọi hàm cũng như trong nhiều trường hợp khác.

Hy vọng bài viết giúp ích cho các bạn trong công việc.