any와 all은 Python 기본함수로서 여러가지 경우에 대해 참/거짓 값을 종합적으로 판단할 때 긴 for 루프를 줄이고 간단하게 만들게 되어 전체 코드가 한 눈에 들어올 수 있도록 도와주고 코드 길이도 줄여주면서 구현도 간편하게 해줄 수 있다. DP, graph, 각종 구현 등 쓰이는 곳이 많은 팔방미인이다. 그러나 그 정의를 정확히 알지 않으면 기대한 것과 전혀 다른 엉뚱한 결과가 나와 머리를 부여잡고 몇 시간 고민해도 답을 못 얻는 경우가 있는데 바로 이 두 함수가 대표적인 사례이다. 확실히 알고 향후 오류를 방지해보자.
리턴(return)의 신중함
먼저 필자가 겪은 오류의 사례를 보자.
return any(color != black and is_cycle(u) for u in graph[v])
그래프에서 dfs를 이용해 cycle을 찾는 문제를 풀던 와중에 적은 코드이다. 여기서 is_cycle은 그 수행중에 cycle이 발견됐다면 True를, cycle이 없이 탐색을 마쳤다면 False를 돌려준다. 그리고 그 와중에 후행 노드들에 대한 탐색 결과를 가져와서(재귀로) 그 결과를 종합해 하나라도(any) cycle이 있었다(True)면 자신을 호출한 함수에 True를 돌려주도록 의도(!)된 줄이다. 그러나 이를 edges = [[1,0]], 즉 노드 1 -> 노드 0 으로 가는 방향 그래프(directed)에 대해 수행해서 위 줄에 대한 결과만을 출력하면 다음 결과가 나타난다.
# edges = [[1,0]], 즉 노드 1 -> 노드 0 으로 가는 방향 그래프(directed)
val = any(color != black and has_cycle(u) for u in graph[v])
print(val)
...
is_cycle(1)
>> 1:0:False
즉, any를 그대로 return 해버린다면, False를 리턴하게 된다. 그러나 아래처럼 코드를 적으면 any가 True일 때만 리턴을 하게 되고, 그렇지 않은 경우는 그저 pass를 한 것과 같게 되어 아무 동작도 일어나지 않는다. 그러한 함수가 있을 수 있냐고? 그것이 바로 '프로시저'라는 것이다. 함수는 반드시 돌려주는 것이 있지만 프로시저는 호출한 함수에 값을 돌려주지 않고 내부적으로 다른 자료를 바꾼다던지 하는 동작을 수행한다. 원래 함수로 값을 돌려주지 않고도 적절한 동작을 수행하는 것으로 충분한 무언가가 존재한다는 것을 고려하면(프로시저) 원래 함수로 값을 돌려주는 데에는 신중해야 한다.
if any(color != black and has_cycle(u) for u in graph[v]):
print(True)
return True
그러므로 다음의 두 줄은 명확히 다른 것이다.
return any(color != black and is_cycle(u) for u in graph[v]) # 줄 1
if any(color != black and is_cycle(u) for u in graph[v]): return True # 줄 2
즉, 위의 줄은 any가 줄 수 있는 값인 True와 False를 반드시 리턴해서 그 다음에 오는 라인들이 실행되지 않게 하지만, 아래의 if는 any가 True를 돌려주었을 때에만 리턴을 해준다. 즉, 다음과 같은 동작이 실행되므로 매우 큰 차이가 있다.
경우별 액션 | any is True | any is False |
줄1 | return True | return False |
줄2 | return True | pass |
통상의 if문을 사용할 때도 유의해야 한다. 아래 코드를 보자.
(1)
if cur.color == gray: return True
if cur.color == white: do other thing
do something
(2)
if cur.color == gray: return True
elif cur.color == white: do other thing
else: do something
(1)과 (2)는 같은 의미이다. if 조건에 해당하면 그 이후 코드는 실행하지 않는다. 그러나 해당하지 않았다면 그 이후 코드는 모두 False라고 간주가 되는 것이다. (2)는 같은 의미이다. 하지만 (1)처럼 실행할때는 첫 if를 수행하고 난 뒤의 남은 라인들에서는 cur.color != gray란 조건이 명시적으로 보이지 않기 때문에 이를 염두에 두고 뒤의 줄들을 해석해야 한다.
any와 all의 디폴트값에 주의하자
다른 분야도 마찬가지지만 정의는 모든 공부의 기초이자 그것이 흔들리면 사상누각이 된다. any와 all의 두 정의를 알아보자(출처: https://docs.python.org/3/library/functions.html)
def all(iterable):
for element in iterable:
if not element:
return False
return True
def any(iterable):
for element in iterable:
if element:
return True
return False
all은 iterable (List, Dictionary, Tuple, set 등 모두 가능) 중에 하나라도 틀린 것이 있다면 False를 돌려주고, 그렇지 않다면, 혹은 심지어 아무것도 False를 고하지 않았다면(이 중에는 iterable이 None인 즉 빈 경우도 포함된다) True를 돌려주는 것이다. 아니, 아무도 틀리다고 하지 않았다면 맞다고 하는 것이 'all' 함수의 역할이라니! 무언가 심오한 기호논리학의 내용이 있을 수 있지만 정의가 그렇다는 설명 밖엔 할 수 없다. 이 점을 정확히 기억하지 않으면 완전탐색이나 recursion 등에서 복잡한 조건을 다룰 때 전혀 반대의 답을 낼 수 있다.
그러나 any는 우리 직관이나 상식과 일관된다. 어느것 하나라도(any) 맞다고 한다면 True를, 그렇지 않다면(즉 아무도 맞다고 하지 않았다면) False를 돌려준다.
현대의 컴퓨터 프로그래밍 언어는 인간의 언어와 가급적 유사하도록 구성되어 있으나 그렇다고 해서 그것이 늘 우리 언어처럼 동작하지 않는다는 것은 너무도 당연하므로 그 정의를 정확히 확인하고 기억해야 한다.
'IT in General > Python' 카테고리의 다른 글
Immutable vs mutable (0) | 2023.10.22 |
---|---|
Iterator와 Generator 사용방법 (0) | 2023.03.15 |
데코레이터와 피보나치 (0) | 2022.03.03 |
itertools.islice, tee, groupby, product (0) | 2022.03.03 |
functools.reduce (0) | 2022.03.02 |