19. 클래스의 기본 요소#

주요 내용

  • __str__()__repr__()

  • 비교 가능 클래스

  • 공개 여부

  • 컨테이너 클래스

참고

참고: 아래 내용은 Problem Solving with Algorithms and Data Structures using Python의 2장 내용을 일부 활용한다.

19.1. 클래스 일반#

클래스를 선언할 때 기본적으로 아래 요소들이 지원되도록 해야 한다.

  • 문서화: 문서화 문자열(독스트링, docstring) 기능을 이용하여 해당 클래스의 기능과 사용법을 설명한다. 세 개의 큰 따옴표(""")로 감싸인 문장으로 작성한다.

  • 출력 지원

    • __str__() 매직 메서드 구현: print()함수를 이용하여 해당 인스턴스를 출력할 때 활용한다.

    • __repr__() 매직 메서드 구현: 대화형 쉘, 디버깅 과정, 또는 print() 함수가 영향을 미치지 못하는 곳에서 해당 인스턴스를 출력할 때 활용된다.

이와 더불어 클래스의 인스턴스들을 대상으로 크기 비교를 하려면 최소 아래 두 매직 메서드를 구현해야 한다.

  • __eq__(): 동등성 비교

  • __lt__(): 작은지 여부 비교

경우에 따라 인스턴스 내부에서만 사용되고 외부로 굳이 알려질 필요가 없는 인스턴스 변수 각각에 대해 아래 사항을 지정할 수 있다.

  • 공개 여부

  • 읽기 전용 여부

다면체 주사위 클래스를 이용하여 파이썬 클래스 선언에 기본적으로 필요한 요소들을 확인한다.

19.1.1. 다면체 주사위 클래스#

다면체 주사위 객체 생성에 사용되는 MSDie 클래스를 선언한 후에 한 단계씩 업데이트 하는 과정을 살펴본다. MSDie 클래스에 기본으로 포함되는 두 메서드는 다음과 같다.

  • __init__() 메서드: 생성되는 주사위는 지정된 개수의 면을 갖는다. 즉, 4면체, 6면체, 7면체 등 다면체 주사위 객체를 생성할 수 있도록 생성자를 정의한다.

  • roll() 메서드: 주사위 객체를 생성할 때 주사위가 가리키는 값을 하나 무작위로 지정하도록 한 다음에 필요에 따라 주사위 굴리기를 실행하고 그 결과를 저장하도록 한다.

import random

class MSDie:
    """
    다면체 주사위
    
    인스턴스 변수: 
        num_sides: 면 개수
        current_value: 주사위를 굴린 결과
    """

    def __init__(self, num_sides):
        self.num_sides = num_sides
        self.current_value = self.roll()   # 주사위 굴리기 먼저 실행

    def roll(self):   # 주사위 굴리기
        self.current_value = random.randrange(1, self.num_sides+1) # 무작위 생성
        return self.current_value

6면체 주사위 객체를 하나 생성하여 5번 굴린 결과를 확인한다.

my_die = MSDie(6)

for i in range(5):
    print(my_die, my_die.current_value)
    my_die.roll()    # 주사위 새로 굴리기
<__main__.MSDie object at 0x0000024EFACE4040> 2
<__main__.MSDie object at 0x0000024EFACE4040> 3
<__main__.MSDie object at 0x0000024EFACE4040> 6
<__main__.MSDie object at 0x0000024EFACE4040> 1
<__main__.MSDie object at 0x0000024EFACE4040> 2

19.1.2. __str__()__repr__()#

print(my_die)num_sidescurrent_value를 활용하여 적절한 출력을 만들어 내도록 하려면 __str__() 매직 메서드가 적절한 문자열을 출력하도록 해야 한다.

import random

class MSDie:
    """
    다면체 주사위
    
    인스턴스 변수: 
        num_sides: 면 개수
        current_value: 주사위를 굴린 결과
    """

    def __init__(self, num_sides):
        self.num_sides = num_sides
        self.current_value = self.roll()   # 주사위 굴리기 먼저 실행

    def roll(self):   # 주사위 굴리기
        self.current_value = random.randrange(1, self.num_sides+1)
        return self.current_value

    def __str__(self):
        return f"MSDie({self.num_sides})"
my_die = MSDie(6)

for i in range(5):
    print(my_die, "- 가리키는 값은", my_die.current_value)
    my_die.roll()    # 주사위 새로 굴리기
MSDie(6) - 가리키는 값은 4
MSDie(6) - 가리키는 값은 5
MSDie(6) - 가리키는 값은 6
MSDie(6) - 가리키는 값은 1
MSDie(6) - 가리키는 값은 1

하지만 print() 함수를 사용하지 않고 값을 확인하려 하면 제대로 작동하지 않는다.

my_die
<__main__.MSDie at 0x24eface5a50>

print() 함수를 사용하더라도 다면체 주사위로 구성된 리스트를 출력하려 하면 원하는 대로 작동하지 않는다. 즉, 리스트의 항목에 대해서는 print() 함수가 영향을 미치지 못한다.

d_list = [MSDie(6), MSDie(20)]
print(d_list)
[<__main__.MSDie object at 0x0000024EFACE45E0>, <__main__.MSDie object at 0x0000024EFACE5420>]

이 문제를 해결하려면 __repr__() 매직 메서드의 반환값을 적절한 문자열로 지정해야 한다.

import random

class MSDie:
    """
    다면체 주사위
    
    인스턴스 변수: 
        num_sides: 면 개수
        current_value: 주사위를 굴린 결과
    """

    def __init__(self, num_sides):
        self.num_sides = num_sides
        self.current_value = self.roll()   # 주사위 굴리기 먼저 실행

    def roll(self):   # 주사위 굴리기
        self.current_value = random.randrange(1, self.num_sides+1)
        return self.current_value

    def __str__(self):
        return f"MSDie({self.num_sides})"

    def __repr__(self):
        return f"MSDie({self.num_sides}):{self.current_value}"
d_list = [MSDie(6), MSDie(20)]
print(d_list)
[MSDie(6):2, MSDie(20):19]

앞서 보인 것처럼 __str__()__repr__()를 구분해서 각자의 역할에 따라 객체를 다른 식으로 보여준다. 하지만 굳이 구분하지 않겠다면 __repr__() 만 정의해도 된다. 그러면 __str__()이 필요한 경우 대신 __repr__()가 사용된다.

import random

class MSDie:
    """
    다면체 주사위
    
    인스턴스 변수: 
        num_sides: 면 개수
        current_value: 주사위를 굴린 결과
    """

    def __init__(self, num_sides):
        self.num_sides = num_sides
        self.current_value = self.roll()   # 주사위 굴리기 먼저 실행

    def roll(self):   # 주사위 굴리기
        self.current_value = random.randrange(1, self.num_sides+1)
        return self.current_value

    def __repr__(self):
        return f"MSDie({self.num_sides}):{self.current_value}"
my_die = MSDie(6)

for i in range(5):
    print(my_die)
    my_die.roll()    # 주사위 새로 굴리기
MSDie(6):6
MSDie(6):6
MSDie(6):3
MSDie(6):1
MSDie(6):5
d_list = [MSDie(6), MSDie(20)]
print(d_list)
[MSDie(6):4, MSDie(20):9]

19.2. 비교 가능 클래스#

비교 연산자

두 주사위 객체의 동등성(equality) 여부를 어떻게 판단할까? 두 주사위가 가리키는 값이 같을 때? 주사위 면의 수가 다르면? 두 주사위의 크기 비교는 어떻게? 이런 질문들에 답하려면 객체 비교와 관련된 몇 개의 매직 메서드를 선언해야 한다.

  • __lt__: 작다 연산자(<) 지원

  • __gt__: 크다 연산자(>) 지원

  • __eq__: 동등성 연산자(==) 지원

  • __le__: 작거나 같다 연산자(<=) 지원

  • __ge__: 크거나 같다 연산자(>=) 지원

  • __ne__: 비동등성(!=) 지원

먼저 두 주사의 크기 비교를 주사위가 가리키는 값(current_value)만 이용하여 지정한다.

import random

class MSDie:
    """
    다면체 주사위
    
    인스턴스 변수: 
        num_sides: 면 개수
        current_value: 주사위를 굴린 결과
    """

    def __init__(self, num_sides):
        self.num_sides = num_sides
        self.current_value = self.roll()   # 주사위 굴리기 먼저 실행

    def roll(self):   # 주사위 굴리기
        self.current_value = random.randrange(1, self.num_sides+1)
        return self.current_value

    def __repr__(self):
        return "MSDie({}) : {}".format(self.num_sides, self.current_value)

    # 크기 비교 연산자 지원
    
    def __eq__(self,other):
        return self.current_value == other.current_value

    def __lt__(self,other):
        return self.current_value < other.current_value

    def __le__(self, other):
        return self.current_value <= other.current_value    

비교 연산자들의 활용법은 모두 아래 형식을 따른다. 아래 표현식에서 self는 비교의 중심이 되는 객체를, other는 비교 대상 객체를 가리킨다.

__eq__(self, other)
x = MSDie(6)
y = MSDie(7)

x.current_value = 6
y.current_value = 5

print(x == y)
print(x < y)
print(x <= y)
False
False
False

최소로 필요한 비교 연산자

앞서 __gt__(), __ge__(), __ne__() 매직 메서드를 정의하지 않았지만 자동으로 지원된다.

print(x > y)
print(x>=y)
print(x != y)
True
True
True

모양이 다른 다면체 비교

모양이 다른 다면체 사이의 비교는 무조건 거짓으로 정할 수도 있다.

import random

class MSDie:
    """
    다면체 주사위
    
    인스턴스 변수: 
        num_sides: 면 개수
        current_value: 주사위를 굴린 결과
    """

    def __init__(self, num_sides):
        self.num_sides = num_sides
        self.current_value = self.roll()   # 주사위 굴리기 먼저 실행

    def roll(self):   # 주사위 굴리기
        self.current_value = random.randrange(1, self.num_sides+1)
        return self.current_value

    def __repr__(self):
        return "MSDie({}) : {}".format(self.num_sides, self.current_value)

    # 크기 비교 연산자 지원
    
    def __eq__(self,other):
        if self.num_sides == other.num_sides:
            return self.current_value == other.current_value
        else:
            return False

    def __lt__(self,other):
        if self.num_sides == other.num_sides:
            return self.current_value < other.current_value
        else:
            return False

    def __le__(self, other):
        if self.num_sides == other.num_sides:
            return self.current_value <= other.current_value
        else:
            return False

모양이 다른 다면체 두 개를 생성한다.

x = MSDie(6)
y = MSDie(7)

이제 모든 비교는 거짓이 된다.

x.current_value = 6
y.current_value = 6

print(x == y)
False
x.current_value = 6
y.current_value = 7

print(x < y)
False
x.current_value = 6
y.current_value = 7

print(x <= y)
False

모양이 동일해야 가리키는 값을 기준으로 비교한다.

x = MSDie(6)
y = MSDie(6)
x.current_value = 6
y.current_value = 6

print(x == y)
True
x.current_value = 6
y.current_value = 7

print(x < y)
True
x.current_value = 6
y.current_value = 7

print(x >= y)
False

19.3. 공개 여부#

자바 언어의 클래스 선언에 사용되는 private, default, protected, public 등과 같은 접근 제어자는 파이썬에서 지원되지 않는다. 파이썬에서 클래스의 모든 것은 공개(public)되기에 클래스 외부로부터의 접근과 수정이 가능하다.

그럼에도 불구하고 일부 변수와 메서드를 특별한 방식으로 이름을 지어 외부 노출을 최대한 줄일 수 있다.

  • 두 개의 밑줄(__)로 시작하기: 숨기고자 하는 속성 변수와 메서드 이름

  • 한 개의 밑줄(_)로 시작하기: 숨길 것 까지는 아니지만 내부용이라 굳이 클래스 외부에서 알 필요까지는 없는 속성 변수와 메서드 이름

아래 코드는 MSDie 클래스의 생성자를 조금 수정하였다. 수정된 내용은 주사위를 굴렸을 때 나온 값에 __hidden1을 곱한 후에 _hidden2로 나눈 결과를 current_value로 가리키도록 하였다.

import random

class MSDie:
    """
    다면체 주사위
    
    인스턴스 변수: 
        num_sides: 면 개수
        current_value: 주사위를 굴린 결과
    """

    def __init__(self, num_sides):
        self.__hidden1 = 3                  # 이름 뒤섞기
        self._hidden2 = 7
        self.num_sides = num_sides
        self.current_value = self.roll()    # 주사위 굴리기 먼저 실행

    def __randNum(self):
        return random.randrange(1, self.num_sides+1)

    def roll(self):   # 주사위 굴리기
        randNum = self.__randNum()
        self.current_value = (self.__hidden1 * randNum) % self._hidden2 
        return self.current_value
x = MSDie(6)

현재 가리키는 값이 이제는 __hidden1_hidden2에 의존한다.

x.current_value
6

그런데 두 밑줄로 시작하는 __hidden1 속성은 인스턴스 변수로 확인할 수 없다.

>>> x.__hidden1
AttributeError                            Traceback (most recent call last)
Input In [18], in <module>
----> 1 x.__hidden1

AttributeError: 'MSDie' object has no attribute '__hidden1'

반면에 하나의 밑줄로 시작하는 _hidden2 속성은 인스턴스 변수로 값이 확인된다.

x._hidden2
7

함수에 대해서도 동일하게 작동한다. __randNum() 함수는 외부에 노출되지 않는다.

>>> x.__randNum()
AttributeError                            Traceback (most recent call last)
c:\Users\gslee\Documents\GitHub\algopy\jupyter-book\python_basic_4.ipynb 셀 56 in <cell line: 1>()
----> 1 x.__randNum

AttributeError: 'MSDie' object has no attribute '__randNum'

19.3.1. __dict__ 속성#

객체 x가 갖는 인스턴스 속성을 확인하면 다음과 같이 속성 변수와 해당 속성값으로 이루어진 사전을 얻는다.

x.__dict__
{'_MSDie__hidden1': 3, '_hidden2': 7, 'num_sides': 6, 'current_value': 6}

그런데 __hidden1 변수 대신에 _MSDie__hidden1과 속성값이 확인된다. 이처럼 두 개의 밑줄로 시작하는 변수의 이름이 내부적으로 클래스 이름이 붙는 방식으로 변경된다. 이를 이름 뒤섞기(name mangling)라 한다. 변경된 이름을 이용하면 속성이 확인된다.

x._MSDie__hidden1
3

19.3.2. __dir__() 매직 메서드#

객체가 사용할 수 있는 모든 속성과 메서드를 리스트로 보려면 __dir__() 매직 메서드를 사용한다.

x.__dir__()
['_MSDie__hidden1',
 '_hidden2',
 'num_sides',
 'current_value',
 '__module__',
 '__doc__',
 '__init__',
 '_MSDie__randNum',
 'roll',
 '__dict__',
 '__weakref__',
 '__new__',
 '__repr__',
 '__hash__',
 '__str__',
 '__getattribute__',
 '__setattr__',
 '__delattr__',
 '__lt__',
 '__le__',
 '__eq__',
 '__ne__',
 '__gt__',
 '__ge__',
 '__reduce_ex__',
 '__reduce__',
 '__subclasshook__',
 '__init_subclass__',
 '__format__',
 '__sizeof__',
 '__dir__',
 '__class__']

dir() 함수는 __dir__() 메서드를 활용한다.

dir(x)
['_MSDie__hidden1',
 '_MSDie__randNum',
 '__class__',
 '__delattr__',
 '__dict__',
 '__dir__',
 '__doc__',
 '__eq__',
 '__format__',
 '__ge__',
 '__getattribute__',
 '__gt__',
 '__hash__',
 '__init__',
 '__init_subclass__',
 '__le__',
 '__lt__',
 '__module__',
 '__ne__',
 '__new__',
 '__reduce__',
 '__reduce_ex__',
 '__repr__',
 '__setattr__',
 '__sizeof__',
 '__str__',
 '__subclasshook__',
 '__weakref__',
 '_hidden2',
 'current_value',
 'num_sides',
 'roll']

위 리스트에서 확인할 수 있듯이 _MSDie__randNum가 목록에 포함되어 있으며 호출이 가능하다.

x._MSDie__randNum()
4

결론적으로 이름 뒤섞기는 일부 속성과 메서드의 이름이 외부로 쉽게 노출되지 않도록 하는 기법이다. 단, 완전히 차단하는 것은 아니다.

19.3.3. 게터와 세터#

하나의 밑줄을 사용하는 _hidden2는 숨길 것 까지는 아니지만 클래스 내부용이란 사실을 반영한 이름이다. 그리고 이런 변수와 메서드는 사용자가 직접 값을 수정하기 보다는 세터(setter)와 게터(getter) 메서드를 이용하여 클래스의 외부와 내부 사이를 중개하도록 하는 것이 좋다. 이렇게 하면 사용자 입장에서는 클래스 내부에 대한 최소한의 정보만 이용하여 객체 속성 정보를 확인하고 이용할 수 있다.

아래 코드는 current_value를 지정하고 확인하는 세터와 게터, 그리고 _hidden2를 지정하는 세터를 선언한다.

import random

class MSDie:
    """
    다면체 주사위
    
    인스턴스 변수: 
        num_sides: 면 개수
        current_value: 주사위를 굴린 결과
    """

    def __init__(self, num_sides):
        self.__hidden1 = 3                  # 이름 뒤섞기
        self._hidden2 = 7
        self.num_sides = num_sides
        self.current_value = self.roll()    # 주사위 굴리기 먼저 실행

    def __randNum(self):
        return random.randrange(1, self.num_sides+1)

    def roll(self):   # 주사위 굴리기
        randNum = self.__randNum()
        self.current_value = (self.__hidden1 * randNum) % self._hidden2 
        return self.current_value

    def get_hidden2(self):
        return self._hidden2
        
    def set_hidden2(self, num):
        self._hidden2 = num

6면체 주사위 객체를 하나 생성한다.

x = MSDie(6)

현재의 _hidden2 가리키는 값은 다음과 같다.

x._hidden2
7

_hidden2가 가리키는 값을 임의의 값으로 변경하려면 set_hidden2() 메서드를 이용한다.

x.set_hidden2(5)
x.get_hidden2()
5
x.set_hidden2(8)
x.get_hidden2()
8

@property 데코레이터

@property 데코레이터를 이용하면 get_hidden2()set_hidden2()처럼 게터와 세터 함수를 모두 정의하는 대신에 hidden2()와 같은 하나의 이름으로 게터와 세터를 정의할 수 있다. 또한 선언된 함수를 마치 하나의 인스턴스 변수처럼 활용하는 것이 가능하다.

  • @property 데코레이터: 게터로 선언하는 함수 앞에 지정.

  • @게터함수이름.setter 데코레이터: 세터로 선언하는 함수 앞에 지정

import random

class MSDie:
    """
    다면체 주사위
    
    인스턴스 변수: 
        num_sides: 면 개수
        current_value: 주사위를 굴린 결과
    """

    def __init__(self, num_sides):
        self.__hidden1 = 3                  # 이름 뒤섞기
        self._hidden2 = 7
        self.num_sides = num_sides
        self.current_value = self.roll()    # 주사위 굴리기 먼저 실행

    def __randNum(self):
        return random.randrange(1, self.num_sides+1)

    def roll(self):   # 주사위 굴리기
        randNum = self.__randNum()
        self.current_value = (self.__hidden1 * randNum) % self._hidden2 
        return self.current_value
    
    @property
    def hidden2(self):
        return self._hidden2
        
    @hidden2.setter
    def hidden2(self, num):
        self._hidden2 = num

6면체 주사위 객체를 하나 생성한다.

x = MSDie(6)

_hidden2가 원래 가리키는 값은 다음과 같다.

x._hidden2
7

그런데 이제는 마치 공개된 하나의 인스턴스 변수처럼 확인할 수도 있다.

x.hidden2
7

인스턴스 변수 할당을 이용하여 _hidden2 변수의 값을 수정할 수도 있다.

x.hidden2 = 11

_hidden2가 변경되어 있음을 확인할 수 있다.

x._hidden2
11

데코레이터

파이썬은 데코레이터decorator라는 기능을 제공하며, 장식자라고도 불린다. 기존에 정의된 함수를 수정하지 않은 채 기능을 추가할 때 사용되며 다양한 종류의 기본으로 제공된다. 또한 사용자가 직접 데코레이터를 정의해서 활용할 수도 있다. 데코레이터에 대한 보다 자세한 내용은 데코레이터 사용하기에서 찾아볼 수 있다.

19.4. 컨테이너 클래스#

모음 자료형을 추상 자료형 클래스를 이용하여 정의하려면 경우에 따라 아래 사항들도 고려해야 한다.

  • len() 함수 지원

  • for 반복문 지원

  • 대괄호 ([]) 인덱싱 지원

모음 자료형 객체가 속하는 클래스를 일명 컨테이너 클래스라 하며, 앞으로 스택, 큐, 그래프, 트리 등을 다룰 때 중요한 역할을 수행한다. 여기서는 Vector 클래스를 이용하여 앞서 언급한 요소들을 구현하는 과정을 살펴본다.

19.4.1. 벡터 자료형 정의#

아래 코드는 Vector 클래스를 직접 구현한다. 상속에서 정의한 Vector 클래스와는 다르게 list 클래스를 상속하지 않고, 다만 저장 장치로 이용만 한다.

class Vector:
    def __init__(self, items):
        """
        items: 벡터 항목으로 사용될 값들. 
               정수 또는 부동소수점들의 리스트, 튜플 등 모음 자료형 사용.
        저장: 리스트 활용
        """ 
        self.items = list(items)
        
    def __repr__(self):
        return f"myArray({self.items})"
    
    def __add__(self, other):
        """항목별 덧셈 연산"""

        # 벡터 길이가 동일하지 않으면 오류 발생시킴
        # raise와 RuntimeError 활용
        if len(self.items) != len(other.items):
            raise RuntimeError("길이가 달라요!")

        # 항목별 덧셈 실행
        main_object = self.items.copy()
        for i in range(len(main_object)):
            main_object[i] += other.items[i]

        return Vector(main_object)
    
oneD1 = Vector([2, 3, 4])
oneD2 = Vector([11, 22, 33])

덧셈 연산이 항목별로 이루어진다.

oneD1 + oneD2
myArray([13, 25, 37])

len() 함수 지원

그런데 포함된 항목의 개수를 쉽게 확인할 수 없다.

>>> len(oneD1)
TypeError                                 Traceback (most recent call last)
Input In [33], in <module>
----> 1 len(oneD1)

TypeError: object of type 'Vector' has no len()

len() 함수가 사용되려면 __len()__ 메서드가 적절하게 선언되어야 한다.

class Vector:
    def __init__(self, items):
        """
        items: 벡터 항목으로 사용될 값들. 리스트, 튜플 등 모음 자료형 사용.
        저장: 리스트 활용
        """ 
        self.items = list(items)
        
    def __repr__(self):
        return f"myArray({self.items})"
    
    def __add__(self, other):
        """항목별 덧셈 연산"""

        # 벡터 길이가 동일하지 않으면 오류 발생시킴
        # raise와 RuntimeError 활용
        if len(self.items) != len(other.items):
            raise RuntimeError("길이가 달라요!")

        # 항목별 덧셈 실행
        main_object = self.items.copy()
        for i in range(len(main_object)):
            main_object[i] += other.items[i]

        return Vector(main_object)

    def __len__(self):
        return len(self.items) # self.items가 리스트이기 때문에 작동함
    
oneD1 = Vector([2, 3, 4])
oneD2 = Vector([11, 22, 33])
len(oneD1)
3

평균값과 표준편차

len() 함수를 이용하여 넘파이 어레이 객체가 제공하는 다양한 메서드를 구현할 수 있다. 예를 들어 아래 코드는 항목들의 평균값과 표준편차를 계산하는 메서드를 제공한다.

class Vector:
    def __init__(self, items):
        """
        items: 벡터 항목으로 사용될 값들. 
               정수 또는 부동소수점들의 리스트, 튜플 등 모음 자료형 사용.
        저장: 리스트 활용
        """ 
        self.items = list(items)
        
    def __repr__(self):
        return f"myArray({self.items})"
    
    def __add__(self, other):
        """항목별 덧셈 연산"""

        # 벡터 길이가 동일하지 않으면 오류 발생시킴
        # raise와 RuntimeError 활용
        if len(self.items) != len(other.items):
            raise RuntimeError("길이가 달라요!")

        # 항목별 덧셈 실행
        main_object = self.items.copy()
        for i in range(len(main_object)):
            main_object[i] += other.items[i]

        return Vector(main_object)

    def __len__(self):
        return len(self.items) # self.items가 리스트이기 때문에 작동함

    def mean(self):
        """항목들의 평균"""
        sum = 0
        for item in self.items:
            sum += item
            
        return sum/len(self)
    
    def std(self):
        """항목들의 표준편차"""
        sum = 0
        for item in self.items:
            sum += (item - self.mean())**2

        return (sum/len(self))**(1/2)
    
oneD1 = Vector([2, 3, 4])
oneD2 = Vector([11, 22, 33])

이제 평균값과 표준편차를 계산할 수 있다.

  • 평균값

oneD1.mean()
3.0
oneD2.mean()
22.0
  • 표준편차

oneD1.std()
0.816496580927726
oneD2.std()
8.981462390204987

19.4.2. for 반복문 지원#

아직은 포함된 항목들을 대상으로 반복문을 실행할 수 없다.

>>> for x in oneD1:
...     print(x)
TypeError                                 Traceback (most recent call last)
Input In [39], in <module>
----> 1 for x in oneD1:
      2     print(x)

TypeError: 'Vector' object is not iterable

아래에서 처럼 __iter__()__next__() 메서드가 적절하게 선언되어야 한다.

  • __iter__() 메서드: 반복적으로 항목을 생성할 수 있는 객체인 이터레이터(iterator) 생성

  • __next__() 메서드: 이터레이터에 기본으로 포함되는 메서드이며 지정된 순서에 따라 항목을 반환함. 함수 본체에서 사용되는 count, max_repeats 인스턴스 변수는 생성자에서 선언되도록 함.

class Vector:
    def __init__(self, items):
        """
        items: 벡터 항목으로 사용될 값들. 리스트, 튜플 등 모음 자료형 사용.
        저장: 리스트 활용
        """ 
        self.items = list(items)
        self.count = 0                  # 항목 카운트
        self.max_repeats = len(items)   # 항목 카운트 최댓값
        
    def __repr__(self):
        return f"myArray({self.items})"
    
    def __add__(self, other):
        """항목별 덧셈 연산"""

        # 벡터 길이가 동일하지 않으면 오류 발생시킴
        # raise와 RuntimeError 활용
        if len(self.items) != len(other.items):
            raise RuntimeError("길이가 달라요!")

        # 항목별 덧셈 실행
        main_object = self.items.copy()
        for i in range(len(main_object)):
            main_object[i] += other.items[i]

        return Vector(main_object)
    
    def __len__(self):
        return len(self.items)

    def mean(self):
        """항목들의 평균"""
        sum = 0
        for item in self.items:
            sum += item
            
        return sum/len(self)
    
    def std(self):
        """항목들의 표준편차"""
        sum = 0
        for item in self.items:
            sum += (item - self.mean())**2

        return (sum/len(self))**(1/2)

    def __iter__(self):
        return self
    
    def __next__(self):
        if self.count >= self.max_repeats:    # 항목 개수만큼만 반복 허용
            raise StopIteration("더 이상 항목이 없어요!")
            
        next_item = self.items[self.count]
        self.count += 1                       # 항목 반환할 때마다 카운트 키우기
        return next_item
    
oneD1 = Vector([2, 3, 4])
oneD2 = Vector([11, 22, 33])        

이제 for 반복문이 지원된다.

oneD3 = oneD1 + oneD2
for x in oneD3:
    print(x)
13
25
37

그런데 for 반복문을 한 번만 사용할 수 있다.

for x in oneD3:
    print(x)

이유는 count=3 이 되어 __next_() 메서드가 StopIteration 오류를 발생시키가 때문이다.

>>> oneD3.count
3
>>> oneD3.__next__()
StopIteration                             Traceback (most recent call last)
Input In [45], in <module>
----> 1 oneD3.__next__()

Input In [40], in Vector.__next__(self)
     43 def __next__(self):
     44     if self.count >= self.max_repeats:    # 항목 개수만큼만 반복 허용
---> 45         raise StopIteration("더 이상 항목이 없어요!")
     47     next_item = self.items[self.count]
     48     self.count += 1                       # 항목 반환할 때마다 카운트 키우기

StopIteration: 더 이상 항목이 없어요!

for 문을 다시 사용하려면 객체를 새로 생성해야 한다.

oneD3 = oneD1 + oneD2

for x in oneD3:
    print(x)
13
25
37

19.4.3. 인덱싱 지원#

항목들의 순서를 고려해서 항목을 확인하고자 하면 오류가 발생한다.

>>> oneD2[0]
TypeError                                 Traceback (most recent call last)
Input In [46], in <module>
----> 1 oneD2[0]

TypeError: 'Vector' object is not subscriptable

물론 수정도 불가능하다.

>>> oneD2[0] = 100
TypeError                                 Traceback (most recent call last)
Input In [47], in <module>
----> 1 oneD2[0] = 100

TypeError: 'Vector' object does not support item assignment

아래 두 메서드를 구현해야 한다.

  • __getitem__() 메서드: 대괄호 인덱싱 지원

  • __setitem__() 메서드: 특정 인덱스 항목 업데이트

class Vector:
    def __init__(self, items):
        """
        items: 벡터 항목으로 사용될 값들. 리스트, 튜플 등 모음 자료형 사용.
        저장: 리스트 활용
        """ 
        self.items = list(items)
        
    def __repr__(self):
        return f"myArray({self.items})"
    
    def __add__(self, other):
        """항목별 덧셈 연산"""

        # 벡터 길이가 동일하지 않으면 오류 발생시킴
        # raise와 RuntimeError 활용
        if len(self.items) != len(other.items):
            raise RuntimeError("길이가 달라요!")

        # 항목별 덧셈 실행
        main_object = self.items.copy()
        for i in range(len(main_object)):
            main_object[i] += other.items[i]

        return Vector(main_object)
    
    def __len__(self):
        return len(self.items)

    def mean(self):
        """항목들의 평균"""
        sum = 0
        for item in self.items:
            sum += item
            
        return sum/len(self)
    
    def std(self):
        """항목들의 표준편차"""
        sum = 0
        for item in self.items:
            sum += (item - self.mean())**2

        return (sum/len(self))**(1/2)

    def __iter__(self):
        return self
    
    def __next__(self):
        if self.count >= self.max_repeats:    # 항목 개수만큼만 반복 허용
            raise StopIteration("더 이상 항목이 없어요!")
            
        next_item = self.items[self.count]
        self.count += 1                       # 항목 반환할 때마다 카운트 키우기
        return next_item

    def __getitem__(self, idx):
        return self.items[idx]
    
    def __setitem__(self, idx, item):
        self.items[idx] = item
        
oneD1 = Vector([2, 3, 4])
oneD2 = Vector([11, 22, 33])

인덱싱이 작동한다.

oneD2[0]
11

항목 업데이트도 된다.

oneD2[0] = 100
oneD2
myArray([100, 22, 33])

슬라이싱(slicing)도 지원한다.

oneD2[1:3]
[22, 33]
oneD2[0:3]
[100, 22, 33]
oneD2[0:3:2]
[100, 33]

__getitem__() 메서드와 for 반복문

__getitem_() 메서드가 지원되면 __iter__()__next__() 메서드가 없어도 for 반복문이 작동한다.

class Vector:
    def __init__(self, items):
        """
        items: 벡터 항목으로 사용될 값들. 리스트, 튜플 등 모음 자료형 사용.
        저장: 리스트 활용
        """ 
        self.items = list(items)
        
    def __repr__(self):
        return f"myArray({self.items})"
    
    def __add__(self, other):
        """항목별 덧셈 연산"""

        # 벡터 길이가 동일하지 않으면 오류 발생시킴
        # raise와 RuntimeError 활용
        if len(self.items) != len(other.items):
            raise RuntimeError("길이가 달라요!")

        # 항목별 덧셈 실행
        main_object = self.items.copy()
        for i in range(len(main_object)):
            main_object[i] += other.items[i]

        return Vector(main_object)
    
    def __len__(self):
        return len(self.items)

    def mean(self):
        """항목들의 평균"""
        sum = 0
        for item in self.items:
            sum += item
            
        return sum/len(self)
    
#     def __iter__(self):
#         return self
    
#     def __next__(self):
#         if self.count >= self.max_repeats:    # 항목 개수만큼만 반복 허용
#             raise StopIteration("더 이상 항목이 없어요!")
            
#         next_item = self.items[self.count]
#         self.count += 1                       # 항목 반환할 때마다 카운트 키우기
#         return next_item

    def __getitem__(self, idx):
        return self.items[idx]
    
    def __setitem__(self, idx, item):
        self.items[idx] = item
        
oneD1 = Vector([2, 3, 4])
oneD2 = Vector([11, 22, 33])
oneD3 = oneD1 + oneD2
for x in oneD3:
    print(x)
13
25
37

__iter__()__next__()를 사용하는 경우와는 다르게 객체를 새로 생성할 필요없이 반복문을 계속해서 활용할 수도 있다.

for x in oneD3:
    print(x)
13
25
37

19.5. 연습문제#

  1. (실습) 파이썬 기초 4부: 클래스 기본 요소