[Python] Skill of coding - 클로저 변수와 스코프

Hyeseong·2020년 12월 9일
1

python skill of coding

목록 보기
7/18

클로저가 변수 스코프와 상호 작용하는 방법

숫자 리스트를 정렬할 때 특정 그룹의 숫자들이 먼저 오도록 우선순위를 매기려고 한다고 하자. 이런 패턴은 사용자 인터페이스를 표현하거나, 다른 것보다 중요한 메시지나 예외 이벤트를 먼저 보여줘야 할 때 유용해요.

이렇게 만드는 일반적인 방법은 리스트의 sort메서드에 헬퍼함수를 key인수로 넘기는 것이에요. 헬퍼의 반환 값은 리스트에 있는 각 아이템을 정렬하는 값으로 사용되요. 헬퍼는 주어진 아이템이 중요한 그룹에 있는지 확인하고 그에 따라 정렬 키를 다르게 할 수 있어요.

def sort_priority(values, group):
    def helper(x):
        if x in group:
            return (0, x)
        return (1, x)
    values.sort(key=helper)

numbers = [8, 3, 1, 2, 5, 4, 7, 6]
group = {2, 3, 5, 7}
sort_priority(numbers, group)
print(numbers)
[2, 3, 5, 7, 1, 4, 6, 8]

함수가 예상대로 동작 이유는 세 가지!

하나, 파이썬은 클로저를 지원해요. 클로저란 자신이 정의된 스코프에 있는 변수를 참조하는 함수입니다. 이 것 때문에 helper함수가 sort_priority의 group인수에 접근할 수 있어요.
둘, 함수는 파이썬에서 일급객체(first-class object)이다. 이 말은 함수를 직접 참조하고, 변수에 할당하고, 다른 함수의 인수로 전달하고, 표현식과 if문 등에서 비교할 수 있다는 말이에요. 따라서 sort메서드에서 클러조 함수를 key인수로 받을 수 있어요.
파이썬에서는 튜플을 비교하는 특정한 규칙이 있어요. 먼저 인덱스 0으로 아이템을 비교하고 그 다음으로 인덱스 1, 다음은 인덱스 2와 같이 진행하는데, hleper 클로저의 반환 값이 정렬 순서를 분리되니 두 그룹으로 나뉘게 한 건이 규칙 떄문이에요.
함수에서 우선순위가 높은 아이템을 반견 했는지 여부를 반환해서 사용자 인터페이스 코드가 그에 따라 동작하게 하면 좋을 거에요. 이런 동작을 추가하는 일은 쉬워보이는데요. 이미 각 숫자가 어느 그룹에 포함되어 있는지 판별하는 클로저 함수가 있어요. 우선 순위가 높은 아이템을 발견했을 때 플래그를 뒤집는 데도 클로저를 사용하는 건 어떨까요? 그러면 함수는 클로저가 수정한 플래그 값을 반환할 수 있어요.


def sort_priority2(numbers, group):
    found = False
    def helper(x):
        if x in group:
            found = True  # Seems simple
            return (0, x)
        return (1, x)
    numbers.sort(key=helper)
    return found
numbers = [8, 3, 1, 2, 5, 4, 7, 6]
group = {2, 3, 5, 7}

found = sort_priority2(numbers, group)
print('Found:', found)
print(numbers)
Found: False
[2, 3, 5, 7, 1, 4, 6, 8]

정렬된 결과는 올바르지만 found결과는 틀렸어요. group에 속한 아이템을 numbers에서 찾을 수 있었지만 함수는 False를 반환했다. 어쨰서 이런일이?

표현식에서 변수를 참조할 때 파이썬 인터프리터는 참조를 해결하려고 다음과 같은 순서로 스코프를 탐색해요.

현재 함수의 스코프
(현재 스코프를 담고 있는 다른 함수 같은) 감싸고 있는 스코프
코드를 포함하고 있는 모듈의 스코프(전역 스코프)
(len이나 str 같은 함수를 담고 있는) 내장 스코프
이 중 어느 스코프에도 참조한 이름으로 된 변수가 정의되어 있지 않으면 NameError 예외가 일어난다.

변수에 값을 값을 할당할 때는 다른 방식으로 동작함. 변수가 이미 현재 스코프에 정의되어 있다면 새로운 값을 얻는다. 파이썬은 변수가 현재 스코프에 존재하지 않으면 변수 정의로 취급한다. 새로 정의되는 변수의 스코프는 그 할당을 포함하고 있는 함수가 된다.

이 할당 동작은 sort_priority2 함수의 반환 값이 잘못된 이유를 설명해준다. found 변수는 hepler 클로저에서 True로 할당된다. 클로저 할당은 sort_priority2에서 일어나는 할당이 아닌 helper 안에서 일어나는 새 변수 정의로 처리된다.

def sort_priority2(numbers, group):
    found = False         # Scope: 'sort_priority2'
    def helper(x):
        if x in group:
            found = True  # Scope: 'helper' -- Bad!
            return (0, x)
        return (1, x)
    numbers.sort(key=helper)
    return found

이 문제는 여러 사람을 놀라게 하는데요. 그래서 스코프 버그라고도해요. 언어 설계자가 의도한겁니다. 함수의 지역 변수가 자신을 포함하는 모듈을 오염시키는 문제를 막아줘요. 그렇지 않으면 함수 안에서 일어나는 모든 할당이 전역 모듈 스코프에 쓰레기를 넣는 결과로 이어졌을 것입니다. 그렇되면 불필요한 할당에 그치지 않고 결과로 만들어지는 전역 변수들의 상호 작용으로 알기 힘든 버그가 생겨요.

데어터 얻어오기

nonlocal문은 특정 변수 이름에 할당할 떄 스코프 탐색이 일어나야 함을 나타내는데요 유일한 제약은 nonlocal이(전역 변수의 오염회피) 모듈 수준 스코프까지는 탐색할 수 없다는 점이에요.


found = sort_priority3(numbers, group)
print('Found:', found)
print(numbers)
Found: True
[2, 3, 5, 7, 1, 4, 6, 8]

nonlocal 문은 클로저에서 데이터를 다른 스코프에 할당하는 시점을 알아보기 쉽게 해줘요. nonlocal문은 변수 할당이 모듈 스코프에 직접 들어가게 하는 global문을 보완해요.

하지만 전역 변수의 안티패턴(anti-patter)과 마찬가지로 간단한 함수이외에는 nonlocal을 사용하지 않도록 주의해주세요. nonlocal의 부작용은 알아내기가 쉽지 않아요. 특히 nonlocal문과 관련 변수에 대한 할당이 멀리 떨어진 긴 함수에서는 이해하기가 더욱 어려워요.

nonlocal을 사용할 때 복잡해지기 시작하면 헬퍼 클래스로 상태를 감싸는 방법을 이용하는게 좋아요. 이제 nonlocal을 사용할 때와 같은 결과를 얻는 클래스를 정의해보조

코드는 약간 더 길지만 이해하기는 쉬워요.


numbers = [8, 3, 1, 2, 5, 4, 7, 6]
group = {2, 3, 5, 7}
sorter = Sorter(group)
numbers.sort(key=sorter)
assert sorter.found is True
print('Found:', found)
print(numbers)
Found: True
[2, 3, 5, 7, 1, 4, 6, 8]

핵심정리

  • 클로저 함수는 자신이 정의된 스코프 중 어디에 있는 변수도 참조 할 수 있어요.
  • 기본적으로 클로저에서 변수를 할당하면 바깥쪽 스코프에는 영향을 미치지 않아요.
  • nonlocal문을 사용하면 클로저를 감싸고 있는 스코프의 변수를 수정 할 수 있음을 열려요.
  • 간단한 함수 이외에는 nonlocal문을 사용하지 마세요.
profile
어제보다 오늘 그리고 오늘 보다 내일...

0개의 댓글