Functools - Sức mạnh của các hàm bậc cao trong Python

Nút nguồn: 1865357

Bài báo này đã được xuất bản như một phần của Blogathon Khoa học Dữ liệu

Giới thiệu

Thư viện chuẩn Python có nhiều mô-đun tuyệt vời để giúp giữ cho mã của bạn sạch hơn và đơn giản hơn và funcools là chắc chắn là một trong những

Bộ nhớ đệm

Hãy bắt đầu với một số chức năng đơn giản nhưng mạnh mẽ nhất của module funcools. Hãy bắt đầu với các hàm lưu trữ (cũng như các trình trang trí) – lru_cache,cachecached_property. Người đầu tiên trong số họ – lru_cache cung cấp một bộ nhớ cache của các kết quả cuối cùng của việc thực hiện các chức năng, hay nói cách khác, ghi nhớ kết quả công việc của chúng:

from functools import lru_cache
import requests @lru_cache(maxsize=32)
def get_with_cache(url): try: r = requests.get(url) return r.text except: return "Not Found" for url in ["https://google.com/", "https://reddit.com/", "https://google.com/", "https://google.com/"]: get_with_cache(url) print(get_with_cache.cache_info())
# CacheInfo(hits=2, misses=4, maxsize=32, currsize=4)
print(get_with_cache.cache_parameters())
# {'maxsize': 32, 'typed': False}

Trong ví dụ này, chúng tôi thực hiện các yêu cầu GET và lưu trữ kết quả của chúng (tối đa 32 kết quả) bằng cách sử dụng trình trang trí @lru_cache. Để xem bộ nhớ đệm có thực sự hoạt động hay không, bạn có thể kiểm tra thông tin bộ đệm của chức năng bằng một phương thức cache_infocho thấy số lần truy cập bộ đệm và số lần truy cập. Một decorator cũng cung cấp các phương thức clear_cachecache_parametersđể hủy bỏ các kết quả được lưu trong bộ nhớ cache và các tham số kiểm tra tương ứng.

Nếu bạn cần bộ đệm ẩn chi tiết hơn, bạn có thể bao gồm một đối số tùy chọn typed=true, cho phép bạn lưu trữ riêng các loại đối số khác nhau vào bộ đệm ẩn.

Một trình trang trí khác cho bộ nhớ đệm trong funcools là một hàm được gọi đơn giản là cache. Nó là một trình bao bọc đơn giản lru_cachebỏ qua đối số max_size, giảm nó và không xóa các giá trị cũ.

Một trình trang trí khác mà bạn có thể sử dụng cho bộ nhớ đệm là cached_property. Như tên gợi ý, nó được sử dụng để lưu trữ kết quả của các thuộc tính lớp. Cơ chế này rất hữu ích nếu bạn có một tài sản đắt tiền để tính toán, nhưng điều đó vẫn giữ nguyên.

from functools import cached_property class Page: @cached_property def render(self, value): # Do something with supplied value... # Long computation that renders HTML page... return html

Ví dụ đơn giản này cho thấy. Chẳng hạn, thuộc tính được lưu trong bộ nhớ cache có thể được sử dụng để lưu vào bộ nhớ cache một trang HTML được hiển thị cần được hiển thị nhiều lần cho người dùng. Điều tương tự cũng có thể được thực hiện đối với các truy vấn cơ sở dữ liệu nhất định hoặc các phép tính toán học dài dòng.

Một vẻ đẹp khác cached_propertylà nó chỉ chạy khi tra cứu nên nó cho phép ta thay đổi giá trị của thuộc tính. Sau khi thay đổi thuộc tính, giá trị đã lưu trong bộ nhớ cache trước đó sẽ không thay đổi, thay vào đó, giá trị mới sẽ được tính toán và lưu vào bộ nhớ cache. Bạn cũng có thể xóa bộ đệm và tất cả những gì bạn cần làm là xóa thuộc tính.

Tôi muốn kết thúc phần này với một cảnh báo về tất cả các trình trang trí ở trên – không sử dụng chúng nếu chức năng của bạn có một số tác dụng phụ hoặc nếu nó tạo ra các đối tượng có thể thay đổi mỗi khi nó được gọi vì đây rõ ràng không phải là chức năng bạn muốn lưu vào bộ đệm .

So sánh và đặt hàng

Có thể bạn đã biết rằng bạn có thể triển khai các toán tử so sánh trong Python chẳng hạn như <, >=or ==, với lt, gtor eq. Tuy nhiên, có thể khá khó chịu khi nhận ra từng eq, lt, le, gtor ge. May mắn thay, funcools có một công cụ trang trí @total_orderingcó thể giúp chúng tôi với điều này, bởi vì tất cả những gì chúng tôi cần thực hiện là eqmột trong các phương thức còn lại và phần còn lại của trình trang trí sẽ được tạo tự động.

from functools import total_ordering @total_ordering
class Number: def __init__(self, value): self.value = value def __lt__(self, other): return self.value Number(3))
# True
print(Number(1) = Number(15))
# True
print(Number(10) <= Number(2))
# False

Bằng cách này, chúng ta có thể thực hiện tất cả các thao tác so sánh mở rộng, mặc dù chỉ có eq và bằng tay lt. Lợi ích rõ ràng nhất là sự tiện lợi, đó là bạn không phải viết tất cả các phương thức ma thuật bổ sung này, nhưng có lẽ điều quan trọng hơn là giảm số lượng mã và khả năng đọc tốt hơn của nó.

Quá tải

Có lẽ tất cả chúng ta đều được dạy rằng không có quá tải trong Python, nhưng thực sự có một cách dễ dàng để triển khai nó bằng cách sử dụng hai hàm từ funcools, cụ thể là phương thức gửi đơn và/hoặc phương thức gửi đơn. Các chức năng này giúp chúng tôi triển khai cái mà chúng tôi gọi là thuật toán gửi nhiều lần cho phép các ngôn ngữ lập trình được nhập động, chẳng hạn như Python, phân biệt giữa các loại trong thời gian chạy.

Một phần

Tất cả chúng ta đều làm việc với nhiều thư viện hoặc khung bên ngoài khác nhau, nhiều trong số đó cung cấp các chức năng và giao diện yêu cầu chúng ta chuyển các cuộc gọi lại, chẳng hạn như cho các hoạt động không đồng bộ hoặc lắng nghe các sự kiện. Điều này không có gì mới, nhưng điều gì sẽ xảy ra nếu chúng ta cũng cần chuyển một số đối số cùng với lệnh gọi lại. Đây là nơi nó có trong funcools tiện dụng.partial. Có thể được sử dụng partialđể đóng băng một số (hoặc tất cả) các đối số cho một hàm bằng cách tạo một đối tượng mới với chữ ký hàm được đơn giản hóa. Bối rối? Hãy cùng xem một số ví dụ thực tế:

def output_result(result, log=None): if log is not None: log.debug(f"Result is: {result}") def concat(a, b): return a + b import logging
from multiprocessing import Pool
from functools import partial logging.basicConfig(level=logging.DEBUG)
logger = logging.getLogger("default") p = Pool()
p.apply_async(concat, ("Hello ", "World"), callback=partial(output_result, log=logger))
p.close()
p.join()

Đoạn mã trên cho thấy cách bạn có thể sử dụng nó partialđể truyền một chức năng ( output_result) cùng với một đối số ( log=logger) như một cuộc gọi lại. Trong trường hợp này, chúng tôi sẽ sử dụng multiprocessing.apply_async, tính toán không đồng bộ kết quả của hàm ( concat) và trả về kết quả của cuộc gọi lại. Tuy nhiên, apply_asyncnó sẽ luôn chuyển kết quả làm đối số đầu tiên và nếu chúng tôi muốn bao gồm bất kỳ đối số bổ sung nào, như trường hợp của log=logger, chúng tôi cần sử dụng một phần.

Chúng tôi đã xem xét một trường hợp sử dụng khá nâng cao và một ví dụ đơn giản hơn sẽ là cách tạo thông thường của một hàm viết bằng thiết bị xuất chuẩn thay vì thiết bị xuất chuẩn:

import sys
from functools import partial print_stderr = partial(print, file=sys.stderr)
print_stderr("This goes to standard error output")

Với thủ thuật đơn giản này, chúng tôi đã tạo một chức năng có thể gọi mới sẽ luôn vượt qua file=sys.stderrlàm đối số được đặt tên thành đầu ra, điều này cho phép chúng tôi đơn giản hóa mã của mình và không phải chỉ định giá trị của đối số được đặt tên mỗi lần.

Và một ví dụ điển hình cuối cùng. chúng ta có thể sử dụng partialkết hợp với một chức năng ít được biết đến iterđể tạo một trình vòng lặp bằng cách chuyển vào một đối tượng có thể gọi được và sentinelin iter, có thể được áp dụng như thế này:

from functools import partial RECORD_SIZE = 64 # Read binary file...
with open("file.data", "rb") as file: records = iter(partial(file.read, RECORD_SIZE), b'') for r in records: # Do something with the record...

Thông thường, khi đọc một tệp, chúng tôi muốn lặp qua các dòng, nhưng trong trường hợp dữ liệu nhị phân, chúng tôi có thể cần lặp qua các bản ghi có kích thước cố định. Bạn có thể làm điều này bằng cách tạo một đối tượng có thể gọi bằng cách sử dụng partialđọc đoạn dữ liệu đã chỉ định và chuyển nó vào iterđể tạo một trình vòng lặp. Trình lặp này sau đó gọi hàm đọc cho đến khi nó chạy đến cuối tệp, luôn chỉ lấy kích thước đoạn được chỉ định ( RECORD_SIZE). Cuối cùng, khi đến cuối tệp, giá trị sentinel(b”) được trả về và quá trình lặp lại dừng lại.

trang trí

Chúng ta đã nói về một số decorator trong các phần trước, nhưng không phải decorators để tạo ra nhiều decorator hơn. Một công cụ trang trí như vậy là funcools.wraps. Để hiểu tại sao bạn cần nó, chúng ta hãy xem một ví dụ:

def decorator(func): def actual_func(*args, **kwargs): """Inner function within decorator, which does the actual work""" print(f"Before Calling {func.__name__}") func(*args, **kwargs) print(f"After Calling {func.__name__}") return actual_func @decorator
def greet(name): """Says hello to somebody""" print(f"Hello, {name}!") greet("Martin")
# Before Calling greet
# Hello, Martin!
# After Calling greet

Ví dụ này cho thấy cách thực hiện một trình trang trí đơn giản. Chúng tôi bọc một chức năng thực hiện một nhiệm vụ cụ thể ( actual_func) với một bộ trang trí bên ngoài và nó trở thành một bộ trang trí, sau đó có thể được áp dụng cho các chức năng khác, chẳng hạn như trường hợp với greet. Khi bạn gọi hàm, greet bạn sẽ thấy rằng nó in ra cả hai tin nhắn từ actual_funcvà của riêng mình. Có vẻ ổn, phải không? Nhưng điều gì sẽ xảy ra nếu chúng ta làm điều này:

print(greet.__name__)
# actual_func
print(greet.__doc__)
# Inner function within decorator, which does the actual work

Khi chúng tôi gọi tên và tài liệu của chức năng trang trí, chúng tôi nhận ra rằng chúng đã được thay thế bằng các giá trị từ chức năng trang trí. Điều này thật tệ vì chúng ta không thể viết lại tất cả các tên hàm và tài liệu khi chúng ta sử dụng một số trình trang trí. Làm thế nào để giải quyết vấn đề này? Tất nhiên, với funcools.wraps:

from functools import wraps def decorator(func): @wraps(func) def actual_func(*args, **kwargs): """Inner function within decorator, which does the actual work""" print(f"Before Calling {func.__name__}") func(*args, **kwargs) print(f"After Calling {func.__name__}") return actual_func @decorator
def greet(name): """Says hello to somebody""" print(f"Hello, {name}!") print(greet.__name__)
# greet
print(greet.__doc__)
# Says hello to somebody

Mục đích duy nhất của chức năng wrapslà sao chép tên, tài liệu, danh sách đối số, v.v., để tránh ghi đè. Xét rằng wrapsnó cũng là một công cụ trang trí, bạn chỉ cần thêm nó vào fact_func của chúng tôi và vấn đề đã được giải quyết!

Giảm

Cuối cùng nhưng không kém phần quan trọng trong funcools mô-đun là điều này giảm. Có lẽ từ các ngôn ngữ khác, bạn có thể biết nó là fold(Haskell). Hàm này nhận một iterable và gấp (thêm) tất cả các giá trị của nó thành một. Có nhiều ứng dụng cho việc này, ví dụ:

from functools import reduce
import operator def product(iterable): return reduce(operator.mul, iterable, 1) def factorial(n): return reduce(operator.mul, range(1, n)) def sum(numbers): # Use `sum` function from standard library instead return reduce(operator.add, numbers, 1) def reverse(iterable): return reduce(lambda x, y: y+x, iterable) print(product([1, 2, 3]))
# 6
print(factorial(5))
# 24
print(sum([2, 6, 8, 3]))
# 20
print(reverse("hello"))
# olleh

Như bạn có thể thấy từ mã, reduce có thể đơn giản hóa hoặc cô đọng mã thành một dòng, nếu không thì sẽ dài hơn nhiều. Như đã nói, thường là một ý tưởng tồi nếu lạm dụng chức năng này chỉ vì mục đích thu gọn mã, làm cho nó “thông minh hơn”, vì nó nhanh chóng trở nên đáng sợ và không thể đọc được. Vì lý do này, theo tôi, nó nên được sử dụng một cách tiết kiệm.

Và nếu bạn nhớ rằng nó reduce thường rút ngắn mọi thứ thành một dòng, nó có thể được kết hợp hoàn hảo với partial:

product = partial(reduce, operator.mul) print(product([1, 2, 3]))
# 6

Và cuối cùng, nếu bạn cần nhiều hơn là kết quả cuối cùng “đã bị sập”, thì bạn có thể sử dụng accumulate– từ một mô-đun tuyệt vời khác itertools. Để tính toán tối đa, nó có thể được sử dụng như sau:

from itertools import accumulate data = [3, 4, 1, 3, 5, 6, 9, 0, 1] print(list(accumulate(data, max)))
# [3, 4, 4, 4, 5, 6, 9, 9, 9]

Kết luận

Như bạn có thể thấy, funcool có rất nhiều chức năng và công cụ trang trí hữu ích có thể giúp cuộc sống của bạn dễ dàng hơn, nhưng đây chỉ là phần nổi của tảng băng chìm. Như mình đã nói ở phần đầu, có rất nhiều hàm trong thư viện chuẩn của Python giúp bạn viết code tốt hơn, nên ngoài các hàm đã giới thiệu ở đây, bạn có thể chú ý đến các module khác, chẳng hạn như operatoror itertool. Đối với bất kỳ truy vấn nào, bạn có thể nhấn vào hộp bình luận của tôi. Tôi sẽ cố gắng hết sức để giải quyết đầu vào của bạn và hy vọng sẽ cung cấp cho bạn đầu ra mong muốn. Bạn cũng có thể liên hệ với tôi trên LinkedIn: -

https://www.linkedin.com/in/shivani-sharma-aba6141b6/

Phương tiện hiển thị trong bài viết này không thuộc sở hữu của Analytics Vidhya và được sử dụng theo quyết định của Tác giả.

Nguồn: https://www.analyticsvidhya.com/blog/2021/08/functools-the-power-of-higher-order-functions-in-python/

Dấu thời gian:

Thêm từ Phân tích Vidhya