Lập trình chức năng HOWTO

tác giả:

A. M. Kuchling

Phát hành:

0,32

Trong tài liệu này, chúng ta sẽ tìm hiểu các tính năng của Python phù hợp để triển khai các chương trình theo kiểu chức năng. Sau phần giới thiệu về các khái niệm về lập trình hàm, chúng ta sẽ xem xét các tính năng ngôn ngữ như iterators và generators và các mô-đun thư viện có liên quan như itertoolsfunctools.

Giới thiệu

Phần này giải thích khái niệm cơ bản về lập trình chức năng; nếu bạn chỉ muốn tìm hiểu về các tính năng của ngôn ngữ Python, hãy chuyển sang phần tiếp theo trên Trình vòng lặp.

Ngôn ngữ lập trình hỗ trợ phân rã vấn đề theo nhiều cách khác nhau:

  • Hầu hết các ngôn ngữ lập trình là procedural: chương trình là danh sách các hướng dẫn cho máy tính biết phải làm gì với đầu vào của chương trình. C, Pascal và thậm chí cả Unix shell là các ngôn ngữ thủ tục.

  • Trong các ngôn ngữ declarative, bạn viết một đặc tả mô tả vấn đề cần giải quyết và việc triển khai ngôn ngữ sẽ tìm ra cách thực hiện tính toán một cách hiệu quả. SQL là ngôn ngữ khai báo mà bạn có thể quen thuộc nhất; truy vấn SQL mô tả tập dữ liệu bạn muốn truy xuất và công cụ SQL quyết định xem nên quét bảng hay sử dụng chỉ mục, mệnh đề con nào sẽ được thực hiện trước, v.v.

  • Các chương trình Object-oriented thao tác các bộ sưu tập đối tượng. Các đối tượng có trạng thái bên trong và các phương thức hỗ trợ truy vấn hoặc sửa đổi trạng thái bên trong này theo một cách nào đó. Smalltalk và Java là các ngôn ngữ hướng đối tượng. C++ và Python là những ngôn ngữ hỗ trợ lập trình hướng đối tượng nhưng không bắt buộc sử dụng các tính năng hướng đối tượng.

  • Lập trình Functional phân tích một vấn đề thành một tập hợp các hàm. Lý tưởng nhất là các hàm chỉ nhận đầu vào và tạo ra đầu ra và không có bất kỳ trạng thái bên trong nào ảnh hưởng đến đầu ra được tạo ra cho một đầu vào nhất định. Các ngôn ngữ chức năng nổi tiếng bao gồm họ ML (ML tiêu chuẩn, OCaml và các biến thể khác) và Haskell.

Các nhà thiết kế của một số ngôn ngữ máy tính chọn nhấn mạnh một cách tiếp cận cụ thể để lập trình. Điều này thường gây khó khăn cho việc viết chương trình sử dụng cách tiếp cận khác. Các ngôn ngữ khác là những ngôn ngữ đa mô hình hỗ trợ một số cách tiếp cận khác nhau. Lisp, C++ và Python là nhiều mô hình; bạn có thể viết các chương trình hoặc thư viện phần lớn mang tính thủ tục, hướng đối tượng hoặc chức năng bằng tất cả các ngôn ngữ này. Trong một chương trình lớn, các phần khác nhau có thể được viết bằng các cách tiếp cận khác nhau; Ví dụ: GUI có thể hướng đối tượng trong khi logic xử lý là thủ tục hoặc chức năng.

Trong một chương trình chức năng, đầu vào chảy qua một tập hợp các chức năng. Mỗi chức năng hoạt động trên đầu vào của nó và tạo ra một số đầu ra. Kiểu chức năng không khuyến khích các hàm có tác dụng phụ sửa đổi trạng thái bên trong hoặc thực hiện các thay đổi khác không hiển thị trong giá trị trả về của hàm. Các hàm hoàn toàn không có tác dụng phụ được gọi là purely functional. Tránh tác dụng phụ có nghĩa là không sử dụng cấu trúc dữ liệu được cập nhật khi chương trình chạy; đầu ra của mọi chức năng chỉ phải phụ thuộc vào đầu vào của nó.

Một số ngôn ngữ rất nghiêm ngặt về độ thuần khiết và thậm chí không có câu lệnh gán như a=3 hoặc c = a + b, nhưng khó tránh khỏi mọi tác dụng phụ, chẳng hạn như in ra màn hình hoặc ghi vào tệp đĩa. Một ví dụ khác là lệnh gọi hàm print() hoặc time.sleep(), cả hai hàm này đều không trả về giá trị hữu ích. Cả hai đều chỉ được gọi vì tác dụng phụ của chúng là gửi một số văn bản tới màn hình hoặc tạm dừng thực thi trong một giây.

Các chương trình Python được viết theo phong cách chức năng thường sẽ không tránh được tất cả các thao tác I/O hoặc tất cả các phép gán; thay vào đó, chúng sẽ cung cấp giao diện có chức năng nhưng sẽ sử dụng các tính năng không có chức năng trong nội bộ. Ví dụ: việc triển khai một hàm sẽ vẫn sử dụng các phép gán cho các biến cục bộ nhưng sẽ không sửa đổi các biến toàn cục hoặc có các tác dụng phụ khác.

Lập trình chức năng có thể được coi là đối lập với lập trình hướng đối tượng. Đối tượng là những viên nang nhỏ chứa một số trạng thái bên trong cùng với một tập hợp các lệnh gọi phương thức cho phép bạn sửa đổi trạng thái này và các chương trình bao gồm việc thực hiện tập hợp các thay đổi trạng thái phù hợp. Lập trình hàm muốn tránh những thay đổi trạng thái càng nhiều càng tốt và hoạt động với luồng dữ liệu giữa các hàm. Trong Python, bạn có thể kết hợp hai cách tiếp cận này bằng cách viết các hàm nhận và trả về các thể hiện đại diện cho các đối tượng trong ứng dụng của bạn (tin nhắn e-mail, giao dịch, v.v.).

Thiết kế chức năng có vẻ giống như một hạn chế kỳ lạ để thực hiện. Tại sao bạn nên tránh các đồ vật và tác dụng phụ? Phong cách chức năng có những ưu điểm về mặt lý thuyết và thực tiễn:

  • Khả năng chứng minh chính thức

  • Tính mô-đun.

  • Khả năng kết hợp.

  • Dễ dàng gỡ lỗi và thử nghiệm.

Khả năng chứng minh hình thức

Lợi ích về mặt lý thuyết là việc xây dựng bằng chứng toán học cho thấy một chương trình chức năng là chính xác sẽ dễ dàng hơn.

Trong một thời gian dài, các nhà nghiên cứu đã quan tâm đến việc tìm cách chứng minh các chương trình là đúng về mặt toán học. Điều này khác với việc kiểm tra một chương trình trên nhiều đầu vào và kết luận rằng đầu ra của nó thường đúng hoặc đọc mã nguồn của chương trình và kết luận rằng mã có vẻ đúng; thay vào đó, mục tiêu là một bằng chứng nghiêm ngặt cho thấy một chương trình tạo ra kết quả đúng cho tất cả các đầu vào có thể có.

Kỹ thuật được sử dụng để chứng minh chương trình đúng là ghi lại invariants, các thuộc tính của dữ liệu đầu vào và các biến của chương trình luôn đúng. Sau đó, đối với mỗi dòng mã, bạn chỉ ra rằng nếu các bất biến X và Y là đúng before thì dòng đó được thực thi, thì các bất biến X' và Y' hơi khác một chút là đúng after thì dòng đó được thực thi. Điều này tiếp tục cho đến khi bạn kết thúc chương trình, tại thời điểm đó các bất biến phải phù hợp với các điều kiện mong muốn ở đầu ra của chương trình.

Việc tránh các bài tập trong lập trình chức năng nảy sinh vì các bài tập rất khó xử lý bằng kỹ thuật này; các phép gán có thể phá vỡ các bất biến đúng trước phép gán mà không tạo ra bất kỳ bất biến mới nào có thể được nhân rộng về sau.

Thật không may, việc chứng minh tính chính xác của chương trình phần lớn là không thực tế và không liên quan đến phần mềm Python. Ngay cả những chương trình tầm thường cũng yêu cầu những bản chứng minh dài vài trang; bằng chứng về tính chính xác đối với một chương trình phức tạp vừa phải sẽ rất lớn và rất ít hoặc không có chương trình nào bạn sử dụng hàng ngày (trình thông dịch Python, trình phân tích cú pháp XML, trình duyệt web của bạn) có thể được chứng minh là đúng. Ngay cả khi bạn viết ra hoặc tạo ra một bằng chứng thì vẫn sẽ có vấn đề về việc xác minh bằng chứng đó; có thể có lỗi trong đó và bạn tin nhầm rằng mình đã chứng minh chương trình là đúng.

Tính mô đun

Một lợi ích thiết thực hơn của lập trình hàm là nó buộc bạn phải chia nhỏ vấn đề của mình thành từng phần nhỏ. Kết quả là các chương trình có nhiều mô-đun hơn. Việc chỉ định và viết một hàm nhỏ thực hiện một việc sẽ dễ dàng hơn so với hàm lớn thực hiện một phép biến đổi phức tạp. Các hàm nhỏ cũng dễ đọc và dễ kiểm tra lỗi hơn.

Dễ dàng gỡ lỗi và kiểm tra

Việc kiểm tra và gỡ lỗi một chương trình kiểu chức năng dễ dàng hơn.

Việc gỡ lỗi được đơn giản hóa vì các hàm thường nhỏ và được chỉ định rõ ràng. Khi một chương trình không hoạt động, mỗi chức năng là một điểm giao diện nơi bạn có thể kiểm tra xem dữ liệu có chính xác hay không. Bạn có thể xem xét đầu vào và đầu ra trung gian để nhanh chóng xác định chức năng gây ra lỗi.

Việc kiểm tra dễ dàng hơn vì mỗi chức năng là một chủ đề tiềm năng cho một bài kiểm tra đơn vị. Các chức năng không phụ thuộc vào trạng thái hệ thống cần được sao chép trước khi chạy thử nghiệm; thay vào đó bạn chỉ cần tổng hợp đầu vào phù hợp và sau đó kiểm tra xem đầu ra có khớp với mong đợi hay không.

Khả năng kết hợp

Khi bạn làm việc trên một chương trình kiểu chức năng, bạn sẽ viết một số hàm với đầu vào và đầu ra khác nhau. Một số chức năng này chắc chắn sẽ được chuyên biệt hóa cho một ứng dụng cụ thể, nhưng những chức năng khác sẽ hữu ích trong nhiều chương trình khác nhau. Ví dụ: hàm lấy đường dẫn thư mục và trả về tất cả các tệp XML trong thư mục hoặc hàm lấy tên tệp và trả về nội dung của nó, có thể được áp dụng cho nhiều tình huống khác nhau.

Theo thời gian, bạn sẽ hình thành một thư viện tiện ích cá nhân. Thông thường, bạn sẽ tập hợp các chương trình mới bằng cách sắp xếp các chức năng hiện có trong một cấu hình mới và viết một số chức năng chuyên dụng cho tác vụ hiện tại.

Trình vòng lặp

Tôi sẽ bắt đầu bằng cách xem xét một tính năng của ngôn ngữ Python, một tính năng quan trọng để viết các chương trình kiểu hàm: các trình vòng lặp.

Trình vòng lặp là một đối tượng đại diện cho một luồng dữ liệu; đối tượng này trả về dữ liệu một phần tử tại một thời điểm. Trình lặp Python phải hỗ trợ một phương thức có tên __next__() không có đối số và luôn trả về phần tử tiếp theo của luồng. Nếu không còn phần tử nào trong luồng, __next__() phải đưa ra ngoại lệ StopIteration. Tuy nhiên, các trình vòng lặp không nhất thiết phải hữu hạn; hoàn toàn hợp lý khi viết một trình vòng lặp tạo ra luồng dữ liệu vô hạn.

Hàm iter() tích hợp sẵn nhận một đối tượng tùy ý và cố gắng trả về một trình vòng lặp sẽ trả về nội dung hoặc phần tử của đối tượng, tăng TypeError nếu đối tượng không hỗ trợ phép lặp. Một số kiểu dữ liệu tích hợp của Python hỗ trợ phép lặp, phổ biến nhất là danh sách và từ điển. Một đối tượng được gọi là iterable nếu bạn có thể lấy một trình vòng lặp cho nó.

Bạn có thể thử nghiệm giao diện lặp lại theo cách thủ công:

>>> L = [1, 2, 3]
>>> it = iter(L)
>>> it
<...iterator object at ...>
>>> it.__next__()  # same as next(it)
1
>>> next(it)
2
>>> next(it)
3
>>> next(it)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
StopIteration
>>>

Python mong đợi các đối tượng có thể lặp lại trong một số ngữ cảnh khác nhau, quan trọng nhất là câu lệnh for. Trong câu lệnh for X in Y, Y phải là một iterator hoặc một đối tượng nào đó mà iter() có thể tạo một iterator. Hai câu lệnh này tương đương nhau:

cho tôi trong iter(obj):
    in(i)

cho tôi trong obj:
    in(i)

Các trình vòng lặp có thể được cụ thể hóa dưới dạng danh sách hoặc bộ dữ liệu bằng cách sử dụng các hàm tạo list() hoặc tuple():

>>> L = [1, 2, 3]
>>> iterator = iter(L)
>>> t = tuple(iterator)
>>> t
(1, 2, 3)

Việc giải nén trình tự cũng hỗ trợ các trình vòng lặp: nếu bạn biết một trình vòng lặp sẽ trả về N phần tử, bạn có thể giải nén chúng thành N-tuple:

>>> L = [1, 2, 3]
>>> iterator = iter(L)
>>> a, b, c = iterator
>>> a, b, c
(1, 2, 3)

Các hàm tích hợp như max()min() có thể nhận một đối số lặp duy nhất và sẽ trả về phần tử lớn nhất hoặc nhỏ nhất. Các toán tử "in""not in" cũng hỗ trợ các trình vòng lặp: X in iterator là đúng nếu X được tìm thấy trong luồng được trình vòng lặp trả về. Bạn sẽ gặp phải vấn đề rõ ràng nếu vòng lặp là vô hạn; max(), min() sẽ không bao giờ quay trở lại và nếu phần tử X không bao giờ xuất hiện trong luồng thì các toán tử "in""not in" cũng sẽ không quay trở lại.

Lưu ý rằng bạn chỉ có thể tiếp tục trong một trình vòng lặp; không có cách nào để lấy phần tử trước đó, đặt lại trình vòng lặp hoặc tạo bản sao của phần tử đó. Các đối tượng Iterator có thể tùy ý cung cấp các khả năng bổ sung này, nhưng giao thức iterator chỉ chỉ định phương thức __next__(). Do đó, các hàm có thể sử dụng tất cả đầu ra của trình vòng lặp và nếu bạn cần thực hiện điều gì đó khác biệt với cùng một luồng, bạn sẽ phải tạo một trình vòng lặp mới.

Các kiểu dữ liệu hỗ trợ Iterator

Chúng ta đã thấy danh sách và bộ dữ liệu hỗ trợ các trình vòng lặp như thế nào. Trên thực tế, bất kỳ loại chuỗi Python nào, chẳng hạn như chuỗi, sẽ tự động hỗ trợ việc tạo một trình vòng lặp.

Gọi iter() trên từ điển sẽ trả về một trình vòng lặp sẽ lặp qua các khóa của từ điển:

>>> m = {'Tháng 1': 1, 'Tháng 2': 2, 'Tháng 3': 3, 'Tháng 4': 4, 'Tháng 5': 5, 'Tháng 6': 6,
... 'Tháng 7': 7, 'Tháng 8': 8, 'Tháng 9': 9, 'Tháng 10': 10, 'Tháng 11': 11, 'Tháng 12': 12}
>>> cho khóa trong m:
... print(key, m[key])
ngày 1 tháng 1
ngày 2 tháng 2
3 tháng 3
ngày 4 tháng 4
ngày 5 tháng 5
ngày 6 tháng 6
ngày 7 tháng 7
ngày 8 tháng 8
ngày 9 tháng 9
ngày 10 tháng 10
ngày 11 tháng 11
ngày 12 tháng 12

Lưu ý rằng bắt đầu với Python 3.7, thứ tự lặp từ điển được đảm bảo giống với thứ tự chèn. Trong các phiên bản trước, hành vi này không được xác định và có thể khác nhau giữa các lần triển khai.

Việc áp dụng iter() cho một từ điển luôn lặp lại các khóa, nhưng từ điển có các phương thức trả về các trình vòng lặp khác. Nếu bạn muốn lặp lại các giá trị hoặc cặp khóa/giá trị, bạn có thể gọi các phương thức values() hoặc items() một cách rõ ràng để có được một trình vòng lặp thích hợp.

Hàm tạo dict() có thể chấp nhận một trình vòng lặp trả về một luồng hữu hạn gồm các bộ dữ liệu (key, value):

>>> L = [('Italy', 'Rome'), ('France', 'Paris'), ('US', 'Washington DC')]
>>> dict(iter(L))
{'Italy': 'Rome', 'France': 'Paris', 'US': 'Washington DC'}

Các tệp cũng hỗ trợ lặp lại bằng cách gọi phương thức readline() cho đến khi không còn dòng nào trong tệp. Điều này có nghĩa là bạn có thể đọc từng dòng của tệp như thế này

cho dòng trong tập tin:
    # do một cái gì đó cho mỗi dòng
    ...

Các bộ có thể lấy nội dung của chúng từ một bộ có thể lặp lại và cho phép bạn lặp lại các phần tử của bộ đó

>>> S = {2, 3, 5, 7, 11, 13}
>>> cho tôi trong S:
... in(i)
2
3
5
7
11
13

Biểu thức trình tạo và hiểu danh sách

Hai thao tác phổ biến trên đầu ra của trình vòng lặp là 1) thực hiện một số thao tác cho mọi phần tử, 2) chọn một tập hợp con các phần tử đáp ứng một số điều kiện. Ví dụ: với một danh sách các chuỗi, bạn có thể muốn loại bỏ khoảng trắng ở cuối mỗi dòng hoặc trích xuất tất cả các chuỗi chứa một chuỗi con nhất định.

Việc hiểu danh sách và biểu thức trình tạo (dạng ngắn: "listcomps" và "genexps") là ký hiệu ngắn gọn cho các hoạt động như vậy, được mượn từ ngôn ngữ lập trình hàm Haskell (https://www.haskell.org/). Bạn có thể loại bỏ tất cả khoảng trắng khỏi luồng chuỗi bằng mã sau:

>>> line_list = ['dòng 1\n', 'dòng 2 \n', ' \n', '']

>>> biểu thức # Generator -- trả về iterator
>>> lột_iter = (line.strip() cho dòng trong line_list)

>>> hiểu # List -- trả về danh sách
>>> lột_list = [line.strip() cho dòng trong line_list]

Bạn chỉ có thể chọn một số thành phần nhất định bằng cách thêm điều kiện "if":

>>> lột_list = [line.strip() cho dòng trong line_list
... nếu dòng != ""]

Với khả năng hiểu danh sách, bạn sẽ nhận được danh sách Python; stripped_list là danh sách chứa các dòng kết quả, không phải trình vòng lặp. Các biểu thức của trình tạo trả về một trình vòng lặp tính toán các giá trị khi cần thiết, không cần cụ thể hóa tất cả các giá trị cùng một lúc. Điều này có nghĩa là việc hiểu danh sách không hữu ích nếu bạn đang làm việc với các trình vòng lặp trả về luồng vô hạn hoặc lượng dữ liệu rất lớn. Biểu thức trình tạo thích hợp hơn trong những tình huống này.

Các biểu thức của trình tạo được bao quanh bởi dấu ngoặc đơn ("()") và phần hiểu danh sách được bao quanh bởi dấu ngoặc vuông ("[]"). Các biểu thức của trình tạo có dạng:

( biểu thức cho expr trong dãy 1
             nếu điều kiện 1
             cho expr2 trong dãy 2
             nếu điều kiện2
             cho expr3 theo trình tự 3
             ...
             nếu điều kiện3
             cho exprN theo chuỗiN
             nếu điều kiệnN )

Một lần nữa, để hiểu danh sách, chỉ có các dấu ngoặc bên ngoài là khác nhau (dấu ngoặc vuông thay vì dấu ngoặc đơn).

Các phần tử của đầu ra được tạo sẽ là các giá trị liên tiếp của expression. Các mệnh đề if đều là tùy chọn; nếu có, expression chỉ được đánh giá và thêm vào kết quả khi condition đúng.

Các biểu thức của trình tạo luôn phải được viết bên trong dấu ngoặc đơn, nhưng các dấu ngoặc đơn báo hiệu lệnh gọi hàm cũng được tính. Nếu bạn muốn tạo một trình vòng lặp sẽ được chuyển ngay đến một hàm, bạn có thể viết

obj_total = sum(obj.count cho obj trong list_all_objects())

Mệnh đề for...in chứa các chuỗi được lặp lại. Các chuỗi không nhất thiết phải có cùng độ dài vì chúng được lặp từ trái sang phải, not song song. Đối với mỗi phần tử trong sequence1, sequence2 được lặp lại từ đầu. sequence3 sau đó được lặp lại cho từng cặp phần tử thu được từ sequence1sequence2.

Nói cách khác, một biểu thức hiểu danh sách hoặc trình tạo tương đương với mã Python sau:

cho expr1 trong dãy 1:
    nếu không (điều kiện 1):
        tiếp tục # Skip yếu tố này
    cho expr2 trong dãy 2:
        nếu không (điều kiện 2):
            tiếp tục # Skip yếu tố này
        ...
        cho exprN theo chuỗiN:
            nếu không (điều kiệnN):
                tiếp tục # Skip yếu tố này

            # Output giá trị của
            biểu thức # the.

Điều này có nghĩa là khi có nhiều mệnh đề for...in nhưng không có mệnh đề if, độ dài của kết quả đầu ra sẽ bằng tích độ dài của tất cả các chuỗi. Nếu bạn có hai danh sách có độ dài 3 thì danh sách đầu ra dài 9 phần tử:

>>> seq1 = 'abc'
>>> seq2 = (1, 2, 3)
>>> [(x, y) for x in seq1 for y in seq2]
[('a', 1), ('a', 2), ('a', 3),
 ('b', 1), ('b', 2), ('b', 3),
 ('c', 1), ('c', 2), ('c', 3)]

Để tránh gây ra sự mơ hồ trong ngữ pháp của Python, nếu expression đang tạo một bộ dữ liệu thì nó phải được bao quanh bằng dấu ngoặc đơn. Việc hiểu danh sách đầu tiên bên dưới là một lỗi cú pháp, trong khi cách hiểu thứ hai là đúng

lỗi # Syntax
[x, y cho x trong seq1 cho y trong seq2]
# Correct
[(x, y) cho x trong seq1 cho y trong seq2]

Máy phát điện

Trình tạo là một lớp hàm đặc biệt giúp đơn giản hóa nhiệm vụ viết các trình vòng lặp. Các hàm thông thường tính toán một giá trị và trả về giá trị đó, nhưng các trình tạo trả về một trình vòng lặp trả về một luồng giá trị.

Bạn chắc chắn đã quen với cách hoạt động của các lệnh gọi hàm thông thường trong Python hoặc C. Khi bạn gọi một hàm, nó sẽ có một vùng tên riêng nơi các biến cục bộ của nó được tạo. Khi hàm đạt đến câu lệnh return, các biến cục bộ sẽ bị hủy và giá trị được trả về cho người gọi. Lệnh gọi sau đó tới cùng một hàm sẽ tạo ra một không gian tên riêng tư mới và một tập hợp các biến cục bộ mới. Tuy nhiên, điều gì sẽ xảy ra nếu các biến cục bộ không bị loại bỏ khi thoát khỏi hàm? Điều gì sẽ xảy ra nếu sau này bạn có thể tiếp tục lại chức năng mà nó đã dừng lại? Đây là những gì máy phát điện cung cấp; chúng có thể được coi là các chức năng có thể tiếp tục.

Đây là ví dụ đơn giản nhất về hàm tạo:

>>> def generate_ints(N):
...    for i in range(N):
...        yield i

Bất kỳ hàm nào chứa từ khóa yield đều là hàm tạo; điều này được phát hiện bởi trình biên dịch bytecode của Python để biên dịch hàm một cách đặc biệt.

Khi bạn gọi một hàm tạo, nó không trả về một giá trị nào; thay vào đó nó trả về một đối tượng trình tạo hỗ trợ giao thức vòng lặp. Khi thực thi biểu thức yield, trình tạo sẽ xuất ra giá trị của i, tương tự như câu lệnh return. Sự khác biệt lớn giữa câu lệnh yield và câu lệnh return là khi đạt tới yield, trạng thái thực thi của trình tạo bị tạm dừng và các biến cục bộ được giữ nguyên. Trong lần gọi tiếp theo tới phương thức __next__() của trình tạo, hàm sẽ tiếp tục thực thi.

Đây là cách sử dụng mẫu của trình tạo generate_ints():

>>> gen = generate_ints(3)
>>> gen
<generator object generate_ints at ...>
>>> next(gen)
0
>>> next(gen)
1
>>> next(gen)
2
>>> next(gen)
Traceback (most recent call last):
  File "stdin", line 1, in <module>
  File "stdin", line 2, in generate_ints
StopIteration

Bạn cũng có thể viết for i in generate_ints(5) hoặc a, b, c = generate_ints(3).

Bên trong hàm tạo, return value khiến StopIteration(value) được nâng lên từ phương thức __next__(). Khi điều này xảy ra hoặc đạt đến đáy của hàm, quá trình xử lý các giá trị kết thúc và trình tạo không thể mang lại bất kỳ giá trị nào nữa.

Bạn có thể đạt được hiệu quả của trình tạo theo cách thủ công bằng cách viết lớp của riêng bạn và lưu trữ tất cả các biến cục bộ của trình tạo dưới dạng biến thể hiện. Ví dụ: việc trả về một danh sách các số nguyên có thể được thực hiện bằng cách đặt self.count thành 0 và để phương thức __next__() tăng self.count rồi trả về nó. Tuy nhiên, đối với một trình tạo có độ phức tạp vừa phải, việc viết một lớp tương ứng có thể phức tạp hơn nhiều.

Bộ thử nghiệm đi kèm với thư viện của Python, Lib/test/test_generators.py, chứa một số ví dụ thú vị hơn. Đây là một trình tạo thực hiện việc duyệt cây theo thứ tự bằng cách sử dụng trình tạo đệ quy.

Trình tạo đệ quy # A tạo ra các lá Cây theo thứ tự.
thứ tự def (t):
    nếu t:
        cho x theo thứ tự (t.left):
            năng suất x

        năng suất t.label

        cho x theo thứ tự (t.right):
            năng suất x

Hai ví dụ khác trong test_generators.py đưa ra giải pháp cho vấn đề N-Queens (đặt N quân hậu trên bàn cờ NxN để không có quân hậu nào đe dọa quân khác) và Chuyến tham quan hiệp sĩ (tìm đường đưa một quân mã đến mọi ô của bàn cờ NxN mà không cần ghé thăm bất kỳ ô nào hai lần).

Truyền giá trị vào trình tạo

Trong Python 2.4 trở về trước, trình tạo chỉ tạo ra đầu ra. Sau khi mã của trình tạo được gọi để tạo trình vòng lặp, không có cách nào chuyển bất kỳ thông tin mới nào vào hàm khi quá trình thực thi của nó được tiếp tục. Bạn có thể kết hợp khả năng này bằng cách làm cho trình tạo xem xét một biến toàn cục hoặc bằng cách chuyển vào một số đối tượng có thể thay đổi mà người gọi sau đó sẽ sửa đổi, nhưng những cách tiếp cận này rất lộn xộn.

Trong Python 2.5 có một cách đơn giản để chuyển các giá trị vào trình tạo. yield đã trở thành một biểu thức, trả về một giá trị có thể được gán cho một biến hoặc được vận hành trên

val = (sản lượng i)

Tôi khuyên bạn nên always đặt dấu ngoặc đơn xung quanh biểu thức yield khi bạn đang làm điều gì đó với giá trị được trả về, như trong ví dụ trên. Các dấu ngoặc đơn không phải lúc nào cũng cần thiết nhưng việc thêm chúng vào luôn sẽ dễ dàng hơn thay vì phải nhớ khi cần.

(PEP 342 giải thích các quy tắc chính xác, đó là biểu thức yield phải luôn được đặt trong ngoặc đơn trừ khi nó xuất hiện ở biểu thức cấp cao nhất ở phía bên phải của phép gán. Điều này có nghĩa là bạn có thể viết val = yield i nhưng phải sử dụng dấu ngoặc đơn khi có một thao tác, như trong val = (yield i) + 12.)

Các giá trị được gửi vào trình tạo bằng cách gọi phương thức send(value) của nó. Phương thức này tiếp tục mã của trình tạo và biểu thức yield trả về giá trị đã chỉ định. Nếu phương thức __next__() thông thường được gọi, yield sẽ trả về None.

Đây là một bộ đếm đơn giản tăng thêm 1 và cho phép thay đổi giá trị của bộ đếm bên trong.

bộ đếm def (tối đa):
    tôi = 0
    trong khi tôi < tối đa:
        val = (sản lượng i)
        giá trị # If được cung cấp, bộ đếm thay đổi
        nếu val không phải  Không:
            tôi = giá trị
        khác:
            tôi += 1

Và đây là một ví dụ về việc thay đổi bộ đếm:

>>> it = counter(10)
>>> next(it)
0
>>> next(it)
1
>>> it.send(8)
8
>>> next(it)
9
>>> next(it)
Traceback (most recent call last):
  File "t.py", line 15, in <module>
    it.next()
StopIteration

yield thường sẽ trả về None nên bạn phải luôn kiểm tra trường hợp này. Đừng chỉ sử dụng giá trị của nó trong các biểu thức trừ khi bạn chắc chắn rằng phương thức send() sẽ là phương thức duy nhất được sử dụng để tiếp tục hàm tạo của bạn.

Ngoài send(), còn có hai phương pháp khác trên máy phát điện:

  • throw(value) được sử dụng để đưa ra một ngoại lệ bên trong trình tạo; ngoại lệ được đưa ra bởi biểu thức yield khi quá trình thực thi của trình tạo bị tạm dừng.

  • close() gửi ngoại lệ GeneratorExit tới trình tạo để kết thúc quá trình lặp. Khi nhận được ngoại lệ này, mã của trình tạo phải tăng GeneratorExit hoặc StopIteration; bắt ngoại lệ và làm bất cứ điều gì khác là bất hợp pháp và sẽ kích hoạt RuntimeError. close() cũng sẽ được trình thu gom rác của Python gọi khi trình tạo được thu gom rác.

    Nếu bạn cần chạy mã dọn dẹp khi xảy ra GeneratorExit, tôi khuyên bạn nên sử dụng bộ try: ... finally: thay vì bắt GeneratorExit.

Tác động tích lũy của những thay đổi này là biến người tạo ra thông tin một chiều thành cả người sản xuất và người tiêu dùng.

Trình tạo cũng trở thành coroutines, một dạng chương trình con tổng quát hơn. Các chương trình con được nhập tại một điểm và thoát tại một điểm khác (đầu hàm và câu lệnh return), nhưng các coroutine có thể được nhập, thoát và tiếp tục ở nhiều điểm khác nhau (câu lệnh yield).

Các chức năng tích hợp

Chúng ta hãy xem xét chi tiết hơn các hàm dựng sẵn thường được sử dụng với các vòng lặp.

Hai trong số các hàm dựng sẵn của Python, map()filter() sao chép các tính năng của biểu thức trình tạo:

map(f, iterA, iterB, ...) trả về một trình lặp theo trình tự

f(iterA[0], iterB[0]), f(iterA[1], iterB[1]), f(iterA[2], iterB[2]), ....

>>> def upper(s):
...     return s.upper()
>>> list(map(upper, ['sentence', 'fragment']))
['SENTENCE', 'FRAGMENT']
>>> [upper(s) for s in ['sentence', 'fragment']]
['SENTENCE', 'FRAGMENT']

Tất nhiên bạn có thể đạt được hiệu quả tương tự với việc hiểu danh sách.

filter(predicate, iter) trả về một trình lặp trên tất cả các phần tử chuỗi đáp ứng một điều kiện nhất định và được sao chép tương tự bằng cách hiểu danh sách. Zz002zz là hàm trả về giá trị thực của một số điều kiện; để sử dụng với filter(), vị từ phải nhận một giá trị duy nhất.

>>> def is_even(x):
...     return (x % 2) == 0
>>> list(filter(is_even, range(10)))
[0, 2, 4, 6, 8]

Điều này cũng có thể được viết dưới dạng hiểu danh sách:

>>> list(x for x in range(10) if is_even(x))
[0, 2, 4, 6, 8]

enumerate(iter, start=0) đếm số phần tử trong 2 bộ dữ liệu trả về có thể lặp lại chứa số đếm (từ start) và từng phần tử.

>>> cho mục trong enumerate(['subject', 'verb', 'object']):
... in(mục)
(0, 'chủ đề')
(1, 'động từ')
(2, 'đối tượng')

enumerate() thường được sử dụng khi lặp qua danh sách và ghi lại các chỉ mục đáp ứng các điều kiện nhất định

f = open('data.txt', 'r')
đối với i, dòng liệt  (f):
    nếu line.strip() == '':
        print('Dòng trống tại dòng #%i' % i)

sorted(iterable, key=None, reverse=False) thu thập tất cả các phần tử của iterable vào một danh sách, sắp xếp danh sách và trả về kết quả đã sắp xếp. Các đối số keyreverse được chuyển qua phương thức sort() của danh sách được xây dựng.

>>> import random
>>> # Generate 8 random numbers between [0, 10000)
>>> rand_list = random.sample(range(10000), 8)
>>> rand_list
[769, 7953, 9828, 6431, 8442, 9878, 6213, 2207]
>>> sorted(rand_list)
[769, 2207, 6213, 6431, 7953, 8442, 9828, 9878]
>>> sorted(rand_list, reverse=True)
[9878, 9828, 8442, 7953, 6431, 6213, 2207, 769]

(Để thảo luận chi tiết hơn về cách sắp xếp, hãy xem Kỹ thuật sắp xếp.)

Các phần dựng sẵn any(iter)all(iter) xem xét các giá trị thực của nội dung của một lần lặp. any() trả về True nếu bất kỳ phần tử nào trong iterable là giá trị đúng và all() trả về True nếu tất cả các phần tử là giá trị đúng:

>>> any([0, 1, 0])
True
>>> any([0, 0, 0])
False
>>> any([1, 1, 1])
True
>>> all([0, 1, 0])
False
>>> all([0, 0, 0])
False
>>> all([1, 1, 1])
True

zip(iterA, iterB, ...) lấy một phần tử từ mỗi lần lặp và trả về chúng trong một bộ dữ liệu

zip(['a', 'b', 'c'], (1, 2, 3)) =>
  ('a', 1), ('b', 2), ('c', 3)

Nó không xây dựng danh sách trong bộ nhớ và loại bỏ tất cả các vòng lặp đầu vào trước khi quay lại; thay vào đó, các bộ dữ liệu được xây dựng và chỉ trả về nếu chúng được yêu cầu. (Thuật ngữ kỹ thuật cho hành vi này là lazy evaluation.)

Trình vòng lặp này được thiết kế để sử dụng với các vòng lặp có cùng độ dài. Nếu các lần lặp có độ dài khác nhau, luồng kết quả sẽ có cùng độ dài với lần lặp ngắn nhất.

zip(['a', 'b'], (1, 2, 3)) =>
  ('a', 1), ('b', 2)

Tuy nhiên, bạn nên tránh làm điều này vì một phần tử có thể được lấy từ các vòng lặp dài hơn và bị loại bỏ. Điều này có nghĩa là bạn không thể tiếp tục sử dụng các trình vòng lặp vì bạn có nguy cơ bỏ qua phần tử bị loại bỏ.

Mô-đun itertools

Mô-đun itertools chứa một số trình vòng lặp thường được sử dụng cũng như các hàm để kết hợp một số trình vòng lặp. Phần này sẽ giới thiệu nội dung của mô-đun bằng cách hiển thị các ví dụ nhỏ.

Các chức năng của mô-đun rơi vào một số lớp rộng:

  • Các hàm tạo một trình vòng lặp mới dựa trên một trình vòng lặp hiện có.

  • Các hàm xử lý các phần tử của trình vòng lặp làm đối số của hàm.

  • Các hàm chọn các phần đầu ra của trình vòng lặp.

  • Một hàm để nhóm đầu ra của một trình vòng lặp.

Tạo các vòng lặp mới

itertools.count(start, step) trả về một luồng vô hạn các giá trị cách đều nhau. Bạn có thể tùy ý cung cấp số bắt đầu, mặc định là 0 và khoảng cách giữa các số, mặc định là 1:

itertools.count() =>
  0, 1, 2, 3, 4, 5, 6, 7, 8, 9, ...
itertools.count(10) =>
  10, 11, 12, 13, 14, 15, 16, 17, 18, 19, ...
itertools.count(10, 5) =>
  10, 15, 20, 25, 30, 35, 40, 45, 50, 55, ...

itertools.cycle(iter) lưu một bản sao nội dung của một iterable được cung cấp và trả về một iterator mới trả về các phần tử của nó từ đầu đến cuối. Trình vòng lặp mới sẽ lặp lại các phần tử này vô tận.

itertools.cycle([1, 2, 3, 4, 5]) =>
  1, 2, 3, 4, 5, 1, 2, 3, 4, 5, ...

itertools.repeat(elem, [n]) trả về phần tử được cung cấp n lần hoặc trả về phần tử vô tận nếu n không được cung cấp.

itertools.repeat('abc') =>
  abc, abc, abc, abc, abc, abc, abc, abc, abc, abc, ...
itertools.repeat('abc', 5) =>
  abc, abc, abc, abc, abc

itertools.chain(iterA, iterB, ...) lấy một số lần lặp tùy ý làm đầu vào và trả về tất cả các phần tử của trình lặp đầu tiên, sau đó là tất cả các phần tử của trình vòng lặp thứ hai, v.v., cho đến khi tất cả các lần lặp đã hết.

itertools.chain(['a', 'b', 'c'], (1, 2, 3)) =>
  a, b, c, 1, 2, 3

itertools.islice(iter, [start], stop, [step]) trả về một luồng là một phần của trình vòng lặp. Với một đối số stop duy nhất, nó sẽ trả về các phần tử stop đầu tiên. Nếu bạn cung cấp chỉ mục bắt đầu, bạn sẽ nhận được các phần tử stop-start và nếu bạn cung cấp giá trị cho step, các phần tử sẽ bị bỏ qua tương ứng. Không giống như việc cắt chuỗi và danh sách của Python, bạn không thể sử dụng các giá trị âm cho start, stop hoặc step.

itertools.islice(range(10), 8) =>
  0, 1, 2, 3, 4, 5, 6, 7
itertools.islice(range(10), 2, 8) =>
  2, 3, 4, 5, 6, 7
itertools.islice(range(10), 2, 8, 2) =>
  2, 4, 6

itertools.tee(iter, [n]) sao chép một trình vòng lặp; nó trả về các trình vòng lặp độc lập n, tất cả sẽ trả về nội dung của trình vòng lặp nguồn. Nếu bạn không cung cấp giá trị cho n thì mặc định là 2. Việc sao chép các trình vòng lặp yêu cầu lưu một số nội dung của trình lặp nguồn, vì vậy, điều này có thể tiêu tốn bộ nhớ đáng kể nếu trình vòng lặp lớn và một trong các trình vòng lặp mới được sử dụng nhiều hơn các trình vòng lặp khác.

itertools.tee( itertools.count() ) =>
   iterA, iterB

 đâu iterA ->
   0, 1, 2, 3, 4, 5, 6, 7, 8, 9, ...

 iterB ->
   0, 1, 2, 3, 4, 5, 6, 7, 8, 9, ...

Gọi hàm trên các phần tử

Mô-đun operator chứa một tập hợp các hàm tương ứng với các toán tử của Python. Một số ví dụ là operator.add(a, b) (thêm hai giá trị), operator.ne(a, b) (giống như a != b) và operator.attrgetter('id') (trả về một lệnh gọi có thể tìm nạp thuộc tính .id).

itertools.starmap(func, iter) giả định rằng iterable sẽ trả về một luồng gồm các bộ dữ liệu và gọi func bằng cách sử dụng các bộ dữ liệu này làm đối số:

itertools.starmap(os.path.join,
                  [('/bin', 'python'), ('/usr', 'bin', 'java'),
                   ('/usr', 'bin', 'perl'), ('/usr', 'bin', 'ruby')])
=>
  /bin/python, /usr/bin/java, /usr/bin/Perl, /usr/bin/ruby

Lựa chọn các phần tử

Một nhóm hàm khác chọn một tập hợp con các phần tử của trình vòng lặp dựa trên một vị từ.

itertools.filterfalse(predicate, iter) ngược lại với filter(), trả về tất cả các phần tử mà vị từ trả về false:

itertools.filterfalse(is_even, itertools.count()) =>
  1, 3, 5, 7, 9, 11, 13, 15, ...

itertools.takewhile(predicate, iter) trả về các phần tử miễn là vị từ trả về true. Khi vị từ trả về sai, trình vòng lặp sẽ báo hiệu kết thúc của nó.

chắc chắn less_than_10(x):
    trả về x < 10

itertools.takewhile(less_than_10, itertools.count()) =>
  0, 1, 2, 3, 4, 5, 6, 7, 8, 9

itertools.takewhile(is_even, itertools.count()) =>
  0

itertools.dropwhile(predicate, iter) loại bỏ các phần tử trong khi vị từ trả về true và sau đó trả về phần còn lại của kết quả lặp lại.

itertools.dropwhile(less_than_10, itertools.count()) =>
  10, 11, 12, 13, 14, 15, 16, 17, 18, 19, ...

itertools.dropwhile(is_even, itertools.count()) =>
  1, 2, 3, 4, 5, 6, 7, 8, 9, 10, ...

itertools.compress(data, selectors) nhận hai vòng lặp và chỉ trả về những phần tử của data mà phần tử tương ứng của selectors là đúng, dừng bất cứ khi nào một trong hai phần tử đó cạn kiệt:

itertools.compress([1, 2, 3, 4, 5], [Đúng, Đúng, Sai, Sai, Đúng]) =>
   1, 2, 5

Hàm tổ hợp

itertools.combinations(iterable, r) trả về một trình vòng lặp cung cấp tất cả các kết hợp r-tuple có thể có của các phần tử có trong iterable.

itertools.combinations([1, 2, 3, 4, 5], 2) =>
  (1, 2), (1, 3), (1, 4), (1, 5),
  (2, 3), (2, 4), (2, 5),
  (3, 4), (3, 5),
  (4, 5)

itertools.combinations([1, 2, 3, 4, 5], 3) =>
  (1, 2, 3), (1, 2, 4), (1, 2, 5), (1, 3, 4), (1, 3, 5), (1, 4, 5),
  (2, 3, 4), (2, 3, 5), (2, 4, 5),
  (3, 4, 5)

Các phần tử trong mỗi bộ dữ liệu vẫn giữ nguyên thứ tự khi iterable trả về chúng. Ví dụ: số 1 luôn đứng trước 2, 3, 4 hoặc 5 trong các ví dụ trên. Một hàm tương tự, itertools.permutations(iterable, r=None), loại bỏ ràng buộc này trên đơn hàng, trả về tất cả các sắp xếp có thể có về độ dài r:

itertools.permutations([1, 2, 3, 4, 5], 2) =>
  (1, 2), (1, 3), (1, 4), (1, 5),
  (2, 1), (2, 3), (2, 4), (2, 5),
  (3, 1), (3, 2), (3, 4), (3, 5),
  (4, 1), (4, 2), (4, 3), (4, 5),
  (5, 1), (5, 2), (5, 3), (5, 4)

itertools.permutations([1, 2, 3, 4, 5]) =>
  (1, 2, 3, 4, 5), (1, 2, 3, 5, 4), (1, 2, 4, 3, 5),
  ...
  (5, 4, 3, 2, 1)

Nếu bạn không cung cấp giá trị cho r thì độ dài của iterable sẽ được sử dụng, nghĩa là tất cả các phần tử đều được hoán vị.

Lưu ý rằng các hàm này tạo ra tất cả các kết hợp có thể có theo vị trí và không yêu cầu nội dung của iterable là duy nhất:

itertools.permutations('aba', 3) =>
  ('a', 'b', 'a'), ('a', 'a', 'b'), ('b', 'a', 'a'),
  ('b', 'a', 'a'), ('a', 'a', 'b'), ('a', 'b', 'a')

Bộ dữ liệu ('a', 'a', 'b') giống hệt nhau xảy ra hai lần, nhưng hai chuỗi 'a' đến từ các vị trí khác nhau.

Hàm itertools.combinations_with_replacement(iterable, r) loại bỏ một hạn chế khác: các phần tử có thể được lặp lại trong một bộ dữ liệu. Về mặt khái niệm, một phần tử được chọn cho vị trí đầu tiên của mỗi bộ dữ liệu và sau đó được thay thế trước phần tử thứ hai được chọn.

itertools.combinations_with_replacement([1, 2, 3, 4, 5], 2) =>
  (1, 1), (1, 2), (1, 3), (1, 4), (1, 5),
  (2, 2), (2, 3), (2, 4), (2, 5),
  (3, 3), (3, 4), (3, 5),
  (4, 4), (4, 5),
  (5, 5)

Nhóm các phần tử

Hàm cuối cùng tôi sẽ thảo luận, itertools.groupby(iter, key_func=None), là hàm phức tạp nhất. key_func(elem) là một hàm có thể tính toán giá trị khóa cho mỗi phần tử được trả về bởi iterable. Nếu bạn không cung cấp chức năng chính, thì khóa chỉ đơn giản là từng phần tử.

groupby() thu thập tất cả các phần tử liên tiếp từ iterable cơ bản có cùng giá trị khóa và trả về một luồng gồm 2 bộ chứa giá trị khóa và một trình vòng lặp cho các phần tử có khóa đó.

city_list = [('Decatur', 'AL'), ('Huntsville', 'AL'), ('Selma', 'AL'),
             ('Neo', 'AK'), ('Nome', 'AK'),
             ("Flagstaff", 'AZ'), ('Phoenix', 'AZ'), ('Tucson', 'AZ'),
             ...
            ]

def get_state(city_state):
    trở về thành phố_state[1]

itertools.groupby(city_list, get_state) =>
  ('AL', iterator-1),
  ('AK', iterator-2),
  ('AZ', iterator-3), ...

 đâu
vòng lặp-1 =>
  ('Decatur', 'AL'), ('Huntsville', 'AL'), ('Selma', 'AL')
vòng lặp-2 =>
  ('Neo', 'AK'), ('Nome', 'AK')
vòng lặp-3 =>
  ('Flagstaff', 'AZ'), ('Phoenix', 'AZ'), ('Tucson', 'AZ')

groupby() giả định rằng nội dung của iterable cơ bản sẽ được sắp xếp dựa trên khóa. Lưu ý rằng các trình lặp được trả về cũng sử dụng trình vòng lặp cơ bản, vì vậy bạn phải sử dụng các kết quả của trình vòng lặp-1 trước khi yêu cầu trình vòng lặp-2 và khóa tương ứng của nó.

Mô-đun functools

Mô-đun functools chứa một số hàm bậc cao hơn. Zz002zz lấy một hoặc nhiều hàm làm đầu vào và trả về một hàm mới. Công cụ hữu ích nhất trong mô-đun này là chức năng functools.partial().

Đối với các chương trình được viết theo kiểu hàm, đôi khi bạn sẽ muốn xây dựng các biến thể của hàm hiện có có điền một số tham số. Hãy xem xét hàm Python f(a, b, c); bạn có thể muốn tạo một hàm mới g(b, c) tương đương với f(1, b, c); bạn đang điền giá trị cho một trong các tham số của f(). Đây được gọi là "ứng dụng chức năng một phần".

Hàm tạo cho partial() lấy các đối số (function, arg1, arg2, ..., kwarg1=value1, kwarg2=value2). Đối tượng kết quả có thể gọi được, vì vậy bạn chỉ cần gọi nó để gọi function với các đối số được điền đầy đủ.

Đây là một ví dụ nhỏ nhưng thực tế:

nhập khẩu funtools

nhật  def (tin nhắn, hệ thống con):
    """Ghi nội dung của 'tin nhắn' tới hệ thống con được chỉ định."""
    print('%s: %s' % (hệ thống con, tin nhắn))
    ...

server_log = functools.partial(log, subsystem='server')
server_log('Không thể mở socket')

functools.reduce(func, iter, [initial_value]) thực hiện tích lũy một thao tác trên tất cả các phần tử của iterable và do đó không thể áp dụng cho các iterable vô hạn. func phải là hàm nhận vào hai phần tử và trả về một giá trị duy nhất. functools.reduce() lấy hai phần tử đầu tiên A và B được iterator trả về và tính toán func(A, B). Sau đó, nó yêu cầu phần tử thứ ba, C, tính toán func(func(A, B), C), kết hợp kết quả này với phần tử thứ tư được trả về và tiếp tục cho đến khi hết phần tử lặp. Nếu iterable không trả về giá trị nào cả, ngoại lệ TypeError sẽ xuất hiện. Nếu giá trị ban đầu được cung cấp, giá trị đó sẽ được sử dụng làm điểm bắt đầu và func(initial_value, A) là phép tính đầu tiên.

>>> toán tử nhập, functools
>>> functools.reduce(operator.concat, ['A', 'BB', 'C'])
'ABBC'
>>> functools.reduce(operator.concat, [])
Traceback (cuộc gọi gần đây nhất):
  ...
TypeError: less() của chuỗi trống không có giá trị ban đầu
>>> functools.reduce(operator.mul, [1, 2, 3], 1)
6
>>> functools.reduce(operator.mul, [], 1)
1

Nếu bạn sử dụng operator.add() với functools.reduce(), bạn sẽ cộng tất cả các phần tử của iterable. Trường hợp này phổ biến đến mức có một phần mềm đặc biệt được tích hợp sẵn tên là sum() để tính toán nó:

>>> import functools, operator
>>> functools.reduce(operator.add, [1, 2, 3, 4], 0)
10
>>> sum([1, 2, 3, 4])
10
>>> sum([])
0

Tuy nhiên, đối với nhiều cách sử dụng functools.reduce(), có thể rõ ràng hơn nếu chỉ viết vòng lặp for rõ ràng:

nhập khẩu funtools
# Instead của:
sản phẩm = functools.reduce(operator.mul, [1, 2, 3], 1)

# You có thể viết:
sản phẩm = 1
cho tôi trong [1, 2, 3]:
    sản phẩm *= tôi

Một chức năng liên quan là itertools.accumulate(iterable, func=operator.add). Nó thực hiện phép tính tương tự, nhưng thay vì chỉ trả về kết quả cuối cùng, accumulate() trả về một trình vòng lặp cũng mang lại từng kết quả một phần:

itertools.accumulate([1, 2, 3, 4, 5]) =>
  1, 3, 6, 10, 15

itertools.accumulate([1, 2, 3, 4, 5], operator.mul) =>
  1, 2, 6, 24, 120

Mô-đun vận hành

Mô-đun operator đã được đề cập trước đó. Nó chứa một tập hợp các hàm tương ứng với các toán tử của Python. Các hàm này thường hữu ích trong mã kiểu hàm vì chúng giúp bạn không phải viết các hàm tầm thường thực hiện một thao tác đơn lẻ.

Một số chức năng trong mô-đun này là:

  • Các phép toán: add(), sub(), mul(), floordiv(), abs(), ...

  • Các phép toán logic: not_(), truth().

  • Hoạt động theo bit: and_(), or_(), invert().

  • So sánh: eq(), ne(), lt(), le(), gt()ge().

  • Nhận dạng đối tượng: is_(), is_not().

Tham khảo tài liệu của mô-đun vận hành để có danh sách đầy đủ.

Các hàm nhỏ và biểu thức lambda

Khi viết các chương trình kiểu hàm, bạn thường sẽ cần các hàm nhỏ đóng vai trò như vị từ hoặc kết hợp các phần tử theo một cách nào đó.

Nếu có sẵn Python hoặc chức năng mô-đun phù hợp, bạn hoàn toàn không cần xác định chức năng mới

bị tước_lines = [line.strip() cho dòng trong dòng]
hiện có_files = bộ lọc (os.path.exists, file_list)

Nếu chức năng bạn cần không tồn tại, bạn cần phải viết nó. Một cách để viết các hàm nhỏ là sử dụng biểu thức lambda. lambda nhận một số tham số và một biểu thức kết hợp các tham số này rồi tạo một hàm ẩn danh trả về giá trị của biểu thức

cộng = lambda x, y: x+y

print_sign = tên lambda, giá trị: name + '=' + str(value)

Một cách khác là chỉ sử dụng câu lệnh def và xác định hàm theo cách thông thường

bộ cộng def(x, y):
    trả về x + y

def print_sign(tên, giá trị):
    tên trả về + '=' + str(value)

Lựa chọn thay thế nào là thích hợp hơn? Đó là một câu hỏi về phong cách; khóa học thông thường của tôi là tránh sử dụng lambda.

Một lý do khiến tôi ưa thích là lambda khá hạn chế về các chức năng mà nó có thể xác định. Kết quả phải có thể tính toán được dưới dạng một biểu thức duy nhất, có nghĩa là bạn không thể có các so sánh if... elif... else đa chiều hoặc các câu lệnh try... except. Nếu bạn cố gắng làm quá nhiều thứ trong câu lệnh lambda, bạn sẽ nhận được một biểu thức quá phức tạp và khó đọc. Nhanh lên, đoạn mã sau đang làm gì?

nhập khẩu funtools
tổng = functools.reduce(lambda a, b: (0, a[1] + b[1]), items)[1]

Bạn có thể tìm ra nó, nhưng phải mất thời gian để giải mã biểu thức để tìm hiểu chuyện gì đang xảy ra. Việc sử dụng các câu lệnh def lồng nhau ngắn sẽ giúp mọi việc tốt hơn một chút

nhập khẩu funtools
def kết hợp (a, b):
    trả về 0, a[1] + b[1]

tổng = functools.reduce(kết hợp, vật phẩm)[1]

Nhưng sẽ tốt nhất nếu tôi chỉ sử dụng vòng lặp for

tổng cộng = 0
cho a, b trong các mục:
    tổng cộng += b

Hoặc sum() tích hợp và biểu thức trình tạo

tổng = sum(b cho a, b trong mục)

Nhiều cách sử dụng functools.reduce() sẽ rõ ràng hơn khi được viết dưới dạng vòng lặp for.

Fredrik Lundh đã từng đề xuất bộ quy tắc sau để tái cấu trúc việc sử dụng lambda:

  1. Viết hàm lambda.

  2. Viết một bình luận giải thích cái quái gì mà lambda làm.

  3. Nghiên cứu nhận xét một lúc và nghĩ ra một cái tên thể hiện được bản chất của nhận xét.

  4. Chuyển đổi lambda thành câu lệnh def, sử dụng tên đó.

  5. Xóa bình luận.

Tôi thực sự thích những quy tắc này, nhưng bạn có quyền không đồng ý về việc liệu phong cách không có lambda này có tốt hơn hay không.

Lịch sử sửa đổi và lời cảm ơn

Tác giả xin cảm ơn những người sau đây đã đưa ra gợi ý, chỉnh sửa và hỗ trợ các bản thảo khác nhau của bài viết này: Ian Bicking, Nick Coghlan, Nick Efford, Raymond Hettinger, Jim Jewett, Mike Krell, Leandro Lameiro, Jussi Salmela, Collin Winter, Blake Winton.

Phiên bản 0.1: đăng ngày 30 tháng 6 năm 2006.

Phiên bản 0.11: đăng ngày 1 tháng 7 năm 2006. Sửa lỗi đánh máy.

Phiên bản 0.2: đăng ngày 10 tháng 7 năm 2006. Hợp nhất các phần genexp và listcomp thành một. Sửa lỗi đánh máy.

Phiên bản 0.21: Đã thêm nhiều tài liệu tham khảo được đề xuất trong danh sách gửi thư của gia sư.

Phiên bản 0.30: Thêm một phần trên mô-đun functional do Collin Winter viết; thêm phần ngắn trên mô-đun vận hành; một số chỉnh sửa khác.

Tài liệu tham khảo

chung

Structure and Interpretation of Computer Programs, của Harold Abelson và Gerald Jay Sussman cùng với Julie Sussman. Bạn có thể tìm thấy cuốn sách tại https://mitpress.mit.edu/sicp. Trong cuốn sách giáo khoa cổ điển về khoa học máy tính này, chương 2 và 3 thảo luận về việc sử dụng trình tự và luồng để tổ chức luồng dữ liệu bên trong một chương trình. Cuốn sách sử dụng Lược đồ cho các ví dụ của nó, nhưng nhiều phương pháp thiết kế được mô tả trong các chương này có thể áp dụng cho mã Python kiểu chức năng.

https://defmacro.org/2006/06/19/fp.html: Phần giới thiệu chung về lập trình chức năng sử dụng các ví dụ Java và có phần giới thiệu lịch sử dài.

https://en.wikipedia.org/wiki/Functional_programming: Mục Wikipedia tổng quát mô tả lập trình chức năng.

https://en.wikipedia.org/wiki/Coroutine: Mục dành cho coroutine.

https://en.wikipedia.org/wiki/Partial_application: Mục nhập cho khái niệm ứng dụng hàm từng phần.

https://en.wikipedia.org/wiki/Currying: Mục nhập cho khái niệm cà ri.

Dành riêng cho Python

https://gnosis.cx/TPiP/: Chương đầu tiên trong cuốn sách Text Processing in Python của David Mertz thảo luận về lập trình hàm để xử lý văn bản, trong phần có tiêu đề "Sử dụng các hàm bậc cao hơn trong xử lý văn bản".

Mertz cũng đã viết một loạt bài gồm 3 phần về lập trình chức năng cho trang DeveloperWorks của IBM; xem part 1, part 2part 3,

Tài liệu Python

Tài liệu cho mô-đun itertools.

Tài liệu cho mô-đun functools.

Tài liệu cho mô-đun operator.

PEP 289: "Biểu thức trình tạo"

PEP 342: "Coroutine thông qua Trình tạo nâng cao" mô tả các tính năng của trình tạo mới trong Python 2.5.