속성 접근 사용자 정의

Beginner

This tutorial is from open-source community. Access the source code

소개

이 랩에서는 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__ 메서드는 다음 단계로 작동합니다.

  1. 먼저 속성 이름이 허용된 집합 (name, shares, price) 에 있는지 확인합니다.
  2. 속성 이름이 허용된 집합에 없으면 AttributeError를 발생시킵니다. 이렇게 하면 원치 않는 속성이 할당되지 않습니다.
  3. 속성이 허용되면 super().__setattr__()을 사용하여 실제로 속성을 설정합니다. 이렇게 하면 허용된 속성에 대해 정상적인 속성 할당 프로세스가 수행됩니다.

이 메서드는 이전 예제에서 살펴본 __slots__를 사용하는 것보다 더 유연합니다. __slots__는 메모리 사용량을 최적화하고 속성을 제한할 수 있지만 상속 작업 시 제한 사항이 있으며 다른 Python 기능과 충돌할 수 있습니다. __setattr__ 접근 방식은 이러한 제한 사항 없이 속성 할당에 대한 유사한 제어를 제공합니다.

프록시를 사용하여 읽기 전용 객체 만들기

이 단계에서는 Python 에서 매우 유용한 패턴인 프록시 클래스를 살펴보겠습니다. 프록시 클래스를 사용하면 원래 코드를 변경하지 않고 기존 객체의 동작 방식을 변경할 수 있습니다. 이는 객체 주위에 특수한 래퍼를 배치하여 새로운 기능이나 제한 사항을 추가하는 것과 같습니다.

프록시란 무엇인가?

프록시는 다른 객체와 여러분 사이에 있는 객체입니다. 원래 객체와 동일한 기능 및 속성 집합을 갖지만 추가 작업을 수행할 수 있습니다. 예를 들어, 객체에 접근할 수 있는 사람을 제어하고, 작업 기록 (로깅) 을 유지하거나, 기타 유용한 기능을 추가할 수 있습니다.

읽기 전용 프록시를 만들어 보겠습니다. 이러한 종류의 프록시는 객체의 속성을 변경하지 못하도록 합니다.

1 단계: 읽기 전용 프록시 클래스 만들기

먼저 읽기 전용 프록시를 정의하는 Python 파일을 만들어야 합니다.

  1. /home/labex/project 디렉토리로 이동합니다.
  2. 이 디렉토리에 readonly_proxy.py라는 새 파일을 만듭니다.
  3. 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 단계: 테스트 파일 만들기

이제 읽기 전용 프록시가 어떻게 작동하는지 확인하기 위해 테스트 파일을 만들겠습니다.

  1. 동일한 /home/labex/project 디렉토리에 test_readonly.py라는 새 파일을 만듭니다.
  2. 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 단계: 테스트 스크립트 실행

프록시 클래스와 테스트 파일을 만든 후에는 테스트 스크립트를 실행하여 결과를 확인해야 합니다.

  1. 터미널을 열고 다음 명령을 사용하여 /home/labex/project 디렉토리로 이동합니다.
cd /home/labex/project
  1. 다음 명령을 사용하여 테스트 스크립트를 실행합니다.
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 클래스는 읽기 전용 기능을 달성하기 위해 두 가지 특수 메서드를 사용합니다.

  1. __getattr__(self, name): 이 메서드는 Python 이 일반적인 방식으로 속성을 찾을 수 없을 때 호출됩니다. ReadonlyProxy 클래스에서는 getattr() 함수를 사용하여 속성 접근 요청을 래핑된 객체로 전달합니다. 따라서 프록시의 속성에 접근하려고 하면 실제로 래핑된 객체에서 속성을 가져옵니다.

  2. __setattr__(self, name, value): 이 메서드는 속성에 값을 할당하려고 할 때 호출됩니다. 구현에서는 AttributeError를 발생시켜 프록시의 속성에 대한 변경을 중지합니다.

  3. __init__ 메서드에서는 self.__dict__를 직접 수정하여 래핑된 객체를 저장합니다. 이는 객체를 할당하는 일반적인 방식을 사용하면 __setattr__ 메서드가 호출되어 오류가 발생하기 때문에 중요합니다.

이 프록시 패턴을 사용하면 원래 클래스를 변경하지 않고 기존 객체 주위에 읽기 전용 계층을 추가할 수 있습니다. 프록시 객체는 래핑된 객체와 똑같이 작동하지만 변경을 할 수 없습니다.

상속의 대안으로서의 위임 (Delegation)

객체 지향 프로그래밍에서 코드를 재사용하고 확장하는 것은 일반적인 작업입니다. 이를 달성하는 두 가지 주요 방법은 상속과 위임입니다.

**상속 (Inheritance)**은 하위 클래스가 상위 클래스에서 메서드와 속성을 상속받는 메커니즘입니다. 하위 클래스는 자체 구현을 제공하기 위해 이러한 상속된 메서드 중 일부를 재정의하도록 선택할 수 있습니다.

반면에 **위임 (Delegation)**은 다른 객체를 포함하고 특정 메서드 호출을 해당 객체로 전달하는 객체를 포함합니다.

이 단계에서는 상속의 대안으로 위임을 살펴보겠습니다. 일부 동작을 다른 객체에 위임하는 클래스를 구현할 것입니다.

위임 예제 설정

먼저 위임 클래스가 상호 작용할 기본 클래스를 설정해야 합니다.

  1. /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"

다음으로 위임 클래스를 만들겠습니다.

  1. 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 인스턴스에 위임합니다.

이제 구현을 확인하기 위해 테스트 파일을 만들어 보겠습니다.

  1. 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}")

마지막으로 테스트 스크립트를 실행합니다.

  1. 터미널에서 다음 명령을 사용하여 테스트 스크립트를 실행합니다.
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. 상속

이제 위임을 전통적인 상속과 비교해 보겠습니다.

  1. 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_amethod_c 메서드를 재정의합니다. method_a 메서드에서는 super()를 사용하여 상위 클래스의 method_a를 호출합니다.

다음으로 상속 예제에 대한 테스트 파일을 만들겠습니다.

  1. 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}")

마지막으로 상속 테스트를 실행합니다.

  1. 터미널에서 다음 명령을 사용하여 상속 테스트를 실행합니다.
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'

주요 차이점 및 고려 사항

위임과 상속의 유사점과 차이점을 살펴보겠습니다.

  1. 메서드 재정의: 위임과 상속 모두 메서드를 재정의할 수 있지만 구문이 다릅니다.

    • 위임에서는 자체 메서드를 정의하고 래핑된 객체의 메서드를 호출할지 여부를 결정합니다.
    • 상속에서는 자체 메서드를 정의하고 super()를 사용하여 상위 메서드를 호출합니다.
  2. 메서드 접근:

    • 위임에서는 정의되지 않은 메서드가 __getattr__ 메서드를 통해 전달됩니다.
    • 상속에서는 정의되지 않은 메서드가 자동으로 상속됩니다.
  3. 유형 관계:

    • 위임을 사용하면 isinstance(delegating_spam, Spam)False를 반환합니다. DelegatingSpam 객체가 Spam 클래스의 인스턴스가 아니기 때문입니다.
    • 상속을 사용하면 isinstance(inheriting_spam, Spam)True를 반환합니다. InheritingSpam 클래스가 Spam 클래스에서 상속되기 때문입니다.
  4. 제한 사항: __getattr__를 통한 위임은 __getitem__, __len__ 등과 같은 특수 메서드에서는 작동하지 않습니다. 이러한 메서드는 위임 클래스에서 명시적으로 정의해야 합니다.

위임은 다음과 같은 상황에서 특히 유용합니다.

  • 계층 구조에 영향을 주지 않고 객체의 동작을 사용자 정의하려는 경우.
  • 공통 상위 항목을 공유하지 않는 여러 객체의 동작을 결합하려는 경우.
  • 상속이 제공하는 것보다 더 많은 유연성이 필요한 경우.

상속은 일반적으로 다음과 같은 경우에 선호됩니다.

  • "is-a" 관계가 명확한 경우 (예: Car 는 Vehicle 임).
  • 코드 전체에서 유형 호환성을 유지해야 하는 경우.
  • 특수 메서드를 상속해야 하는 경우.

요약

이 랩에서는 속성 접근 및 동작을 사용자 정의하기 위한 강력한 Python 메커니즘에 대해 배웠습니다. __setattr__를 사용하여 객체에 설정할 수 있는 속성을 제어하는 방법을 탐구하여 객체 속성에 대한 제어된 접근을 가능하게 했습니다. 또한, 기존 객체를 래핑하여 기능을 유지하면서 수정을 방지하는 읽기 전용 프록시를 구현했습니다.

또한 코드 재사용 및 사용자 정의를 위한 위임과 상속의 차이점을 자세히 살펴보았습니다. __getattr__를 사용하여 메서드 호출을 래핑된 객체로 전달하는 방법을 배웠습니다. 이러한 기술은 제어된 인터페이스를 만들고, 접근 제한을 구현하고, 횡단 (cross-cutting) 동작을 추가하고, 여러 소스에서 동작을 구성하는 데 유용하며, 표준 상속을 넘어 객체 동작을 제어하는 유연한 방법을 제공합니다. 이러한 패턴을 이해하면 더 유지 관리 가능하고 유연한 Python 코드를 작성하는 데 도움이 됩니다.