Python all, any 의 short circuit

DoonDoon·2019년 7월 14일
0
post-thumbnail

회사에서 코드리뷰 중 all, anyshort-circuit evaluation 에 대한 피드백을 받을 내용을 바탕으로 알게 된 것들을 정리합니다

TL;DR


all(), any()경우에 따라 short-circuit evaluation 을 수행하지 않습니다

Contents


  1. all? any? short-circuit?
  2. all, any 의 동작
  3. 우리가 기대한 것과 실제 동작
  4. Better way
  5. 가독성과 성능의 Trade-off

all? any? short-circuit?

all(iterable)

all() 은 인자로 전달된 모든 값이 True 인지 아닌지를 확인 합니다
Return True if all elements of the iterable are true (or if the iterable is empty). Equivalent to:

def all(iterable):
    for element in iterable:
      if not element:
        return False
    return True

any(iterable)

any() 는 인자로 전달된 모든 값 중 하나라도 True 가 있는지 확인 합니다
Return True if any element of the iterable is true. If the iterable is empty, return False. Equivalent to:

def any(iterable):
    for element in iterable:
        if element:
            return True
    return False

short-circuit evaluation

and/or 연산을 함에 있어서 첫 번째 인수로 값을 평가하기에 충분하다면
두 번째 (또는 그 이후) 인수는 평가하지 않는 것을 의미합니다

예를 들어 아래와 같은 상황입니다

False ans True
True or False

and 연산을 함에 있어 False 가 있다는 건 나머지 값을 몰라도 연산의 결과가 False 임을 의미합니다
or 연산 또한 마찬가지겠죠

all, any 의 동작

내장 모듈이므로 실제 코드는 cpython 을 참고해 주세요
아래는 파이썬 문서에서 "대략 아래 코드처럼 동작한다" 고 설명한 부분 입니다

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(), any() 코드의 3번째 줄을 보시면, Truthy, Falsy 한 값이 발견되면
그 순간 바로 함수를 종료합니다. 따라서 short-circuit evaluation 을 하고 있다고 할 수 있겠죠?
그럼 저는 이 글을 쓰는 의미가 없었을 겁니다

우리가 기대한 것과 실제 동작


파이썬의 리스트... 가 아니라 어떤 Container Object 가 생성될때는
내부 요소가 생성시에 전부 값으로 평가 됩니다.

>>> import dis
>>> dis.dis('[1, 2, 3]')
  1           0 LOAD_CONST               0 (1)
              2 LOAD_CONST               1 (2)
              4 LOAD_CONST               2 (3)
              6 BUILD_LIST               3
              8 RETURN_VALUE

만약, 아래와 같이 긴 if 문이 있어서 all() 을 이용해 변수로 처리하고
이를 if 문에서 평가한다면 어떨가요

user = User.objects.get(pk=1)

is_authenticated = all([
    user.is_authenticated(),
    not user.is_banned,
    not user.session.expired,
])

if is_authenticated:
    print('Hello World!')

Django 를 예를 들어 설명하겠습니다.
Django 모델의 인스턴스에서 어떤 동작을 하면 대개 Database Query 가 발생합니다.

만약, user.session.expired 라는 동작이
꽤 실행 시간이 오래 걸린다면 최대한 user.session.expired 가 평가되기 전에
다른 곳에서 False 가 발생해서 short-circuit evaluation 이 일어나길 바래야 합니다

그런데 위의 list 생성 시의 동작을 보면 LOAD_CONST 를 통해 값으로 평가되어 버립니다
(LOAD_CONST 는 상수를 요소로 넣었을 때지만, 함수를 호출하여 그 결과를 값으로 쓰거나 할 경우 CALL_FUNCTION 입니다)

저는 가독성을 위해 많은 조건을 all() 함수 배열로 넣어서
해당 조건을 대표할 수 있는 이름을 변수명으로 하여 가독성을 높이고자 했습니다...만

이렇게 하는 것이 좋지 못한 패턴이라는 코드리뷰 피드백을 받았습니다

Better Way


위 코드를 short-circuit evaluate 되도록 하려면 어떻게 변경해야 할까요?
간단하게도, if 조건식 안으로 옮겨주면 됩니다

user = User.objects.get(pk=1)

if (user.is_authenticated()
    and not user.is_banned
    and not user.session.expired):
    print('Hello World!')

if () 안의 내용 중 False 가 있으면 거기까지만 평가한다는건
아래와 같이 증명해 볼 수 있습니다

>>> counter = 0
>>> def true():
...     global counter
...     print(True)
...     counter += 1
...     return True
... 
>>> def false():
...     global counter
...     print(False)
...     counter += 1
...     return False
... 
>>> if (true() and false() and true()):
...     pass
... else:
...     print(counter)
... 
True
False
2  

true(), false() 함수를 만들고, 각각이 True, False 를 반환하게 합니다
그리고 전역 변수 counter 의 값을 호출될 때 마다 1씩 증가시킵니다

if 조건문에서 2번 true(), 1번 false() 함수가 호출되어, counter 의 값이 3이어야 하지만
false() 에서 short-circuit evaluation 되어, counter 의 값이 2임을 확인할 수 있습니다

만약 어쨌든 if 문이 길어지는게 싫어 🐶 라고 하신다면
아래와 같이 해볼 수 있을겁니다

>>> def conditions():
...     yield true()
...     yield false()
...     yield true()
... 
>>> is_true = all(conditions())
True
False
>>> if is_true:
...     pass
... else:
...     print(counter)
...
2

조건을 제너레이터 함수로 만든 다음, all() 에서 short-circuit 시키면 됩니다
정말 성능과 가독성을 중요하게 하고자 하면 해볼 수 있을 것 같습니다

가독성과 성능의 Trade-off


보기 좋은 떡이 먹기도 좋다 는 말처럼
코드도 보기 좋은 코드가 성능도 좋다
이렇게 아름다운 세상이면 좋겠지만, 현실은 그렇지 않았습니다

그런 맥락에서 가독성과 성능의 갈림길에서 선택은 항상 발생한다고 생각합니다
퍼포먼스를 잘 고려해서 가독성을 선택할 지, 성능을 생각할지는 항상 고민해야 할 것 같습니다

다만, 비싼 I/O 작업 만큼은 (Database, Netwwork 등등...)
가독성을 조금 포기하더라도 성능을 추구하는게 사용자를 위한 선택이 아닐까
조심스럽게 고민해 보게 되었습니다

사족...🐍


코드리뷰가 있는 곳에서 일하게 되어서 배우는 것도 많고 모르던 사실도 많이 공부하게 됩니다

코드리뷰가 의외로 도입 되기 힘든 문화라는걸 얼마 전에 알게되어서
(서로 예민하게 받아들이거나, 불필요한 논쟁을 피하고자 아예 도입하지 않는 경우도...)
성숙한 코드리뷰 문화가 있는 조직에서 일하게 되어 대단히 만족스럽습니다 ㅇㅅㅇ

코드리뷰 문화가 많은 기업에서 도입되고 자리잡아서
이런 사소한 부분도 고민하고 고칠 수 있는 기회가 많아졌으면 좋겠습니다
(페어프로그래밍도!)

profile
Espresso Cream Cat DD

1개의 댓글

comment-user-thumbnail
2020년 1월 14일

좋은 글 감사합니답!

답글 달기