컴프리헨션에서 같은 계산을 위해 여러 위치에서 공유하는 경우가 흔합니다.
아래의 예씨는 회사에서 주문을 관리하기 위한 프로그램이고 재고를 확인하는 과정입니다.
stock = {
'못': 125,
'나사못': 35,
'나비너트': 8,
'와셔': 24,
}
order = ['나사못', '나비너트', '클립']
def get_batches(count, size):
return count // size
result = {}
for name in order:
count = stock.get(name, 0)
batches = get_batches(count, 8)
if batches:
result[name] = batches
print(result)
{'나사못': 4, '나비너트': 1}
# Dictionary Comprehension
found = {name: get_batches(stock.get(name, 0), 8)
for name in order
if get_batches(stock.get(name, 0), 8)}
print(found)
{'나사못': 4, '나비너트': 1}
앞의 코드 보다는 짧으나 get_batches
가 반복된다는 단점이 있습니다. 두 식을 항상 똑같이 변경해야 합니다. 첫 번째 get_batches
에서 두 번째 인자를 8에서 4로 바꿀 경우, 결과가 달라집니다.
has_bug = {name: get_batches(stock.get(name, 0), 4)
for name in order
if get_batches(stock.get(name, 0), 8)}
print('예상:', found)
print('실졔: ', has_bug)
예상: {'나사못': 4, '나비너트': 1}
실졔: {'나사못': 8, '나비너트': 2}
이러한 문제의 해결점은 :=
왈러스 연산자를 사용하는 것입니다. 왈러스 연산자는 컴프리헨션의 일부분에 대입식을 만듭니다.
found = {name: batches for name in order
if (batches := get_batches(stock.get(name, 0), 8))}
다시 두 코드를 모아놓고 비교하면, :=
의 사용으로 훨씬 간편해졌음을 알 수 있습니다.
# 기존 코드
has_bug = {name: get_batches(stock.get(name, 0), 4)
for name in order
if get_batches(stock.get(name, 0), 8)}
# ':=' 사용
found = {name: batches for name in order
if (batches := get_batches(stock.get(name, 0), 8))}
**:=
의 사용으로 달라진 점**
order
키와 get_batches
를 한 번만 조회하여 그 결과를 변수 batches
에 저장할 수 있습니다.get_batches
를 다시 호출할 필요 없이 딕셔너리를 만들 수 있습니다.에러가 발생하는 코드
result = {name: (tenth := count // 10)
for name, count in stock.items() if tenth > 0}
NameError: name 'tenth' is not defined
오류가 발생하는 부분은 어디일까요?
바로 tenth := count // 10
가 아닌 if tenth > 0
의 코드에서 발생합니다. if 절은 for … 과 변수 영역이 같습니다. 그러나 tenth
는 for … 내부에 정의돼 있지 않으므로 값을 읽을 때 오류가 발생합니다.
result = {name: tenth for name, count in stock.items()
if (tenth := count // 10) > 0}
print(result)
{'못': 12, '나사못': 3, '와셔': 2}
대입식을 조건쪽으로 옮기고 대입식에서 만들어진 변수 이름을 컴프리헨션 값에서 참조하면 이 문제를 해결할 수 있습니다.
누출
컴프리헨션이 값 부분에서 왈러스 연산자를 사용할 떄 그 값에 대한 조건 부분이 없다면 루프 밖 영역으로 로프 변수가 누출됩니다.
half = [(last := count // 2) for count in stock.values()]
print(f'{half}의 마지막 원소는 {last}')
[62, 17, 4, 12]의 마지막 원소는 12
for 문에서의 누출
for count in stock.values(): # 루프 변수가 누출됨
pass
print(f'{list(stock.values())}의 마지막 원소는 {count}')
[125, 35, 8, 24]의 마지막 원소는 24
하지만 컴프리헨션의 루프 변수인 경우에는 비슷한 누출이 발생하지 않습니다.
half = [count // 2 for count in stock.values()]
print(half) # 작동함
print(count) # 루프 변수가 누출되지 않기 때문에 예외가 발생함
[62, 17, 4, 12]
24 # 제대로 작동시
Traceback … # 오류 발생시
NameError: name ‘count’ is not defined
따라서 루프 변수를 누출하지 않는 편이 낫습니다. 컴프리헨션에서 대입시글 조건에만 사용하는 것을 권장하는 이유입니다.
대입식은 제너레이터의 경우에도 동일하게 작동합니다.
딕셔너리 인스턴스 대신 제품 이름과 현재 재고 수량 상으로 이뤄진 이터레이터를 만듭니다.
stock = {
'못': 125,
'나사못': 35,
'나비너트': 8,
'와셔': 24,
}
order = ['나사못', '나비너트', '클립']
found = ((name, batches) for name in order
if (batches := get_batches(stock.get(name, 0), 8)))
print(next(found))
print(next(found))
('나사못', 4)
('나비너트', 1)
요약