소개
이 랩에서는 Python 객체 지향 프로그래밍의 기본적인 측면인 속성 접근에 대해 배우게 됩니다. Python 은 개발자가 특수 메서드를 통해 클래스에서 속성에 접근하고, 설정하고, 관리하는 방식을 사용자 정의할 수 있도록 합니다. 이는 객체 동작을 제어하는 강력한 방법을 제공합니다.
또한, Python 클래스에서 속성 접근을 사용자 정의하는 방법, 위임 (delegation) 과 상속 (inheritance) 의 차이점을 이해하고, Python 객체에서 사용자 정의 속성 관리를 구현하는 연습을 하게 됩니다.
속성 제어를 위한 __setattr__ 이해
Python 에는 객체의 속성에 접근하고 수정하는 방식을 사용자 정의할 수 있는 특수 메서드가 있습니다. 이러한 중요한 메서드 중 하나가 __setattr__()입니다. 이 메서드는 객체의 속성에 값을 할당하려고 할 때마다 실행됩니다. 이를 통해 속성 할당 프로세스를 세밀하게 제어할 수 있습니다.
__setattr__란 무엇인가?
__setattr__(self, name, value) 메서드는 모든 속성 할당에 대한 인터셉터 역할을 합니다. obj.attr = value와 같은 간단한 할당 문을 작성하면 Python 은 단순히 값을 직접 할당하지 않습니다. 대신 내부적으로 obj.__setattr__("attr", value)를 호출합니다. 이 메커니즘은 속성 할당 중에 어떤 일이 발생해야 하는지 결정할 수 있는 권한을 제공합니다.
이제 __setattr__을 사용하여 클래스에서 설정할 수 있는 속성을 제한하는 실제 예제를 살펴보겠습니다.
1 단계: 새 파일 만들기
먼저 WebIDE 에서 새 파일을 엽니다. "File" 메뉴를 클릭한 다음 "New File"을 선택하여 이 작업을 수행할 수 있습니다. 이 파일의 이름을 restricted_stock.py로 지정하고 /home/labex/project 디렉토리에 저장합니다. 이 파일에는 __setattr__을 사용하여 속성 할당을 제어하는 클래스 정의가 포함됩니다.
2 단계: restricted_stock.py에 코드 추가
다음 코드를 restricted_stock.py 파일에 추가합니다. 이 코드는 RestrictedStock 클래스를 정의합니다.
class RestrictedStock:
def __init__(self, name, shares, price):
self.name = name
self.shares = shares
self.price = price
def __setattr__(self, name, value):
## Only allow specific attributes
if name not in {'name', 'shares', 'price'}:
raise AttributeError(f'Cannot set attribute {name}')
## If attribute is allowed, set it using the parent method
super().__setattr__(name, value)
__init__ 메서드에서 name, shares, price 속성으로 객체를 초기화합니다. __setattr__ 메서드는 할당되는 속성 이름이 허용된 속성 집합 (name, shares, price) 에 있는지 확인합니다. 그렇지 않은 경우 AttributeError를 발생시킵니다. 속성이 허용되면 상위 클래스의 __setattr__ 메서드를 사용하여 실제로 속성을 설정합니다.
3 단계: 테스트 파일 만들기
test_restricted.py라는 새 파일을 만들고 다음 코드를 추가합니다. 이 코드는 RestrictedStock 클래스의 기능을 테스트합니다.
from restricted_stock import RestrictedStock
## Create a new stock
stock = RestrictedStock('GOOG', 100, 490.1)
## Test accessing existing attributes
print(f"Name: {stock.name}")
print(f"Shares: {stock.shares}")
print(f"Price: {stock.price}")
## Test modifying an existing attribute
print("\nChanging shares to 75...")
stock.shares = 75
print(f"New shares value: {stock.shares}")
## Test setting an invalid attribute
try:
print("\nTrying to set an invalid attribute 'share'...")
stock.share = 50
except AttributeError as e:
print(f"Error: {e}")
이 코드에서는 먼저 RestrictedStock 클래스를 가져옵니다. 그런 다음 클래스의 인스턴스를 만듭니다. 기존 속성에 접근하고, 기존 속성을 수정하고, 마지막으로 유효하지 않은 속성을 설정하여 __setattr__ 메서드가 예상대로 작동하는지 테스트합니다.
4 단계: 테스트 파일 실행
WebIDE 에서 터미널을 열고 다음 명령을 실행하여 test_restricted.py 파일을 실행합니다.
cd /home/labex/project
python3 test_restricted.py
이러한 명령을 실행하면 다음과 유사한 출력이 표시됩니다.
Name: GOOG
Shares: 100
Price: 490.1
Changing shares to 75...
New shares value: 75
Trying to set an invalid attribute 'share'...
Error: Cannot set attribute share
작동 방식
RestrictedStock 클래스의 __setattr__ 메서드는 다음 단계로 작동합니다.
- 먼저 속성 이름이 허용된 집합 (
name,shares,price) 에 있는지 확인합니다. - 속성 이름이 허용된 집합에 없으면
AttributeError를 발생시킵니다. 이렇게 하면 원치 않는 속성이 할당되지 않습니다. - 속성이 허용되면
super().__setattr__()을 사용하여 실제로 속성을 설정합니다. 이렇게 하면 허용된 속성에 대해 정상적인 속성 할당 프로세스가 수행됩니다.
이 메서드는 이전 예제에서 살펴본 __slots__를 사용하는 것보다 더 유연합니다. __slots__는 메모리 사용량을 최적화하고 속성을 제한할 수 있지만 상속 작업 시 제한 사항이 있으며 다른 Python 기능과 충돌할 수 있습니다. __setattr__ 접근 방식은 이러한 제한 사항 없이 속성 할당에 대한 유사한 제어를 제공합니다.
프록시를 사용하여 읽기 전용 객체 만들기
이 단계에서는 Python 에서 매우 유용한 패턴인 프록시 클래스를 살펴보겠습니다. 프록시 클래스를 사용하면 원래 코드를 변경하지 않고 기존 객체의 동작 방식을 변경할 수 있습니다. 이는 객체 주위에 특수한 래퍼를 배치하여 새로운 기능이나 제한 사항을 추가하는 것과 같습니다.
프록시란 무엇인가?
프록시는 다른 객체와 여러분 사이에 있는 객체입니다. 원래 객체와 동일한 기능 및 속성 집합을 갖지만 추가 작업을 수행할 수 있습니다. 예를 들어, 객체에 접근할 수 있는 사람을 제어하고, 작업 기록 (로깅) 을 유지하거나, 기타 유용한 기능을 추가할 수 있습니다.
읽기 전용 프록시를 만들어 보겠습니다. 이러한 종류의 프록시는 객체의 속성을 변경하지 못하도록 합니다.
1 단계: 읽기 전용 프록시 클래스 만들기
먼저 읽기 전용 프록시를 정의하는 Python 파일을 만들어야 합니다.
/home/labex/project디렉토리로 이동합니다.- 이 디렉토리에
readonly_proxy.py라는 새 파일을 만듭니다. readonly_proxy.py파일을 열고 다음 코드를 추가합니다.
class ReadonlyProxy:
def __init__(self, obj):
## Store the wrapped object directly in __dict__ to avoid triggering __setattr__
self.__dict__['_obj'] = obj
def __getattr__(self, name):
## Forward attribute access to the wrapped object
return getattr(self._obj, name)
def __setattr__(self, name, value):
## Block all attribute assignments
raise AttributeError("Cannot modify a read-only object")
이 코드에서는 ReadonlyProxy 클래스가 정의됩니다. __init__ 메서드는 래핑하려는 객체를 저장합니다. __setattr__ 메서드를 호출하지 않도록 직접 저장하기 위해 self.__dict__를 사용합니다. __getattr__ 메서드는 프록시의 속성에 접근하려고 할 때 사용됩니다. 단순히 요청을 래핑된 객체로 전달합니다. __setattr__ 메서드는 속성을 변경하려고 할 때 호출됩니다. 변경을 방지하기 위해 오류를 발생시킵니다.
2 단계: 테스트 파일 만들기
이제 읽기 전용 프록시가 어떻게 작동하는지 확인하기 위해 테스트 파일을 만들겠습니다.
- 동일한
/home/labex/project디렉토리에test_readonly.py라는 새 파일을 만듭니다. test_readonly.py파일에 다음 코드를 추가합니다.
from stock import Stock
from readonly_proxy import ReadonlyProxy
## Create a normal Stock object
stock = Stock('AAPL', 100, 150.75)
print("Original stock object:")
print(f"Name: {stock.name}")
print(f"Shares: {stock.shares}")
print(f"Price: {stock.price}")
print(f"Cost: {stock.cost}")
## Modify the original stock object
stock.shares = 200
print(f"\nAfter modification - Shares: {stock.shares}")
print(f"After modification - Cost: {stock.cost}")
## Create a read-only proxy around the stock
readonly_stock = ReadonlyProxy(stock)
print("\nRead-only proxy object:")
print(f"Name: {readonly_stock.name}")
print(f"Shares: {readonly_stock.shares}")
print(f"Price: {readonly_stock.price}")
print(f"Cost: {readonly_stock.cost}")
## Try to modify the read-only proxy
try:
print("\nAttempting to modify the read-only proxy...")
readonly_stock.shares = 300
except AttributeError as e:
print(f"Error: {e}")
## Show that the original object is unchanged
print(f"\nOriginal stock shares are still: {stock.shares}")
이 테스트 코드에서는 먼저 일반 Stock 객체를 만들고 해당 정보를 출력합니다. 그런 다음 해당 속성 중 하나를 수정하고 업데이트된 정보를 출력합니다. 다음으로, Stock 객체에 대한 읽기 전용 프록시를 만들고 해당 정보를 출력합니다. 마지막으로, 읽기 전용 프록시를 수정하려고 시도하고 오류가 발생할 것으로 예상합니다.
3 단계: 테스트 스크립트 실행
프록시 클래스와 테스트 파일을 만든 후에는 테스트 스크립트를 실행하여 결과를 확인해야 합니다.
- 터미널을 열고 다음 명령을 사용하여
/home/labex/project디렉토리로 이동합니다.
cd /home/labex/project
- 다음 명령을 사용하여 테스트 스크립트를 실행합니다.
python3 test_readonly.py
다음과 유사한 출력이 표시됩니다.
Original stock object:
Name: AAPL
Shares: 100
Price: 150.75
Cost: 15075.0
After modification - Shares: 200
After modification - Cost: 30150.0
Read-only proxy object:
Name: AAPL
Shares: 200
Price: 150.75
Cost: 30150.0
Attempting to modify the read-only proxy...
Error: Cannot modify a read-only object
Original stock shares are still: 200
프록시 작동 방식
ReadonlyProxy 클래스는 읽기 전용 기능을 달성하기 위해 두 가지 특수 메서드를 사용합니다.
__getattr__(self, name): 이 메서드는 Python 이 일반적인 방식으로 속성을 찾을 수 없을 때 호출됩니다.ReadonlyProxy클래스에서는getattr()함수를 사용하여 속성 접근 요청을 래핑된 객체로 전달합니다. 따라서 프록시의 속성에 접근하려고 하면 실제로 래핑된 객체에서 속성을 가져옵니다.__setattr__(self, name, value): 이 메서드는 속성에 값을 할당하려고 할 때 호출됩니다. 구현에서는AttributeError를 발생시켜 프록시의 속성에 대한 변경을 중지합니다.__init__메서드에서는self.__dict__를 직접 수정하여 래핑된 객체를 저장합니다. 이는 객체를 할당하는 일반적인 방식을 사용하면__setattr__메서드가 호출되어 오류가 발생하기 때문에 중요합니다.
이 프록시 패턴을 사용하면 원래 클래스를 변경하지 않고 기존 객체 주위에 읽기 전용 계층을 추가할 수 있습니다. 프록시 객체는 래핑된 객체와 똑같이 작동하지만 변경을 할 수 없습니다.
상속의 대안으로서의 위임 (Delegation)
객체 지향 프로그래밍에서 코드를 재사용하고 확장하는 것은 일반적인 작업입니다. 이를 달성하는 두 가지 주요 방법은 상속과 위임입니다.
**상속 (Inheritance)**은 하위 클래스가 상위 클래스에서 메서드와 속성을 상속받는 메커니즘입니다. 하위 클래스는 자체 구현을 제공하기 위해 이러한 상속된 메서드 중 일부를 재정의하도록 선택할 수 있습니다.
반면에 **위임 (Delegation)**은 다른 객체를 포함하고 특정 메서드 호출을 해당 객체로 전달하는 객체를 포함합니다.
이 단계에서는 상속의 대안으로 위임을 살펴보겠습니다. 일부 동작을 다른 객체에 위임하는 클래스를 구현할 것입니다.
위임 예제 설정
먼저 위임 클래스가 상호 작용할 기본 클래스를 설정해야 합니다.
/home/labex/project디렉토리에base_class.py라는 새 파일을 만듭니다. 이 파일은method_a,method_b,method_c의 세 가지 메서드를 가진Spam이라는 클래스를 정의합니다. 각 메서드는 메시지를 출력하고 결과를 반환합니다. 다음은base_class.py에 넣을 코드입니다.
class Spam:
def method_a(self):
print('Spam.method_a executed')
return "Result from Spam.method_a"
def method_b(self):
print('Spam.method_b executed')
return "Result from Spam.method_b"
def method_c(self):
print('Spam.method_c executed')
return "Result from Spam.method_c"
다음으로 위임 클래스를 만들겠습니다.
delegator.py라는 새 파일을 만듭니다. 이 파일에서는 동작의 일부를Spam클래스의 인스턴스에 위임하는DelegatingSpam이라는 클래스를 정의합니다.
from base_class import Spam
class DelegatingSpam:
def __init__(self):
## Create an instance of Spam that we'll delegate to
self._spam = Spam()
def method_a(self):
## Override method_a but also call the original
print('DelegatingSpam.method_a executed')
result = self._spam.method_a()
return f"Modified {result}"
def method_c(self):
## Completely override method_c
print('DelegatingSpam.method_c executed')
return "Result from DelegatingSpam.method_c"
def __getattr__(self, name):
## For any other attribute/method, delegate to self._spam
print(f"Delegating {name} to the wrapped Spam object")
return getattr(self._spam, name)
__init__ 메서드에서 Spam 클래스의 인스턴스를 만듭니다. method_a 메서드는 원래 메서드를 재정의하지만 Spam 클래스의 method_a도 호출합니다. method_c 메서드는 원래 메서드를 완전히 재정의합니다. __getattr__ 메서드는 DelegatingSpam 클래스에 존재하지 않는 속성 또는 메서드에 접근할 때 호출되는 Python 의 특수 메서드입니다. 그런 다음 호출을 Spam 인스턴스에 위임합니다.
이제 구현을 확인하기 위해 테스트 파일을 만들어 보겠습니다.
test_delegation.py라는 테스트 파일을 만듭니다. 이 파일은DelegatingSpam클래스의 인스턴스를 만들고 해당 메서드를 호출합니다.
from delegator import DelegatingSpam
## Create a delegating object
spam = DelegatingSpam()
print("Calling method_a (overridden with delegation):")
result_a = spam.method_a()
print(f"Result: {result_a}\n")
print("Calling method_b (not defined in DelegatingSpam, delegated):")
result_b = spam.method_b()
print(f"Result: {result_b}\n")
print("Calling method_c (completely overridden):")
result_c = spam.method_c()
print(f"Result: {result_c}\n")
## Try accessing a non-existent method
try:
print("Calling non-existent method_d:")
spam.method_d()
except AttributeError as e:
print(f"Error: {e}")
마지막으로 테스트 스크립트를 실행합니다.
- 터미널에서 다음 명령을 사용하여 테스트 스크립트를 실행합니다.
cd /home/labex/project
python3 test_delegation.py
다음과 유사한 출력이 표시됩니다.
Calling method_a (overridden with delegation):
DelegatingSpam.method_a executed
Spam.method_a executed
Result: Modified Result from Spam.method_a
Calling method_b (not defined in DelegatingSpam, delegated):
Delegating method_b to the wrapped Spam object
Spam.method_b executed
Result: Result from Spam.method_b
Calling method_c (completely overridden):
DelegatingSpam.method_c executed
Result: Result from DelegatingSpam.method_c
Calling non-existent method_d:
Delegating method_d to the wrapped Spam object
Error: 'Spam' object has no attribute 'method_d'
위임 vs. 상속
이제 위임을 전통적인 상속과 비교해 보겠습니다.
inheritance_example.py라는 파일을 만듭니다. 이 파일에서는Spam클래스에서 상속하는InheritingSpam이라는 클래스를 정의합니다.
from base_class import Spam
class InheritingSpam(Spam):
def method_a(self):
## Override method_a but also call the parent method
print('InheritingSpam.method_a executed')
result = super().method_a()
return f"Modified {result}"
def method_c(self):
## Completely override method_c
print('InheritingSpam.method_c executed')
return "Result from InheritingSpam.method_c"
InheritingSpam 클래스는 method_a 및 method_c 메서드를 재정의합니다. method_a 메서드에서는 super()를 사용하여 상위 클래스의 method_a를 호출합니다.
다음으로 상속 예제에 대한 테스트 파일을 만들겠습니다.
test_inheritance.py라는 테스트 파일을 만듭니다. 이 파일은InheritingSpam클래스의 인스턴스를 만들고 해당 메서드를 호출합니다.
from inheritance_example import InheritingSpam
## Create an inheriting object
spam = InheritingSpam()
print("Calling method_a (overridden with super call):")
result_a = spam.method_a()
print(f"Result: {result_a}\n")
print("Calling method_b (inherited from parent):")
result_b = spam.method_b()
print(f"Result: {result_b}\n")
print("Calling method_c (completely overridden):")
result_c = spam.method_c()
print(f"Result: {result_c}\n")
## Try accessing a non-existent method
try:
print("Calling non-existent method_d:")
spam.method_d()
except AttributeError as e:
print(f"Error: {e}")
마지막으로 상속 테스트를 실행합니다.
- 터미널에서 다음 명령을 사용하여 상속 테스트를 실행합니다.
cd /home/labex/project
python3 test_inheritance.py
다음과 유사한 출력이 표시됩니다.
Calling method_a (overridden with super call):
InheritingSpam.method_a executed
Spam.method_a executed
Result: Modified Result from Spam.method_a
Calling method_b (inherited from parent):
Spam.method_b executed
Result: Result from Spam.method_b
Calling method_c (completely overridden):
InheritingSpam.method_c executed
Result: Result from InheritingSpam.method_c
Calling non-existent method_d:
Error: 'InheritingSpam' object has no attribute 'method_d'
주요 차이점 및 고려 사항
위임과 상속의 유사점과 차이점을 살펴보겠습니다.
메서드 재정의: 위임과 상속 모두 메서드를 재정의할 수 있지만 구문이 다릅니다.
- 위임에서는 자체 메서드를 정의하고 래핑된 객체의 메서드를 호출할지 여부를 결정합니다.
- 상속에서는 자체 메서드를 정의하고
super()를 사용하여 상위 메서드를 호출합니다.
메서드 접근:
- 위임에서는 정의되지 않은 메서드가
__getattr__메서드를 통해 전달됩니다. - 상속에서는 정의되지 않은 메서드가 자동으로 상속됩니다.
- 위임에서는 정의되지 않은 메서드가
유형 관계:
- 위임을 사용하면
isinstance(delegating_spam, Spam)은False를 반환합니다.DelegatingSpam객체가Spam클래스의 인스턴스가 아니기 때문입니다. - 상속을 사용하면
isinstance(inheriting_spam, Spam)은True를 반환합니다.InheritingSpam클래스가Spam클래스에서 상속되기 때문입니다.
- 위임을 사용하면
제한 사항:
__getattr__를 통한 위임은__getitem__,__len__등과 같은 특수 메서드에서는 작동하지 않습니다. 이러한 메서드는 위임 클래스에서 명시적으로 정의해야 합니다.
위임은 다음과 같은 상황에서 특히 유용합니다.
- 계층 구조에 영향을 주지 않고 객체의 동작을 사용자 정의하려는 경우.
- 공통 상위 항목을 공유하지 않는 여러 객체의 동작을 결합하려는 경우.
- 상속이 제공하는 것보다 더 많은 유연성이 필요한 경우.
상속은 일반적으로 다음과 같은 경우에 선호됩니다.
- "is-a" 관계가 명확한 경우 (예: Car 는 Vehicle 임).
- 코드 전체에서 유형 호환성을 유지해야 하는 경우.
- 특수 메서드를 상속해야 하는 경우.
요약
이 랩에서는 속성 접근 및 동작을 사용자 정의하기 위한 강력한 Python 메커니즘에 대해 배웠습니다. __setattr__를 사용하여 객체에 설정할 수 있는 속성을 제어하는 방법을 탐구하여 객체 속성에 대한 제어된 접근을 가능하게 했습니다. 또한, 기존 객체를 래핑하여 기능을 유지하면서 수정을 방지하는 읽기 전용 프록시를 구현했습니다.
또한 코드 재사용 및 사용자 정의를 위한 위임과 상속의 차이점을 자세히 살펴보았습니다. __getattr__를 사용하여 메서드 호출을 래핑된 객체로 전달하는 방법을 배웠습니다. 이러한 기술은 제어된 인터페이스를 만들고, 접근 제한을 구현하고, 횡단 (cross-cutting) 동작을 추가하고, 여러 소스에서 동작을 구성하는 데 유용하며, 표준 상속을 넘어 객체 동작을 제어하는 유연한 방법을 제공합니다. 이러한 패턴을 이해하면 더 유지 관리 가능하고 유연한 Python 코드를 작성하는 데 도움이 됩니다.