클래스와 캡슐화

Beginner

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

소개

클래스를 작성할 때 내부 세부 사항을 캡슐화하려는 경향이 있습니다. 이 섹션에서는 개인 변수 (private variables) 및 속성 (properties) 을 포함하여 이를 위한 몇 가지 Python 프로그래밍 관용구를 소개합니다.

Public vs Private (공개 vs 비공개)

클래스의 주요 역할 중 하나는 객체의 데이터와 내부 구현 세부 사항을 캡슐화하는 것입니다. 그러나 클래스는 또한 외부 세계가 객체를 조작하기 위해 사용해야 하는 공개 (public) 인터페이스를 정의합니다. 구현 세부 사항과 공개 인터페이스 간의 이러한 구분은 중요합니다.

문제점

Python 에서는 클래스와 객체에 대한 거의 모든 것이 공개 (open) 되어 있습니다.

  • 객체 내부를 쉽게 검사할 수 있습니다.
  • 원하는 대로 변경할 수 있습니다.
  • 접근 제어 (예: 비공개 클래스 멤버) 에 대한 강력한 개념이 없습니다.

이는 내부 구현 (internal implementation) 세부 사항을 격리하려는 경우 문제가 됩니다.

Python 캡슐화 (Encapsulation)

Python 은 무언가의 의도된 사용을 나타내기 위해 프로그래밍 규칙에 의존합니다. 이러한 규칙은 명명법을 기반으로 합니다. 언어가 규칙을 강제하는 것과는 반대로, 프로그래머가 규칙을 준수해야 한다는 일반적인 태도가 있습니다.

비공개 속성 (Private Attributes)

선행 _가 있는 모든 속성 이름은 비공개 (private) 로 간주됩니다.

class Person(object):
    def __init__(self, name):
        self._name = 0

앞서 언급했듯이, 이것은 단지 프로그래밍 스타일일 뿐입니다. 여전히 접근하고 변경할 수 있습니다.

>>> p = Person('Guido')
>>> p._name
'Guido'
>>> p._name = 'Dave'
>>>

일반적으로, 선행 _가 있는 모든 이름은 변수, 함수 또는 모듈 이름에 관계없이 내부 구현으로 간주됩니다. 이러한 이름을 직접 사용하고 있다면, 아마도 무언가를 잘못하고 있는 것입니다. 더 높은 수준의 기능을 찾아보세요.

단순 속성 (Simple Attributes)

다음 클래스를 고려해 보세요.

class Stock:
    def __init__(self, name, shares, price):
        self.name = name
        self.shares = shares
        self.price = price

놀라운 특징은 속성을 어떤 값으로든 설정할 수 있다는 것입니다.

>>> s = Stock('IBM', 50, 91.1)
>>> s.shares = 100
>>> s.shares = "hundred"
>>> s.shares = [1, 0, 0]
>>>

이것을 보고 몇 가지 추가적인 검사를 원한다고 생각할 수 있습니다.

s.shares = '50'     ## Raise a TypeError, this is a string

어떻게 하시겠습니까?

관리형 속성 (Managed Attributes)

한 가지 접근 방식: 접근자 메서드 (accessor methods) 도입.

class Stock:
    def __init__(self, name, shares, price):
        self.name = name
        self.set_shares(shares)
        self.price = price

    ## Function that layers the "get" operation
    def get_shares(self):
        return self._shares

    ## Function that layers the "set" operation
    def set_shares(self, value):
        if not isinstance(value, int):
            raise TypeError('Expected an int')
        self._shares = value

이것이 기존 코드를 모두 망가뜨린다는 것은 유감입니다. s.shares = 50s.set_shares(50)이 됩니다.

프로퍼티 (Properties)

이전 패턴에 대한 대안적인 접근 방식이 있습니다.

class Stock:
    def __init__(self, name, shares, price):
        self.name = name
        self.shares = shares
        self.price = price

    @property
    def shares(self):
        return self._shares

    @shares.setter
    def shares(self, value):
        if not isinstance(value, int):
            raise TypeError('Expected int')
        self._shares = value

이제 일반적인 속성 접근은 @property@shares.setter 아래에서 getter 및 setter 메서드를 트리거합니다.

>>> s = Stock('IBM', 50, 91.1)
>>> s.shares         ## Triggers @property
50
>>> s.shares = 75    ## Triggers @shares.setter
>>>

이 패턴을 사용하면 소스 코드에 변경 사항이 필요하지 않습니다. 새로운 setter__init__() 메서드 내부를 포함하여 클래스 내에서 할당이 있을 때도 호출됩니다.

class Stock:
    def __init__(self, name, shares, price):
        ...
        ## This assignment calls the setter below
        self.shares = shares
        ...

    ...
    @shares.setter
    def shares(self, value):
        if not isinstance(value, int):
            raise TypeError('Expected int')
        self._shares = value

프로퍼티와 private 이름 사용 사이에는 종종 혼란이 있습니다. 프로퍼티는 내부적으로 _shares와 같은 private 이름을 사용하지만, 클래스의 나머지 부분 (프로퍼티가 아닌 부분) 은 shares와 같은 이름을 계속 사용할 수 있습니다.

프로퍼티는 계산된 데이터 속성에도 유용합니다.

class Stock:
    def __init__(self, name, shares, price):
        self.name = name
        self.shares = shares
        self.price = price

    @property
    def cost(self):
        return self.shares * self.price
    ...

이를 통해 추가 괄호를 없애고 실제로는 메서드라는 사실을 숨길 수 있습니다.

>>> s = Stock('GOOG', 100, 490.1)
>>> s.shares ## Instance variable
100
>>> s.cost   ## Computed Value
49010.0
>>>

균일한 접근 (Uniform access)

마지막 예제는 객체에 더 균일한 인터페이스를 어떻게 적용하는지 보여줍니다. 이렇게 하지 않으면 객체를 사용하는 것이 혼란스러울 수 있습니다.

>>> s = Stock('GOOG', 100, 490.1)
>>> a = s.cost() ## Method
49010.0
>>> b = s.shares ## Data attribute
100
>>>

cost에는 ()가 필요한데, shares에는 왜 필요하지 않을까요? 프로퍼티가 이를 해결할 수 있습니다.

데코레이터 구문 (Decorator Syntax)

@ 구문은 "데코레이션 (decoration)"으로 알려져 있습니다. 이는 바로 뒤에 오는 함수 정의에 적용되는 수정자를 지정합니다.

...
@property
def cost(self):
    return self.shares * self.price

자세한 내용은 섹션 7 에서 제공됩니다.

__slots__ 속성 (Attribute)

속성 이름의 집합을 제한할 수 있습니다.

class Stock:
    __slots__ = ('name','_shares','price')
    def __init__(self, name, shares, price):
        self.name = name
        ...

다른 속성에 대해서는 오류를 발생시킵니다.

>>> s.price = 385.15
>>> s.prices = 410.2
Traceback (most recent call last):
File "<stdin>", line 1, in ?
AttributeError: 'Stock' object has no attribute 'prices'

이것은 오류를 방지하고 객체의 사용을 제한하지만, 실제로 성능을 위해 사용되며 Python 이 메모리를 더 효율적으로 사용하도록 만듭니다.

캡슐화에 대한 최종 코멘트 (Final Comments on Encapsulation)

private 속성, property, slots 등을 과도하게 사용하지 마십시오. 이들은 특정 목적을 위해 사용되며 다른 Python 코드를 읽을 때 볼 수 있습니다. 하지만, 대부분의 일상적인 코딩에는 필요하지 않습니다.

연습 문제 5.6: 간단한 Property

Property 는 객체에 "계산된 속성 (computed attributes)"을 추가하는 유용한 방법입니다. stock.py에서 Stock 객체를 생성했습니다. 객체에서 서로 다른 종류의 데이터를 추출하는 방식에 약간의 불일치가 있음을 확인하십시오.

>>> from stock import Stock
>>> s = Stock('GOOG', 100, 490.1)
>>> s.shares
100
>>> s.price
490.1
>>> s.cost()
49010.0
>>>

특히, cost가 메서드이기 때문에 추가로 ()를 추가해야 하는 방식을 확인하십시오.

cost()를 property 로 변환하면 cost()에 추가로 ()를 제거할 수 있습니다. Stock 클래스를 가져와서 비용 계산이 다음과 같이 작동하도록 수정하십시오.

>>> ================================ RESTART ================================
>>> from stock import Stock
>>> s = Stock('GOOG', 100, 490.1)
>>> s.cost
49010.0
>>>

s.cost()를 함수로 호출해보고, cost가 property 로 정의되었으므로 작동하지 않는지 확인하십시오.

>>> s.cost()
... fails ...
>>>

이 변경을 하면 이전의 pcost.py 프로그램이 중단될 수 있습니다. cost() 메서드에서 ()를 제거해야 할 수도 있습니다.

연습 문제 5.7: Property 와 Setter

shares 속성을 수정하여 값을 private 속성에 저장하고, property 함수 쌍을 사용하여 항상 정수 값으로 설정되도록 합니다. 예상되는 동작의 예는 다음과 같습니다.

>>> ================================ RESTART ================================
>>> from stock import Stock
>>> s = Stock('GOOG',100,490.10)
>>> s.shares = 50
>>> s.shares = 'a lot'
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: expected an integer
>>>

연습 문제 5.8: 슬롯 (slots) 추가

Stock 클래스를 수정하여 __slots__ 속성을 갖도록 합니다. 그런 다음, 새로운 속성을 추가할 수 없는지 확인합니다.

>>> ================================ RESTART ================================
>>> from stock import Stock
>>> s = Stock('GOOG', 100, 490.10)
>>> s.name
'GOOG'
>>> s.blah = 42
... see what happens ...
>>>

__slots__를 사용하면 Python 은 객체의 보다 효율적인 내부 표현을 사용합니다. 위에서 s의 기본 딕셔너리를 검사하려고 하면 어떻게 될까요?

>>> s.__dict__
... see what happens ...
>>>

__slots__는 데이터 구조로 사용되는 클래스에 대한 최적화로 가장 일반적으로 사용된다는 점에 유의해야 합니다. 슬롯을 사용하면 이러한 프로그램이 훨씬 적은 메모리를 사용하고 약간 더 빠르게 실행됩니다. 그러나 다른 대부분의 클래스에서는 __slots__를 사용하지 않는 것이 좋습니다.

요약

축하합니다! 클래스 및 캡슐화 (Encapsulation) 랩을 완료했습니다. LabEx 에서 더 많은 랩을 연습하여 기술을 향상시킬 수 있습니다.