본문 바로가기

Computer/Python

변수 영역과 클로저

반응형

숫자로 이루어진 리스트를 정렬하되 정렬한 리스트의 앞쪽에는 우선순위를 부여한 몇몇 숫자를 위치시켜야 한다고 가정해봅시다. 이러한 경우에는 리스트의 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)
  • 파이썬은 자신이 정의된 영역 밖에 변수를 참조하는 함수 클로저 (closure)를 지원하기 때문에 도우미 함수 "helper"가 "sort_priority" 함수의 group 인자에 접근할 수 있습니다.
  • 파이썬에서는 함수가 일급시민 (first-class citizen) 객체로 함수를 변수에 대입, 다른 함수에 인자로 전달, 다른 함수에서 반환하는 동작이 가능합니다. 따라서 리스트의 sort 메소드에 클로저 함수를 key 인자로 받을 수 있습니다.
  • 파이썬에서는 시퀀스를 비교할 때 0번 인덱스에 있는 값을 비교한 다음, 이 값이 가으면 다시 1번 인덱스에 있는 값을 비교하여 오름차순으로 정렬합니다. 위의 경우에는 group 인자에 속한 원소가 0번 인덱스로 0을 할당받으므로 결과는 [2,3,5,7,1,4,6,8] 이 됩니다.

여기서 더 나아가 우선순위가 높은 원소가 있을 때와 아닌 때를 구분해 처리할 수 있다면 더 좋을 것 같습니다. 따라서 "sort_priority" 함수 안에 found 라는 변수를 선언하고 "helper" 클로저 함수안에서 그룹에 속한 원소가 있을 때 found 변수를 True로 바꿔줍니다.

def sort_priority(values, group):
    found = False
    def helper(x):
        if x in group:
            found = True
            return (0, x)
        return (1, x)

    values.sort(key=helper)
    return found

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

정렬 결과는 맞지만 우선순위가 높은 그룹에 속한 원소가 있더라도 False가 출력됩니다. 왜 그럴까요? 파이썬에서 변수를 참조할 때는 1) 현재 함수의 영역, 2) 현재 함수를 둘러싼 영역, 3) 현재 코드가 들어 있는 모듈의 영역 (global scope), 4) 내장 영역 (len, str 등의 내장 함수) 순으로 영역을 탐색하고 이름에 해당하는 변수가 없으면 NameError 예외를 발생시킵니다.

하지만 위의 코드는 변수를 탐색하는 것이 아닌 변수에 값을 새롭게 대입하는 것으로 변수가 현재 영역에 이미 정의되어 있다면 그 변수의 값만 새로운 값으로 바뀌지만 변수가 현재 영역에 정의되어 있지 않다면 파이썬은 변수 대입을 변수 정의로 취급합니다. 따라서 위의 "helper" 클로저 함수 내의 "found = True" 구문으로 인해 클로저 함수 내에서의 found 변수는 클로저 함수 내부 영역의 새로운 변수로 취급됩니다. 따라서 우리가 의도한 "sort_priority" 함수의 found 변수의 값이 변하지 않습니다.

이 문제가 언뜻 당연해 보여도 자주 실수하는 문제이고 디버깅하기 은근히 쉽지 않습니다. 이는 함수에서 사용한 지역 변수가 그 함수를 포함하고 있는 모듈 여역을 더럽히지 못하게 막는 것으로 이런 식으로 처리하지 않으면 함수 내에서 사용한 모든 대입문이 전역 모듈 영역에 쓰레기 변수를 추가하게 됩니다.


파이썬에서는 클로저 밖으로 데이터를 끌어내는 특별한 nonlocal 구문이 있습니다. nonlocal 문이 지정된 변수에 대해서는 앞에서 설명한 영역 결정 규칙에 따라 대입된 변수의 영역이 결정되지만 전역 영역을 더럽히지 못하도록 모듈 수준 영역까지 변수 이름을 찾아 올라가지 않습니다. nonlocal 문은 대입할 데이터가 클로저 밖에 있어서 다른 영역에 속한다는 사실을 분명히 알려주고 다음과 같이 nonlocal 구문으로 found 변수를 선언해주면 True가 출력됩니다. 

def sort_priority(values, group):
    found = False
    def helper(x):
        nonlocal found
        if x in group:
            found = True
            return (0, x)
        return (1, x)

    values.sort(key=helper)
    return found

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

하지만 전역 변수를 사용하는 global 구문과 마찬가지로 nonlocal 구문도 코드가 복잡해지고 함수 동작을 이해하기 어려워지기 때문에 사용하지 않는 것이 낫습니다. 차라리 다음과 같이 정렬 함수를 클래스로 구현하는 것이 더 깔끔합니다.

class Sorter:
    def __init__(self, group):
        self.group = group
        self.found = False

    def __call__(self, x):
        if x in self.group:
            self.found = True
            return (0, x)
        return (1, x)

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
반응형

'Computer > Python' 카테고리의 다른 글

데코레이터와 functools.wrap  (0) 2021.06.27
Public, Private Attributes  (0) 2021.06.27
스레드 세이프 (Thread-safe)  (0) 2021.06.20
GIL (Global Interpreter Lock), Multi-Threading  (0) 2021.06.20
제너레이터와 yield (1)  (0) 2021.06.14