[Python] 표준 라이브러리
동빈나 채널의 파이썬 문법 부수기 유튜브 강의를 참고하여 정리한 내용이다.
파이썬에는 기본적으로 제공하는 표준 라이브러리가 많다.
itertools, collections, heapq, bisect, math 정도는 익숙해두면 구현 시간을 확실히 줄일 수 있다.
내장 함수
import 없이 바로 쓸 수 있는 함수들이다.
앞서 게시물에서 다뤘지만, 표준 라이브러리와 함께 자주 등장하는 것들을 정리한다.
nums = [3, 1, 4, 1, 5, 9, 2, 6]
print(sum(nums)) # 31
print(min(nums)) # 1
print(max(nums)) # 9
# sorted는 새 리스트를 반환한다
print(sorted(nums)) # [1, 1, 2, 3, 4, 5, 6, 9]
print(sorted(nums, reverse=True)) # [9, 6, 5, 4, 3, 2, 1, 1]
print(sorted(nums, key=lambda x: -x)) # [9, 6, 5, 4, 3, 2, 1, 1]
# 여러 기준으로 정렬
pairs = [(1, 3), (2, 1), (1, 2), (2, 3)]
print(sorted(pairs, key=lambda p: (p[0], p[1]))) # [(1, 2), (1, 3), (2, 1), (2, 3)]
eval()은 문자열을 파이썬 표현식으로 평가해서 결과를 반환한다.
print(eval("1 + 2")) # 3
print(eval("10 * 5 - 3")) # 47
print(eval("[1, 2, 3]")) # [1, 2, 3]
itertools
순열, 조합 계산할 때 직접 구현하면 버그가 생기기 쉬운데, itertools에 다 있다.
from itertools import permutations, combinations, product, combinations_with_replacement
permutations - 순열
순서가 중요한 경우. nPr.
from itertools import permutations
data = ['A', 'B', 'C']
result = list(permutations(data, 2))
print(result)
# [('A', 'B'), ('A', 'C'), ('B', 'A'), ('B', 'C'), ('C', 'A'), ('C', 'B')]
# 전체 순열 (3P3)
result = list(permutations(data))
print(len(result)) # 6
combinations - 조합
순서가 없는 경우. nCr.
from itertools import combinations
data = ['A', 'B', 'C', 'D']
result = list(combinations(data, 2))
print(result)
# [('A', 'B'), ('A', 'C'), ('A', 'D'), ('B', 'C'), ('B', 'D'), ('C', 'D')]
print(len(result)) # 6 (4C2)
product - 중복 순열
같은 원소를 여러 번 선택할 수 있는 순열.
from itertools import product
data = ['A', 'B']
result = list(product(data, repeat=2))
print(result)
# [('A', 'A'), ('A', 'B'), ('B', 'A'), ('B', 'B')]
# 두 리스트의 카르테시안 곱
result = list(product([1, 2], ['x', 'y']))
print(result)
# [(1, 'x'), (1, 'y'), (2, 'x'), (2, 'y')]
combinations_with_replacement - 중복 조합
순서 없이 중복 허용.
from itertools import combinations_with_replacement
data = ['A', 'B', 'C']
result = list(combinations_with_replacement(data, 2))
print(result)
# [('A', 'A'), ('A', 'B'), ('A', 'C'), ('B', 'B'), ('B', 'C'), ('C', 'C')]
| 함수 | 설명 | 개수 |
|---|---|---|
permutations(iter, r) |
순열 (순서 O, 중복 X) | nPr |
combinations(iter, r) |
조합 (순서 X, 중복 X) | nCr |
product(iter, repeat=r) |
중복 순열 (순서 O, 중복 O) | n^r |
combinations_with_replacement(iter, r) |
중복 조합 (순서 X, 중복 O) | nHr |
collections
Counter - 등장 횟수 세기
리스트나 문자열에서 각 원소의 등장 횟수를 딕셔너리처럼 반환한다.
from collections import Counter
data = ['apple', 'banana', 'apple', 'cherry', 'banana', 'apple']
counter = Counter(data)
print(counter) # Counter({'apple': 3, 'banana': 2, 'cherry': 1})
print(counter['apple']) # 3
print(counter['grape']) # 0 (없으면 0 반환, KeyError 없음)
# 가장 많이 등장한 원소 n개
print(counter.most_common(2)) # [('apple', 3), ('banana', 2)]
print(counter.most_common(1)) # [('apple', 3)]
# 문자열도 가능
s = "abracadabra"
char_count = Counter(s)
print(char_count) # Counter({'a': 5, 'b': 2, 'r': 2, 'c': 1, 'd': 1})
Counter끼리 더하거나 뺄 수 있다.
c1 = Counter({'a': 3, 'b': 2})
c2 = Counter({'a': 1, 'b': 4, 'c': 1})
print(c1 + c2) # Counter({'b': 6, 'a': 4, 'c': 1})
print(c1 - c2) # Counter({'a': 2}) # 음수는 제외
deque - 덱 (양방향 큐)
리스트의 append()와 pop(0)을 쓰면 앞쪽 삭제가 O(n)이다. deque는 양쪽 삽입/삭제가 모두 O(1)이다.
from collections import deque
dq = deque([1, 2, 3])
dq.append(4) # 오른쪽 추가: [1, 2, 3, 4]
dq.appendleft(0) # 왼쪽 추가: [0, 1, 2, 3, 4]
dq.pop() # 오른쪽 제거: [0, 1, 2, 3]
dq.popleft() # 왼쪽 제거: [1, 2, 3]
print(list(dq)) # [1, 2, 3]
BFS에서 큐로 쓰는 게 전형적인 패턴이다.
from collections import deque
def bfs(graph, start):
visited = set()
queue = deque([start])
visited.add(start)
result = []
while queue:
node = queue.popleft()
result.append(node)
for next_node in graph[node]:
if next_node not in visited:
visited.add(next_node)
queue.append(next_node)
return result
rotate(n)으로 원소를 n칸 회전시킬 수도 있다.
dq = deque([1, 2, 3, 4, 5])
dq.rotate(2) # 오른쪽으로 2칸: [4, 5, 1, 2, 3]
dq.rotate(-1) # 왼쪽으로 1칸: [5, 1, 2, 3, 4]
defaultdict - 기본값 있는 딕셔너리
없는 키를 조회해도 KeyError가 발생하지 않고 기본값을 반환한다.
from collections import defaultdict
# int: 기본값 0
counter = defaultdict(int)
for char in "hello":
counter[char] += 1
print(dict(counter)) # {'h': 1, 'e': 1, 'l': 2, 'o': 1}
# list: 기본값 []
graph = defaultdict(list)
edges = [(1, 2), (1, 3), (2, 4), (3, 4)]
for u, v in edges:
graph[u].append(v)
graph[v].append(u)
print(dict(graph)) # {1: [2, 3], 2: [1, 4], 3: [1, 4], 4: [2, 3]}
그래프를 인접 리스트로 구현할 때 defaultdict(list)가 편하다.
heapq - 힙
파이썬의 heapq는 최솟값이 항상 맨 앞에 오는 최소 힙이다.
우선순위 큐가 필요할 때 쓴다. 삽입과 삭제가 O(log n)이다.
import heapq
heap = []
heapq.heappush(heap, 3)
heapq.heappush(heap, 1)
heapq.heappush(heap, 4)
heapq.heappush(heap, 1)
heapq.heappush(heap, 5)
print(heap) # [1, 1, 4, 3, 5] (내부 배열, 정렬된 것처럼 보이지 않음)
print(heapq.heappop(heap)) # 1 (최솟값)
print(heapq.heappop(heap)) # 1
print(heapq.heappop(heap)) # 3
리스트를 힙으로 변환하려면 heapify()를 쓴다. O(n)이다.
import heapq
nums = [5, 3, 8, 1, 2]
heapq.heapify(nums)
print(nums) # [1, 2, 8, 3, 5]
print(heapq.heappop(nums)) # 1
print(heapq.heappop(nums)) # 2
최대 힙
파이썬은 최소 힙만 제공한다. 최대 힙이 필요하면 값에 -1을 곱해서 넣는다.
import heapq
nums = [3, 1, 4, 1, 5, 9]
max_heap = []
for n in nums:
heapq.heappush(max_heap, -n)
# 최댓값부터 꺼내기
while max_heap:
print(-heapq.heappop(max_heap), end=' ')
# 9 5 4 3 1 1
nsmallest, nlargest
힙을 직접 쓰지 않고 가장 작은/큰 n개를 뽑을 수 있다.
import heapq
nums = [3, 1, 4, 1, 5, 9, 2, 6]
print(heapq.nsmallest(3, nums)) # [1, 1, 2]
print(heapq.nlargest(3, nums)) # [9, 6, 5]
bisect - 이진 탐색
정렬된 리스트에서 이진 탐색으로 삽입 위치를 찾는다. 직접 구현하지 않아도 된다.
from bisect import bisect_left, bisect_right
from bisect import bisect_left, bisect_right
a = [1, 2, 4, 4, 5, 7]
# bisect_left: 값이 들어갈 가장 왼쪽 위치
print(bisect_left(a, 4)) # 2
print(bisect_left(a, 3)) # 2
# bisect_right: 값이 들어갈 가장 오른쪽 위치
print(bisect_right(a, 4)) # 4
print(bisect_right(a, 3)) # 2
값이 특정 범위에 속하는 원소 개수
from bisect import bisect_left, bisect_right
def count_range(a, left, right):
return bisect_right(a, right) - bisect_left(a, left)
a = [1, 2, 3, 3, 3, 4, 5]
print(count_range(a, 3, 4)) # 4 (3이 3개, 4가 1개)
print(count_range(a, 2, 3)) # 4 (2가 1개, 3이 3개)
print(count_range(a, 1, 1)) # 1
이진 탐색으로 값 존재 여부 확인
from bisect import bisect_left
def binary_search(a, target):
idx = bisect_left(a, target)
if idx < len(a) and a[idx] == target:
return True
return False
a = [1, 3, 5, 7, 9]
print(binary_search(a, 5)) # True
print(binary_search(a, 4)) # False
math - 수학 함수
import math
import math
# 팩토리얼
print(math.factorial(5)) # 120
print(math.factorial(0)) # 1
# 제곱근
print(math.sqrt(16)) # 4.0
print(math.sqrt(2)) # 1.4142135623730951
# 올림, 내림
print(math.ceil(3.2)) # 4
print(math.floor(3.9)) # 3
# 상수
print(math.pi) # 3.141592653589793
print(math.inf) # inf (무한대)
print(-math.inf) # -inf
# 무한대 활용: 초기 최솟값
min_val = math.inf
for n in [3, 1, 4, 1, 5]:
min_val = min(min_val, n)
print(min_val) # 1
gcd, lcm
최대공약수(GCD)와 최소공배수(LCM)이다.
import math
# GCD (파이썬 3.5+)
print(math.gcd(12, 8)) # 4
print(math.gcd(100, 75)) # 25
# LCM (파이썬 3.9+)
print(math.lcm(4, 6)) # 12
print(math.lcm(12, 8)) # 24
Python 3.9 미만이면 lcm이 없어서 직접 구현해야 한다.
import math
def lcm(a, b):
return a * b // math.gcd(a, b)
print(lcm(4, 6)) # 12
print(lcm(12, 8)) # 24
# 여러 수의 GCD와 LCM (파이썬 3.9+)
nums = [12, 8, 4]
print(math.gcd(*nums)) # 4
print(math.lcm(*nums)) # 24
functools - 함수형 유틸리티
reduce
이터러블의 원소를 왼쪽부터 누적해서 하나의 값으로 만든다.
from functools import reduce
nums = [1, 2, 3, 4, 5]
# 누적 곱
result = reduce(lambda acc, x: acc * x, nums)
print(result) # 120 (1*2*3*4*5)
# 초기값 지정
result = reduce(lambda acc, x: acc + x, nums, 10)
print(result) # 25 (10+1+2+3+4+5)
lru_cache - 메모이제이션
재귀 함수에서 같은 인수로 반복 호출되는 경우 결과를 캐싱한다.
from functools import lru_cache
@lru_cache(maxsize=None)
def fib(n):
if n <= 1:
return n
return fib(n - 1) + fib(n - 2)
print(fib(50)) # 12586269025 (캐싱 없이는 매우 느림)
print(fib(100)) # 354224848179261915075
maxsize=None이면 캐시 크기 제한이 없다. @cache (Python 3.9+)가 같은 역할이다.
from functools import cache
@cache
def fib(n):
if n <= 1:
return n
return fib(n - 1) + fib(n - 2)
sys
setrecursionlimit - 재귀 깊이 제한
파이썬 기본 재귀 깊이는 1000이다. DFS나 깊은 재귀가 필요한 문제에서 먼저 설정한다.
import sys
sys.setrecursionlimit(100000)
maxsize - 최대 정수값
무한대 대신 쓸 수 있는 큰 정수가 필요할 때.
import sys
print(sys.maxsize) # 9223372036854775807 (2^63 - 1)
INF = sys.maxsize
float('inf') 또는 math.inf를 쓰는 경우가 더 많긴 하다. 정수 비교에서는 sys.maxsize가 편할 때가 있다.
stdin - 빠른 입력
대량의 입력을 받을 때 input() 대신 쓴다. 09-function.md의 입출력 최적화와 이어지는 내용이다.
import sys
input = sys.stdin.readline
n = int(input())
data = list(map(int, input().split()))
에러 상황
RecursionError - lru_cache와 재귀 깊이
lru_cache를 써도 재귀 깊이 제한은 그대로다. 스택 초과가 나면 setrecursionlimit도 함께 설정해야 한다.
from functools import lru_cache
import sys
sys.setrecursionlimit(100000)
@lru_cache(maxsize=None)
def fib(n):
if n <= 1:
return n
return fib(n - 1) + fib(n - 2)
TypeError - bisect는 정렬된 리스트에서만
from bisect import bisect_left
a = [3, 1, 4, 1, 5] # 정렬 안 됨
bisect_left(a, 3) # 결과가 틀림 (에러는 안 남)
a.sort()
bisect_left(a, 3) # 정상 동작
bisect는 리스트가 정렬되어 있다고 가정한다. 정렬 안 된 리스트에 쓰면 에러는 안 나지만 결과가 틀린다.
Counter.most_common 인수 생략
from collections import Counter
c = Counter("abracadabra")
print(c.most_common()) # 전체를 빈도순으로
print(c.most_common(2)) # 상위 2개
인수를 생략하면 전체를 빈도순으로 반환한다. None이 아니라 그냥 생략 할 수 있다.