Functools -พลังของฟังก์ชันระดับสูงใน Python

โหนดต้นทาง: 1865357

บทความนี้เผยแพร่โดยเป็นส่วนหนึ่งของไฟล์ Blogathon วิทยาศาสตร์ข้อมูล

บทนำ

Python Standard Library มีโมดูลที่ยอดเยี่ยมมากมายที่จะช่วยให้โค้ดของคุณสะอาดขึ้นและเรียบง่ายขึ้นและเครื่องมือ functools นั้น แน่นอนหนึ่งใน

แคช

มาเริ่มกันที่ฟังก์ชันที่เรียบง่ายแต่ทรงพลังของโมดูล functools เริ่มต้นด้วยฟังก์ชั่นแคช (เช่นเดียวกับมัณฑนากร) – lru_cache,cache และ cached_property. คนแรกของพวกเขา - lru_cache ให้แคชของผลลัพธ์สุดท้ายของการทำงานของฟังก์ชั่นหรือกล่าวอีกนัยหนึ่งคือจดจำผลงานของพวกเขา:

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}

ในตัวอย่างนี้ เราส่งคำขอ GET และแคชผลลัพธ์ (สูงสุด 32 ผลลัพธ์) โดยใช้มัณฑนากร @lru_cache. หากต้องการดูว่าการแคชใช้งานได้จริงหรือไม่ คุณสามารถตรวจสอบข้อมูลแคชของฟังก์ชันได้โดยใช้เมธอด cache_infoที่แสดงจำนวนแคชและ Hit มัณฑนากรยังมีวิธีการ clear_cacheและ cache_parametersสำหรับการยกเลิกผลลัพธ์แคชและพารามิเตอร์การทดสอบตามลำดับ

หากคุณต้องการแคชที่ละเอียดยิ่งขึ้น คุณสามารถรวมอาร์กิวเมนต์ทางเลือก typed=true ซึ่งช่วยให้คุณแคชอาร์กิวเมนต์ประเภทต่างๆ แยกกันได้

มัณฑนากรอีกตัวหนึ่งสำหรับการแคชใน functools คือฟังก์ชันที่เรียกว่าเพียง cache. เป็นกระดาษห่อธรรมดา lru_cacheที่ละเว้นอาร์กิวเมนต์ max_size ลดลง และไม่ลบค่าเก่า

มัณฑนากรอื่นที่คุณสามารถใช้สำหรับแคชคือ cached_property. ตามชื่อที่แนะนำ มันถูกใช้เพื่อแคชผลลัพธ์ของแอตทริบิวต์คลาส ช่างนี้มีประโยชน์มากหากคุณมีคุณสมบัติที่มีราคาแพงในการคำนวณ แต่ยังคงเหมือนเดิม

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

ตัวอย่างง่ายๆนี้แสดงให้เห็น ตามที่สามารถใช้ได้ คุณสมบัติแคช ตัวอย่างเช่น เพื่อแคชหน้า HTML ที่แสดงผลซึ่งจำเป็นต้องแสดงให้ผู้ใช้เห็นซ้ำแล้วซ้ำอีก สามารถทำได้เช่นเดียวกันสำหรับการสืบค้นฐานข้อมูลบางรายการหรือการคำนวณทางคณิตศาสตร์ที่ยาวนาน

สวยไปอีกแบบ cached_propertyคือมันทำงานเฉพาะในการค้นหา ดังนั้นจึงทำให้เราสามารถเปลี่ยนแปลงค่าของแอตทริบิวต์ได้ หลังจากเปลี่ยนแอตทริบิวต์ ค่าที่แคชไว้ก่อนหน้านี้จะไม่เปลี่ยนแปลง แต่ค่าใหม่จะถูกคำนวณและแคชแทน คุณยังสามารถล้างแคชได้ และสิ่งที่คุณต้องทำคือลบแอตทริบิวต์

ฉันต้องการจบส่วนนี้โดยมีข้อแม้เกี่ยวกับมัณฑนากรด้านบนทั้งหมด อย่าใช้หากฟังก์ชันของคุณมีผลข้างเคียงหรือถ้ามันสร้างวัตถุที่เปลี่ยนแปลงได้ทุกครั้งที่มีการเรียกใช้ เนื่องจากสิ่งเหล่านี้ไม่ใช่ฟังก์ชันที่คุณต้องการแคชอย่างชัดเจน .

การเปรียบเทียบและการสั่งซื้อ

คุณอาจรู้อยู่แล้วว่าคุณสามารถใช้ตัวดำเนินการเปรียบเทียบใน Python เช่น <, >=or ==มี lt, gtor eq. อย่างไรก็ตาม มันค่อนข้างน่าหงุดหงิดที่จะตระหนักถึงแต่ละ eq, lt, le, gtor ge. โชคดีที่ functools มีมัณฑนากร @total_orderingที่สามารถช่วยเราได้ เพราะสิ่งที่เราต้องทำคือ eqหนึ่งในวิธีที่เหลือและมัณฑนากรที่เหลือจะถูกสร้างขึ้นโดยอัตโนมัติ

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

ด้วยวิธีนี้ เราสามารถใช้การดำเนินการเปรียบเทียบแบบขยายทั้งหมดได้ แม้ว่าจะมีเพียง eq และด้วยมือ lt ประโยชน์ที่ชัดเจนที่สุดคือความสะดวก ซึ่งก็คือคุณไม่จำเป็นต้องเขียนวิธีการเพิ่มเติมเหล่านี้ทั้งหมด แต่อาจมีความสำคัญมากกว่าที่จะลดจำนวนโค้ดและความสามารถในการอ่านที่ดีขึ้น

เกินพิกัด

เราทุกคนคงได้รับการสอนว่าไม่มีการโอเวอร์โหลดใน Python แต่จริงๆ แล้วมีวิธีง่ายๆ ในการใช้งานโดยใช้สองฟังก์ชันจาก functools คือ single dispatch และ/หรือ single dispatch method ฟังก์ชันเหล่านี้ช่วยให้เราสามารถนำสิ่งที่เราเรียกว่าอัลกอริธึมการจัดส่งหลายรายการมาใช้ได้ ซึ่งช่วยให้ภาษาโปรแกรมที่พิมพ์แบบไดนามิก เช่น Python สามารถแยกแยะระหว่างประเภทต่างๆ ได้ในขณะใช้งานจริง

เป็นบางส่วน

เราทุกคนทำงานกับไลบรารีหรือเฟรมเวิร์กภายนอกต่างๆ ซึ่งหลายแห่งมีฟังก์ชันและอินเทอร์เฟซที่ต้องการให้เราส่งการเรียกกลับ เช่น สำหรับการดำเนินการแบบอะซิงโครนัสหรือการฟังเหตุการณ์ นี่ไม่ใช่สิ่งใหม่ แต่ถ้าเราต้องส่งข้อโต้แย้งไปพร้อมกับการโทรกลับด้วย นี่คือที่มาใน functools ที่มีประโยชน์.partial. สามารถใช้ได้ partialเพื่อตรึงอาร์กิวเมนต์บางส่วน (หรือทั้งหมด) ของฟังก์ชันโดยการสร้างออบเจ็กต์ใหม่ที่มีลายเซ็นฟังก์ชันแบบง่าย สับสน? มาดูตัวอย่างการใช้งานจริงกัน:

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()

รหัสด้านบนแสดงวิธีการใช้งาน partialเพื่อส่งผ่านฟังก์ชัน ( output_result) พร้อมกับอาร์กิวเมนต์ ( log=logger) เป็นการโทรกลับ ในกรณีนี้ เราจะใช้ multiprocessing.apply_async ซึ่งคำนวณผลลัพธ์ของฟังก์ชันแบบอะซิงโครนัส ( concat) และส่งคืนผลลัพธ์ของการโทรกลับ อย่างไรก็ตาม, apply_asyncมันจะส่งผ่านผลลัพธ์เป็นอาร์กิวเมนต์แรกเสมอ และถ้าเราต้องการรวมอาร์กิวเมนต์เพิ่มเติม เช่นกรณีของ log=logger เราจำเป็นต้องใช้บางส่วน

เราได้พิจารณากรณีการใช้งานที่ค่อนข้างสูงแล้ว และตัวอย่างที่ง่ายกว่าคือการสร้างฟังก์ชันตามปกติที่เขียนด้วย stderr แทนที่จะเป็น stdout:

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

ด้วยเคล็ดลับง่ายๆ นี้ เราได้สร้างฟังก์ชัน callable ใหม่ที่จะผ่านเสมอ file=sys.stderrเป็นอาร์กิวเมนต์ที่มีชื่อสำหรับเอาต์พุต ซึ่งช่วยให้เราลดความซับซ้อนของโค้ดของเราและไม่ต้องระบุค่าของอาร์กิวเมนต์ที่มีชื่อทุกครั้ง

และตัวอย่างที่ดีประการสุดท้าย เราสามารถใช้ partialร่วมกับฟังก์ชันที่ไม่ค่อยมีใครรู้จัก iterเพื่อสร้างตัววนซ้ำโดยส่งผ่านวัตถุที่เรียกได้และ sentinelin iterซึ่งสามารถนำไปใช้ได้ดังนี้

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...

โดยปกติ เมื่ออ่านไฟล์ เราต้องการวนซ้ำบนบรรทัด แต่ในกรณีของข้อมูลไบนารี เราอาจต้องวนซ้ำระเบียนที่มีขนาดคงที่ คุณสามารถทำได้โดยการสร้างวัตถุที่เรียกได้โดยใช้ partialที่อ่านกลุ่มข้อมูลที่ระบุและส่งผ่านใน iterเพื่อสร้างตัววนซ้ำ ตัววนซ้ำนี้จะเรียกใช้ฟังก์ชันการอ่านจนกว่าจะถึงจุดสิ้นสุดของไฟล์ โดยจะใช้เฉพาะขนาดก้อนที่ระบุเท่านั้น ( RECORD_SIZE). สุดท้าย เมื่อถึงจุดสิ้นสุดของไฟล์ ค่า sentinel(NS ") ถูกส่งกลับและการวนซ้ำหยุด

ตกแต่ง

เราได้พูดคุยเกี่ยวกับนักตกแต่งบางส่วนแล้วในส่วนก่อนหน้า แต่ไม่ใช่นักตกแต่งเพื่อสร้างนักตกแต่งเพิ่มเติม มัณฑนากรคนหนึ่งคือ functools.wraps. เพื่อทำความเข้าใจว่าทำไมคุณถึงต้องการมัน มาดูตัวอย่างกัน:

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

ตัวอย่างนี้แสดงให้เห็นว่ามัณฑนากรแบบง่ายสามารถนำไปใช้ได้อย่างไร เราห่อฟังก์ชันที่ทำงานเฉพาะ ( actual_func) กับมัณฑนากรภายนอก และกลายเป็นมัณฑนากรเอง ซึ่งสามารถนำไปใช้กับฟังก์ชันอื่นได้ เช่น กรณีที่มี greet. เมื่อคุณเรียกใช้ฟังก์ชัน greet คุณจะเห็นว่ามันพิมพ์ข้อความทั้งจาก actual_funcและด้วยตัวของมันเอง ดูไม่เป็นไรใช่ไหม แต่จะเกิดอะไรขึ้นถ้าเราทำสิ่งนี้:

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

เมื่อเราเรียกชื่อและเอกสารประกอบของฟังก์ชั่นการตกแต่ง เราตระหนักว่ามันถูกแทนที่ด้วยค่าจากฟังก์ชั่นมัณฑนากร สิ่งนี้ไม่ดีเพราะเราไม่สามารถเขียนชื่อฟังก์ชันและเอกสารทั้งหมดของเราใหม่เมื่อเราใช้มัณฑนากรบางตัว ปัญหานี้สามารถแก้ไขได้อย่างไร? แน่นอนด้วย functools.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

จุดประสงค์เดียวของฟังก์ชัน wrapsคือการคัดลอกชื่อ เอกสาร รายการอาร์กิวเมนต์ ฯลฯ เพื่อป้องกันการเขียนทับ พิจารณาว่า wrapsนอกจากนี้ยังเป็นมัณฑนากรคุณสามารถเพิ่มลงใน actual_func ของเราและปัญหาได้รับการแก้ไข!

ลด

สุดท้ายแต่ไม่ท้ายสุดในโมดูล functools คือการลดลงนี้ บางทีจากภาษาอื่น ๆ คุณอาจจะรู้ว่ามันเป็น fold(ฮาสเคล). ฟังก์ชันนี้ใช้ iterable และพับ (เพิ่ม) ค่าทั้งหมดให้เป็นค่าเดียว มีแอปพลิเคชันมากมายสำหรับสิ่งนี้ ตัวอย่างเช่น:

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

ดังที่คุณเห็นจากรหัส reduce สามารถลดความซับซ้อนหรือย่อโค้ดให้เป็นบรรทัดเดียวได้ มิฉะนั้นอาจยาวกว่านั้นมาก จากที่กล่าวมา มักจะเป็นความคิดที่ไม่ดีที่จะใช้ฟังก์ชันนี้ในทางที่ผิดเพียงเพื่อลดขนาดโค้ด ทำให้ "ฉลาดขึ้น" เนื่องจากจะกลายเป็นเรื่องที่น่ากลัวและอ่านไม่ได้อย่างรวดเร็ว ด้วยเหตุนี้ในความคิดของฉันจึงควรใช้เท่าที่จำเป็น

และถ้าคุณจำได้ว่ามัน reduce มักจะย่อทุกอย่างให้เหลือบรรทัดเดียวก็สามารถนำมารวมกันได้อย่างลงตัว partial:

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

และสุดท้าย ถ้าคุณต้องการมากกว่าแค่ผลลัพธ์ "ยุบ" สุดท้าย คุณสามารถใช้ accumulate– จากโมดูลที่ยอดเยี่ยมอื่น itertools. คำนวณค่าสูงสุดได้ดังนี้

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]

สรุป

อย่างที่คุณเห็น functools มีฟังก์ชันที่มีประโยชน์มากมายและตัวตกแต่งที่สามารถทำให้ชีวิตของคุณง่ายขึ้น แต่นี่เป็นเพียงส่วนเล็กๆ ของภูเขาน้ำแข็ง อย่างที่ฉันบอกไปในตอนต้น มีฟังก์ชันมากมายในไลบรารีมาตรฐาน Python ที่ช่วยให้คุณเขียนโค้ดได้ดีขึ้น ดังนั้นนอกเหนือจากฟังก์ชันที่เรากล่าวถึงในที่นี้ คุณยังสามารถให้ความสนใจกับโมดูลอื่นๆ เช่น operatoror itertool. สำหรับข้อสงสัยใด ๆ คุณสามารถกดช่องแสดงความคิดเห็นของฉัน ฉันจะพยายามอย่างเต็มที่เพื่อแก้ปัญหาอินพุตของคุณและหวังว่าจะได้ผลลัพธ์ที่คุณต้องการ คุณสามารถติดต่อฉันได้ทาง LinkedIn:-

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

สื่อที่แสดงในบทความนี้ไม่ใช่ของ Analytics Vidhya และใช้ดุลยพินิจของผู้เขียน

ที่มา: https://www.analyticsvidhya.com/blog/2021/08/functools-the-power-of-higher-order-functions-in-python/

ประทับเวลา:

เพิ่มเติมจาก การวิเคราะห์ วิทยา