소개
이 랩에서는 Python 의 클로저 (closure) 에 대해 자세히 알아보겠습니다. 클로저는 함수가 외부 함수 실행이 완료된 후에도 외부 범위 (enclosing scope) 의 변수를 기억하고 접근할 수 있게 해주는 강력한 프로그래밍 개념입니다.
또한 클로저를 데이터 구조로 이해하고, 코드 생성기로서 탐구하며, 클로저를 사용하여 타입 검사 (type-checking) 를 구현하는 방법을 배우게 됩니다. 이 랩은 Python 클로저의 다소 특이하고 강력한 측면을 발견하는 데 도움이 될 것입니다.
데이터 구조로서의 클로저
Python 에서 클로저는 데이터를 캡슐화 (encapsulate) 하는 강력한 방법을 제공합니다. 캡슐화는 데이터를 비공개로 유지하고 이에 대한 접근을 제어하는 것을 의미합니다. 클로저를 사용하면 클래스나 전역 변수를 사용하지 않고도 개인 데이터를 관리하고 수정하는 함수를 만들 수 있습니다. 전역 변수는 코드의 어느 곳에서나 접근하고 수정할 수 있으므로 예상치 못한 동작을 초래할 수 있습니다. 반면에 클래스는 더 복잡한 구조를 필요로 합니다. 클로저는 데이터 캡슐화를 위한 더 간단한 대안을 제공합니다.
이 개념을 시연하기 위해 counter.py라는 파일을 만들어 보겠습니다.
WebIDE 를 열고
/home/labex/project디렉토리에counter.py라는 새 파일을 만듭니다. 여기에서 클로저 기반 카운터를 정의하는 코드를 작성합니다.파일에 다음 코드를 추가합니다.
def counter(value):
"""
증가 및 감소 함수가 있는 카운터를 생성합니다.
인수:
value: 카운터의 초기 값
반환값:
두 개의 함수: 카운터를 증가시키는 함수 하나, 감소시키는 함수 하나
"""
def incr():
nonlocal value
value += 1
return value
def decr():
nonlocal value
value -= 1
return value
return incr, decr
이 코드에서 counter()라는 함수를 정의합니다. 이 함수는 초기 value를 인수로 받습니다. counter() 함수 내부에서 incr()과 decr()이라는 두 개의 내부 함수를 정의합니다. 이 내부 함수는 동일한 value 변수에 대한 접근을 공유합니다. nonlocal 키워드는 Python 에게 외부 범위 (enclosing scope, counter() 함수) 에서 value 변수를 수정하려는 것을 알려주는 데 사용됩니다. nonlocal 키워드가 없으면 Python 은 외부 범위의 value를 수정하는 대신 내부 함수 내에 새로운 지역 변수를 생성합니다.
- 이제 이 동작을 확인하기 위해 테스트 파일을 만들어 보겠습니다. 다음 내용으로
test_counter.py라는 새 파일을 만듭니다.
from counter import counter
## 0 에서 시작하는 카운터 생성
up, down = counter(0)
## 카운터를 여러 번 증가시킵니다.
print("Incrementing the counter:")
print(up()) ## Should print 1
print(up()) ## Should print 2
print(up()) ## Should print 3
## 카운터를 감소시킵니다.
print("\nDecrementing the counter:")
print(down()) ## Should print 2
print(down()) ## Should print 1
이 테스트 파일에서 먼저 counter.py 파일에서 counter() 함수를 가져옵니다. 그런 다음 counter(0)을 호출하고 반환된 함수를 up과 down으로 언패킹하여 0 에서 시작하는 카운터를 생성합니다. 그런 다음 up() 함수를 여러 번 호출하여 카운터를 증가시키고 결과를 출력합니다. 그 후, down() 함수를 호출하여 카운터를 감소시키고 결과를 출력합니다.
- 터미널에서 다음 명령을 실행하여 테스트 파일을 실행합니다.
python3 test_counter.py
다음 출력을 볼 수 있습니다.
Incrementing the counter:
1
2
3
Decrementing the counter:
2
1
여기에는 클래스 정의가 포함되어 있지 않다는 점에 유의하십시오. up() 및 down() 함수는 전역 변수도 인스턴스 속성도 아닌 공유 값을 조작하고 있습니다. 이 값은 클로저에 저장되어 counter()에서 반환된 함수만 접근할 수 있습니다.
이것은 클로저를 데이터 구조로 사용하는 방법의 예입니다. 캡슐화된 변수 value는 함수 호출 간에 유지되며, 이에 접근하는 함수에 대해 비공개입니다. 즉, 코드의 다른 부분에서는 이 value 변수에 직접 접근하거나 수정할 수 없으므로 데이터 보호 수준을 제공합니다.
코드 생성기로서의 클로저
이 단계에서는 클로저를 사용하여 동적으로 코드를 생성하는 방법을 배우겠습니다. 특히, 클로저를 사용하여 클래스 속성에 대한 타입 검사 시스템을 구축할 것입니다.
먼저, 클로저가 무엇인지 이해해 보겠습니다. 클로저는 메모리에 존재하지 않더라도 외부 범위 (enclosing scope) 의 값을 기억하는 함수 객체입니다. Python 에서 클로저는 중첩된 함수가 외부 함수의 값을 참조할 때 생성됩니다.
이제 타입 검사 시스템을 구현하기 시작해 보겠습니다.
/home/labex/project디렉토리에 다음 코드를 사용하여typedproperty.py라는 새 파일을 만듭니다.
## typedproperty.py
def typedproperty(name, expected_type):
"""
타입 검사를 사용하여 속성을 생성합니다.
인수:
name: 속성의 이름
expected_type: 속성 값의 예상 타입
반환값:
타입 검사를 수행하는 속성 객체
"""
private_name = '_' + name
@property
def value(self):
return getattr(self, private_name)
@value.setter
def value(self, val):
if not isinstance(val, expected_type):
raise TypeError(f'Expected {expected_type}')
setattr(self, private_name, val)
return value
이 코드에서 typedproperty 함수는 클로저입니다. name과 expected_type의 두 인수를 받습니다. @property 데코레이터는 개인 속성의 값을 검색하는 속성에 대한 getter 메서드를 생성하는 데 사용됩니다. @value.setter 데코레이터는 설정되는 값이 예상 타입인지 확인하는 setter 메서드를 생성합니다. 그렇지 않으면 TypeError를 발생시킵니다.
- 이제 이러한 타입 속성을 사용하는 클래스를 만들어 보겠습니다. 다음 코드를 사용하여
stock.py라는 파일을 만듭니다.
from typedproperty import typedproperty
class Stock:
"""타입 검사된 속성을 가진 주식을 나타내는 클래스입니다."""
name = typedproperty('name', str)
shares = typedproperty('shares', int)
price = typedproperty('price', float)
def __init__(self, name, shares, price):
self.name = name
self.shares = shares
self.price = price
Stock 클래스에서 typedproperty 함수를 사용하여 name, shares, 및 price에 대한 타입 검사된 속성을 생성합니다. Stock 클래스의 인스턴스를 생성하면 타입 검사가 자동으로 적용됩니다.
- 이 동작을 확인하기 위해 테스트 파일을 만들어 보겠습니다. 다음 코드를 사용하여
test_stock.py라는 파일을 만듭니다.
from stock import Stock
## 올바른 타입으로 주식 생성
s = Stock('GOOG', 100, 490.1)
print(f"Stock name: {s.name}")
print(f"Stock shares: {s.shares}")
print(f"Stock price: {s.price}")
## 잘못된 타입으로 속성을 설정하려고 시도
try:
s.shares = "hundred" ## This should raise a TypeError
print("Type check failed")
except TypeError as e:
print(f"Type check succeeded: {e}")
이 테스트 파일에서 먼저 올바른 타입으로 Stock 객체를 생성합니다. 그런 다음 shares 속성을 문자열로 설정하려고 시도합니다. 예상 타입이 정수이므로 TypeError가 발생해야 합니다.
- 테스트 파일을 실행합니다.
python3 test_stock.py
다음과 유사한 출력을 볼 수 있습니다.
Stock name: GOOG
Stock shares: 100
Stock price: 490.1
Type check succeeded: Expected <class 'int'>
이 출력은 타입 검사가 올바르게 작동하고 있음을 보여줍니다.
- 이제 일반적인 타입에 대한 편의 함수를 추가하여
typedproperty.py를 향상시켜 보겠습니다. 파일 끝에 다음 코드를 추가합니다.
def String(name):
"""타입 검사를 사용하여 문자열 속성을 생성합니다."""
return typedproperty(name, str)
def Integer(name):
"""타입 검사를 사용하여 정수 속성을 생성합니다."""
return typedproperty(name, int)
def Float(name):
"""타입 검사를 사용하여 부동 소수점 속성을 생성합니다."""
return typedproperty(name, float)
이러한 함수는 typedproperty 함수를 래핑하여 일반적인 타입의 속성을 더 쉽게 생성할 수 있도록 합니다.
- 이러한 편의 함수를 사용하는
stock_enhanced.py라는 새 파일을 만듭니다.
from typedproperty import String, Integer, Float
class Stock:
"""타입 검사된 속성을 가진 주식을 나타내는 클래스입니다."""
name = String('name')
shares = Integer('shares')
price = Float('price')
def __init__(self, name, shares, price):
self.name = name
self.shares = shares
self.price = price
이 Stock 클래스는 편의 함수를 사용하여 타입 검사된 속성을 생성하므로 코드를 더 읽기 쉽게 만듭니다.
- 향상된 버전을 테스트하기 위해
test_stock_enhanced.py라는 테스트 파일을 만듭니다.
from stock_enhanced import Stock
## 올바른 타입으로 주식 생성
s = Stock('GOOG', 100, 490.1)
print(f"Stock name: {s.name}")
print(f"Stock shares: {s.shares}")
print(f"Stock price: {s.price}")
## 잘못된 타입으로 속성을 설정하려고 시도
try:
s.price = "490.1" ## This should raise a TypeError
print("Type check failed")
except TypeError as e:
print(f"Type check succeeded: {e}")
이 테스트 파일은 이전 파일과 유사하지만 향상된 Stock 클래스를 테스트합니다.
- 테스트를 실행합니다.
python3 test_stock_enhanced.py
다음과 유사한 출력을 볼 수 있습니다.
Stock name: GOOG
Stock shares: 100
Stock price: 490.1
Type check succeeded: Expected <class 'float'>
이 단계에서는 클로저를 사용하여 코드를 생성하는 방법을 시연했습니다. typedproperty 함수는 타입 검사를 수행하는 속성 객체를 생성하고, String, Integer, 및 Float 함수는 일반적인 타입에 대한 특수화된 속성을 생성합니다.
디스크립터를 사용하여 속성 이름 제거
이전 단계에서 타입 속성을 생성할 때 속성 이름을 명시적으로 지정해야 했습니다. 이는 속성 이름이 이미 클래스 정의에 지정되어 있으므로 중복됩니다. 이 단계에서는 디스크립터 (descriptor) 를 사용하여 이러한 중복을 제거합니다.
Python 에서 디스크립터는 속성 접근 방식을 제어하는 특수한 객체입니다. 디스크립터에서 __set_name__ 메서드를 구현하면 클래스 정의에서 속성 이름을 자동으로 가져올 수 있습니다.
새 파일을 만들어 시작해 보겠습니다.
- 다음 코드를 사용하여
improved_typedproperty.py라는 새 파일을 만듭니다.
## improved_typedproperty.py
class TypedProperty:
"""
타입 검사를 수행하는 디스크립터입니다.
이 디스크립터는 클래스 정의에서 속성 이름을 자동으로 캡처합니다.
"""
def __init__(self, expected_type):
self.expected_type = expected_type
self.name = None
def __set_name__(self, owner, name):
## 이 메서드는 디스크립터가 클래스 속성에 할당될 때 호출됩니다.
self.name = name
def __get__(self, instance, owner):
if instance is None:
return self
return instance.__dict__.get(self.name)
def __set__(self, instance, value):
if not isinstance(value, self.expected_type):
raise TypeError(f'Expected {self.expected_type}')
instance.__dict__[self.name] = value
## 편의 함수
def String():
"""타입 검사를 사용하여 문자열 속성을 생성합니다."""
return TypedProperty(str)
def Integer():
"""타입 검사를 사용하여 정수 속성을 생성합니다."""
return TypedProperty(int)
def Float():
"""타입 검사를 사용하여 부동 소수점 속성을 생성합니다."""
return TypedProperty(float)
이 코드는 속성에 할당된 값의 타입을 확인하는 TypedProperty라는 디스크립터 클래스를 정의합니다. __set_name__ 메서드는 디스크립터가 클래스 속성에 할당될 때 자동으로 호출됩니다. 이를 통해 디스크립터는 속성 이름을 수동으로 지정하지 않고도 캡처할 수 있습니다.
다음으로, 이러한 향상된 타입 속성을 사용하는 클래스를 만들 것입니다.
- 향상된 타입 속성을 사용하는
stock_improved.py라는 새 파일을 만듭니다.
from improved_typedproperty import String, Integer, Float
class Stock:
"""타입 검사된 속성을 가진 주식을 나타내는 클래스입니다."""
## 더 이상 속성 이름을 지정할 필요가 없습니다.
name = String()
shares = Integer()
price = Float()
def __init__(self, name, shares, price):
self.name = name
self.shares = shares
self.price = price
타입 속성을 생성할 때 속성 이름을 지정할 필요가 없다는 점에 유의하십시오. 디스크립터는 클래스 정의에서 자동으로 속성 이름을 가져옵니다.
이제 향상된 클래스를 테스트해 보겠습니다.
- 향상된 버전을 테스트하기 위해
test_stock_improved.py라는 테스트 파일을 만듭니다.
from stock_improved import Stock
## 올바른 타입으로 주식 생성
s = Stock('GOOG', 100, 490.1)
print(f"Stock name: {s.name}")
print(f"Stock shares: {s.shares}")
print(f"Stock price: {s.price}")
## 잘못된 타입으로 속성 설정 시도
try:
s.name = 123 ## Should raise TypeError
print("Name type check failed")
except TypeError as e:
print(f"Name type check succeeded: {e}")
try:
s.shares = "hundred" ## Should raise TypeError
print("Shares type check failed")
except TypeError as e:
print(f"Shares type check succeeded: {e}")
try:
s.price = "490.1" ## Should raise TypeError
print("Price type check failed")
except TypeError as e:
print(f"Price type check succeeded: {e}")
마지막으로, 모든 것이 예상대로 작동하는지 확인하기 위해 테스트를 실행합니다.
- 테스트를 실행합니다.
python3 test_stock_improved.py
다음과 유사한 출력을 볼 수 있습니다.
Stock name: GOOG
Stock shares: 100
Stock price: 490.1
Name type check succeeded: Expected <class 'str'>
Shares type check succeeded: Expected <class 'int'>
Price type check succeeded: Expected <class 'float'>
이 단계에서는 디스크립터와 __set_name__ 메서드를 사용하여 타입 검사 시스템을 개선했습니다. 이를 통해 중복된 속성 이름 지정을 제거하여 코드를 더 짧게 만들고 오류 발생 가능성을 줄였습니다.
__set_name__ 메서드는 디스크립터의 매우 유용한 기능입니다. 이를 통해 클래스 정의에서 사용되는 방식에 대한 정보를 자동으로 수집할 수 있습니다. 이를 사용하여 이해하고 사용하기 쉬운 API 를 만들 수 있습니다.
요약
이 랩에서는 Python 의 클로저 (closure) 에 대한 고급 측면에 대해 배웠습니다. 먼저, 클로저를 데이터 구조로 사용하는 것을 탐구했습니다. 이는 데이터를 캡슐화하고 클래스나 전역 변수에 의존하지 않고도 함수가 호출 간에 상태를 유지할 수 있도록 합니다. 둘째, 클로저가 코드 생성기 (code generator) 역할을 하여 속성 유효성 검사에 대한 보다 기능적인 접근 방식으로 타입 검사 (type checking) 를 갖춘 속성 객체를 생성하는 방법을 살펴보았습니다.
또한 디스크립터 프로토콜 (descriptor protocol) 과 __set_name__ 메서드를 사용하여 클래스 정의에서 이름을 자동으로 캡처하는 우아한 타입 검사 속성을 만드는 방법을 배웠습니다. 이러한 기술은 클로저의 강력함과 유연성을 보여주며, 복잡한 동작을 간결하게 구현할 수 있도록 합니다. 클로저와 디스크립터를 이해하면 유지 관리 가능하고 강력한 Python 코드를 생성하기 위한 더 많은 도구를 얻을 수 있습니다.