3 โมดูลลับใน Python ที่ติดมาให้แล้ว · collections · itertools · functools เขียนโค้ดสั้นลงโดยไม่ต้อง pip install
collections · itertools · functools คือสามโมดูลที่ติดมากับ Python ทุกเครื่องอยู่แล้ว ไม่ต้องลงอะไรเพิ่ม แค่หยิบมาใช้ โค้ดที่เคยต้องเขียนหลายบรรทัดก็เหลือบรรทัดเดียว อ่านง่ายขึ้นและเร็วขึ้น

collections · itertools · functools คือสามโมดูลในไลบรารีมาตรฐานของ Python ที่ติดมากับทุกเครื่องอยู่แล้ว ไม่ต้อง pip install อะไรเพิ่มเลย แค่ import ก็ใช้ได้ทันที แต่หลายคนที่ผ่านพื้นฐานมาแล้วกลับไม่เคยลองใช้ เลยยังนั่งเขียนลูปนับของเอง เขียน if key not in d เอง หรือเขียนฟังก์ชันจำผลลัพธ์เอง ทั้งที่ของพวกนี้มีคนทำมาให้แล้ว
ทั้งสามตัวแก้ปัญหาคนละด้านกัน · collections ให้โครงสร้างข้อมูลสำเร็จรูปไว้นับและจัดกลุ่ม · itertools ให้เครื่องมือวนลูปที่ประหยัดหน่วยความจำ · functools ให้เครื่องมือจัดการฟังก์ชัน เช่นการจำผลลัพธ์ไว้ไม่ให้คำนวณซ้ำ จุดร่วมคือช่วยให้โค้ดที่เคยยาวหลายบรรทัดเหลือบรรทัดเดียว อ่านรู้เรื่องขึ้น และในหลายเคสยังเร็วขึ้นด้วย บทความนี้หยิบมาเฉพาะตัวที่คุ้มที่สุดในแต่ละโมดูล พร้อมตัวอย่างเทียบให้เห็นว่าโค้ดเดิม vs โค้ดที่ใช้โมดูลแล้วต่างกันแค่ไหน
collections — เลิกนับของเองได้แล้ว

โจทย์ที่เจอบ่อยที่สุดในงานเขียนสคริปต์คือ "นับว่าอะไรโผล่มากี่ครั้ง" แบบเขียนเองต้องเช็กก่อนทุกครั้งว่าเคยเห็น key นี้ยัง ถ้ายังก็ตั้งเป็น 1 ถ้าเคยก็บวกเพิ่ม
counts = {}
for word in words:
if word not in counts:
counts[word] = 0
counts[word] += 1Counter จาก collections ยุบทั้งหมดนั้นเหลือบรรทัดเดียว มันเป็น dict ชนิดหนึ่งที่เอาไว้นับของโดยเฉพาะ ส่งอะไรที่วนได้เข้าไปแล้วมันนับให้เลย
from collections import Counter
counts = Counter(words)จุดที่ Counter เหนือ dict ธรรมดาคือ ถ้าเรียก key ที่ไม่มี มันจะคืนค่า 0 ไม่ใช่ KeyError เลยไม่ต้องกลัวพังตอนเรียกของที่ยังไม่เคยเจอ อีกอย่างคือมี most_common(n) ไว้ดึงตัวที่โผล่บ่อยสุด n อันดับแรกได้ในบรรทัดเดียว เช่น Counter(words).most_common(3) ก็ได้สามคำที่เจอบ่อยที่สุดเรียงมาให้แล้ว
ตัวที่สองคือ defaultdict เอาไว้ตอนต้องการ "จัดกลุ่ม" — รวมหลายค่าไว้ใต้ key เดียว แบบเขียนเองก็ต้องคอยเช็กว่า key นี้มี list รออยู่หรือยัง ถ้ายังก็สร้างก่อน
groups = {}
for name, dept in people:
if dept not in groups:
groups[dept] = []
groups[dept].append(name)defaultdict(list) ตัดขั้นตอนเช็กทิ้งไปเลย เพราะพอเจอ key ที่ยังไม่มี มันจะเรียก factory ที่เราใส่ไว้ (list) สร้างค่าเริ่มต้นให้เองอัตโนมัติ
from collections import defaultdict
groups = defaultdict(list)
for name, dept in people:
groups[dept].append(name)อยากนับก็ใช้ defaultdict(int) อยากเก็บแบบไม่ซ้ำก็ defaultdict(set) เอกสารทางการของ Python เองยังระบุว่าแพตเทิร์นจัดกลุ่มแบบนี้ ถ้าใช้ defaultdict(list) จะ "สั้นและเร็วกว่า" การใช้ dict.setdefault()
ตัวที่สามคือ deque (อ่านว่า "เด็ค") เป็นคิวสองหัวที่เพิ่มและดึงของจากหัวหรือท้ายก็ได้ในเวลาคงที่ (O(1)) ต่างจาก list ที่เวลา pop(0) หรือ insert(0, v) ต้องขยับของทั้งก้อน (O(n)) ถ้างานไหนต้องเติม-ดึงจากด้านหน้าบ่อย deque จะเร็วกว่าชัดเจน ลูกเล่นที่ใช้บ่อยคือใส่ maxlen ได้ พอของเต็มแล้วเติมเข้าด้านหนึ่ง ของจากอีกด้านจะหลุดออกเอง เหมือนคำสั่ง tail ที่เก็บแค่บรรทัดท้าย ๆ
from collections import deque
def tail(filename, n=10):
return deque(open(filename), n)แค่นี้ก็ได้ n บรรทัดสุดท้ายของไฟล์โดยไม่ต้องโหลดทั้งไฟล์มาเก็บ
itertools — วนลูปโดยไม่ต้องกองข้อมูลไว้ในหน่วยความจำ
itertools คือชุดเครื่องมือสำหรับวนข้อมูลแบบ lazy หมายความว่ามันคำนวณทีละตัวตอนที่ต้องใช้จริง ไม่ต้องสร้าง list ทั้งก้อนค้างไว้ในหน่วยความจำก่อน พอข้อมูลใหญ่ขึ้น ความต่างนี้สำคัญมาก
ตัวที่หยิบมาใช้ได้ทันทีมีไม่กี่ตัว · chain ต่อหลายลำดับให้เป็นอันเดียว เลยไม่ต้องเขียนลูปซ้อนแค่เพื่อไล่ของจากหลายลิสต์ · islice หยิบเฉพาะช่วงที่ต้องการจาก iterator โดยไม่ต้องแปลงเป็น list ก่อน เหมาะกับงานที่อยากได้แค่ 100 แถวแรกจากไฟล์ใหญ่ · pairwise คืนคู่ที่อยู่ติดกัน เหมาะกับงานเทียบค่าปัจจุบันกับค่าก่อนหน้า · batched แบ่งข้อมูลเป็นก้อนละ n ตัว เหมาะกับการส่งทีละ batch เข้า API
ตัวที่คนเข้าใจผิดบ่อยที่สุดคือ groupby เพราะมันจัดกลุ่มของที่อยู่ติดกันตาม key ที่กำหนด แต่มีกับดักสำคัญ
นอกจากนี้ยังมี accumulate ที่คืนค่าสะสมทีละขั้น เช่นผลรวมสะสม (running total) จะใส่ฟังก์ชันอื่นเข้าไปก็ได้ เช่น operator.mul สำหรับผลคูณสะสม หรือ max สำหรับค่าสูงสุดสะสม จุดนี้ต่างจาก reduce ใน functools ที่จะกล่าวถัดไป ตรงที่ accumulate ให้ค่าระหว่างทางทุกขั้น ส่วน reduce ให้แค่ค่าสุดท้ายค่าเดียว
functools — จำผลลัพธ์ไว้ ไม่ต้องคำนวณซ้ำ

ของเด็ดที่สุดของ functools คือ lru_cache กับ cache มันคือ decorator ที่แปะหัวฟังก์ชันเพื่อจำผลลัพธ์ไว้ พอเรียกด้วย argument เดิมอีกครั้ง มันคืนค่าที่จำไว้เลยโดยไม่คำนวณใหม่ ตัวอย่างคลาสสิกคือฟังก์ชัน Fibonacci แบบเรียกตัวเอง ที่ปกติช้ามากเพราะคำนวณค่าเดิมซ้ำเป็นพันเป็นหมื่นรอบ
from functools import cache
@cache
def fib(n):
if n < 2:
return n
return fib(n - 1) + fib(n - 2)แค่บรรทัด @cache บรรทัดเดียว การเรียกซ้ำที่เคยโตแบบ exponential ก็กลายเป็นเชิงเส้นทันที เพราะแต่ละค่าคำนวณแค่ครั้งเดียว ความต่างของสองตัวนี้คือ cache (มีตั้งแต่ 3.9) จำผลลัพธ์ได้ไม่จำกัดจำนวนและไม่ทิ้งค่าเก่า จึงเบาและเร็วกว่า · ส่วน lru_cache(maxsize=128) จะจำแค่ผลลัพธ์ล่าสุดตามจำนวนที่กำหนด พอเต็มก็ทิ้งตัวที่ไม่ได้ใช้นานสุดออก (LRU) เหมาะกับงานที่ argument หลากหลายจนจำหมดไม่ไหว
ตัวที่สองคือ partial เอาไว้ "ล็อก" argument บางตัวของฟังก์ชันไว้ล่วงหน้า แล้วได้ฟังก์ชันใหม่ที่เรียกง่ายขึ้น เช่นอยากได้ฟังก์ชันแปลงเลขฐานสองเป็นฐานสิบ แทนที่จะเขียนใหม่ทั้งตัว ก็เอา int ที่มีอยู่แล้วมาล็อก base=2 ไว้
from functools import partial
basetwo = partial(int, base=2)
basetwo('10010') # ได้ 18ตัวที่สามคือ reduce ใช้ฟังก์ชันสองตัวแปรยุบทั้งลำดับให้เหลือค่าเดียว โดยไล่ทำจากซ้ายไปขวา เช่น reduce(lambda x, y: x + y, [1, 2, 3, 4, 5]) คือการบวกสะสมจนได้ 15 ใช้ได้เมื่ออยากได้แค่ผลรวมสุดท้าย ถ้าอยากเห็นค่าระหว่างทางด้วยให้กลับไปใช้ accumulate ของ itertools แทน
เริ่มจาก import บรรทัดเดียววันนี้เลย
ไม่ต้องรื้อโปรเจกต์ทั้งโปรเจกต์ ลองแค่สามก้าวนี้ในงานที่เขียนอยู่ตอนนี้ก็พอ
- ถ้าเจอโค้ดส่วนไหนที่กำลังนับของด้วย dict เปล่า ๆ ลองเปลี่ยนเป็น
from collections import Counterแล้วส่งข้อมูลเข้าไปตรง ๆ - ลูปที่ต่อ
if key not in d: d[key] = []ก่อนทุกครั้ง ลองเปลี่ยนdเป็นdefaultdict(list)แล้วลบบรรทัดเช็กทิ้ง - ฟังก์ชันคำนวณหนักที่เรียกด้วยค่าเดิมซ้ำ ๆ ลองแปะ
@cacheที่หัว แล้ววัดเวลาก่อน-หลังเทียบกัน
ทั้งหมดนี้เป็นรายละเอียดชื่อฟังก์ชัน·พารามิเตอร์·พฤติกรรม ที่อิงจากเอกสารทางการของ Python (Python documentation) โดยตรง โมดูลพวกนี้ยังมีของอีกเยอะที่บทความนี้ไม่ได้แตะ เช่น namedtuple ChainMap ใน collections หรือ cached_property singledispatch ใน functools ตามไปเปิดเอกสารอ่านต่อได้
บางทีเครื่องมือที่ดีที่สุดก็ไม่ใช่ของที่ต้องไปหาโหลดเพิ่ม แต่เป็นของที่ติดมากับเครื่องตั้งแต่วันแรก แค่เรายังไม่เคยเปิดดู
ที่มา:
- เอกสาร collections — Container datatypes จาก Python documentation
- เอกสาร itertools — Functions creating iterators for efficient looping จาก Python documentation
- เอกสาร functools — Higher-order functions and operations on callable objects จาก Python documentation



