Lập trình hàm là một mô hình lập trình trong đó phương pháp tính toán chính là đánh giá các hàm. Trong hướng dẫn này, bạn sẽ khám phá lập trình hàm bằng Python.
Lập trình hàm thường đóng một vai trò không lớn lắm trong code Python. Nhưng sẽ tốt hơn nếu bạn quen với nó. Ở mức tối thiểu, đôi khi bạn sẽ gặp phải khi đọc code do người khác viết. Bạn thậm chí có thể tìm thấy các tình huống thuận lợi khi sử dụng các khả năng lập trình hàm của Python trong mã của riêng bạn thay vì viết mã một cách thông thường.
Lập trình hàm là gì?
Một hàm thuần túy là một hàm có giá trị đầu ra phụ thuộc chính xác vào các giá trị đầu vào của nó mà không có bất kỳ tác dụng phụ nào có thể quan sát được. Trong lập trình hàm, một chương trình hoàn toàn bao gồm việc đánh giá các hàm thuần túy. Quá trình tính toán tiến hành bằng các lệnh gọi hàm lồng nhau, không có thay đổi đối với trạng thái hoặc dữ liệu.
Mô hình lập trình hàm phổ biến vì nó cung cấp một số lợi thế so với các mô hình lập trình khác. Các code trong lập trình hàm:
- Mã cấp cao: Bạn đang mô tả kết quả bạn muốn thay vì chỉ định rõ ràng các bước cần thiết để đạt được điều đó. Các câu lệnh đơn rất ngắn gọn..
- Minh bạch: Hành vi của một hàm thuần túy chỉ phụ thuộc vào đầu vào và đầu ra của nó, không có các giá trị trung gian. Điều đó giúp loại bỏ khả năng xảy ra các tác dụng phụ, tạo điều kiện thuận lợi cho việc gỡ lỗi.
- Có thể thực hiện song song: Do đặc tính không có tác dụng phụ có thể dễ dàng chạy song song với nhau hơn.
Nhiều ngôn ngữ lập trình hỗ trợ một số mức độ lập trình hàm khác nhau. Trong một số ngôn ngữ, hầu như tất cả mã đều tuân theo mô hình lập trình hàm. Haskell là một trong những ví dụ như vậy. Ngược lại, Python cũng hỗ trợ lập trình hàm nhưng đồng thời chứa các tính năng của các mô hình lập trình khác.
Python hỗ trợ lập trình hàm tốt như thế nào?
Trong lập trình hàm, sẽ rất hữu ích nếu một hàm trong một ngôn ngữ lập trình nhất định có hai khả năng:
- Lấy một hàm khác làm đối số
- Trả về một hàm khác cho trình gọi của nó
Python hỗ trợ tốt ở cả hai khía cạnh này. Có thể bạn đã biết, mọi thứ trong chương trình Python đều là một đối tượng. Tất cả các đối tượng trong Python ít nhiều đều có tầm vóc như nhau và các hàm cũng không ngoại lệ.
Trong Python, các hàm là đối tượng hạng nhất. Điều đó có nghĩa là các hàm có cùng đặc điểm với các giá trị như chuỗi và số. Bất cứ điều gì bạn mong đợi có thể thực hiện với một chuỗi hoặc số, bạn cũng có thể làm với một hàm.
Ví dụ, bạn có thể gán một hàm cho một biến. Sau đó, bạn có thể sử dụng biến đó giống như cách bạn sử dụng chính hàm:
>>> def func():
... print("Tôi là một hàm()!")
...
>>> func()
Tôi là một hàm()!
>>> another_name = func
>>> another_name()
Tôi là một hàm()!
Phép gán another_name = func tạo ra một tham chiếu mới đến func() có tên là another_name. Sau đó, bạn có thể gọi hàm bằng func hoặc another_name.
Bạn có thể hiển thị một hàm ra console với print(), thêm nó vào thành một phần tử của một danh sách hoặc thậm chí sử dụng nó làm khóa của dictionary:
>>> def func():
... print("Tôi là một hàm()!")
...
>>> print("cat", func, 42)
cat <function func at 0x7f81b4d29bf8> 42
>>> objects = ["cat", func, 42]
>>> objects[1]
<function func at 0x7f81b4d29bf8>
>>> objects[1]()
Tôi là một hàm()!
>>> d = {"cat": 1, func: 2, 42: 3}
>>> d[func]
2
Với mục đích hiện tại, điều quan trọng là các hàm trong Python đáp ứng hai tiêu chí có lợi cho lập trình hàm được liệt kê ở trên. Bạn có thể truyền một hàm cho một hàm khác dưới dạng đối số:
>>> def trong():
... print("Tôi là hàm bên trong()!")
...
>>> def ngoai(function):
... function()
...
>>> ngoai(trong)
Tôi là hàm bên trong()!
Đây là những gì đang xảy ra trong ví dụ trên:
- Lời gọi hàm chuyển trong() làm đối số cho hàm ngoai().
- Bên trong ngoai(), Python liên kết trong() với tham số là hàm.
- ngoai() sau đó có thể gọi trong() trực tiếp thông qua hàm.
Khi bạn truyền một hàm cho một hàm khác, hàm truyền vào đôi khi được gọi là lệnh gọi lại vì lệnh gọi hàm bên trong có thể thay đổi hành vi của hàm bên ngoài. Một ví dụ điển hình về điều này là hàm sorted() trong Python. Thông thường, nếu bạn chuyển một danh sách các giá trị chuỗi vào sorted(), thì nó sẽ sắp xếp chúng theo thứ tự alphabet:
>>> animals = ["ferret", "vole", "dog", "gecko"]
>>> sorted(animals)
['dog', 'ferret', 'gecko', 'vole']
Tuy nhiên, sorted() nhận một đối số khóa tùy chọn chỉ định một hàm gọi lại có thể dùng làm khóa sắp xếp. Ví dụ, bạn có thể sắp xếp theo độ dài chuỗi:
>>> animals = ["ferret", "vole", "dog", "gecko"]
>>> sorted(animals, key=len)
['dog', 'vole', 'gecko', 'ferret']
sorted() cũng có thể nhận một đối số tùy chọn chỉ định sắp xếp theo thứ tự ngược lại. Nhưng bạn có thể quản lý điều tương tự bằng cách xác định hàm gọi lại của riêng bạn để đảo ngược ý nghĩa của len():
>>> animals = ["ferret", "vole", "dog", "gecko"]
>>> sorted(animals, key=len, reverse=True)
['ferret', 'gecko', 'vole', 'dog']
>>> def reverse_len(s):
... return -len(s)
...
>>> sorted(animals, key=reverse_len)
['ferret', 'gecko', 'vole', 'dog']
Cũng giống như bạn có thể truyền một hàm cho một hàm khác dưới dạng đối số, một hàm cũng có thể chỉ định một hàm khác làm giá trị trả về của nó:
>>> def ngoai():
... def trong():
... print("Tôi là hàm bên trong()!")
...
... # Hàm ngoai() trả về hàm trong()
... return trong
...
>>> func = ngoai()
>>> func
<function outer.<locals>.inner at 0x7f18bc85faf0>
>>> func()
Tôi là hàm bên trong()!
>>> ngoai()()
Tôi là hàm bên trong()!
Như bạn thấy, Python hỗ trợ lập trình chức năng một cách độc đáo. Tuy nhiên, trước khi bạn chuyển sang mã code lập trình hàm, có một khái niệm nữa sẽ hữu ích để bạn khám phá: biểu thức lambda.
Định nghĩa một hàm ẩn danh với lambda
Tất cả những việc Lập trình hàm gọi các hàm và truyền các tham số hàm, vì vậy nó đương nhiên liên quan đến việc định nghĩa rất nhiều hàm. Bạn luôn có thể định nghĩa một hàm theo cách thông thường, sử dụng từ khóa def.
Tuy nhiên, đôi khi, thật tiện lợi khi có thể xác định nhanh một hàm ẩn danh mà không cần phải đặt tên cho nó. Trong Python, bạn có thể làm điều này với biểu thức lambda.
lambda <parameter_list>: <expression>
Một ví dụ đơn giản như sau
>>> reverse = lambda s: s[::-1]
>>> reverse("nghia")
'aihgn'
Hàm định nghĩa theo cách này hoàn toàn tương tự định nghĩa bằng cách sử dụng def. Tuy nhiên, không cần thiết phải gán một biến cho biểu thức lambda trước khi gọi nó. Bạn cũng có thể gọi trực tiếp hàm được định nghĩa bởi biểu thức lambda:
>>> (lambda s: s[::-1])(" nghia")
'aihgn'
Đây là một ví dụ khác:
>>> (lambda x1, x2, x3: (x1 + x2 + x3) / 3)(9, 6, 6)
7.0
>>> (lambda x1, x2, x3: (x1 + x2 + x3) / 3)(1.4, 1.1, 0.5)
1.0
Một ví dụ khác, hãy nhớ lại ở trên khi bạn xác định một hàm reverse_len() để phục vụ như một key cho hàm sorted(). Bạn cũng có thể sử dụng một hàm lambda ở đây:
>>> animals = ["ferret", "vole", "dog", "gecko"]
>>> sorted(animals, key=lambda s: -len(s))
['ferret', 'gecko', 'vole', 'dog']
Một biểu thức lambda thường sẽ có một danh sách tham số, nhưng nó không bắt buộc. Bạn có thể xác định một hàm lambda mà không có tham số. Giá trị trả về sau đó không phụ thuộc vào bất kỳ tham số đầu vào nào:
>>> bon_muoi_hai = lambda: 42
>>> bon_muoi_hai()
42
Lưu ý rằng bạn chỉ có thể định nghĩa các hàm khá thô sơ với lambda. Giá trị trả về từ một biểu thức lambda chỉ có thể là một biểu thức duy nhất. Một biểu thức lambda không được chứa các câu lệnh như gán hoặc trả về, cũng như không được chứa các cấu trúc điều khiển như for, while, if, else hoặc def.
Như đã biết, một hàm được định nghĩa bằng def có thể trả về nhiều giá trị một cách hiệu quả. Nếu câu lệnh trả về trong một hàm chứa một số giá trị được phân tách bằng dấu phẩy, thì Python sẽ đóng gói chúng và trả về dưới dạng một tuple:
>>> def func(x):
... return x, x ** 2, x ** 3
...
>>> func(3)
(3, 9, 27)
Trả về tuple kiểu này không hoạt động với một hàm lambda ẩn danh:
>>> (lambda x: x, x ** 2, x ** 3)(3)
<stdin>:1: SyntaxWarning: 'tuple' object is not callable; perhaps you missed a comma?
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
NameError: name 'x' is not defined
Nhưng bạn có thể trả về một tuple từ một hàm lambda. Bạn chỉ cần biểu thị tuple một cách rõ ràng bằng dấu ngoặc đơn. Bạn cũng có thể trả về list hoặc dictionary từ hàm lambda:
>>> (lambda x: (x, x ** 2, x ** 3))(3)
(3, 9, 27)
>>> (lambda x: [x, x ** 2, x ** 3])(3)
[3, 9, 27]
>>> (lambda x: {1: x, 2: x ** 2, 3: x ** 3})(3)
{1: 3, 2: 9, 3: 27}
Một biểu thức lambda có không gian tên cục bộ của riêng nó, do đó, các tên tham số không xung đột với các tên biến toàn cục. Một biểu thức lambda có thể truy cập các biến toàn cục, nhưng nó không thể sửa đổi chúng.
Tiếp theo, đã đến lúc đi sâu vào lập trình hàm trong Python. Bạn sẽ thấy các hàm lambda đặc biệt thuận tiện như thế nào khi viết mã trong lập trình hàm.
Python cung cấp hai hàm tích hợp, map() và filter(), phù hợp với mô hình lập trình hàm. Hàm thứ ba, reduce(), không còn là thành phần của core python nhưng vẫn có sẵn từ một mô-đun được gọi là functools. Mỗi một trong ba hàm này nhận một hàm khác làm đối số của nó.
Áp dụng một hàm cho một danh sách với map()
Hàm đầu tiên là map(), là một hàm tích hợp sẵn trong Python. Với map(), bạn có thể lần lượt áp dụng một hàm cho từng phần tử trong một dãy. Điều này có thể cho phép một số mã rất ngắn gọn vì câu lệnh map() thường có thể thay thế cho một vòng lặp.
Gọi map() Với một dãy đơn
map(<f>, <iterable>)
Đây là một ví dụ.
>>> def reverse(s):
... return s[::-1]
...
>>> animals = ["cat", "dog", "hedgehog", "gecko"]
>>> iterator = map(reverse, animals)
>>> iterator
<map object at 0x7fd3558cbef0>
Nhưng hãy nhớ rằng map() không trả về một list. Nó trả về một trình lặp được gọi là một map object. Để lấy các giá trị từ đối tượng này, bạn cần phải lặp lại nó hoặc sử dụng list():
>>> iterator = map(reverse, animals)
>>> for i in iterator:
... print(i)
...
tac
god
gohegdeh
okceg
>>> iterator = map(reverse, animals)
>>> list(iterator)
['tac', 'god', 'gohegdeh', 'okceg']
Trong ví dụ này, reverse() là một hàm khá ngắn, một hàm mà bạn có thể không cần định nghĩa tường minh để sử dụng với map().Bạn có thể sử dụng một hàm lambda ẩn danh để thay thế:
>>> animals = ["cat", "dog", "hedgehog", "gecko"]
>>> iterator = map(lambda s: s[::-1], animals)
>>> list(iterator)
['tac', 'god', 'gohegdeh', 'okceg']
>>> # Kết hợp trên 1 dòng:
>>> list(map(lambda s: s[::-1], ["cat", "dog", "hedgehog", "gecko"]))
['tac', 'god', 'gohegdeh', 'okceg']
Nếu có thể lặp lại chứa các mục không phù hợp với hàm được chỉ định, thì Python sẽ đưa ra một lỗi:
>>> list(map(lambda s: s[::-1], ["cat", "dog", 3.14159, "gecko"]))
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "<stdin>", line 1, in <lambda>
TypeError: 'float' object is not subscriptable
Gọi map() với nhiều dãy
map(<f>, <iterable₁>, <iterable₂>, ..., <iterableₙ>)
Một ví dụ sẽ giúp cú pháp rõ hơn:
>>> def f(a, b, c):
... return a + b + c
...
>>> list(map(f, [1, 2, 3], [10, 20, 30], [100, 200, 300]))
[111, 222, 333]
Trong trường hợp này, f() nhận ba đối số. Tương ứng, có ba đối số có thể lặp lại cho map(): danh sách [1, 2, 3], [10, 20, 30] và [100, 200, 300]. Mục đầu tiên được trả về là kết quả của việc áp dụng f() cho phần tử đầu tiên trong mỗi danh sách: f(1, 10, 100). Mục thứ hai được trả về là f(2, 20, 200) và mục thứ ba là f(3, 30, 300), như thể hiện trong sơ đồ sau:
Giá trị trả về từ map() là một trình lặp tạo ra danh sách [111, 222, 333].
Chọn các phần tử từ dãy với filter()
filter() cho phép bạn chọn lọc các mục từ một dãy dựa trên đánh giá của hàm đã cho. Nó được gọi như sau:
filter(<f>, <iterable>)
filter (<f>, <iterable>) áp dụng hàm <f> cho từng phần tử của <iterable> và trả về một trình lặp mang lại tất cả các phần tử mà <f> trả về true.
Xét ví dụ
>>> def lon_hon_100(x):
... return x > 100
...
>>> list(filter(lon_hon_100, [1, 111, 2, 222, 3, 333]))
[111, 222, 333]
Trong trường hợp này, lon_hon_100() là true cho các phần tử 111, 222 và 333, vì vậy các mục này vẫn còn, trong khi 1, 2 và 3 bị loại bỏ. Như trong các ví dụ trước, hàm này là một hàm ngắn và bạn có thể thay thế nó bằng một biểu thức lambda:
>>> list(filter(lambda x: x > 100, [1, 111, 2, 222, 3, 333]))
[111, 222, 333]
Ví dụ tiếp theo sử dụng hàm range (). range(n) tạo ra một trình lặp cho ra các số nguyên từ 0 đến n – 1. Ví dụ sau sử dụng filter() để chỉ chọn các số chẵn từ danh sách và lọc bỏ các số lẻ:
>>> list(range(10))
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
>>> def is_even(x):
... return x % 2 == 0
...
>>> list(filter(is_even, range(10)))
[0, 2, 4, 6, 8]
>>> list(filter(lambda x: x % 2 == 0, range(10)))
[0, 2, 4, 6, 8]
Kết luận
Lập trình hàm là một mô hình lập trình trong đó phương pháp tính toán chính là đánh giá các hàm thuần túy. Mặc dù Python chủ yếu không phải là một ngôn ngữ lập trình hàm, nhưng bạn nên làm quen với lambda, map(), filter() vì chúng có thể giúp bạn viết mã ngắn gọn hơn và có thể tính toán song song. Bạn cũng sẽ thấy chúng trong mã mà những người khác đã viết.
Nếu bạn quan tâm đến sử dụng kỹ năng python để tạo một game thì có thể tham khảo thêm khóa học Học python cơ bản thông qua lập trình một game