코드 추상화: 클래스와 객체 2부

안내: python-textbook.readthedocs.io의 Classes 내용을 요약 및 수정한 내용입니다.

메서드 종류

클래스에서 선언된 메서드는 세 종류로 나뉜다.

  1. 인스턴스 메서드(instance method)
    • 첫째 매개변수로 self 사용 (관례)
    • 클래스의 인스턴스가 생성된 후에 인스턴스와 함께 사용.
  2. 클래스 메서드(class method)
    • 첫째 매개변수로 cls 사용 (관례)
    • 클래스 내의 모든 속성과 메서드를 cls 지정자와 함께 사용 가능 단, 인스턴스 속성 사용 불가.
    • 인스턴스 없이 클래스 이름과 함께 사용
    • 해당 클래스의 모든 인스턴스에서도 사용 가능
  3. 정적 메서드(static method)
    • 첫째 매개변수에 대한 의무사항 없음.
    • 클래스 내의 모든 속성과 메서드를 클래스 이름과 함께 사용해야 함. 단, 인스턴스 속성 사용 불가.
    • 인스턴스 없이 클래스 이름과 함께 사용
    • 해당 클래스의 모든 인스턴스에서도 사용 가능

클래스 메서드와 정적 메서드는 항상 장식자와 함께 선언되어야 한다. 반면에 self를 첫째 매개변수로 사용하는 인스턴스 메서드는 특별한 장식자가 필요 없다. 예를 들어, 코드 추상화: 클래스와 객체 1부에서 살펴본 모든 메서드는 인스턴스 메서드이다.

클래스 장식자

장식자(decorator)는 다른 함수의 기능에 다른 기능을 추가할 때 사용되는 함수이다. 즉, 장식자는 함수를 인자로 받아 그 함수가 하는 일에 더해 다른 기능도 수행하는 함수를 리턴값으로 내준다. 이런 장식자를 함수로 정의할 수 있는 이유는 함수가 제1종 객체이기 때문이다. 즉, 다른 함수의 인자 또는 리턴값으로 사용될 수 있다.

참고: 장식자를 기존의 함수에 유용한 추가기능을 제공하여 포장한다는 의미에서 래퍼(wrapper)의 일종으로 간주한다.

파이썬에서 기본으로 제공하는 장식자가 매우 다양하며, 사용자가 직접 장식자를 정의할 수도 있다. 여기서는 클래스에서 선언된 메서드의 종류를 구분하기 위해 사용되는 두 개의 장식자 @classmethod@staticmethod를 소개한다.

클래스 메서드 장식자: @classmethod

클래스의 인스턴스를 생성하지 않아도 클래스 이름과 함께 사용할 수 있는 메서드를 클래스 메서드(class method)라 부르며, @classmethod라는 장식자와 함께 선언된다.

@classmethod
def 함수이름(cls,인자1, ..., 인자k):
    본문

클래스 메서드 역시 첫째 인자로 해당 클래스를 받을 준비를 하는 매개변수를 반드시 사용해야 한다. 하지만 self 대신에 클래스(class) 자체를 가리킨다는 의미로 cls를 관례적으로 사용한다. 클래스 메서드를 호출하면 cls에 해당 클래스 이름이 자동으로 삽입된다. 따라서 클래스 메서드를 호출할 때 첫째 인자는 생략한다.

클래스 메서드를 선언할 때 사용되는 cls 매개변수는 클래스 자신을 가리키는 지정자 역할을 수행한다. 따라서 클래스 메서드 내부에서는 인스턴스 속성과 인스턴스 메서드를 활용하지 못한다.

클래스 메서드 활용법

클래스 메서드를 사용하는 이유는 크게 두 가지이다.

첫째, 상수(constant) 역할을 수행하는 값이나 클래스 속성을 직접 활용하고자 할 때 유용하다. 이는 상수와 클래스 속성을 활용하기 위해 특정 객체가 필요하지 않기 때문이다. 또한 경우에 따라 연관된 상수나 함수들을 하나로 클래스로 묶어서 활용할 수도 있다. 이런 경우 굳이 객체를 생성할 필요가 없다.

In [1]:
class Title:
    TITLES = ('Dr', 'Mr', 'Mrs', 'Ms')

    @classmethod
    def allowed_titles_starting_with(cls, startswith):
        # startwith로 시작하는 타이틀 찾기
        return [t for t in cls.TITLES if t.startswith(startswith)]

    @classmethod
    def allowed_titles_ending_with(cls, endswith):
        # endswith로 끝나는 타이틀 찾기
        return [t for t in cls.TITLES if t.endswith(endswith)]


print(Title.allowed_titles_starting_with("M"))
print(Title.allowed_titles_ending_with("s"))
['Mr', 'Mrs', 'Ms']
['Mrs', 'Ms']

PythonTutor 활용 1

위 코드를 PythonTutor: 클래스 장식자 예제 1에서 실행하면서 확인할 수 있다.

둘째, 리턴값으로 해당 클래스의 인스턴스를 생성하는 클래스 메서드를 사용하는 경우가 종종 있다. 이렇게 하면 해당 클래스의 인스턴스를 생성하기 위한 준비사항을 이 클래스 메서드가 알아서 처리해준다. 예를 들어, 아래 Person 클래스의 fromDict 메서드는 특정 텍스트 파일에 저장된 정보를 확인한 후 그 정보를 적절히 활용하여 Person 클래스의 인스턴스를 생성해준다.

In [2]:
class Person:

    def __init__(self, name, surname):
        self.name = name
        self.surname = surname

    @classmethod
    def fromDict(cls, nameDict):
        # {'이름': 'Jane', '성': 'Doe'} 형식의 사전 자료형에서
        # 이름과 성 정보를 추출해서 Person 클래스 객체 생성
        params = nameDict.values()
        return cls(*params)
    
janeDoe = {'이름':'Jane', '성':'Doe'}

jDoe = Person.fromDict(janeDoe)

print(jDoe.name)
print(jDoe.surname)
Jane
Doe

PythonTutor 활용 2

위 코드를 PythonTutor: 클래스 장식자 예제 2에서 실행하면서 확인할 수 있다.

정적 메서드 장식자: @staticmethod

정적 메서드는 클래스나 인스턴스를 지정하는 인자를 사용하지 않는다. 따라서 일반 함수를 선언하는 것과 완벽하게 동일하다. 다만, 클래스 내부에서 선언되었기 때문에 항상 해당 클래스의 이름을 지정자로 사용하여 호출된다. 또한 정적 메서드의 본문에서 해당 클래스의 인스턴스 속성과 인스턴스 메서드는 활용될 수 없다. 반면에 인스턴스 메서드는 클래스 메서드와 정적 메서드를 모두 활용할 수 있다.

결론적으로, 클래스 메서드의 선언 및 활용 방식과 거의 동일하다. 다만, 정적 메서드는 클래스를 지정할 때 cls 대신에 해당 클래스의 이름을 직접 언급해야 한다는 차이점이 있을 뿐이다. 아래 예제가 세 종류의 메서드 활용법을 잘 보여준다.

In [3]:
class Person:
    TITLES = ('Dr', 'Mr', 'Mrs', 'Ms')

    def __init__(self, name, surname):
        self.name = name
        self.surname = surname

    # 인스턴스 메서드
    def fullname(self):
        return f"{self.name} {self.surname}"

    # 클래스 메서드
    @classmethod
    def allowed_titles_starting_with(cls, startswith):
        return [t for t in cls.TITLES if t.startswith(startswith)]

    # 정적 메서드
    @staticmethod
    def allowed_titles_ending_with(endswith):
        return [t for t in Person.TITLES if t.endswith(endswith)]


jane = Person("Jane", "Smith")

print(jane.fullname())

print(jane.allowed_titles_starting_with("M"))
print(Person.allowed_titles_starting_with("M"))

print(jane.allowed_titles_ending_with("s"))
print(Person.allowed_titles_ending_with("s"))
Jane Smith
['Mr', 'Mrs', 'Ms']
['Mr', 'Mrs', 'Ms']
['Mrs', 'Ms']
['Mrs', 'Ms']

PythonTutor 활용 3

위 코드를 PythonTutor: 장식자 예제에서 실행하면서 확인할 수 있다.

연습 4

  1. 다음 속성과 메서드를 포함하는 클래스 Numbers를 정의하라.
    • MULTIPLIER: 클래스 속성
    • __init__ 메서드: 숫자 두 개를 입력받아 각각 인스턴스 속성 xy로 저장.
    • add: 인스턴스 메서드. xy의 합 내주기
    • multiply: 클래스 메서드. 하나의 숫자 a를 입력 받아 MULTIPLIER와 곱센 결과 내주기
    • subtract: 정적 메서드. bc 숫자 두 개를 입력 받아 b-c 내주기
In [4]:
class Numbers:
    MULTIPLIER = 3.5

    def __init__(self, x, y):
        self.x = x
        self.y = y

    def add(self):
        return self.x + self.y

    @classmethod
    def multiply(cls, a):
        return cls.MULTIPLIER * a

    @staticmethod
    def subtract(b, c):
        return b - c
In [5]:
twoAndfive = Numbers(2,5)

print(twoAndfive.add())

print(Numbers.multiply(4))
print(twoAndfive.multiply(4))

print(Numbers.subtract(7, 2))
print(twoAndfive.subtract(7,2))
7
14.0
14.0
5
5

object 클래스와 매직 메서드

생성된 객체와 관련된 속성과 메서드를 확인하려면 dir 함수를 활용한다. 예를 들어, 다음 Person 클래스의 인스턴스인 jane을 생성해보자.

In [6]:
class Person:
    def __init__(self, name, surname):
        self.name = name
        self.surname = surname

    def fullname(self):
        return f"{self.name} {self.surname}"

jane = Person("Jane", "Smith")

이제 dir 함수를 이용하여 jane이 가리키는 객체의 속성과 메서드를 확인해보자.

In [7]:
dir(jane)
Out[7]:
['__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__',
 'fullname',
 'name',
 'surname']

최상위 클래스: object

놀랍게도 Person 클래스를 선언할 때 명시된 속성과 메서드 이외에 추가로 많은 이름이 보인다. 이유는 다음과 같다.

  • 파이썬의 모든 클래스는 object라는 클래스를 상속한다.
  • 하나의 클래스를 상속하면 해당 클래스의 속성과 메서드도 모두 함께 상속받는다.
  • object 클래스에는 위에서 언급된, 이중 밑줄로 감싸인 속성과 메서드가 선언되어 있다.

상속의 대상인 클래스를 상위 클래스(superclass) 또는 부모 클래스(parent class), 상속하는 클래스를 하위 클래스(subclass) 또는 자식 클래스(child class) 라고 부른다. 이런 의미에서 object는 최상위에 위치한 클래스이다. object 클래스에 포함된 속성과 메서드는 모두 양끝이 이중 밑줄로 감싸이며, object 클래스의 메서드를 특별히 매직 메서드(magic method)라 부른다. 따라서 임의의 클래스는 object 클래스에서 선언된 매직 메서드와 속성을 모두 상속받는다.

Person 클래스를 엄밀히 정의하려면 다음과 같이 상속하는 object 클래스를 명시해야 한다. 하지만 상속 대상이 object 클래스 뿐인 경우 명시하지 않아도 되며, 괄호도 생략한다.


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

    def fullname(self):
        return f"{self.name} {self.surname}"

주의: 파이썬 2에서는 object를 반드시 명시해야 한다. 상속에 대해서는 일단 이 정도만 알고 있으면 되며, 보다 자세한 설명은 추후 다룬다.

object 클래스의 속성과 매직 메서드 기능

초기 설정 메서드 __init__는 굳이 선언될 필요가 없다고 앞서 언급하였는데, 그 이유가 바로 상속하는 부모 클래스의 __init__ 메서드가 자동으로 사용되기 때문이다. 하지만 위와 같이 __init__ 메서드를 선언하면 새로 정의된 함수가 사용된다.

참고: object 클래스의 __init__ 메서드의 기본 기능은 아무 일도 하지 않는 것이다.

모든 매직 메서드는 고유의 기능을 수행한다. 따라서, 특별한 사유가 없으면 매직 메서드를 다시 정의하는 일은 피해야 한다. 여기서는 주요 매직 메서드의 기본 기능을 간략하게 확인한다.

__repr__ 메서드와 __str__ 메서드

숫자, 문자열, 리스트, 튜플, 사전 등을 확인하거나 출력하면 우리에게 매우 친숙한 방식으로 보여진다. 예를 들어, 리스트의 경우는 값을 확인하는 거와 출력하는 데에 차이가 없다.

In [8]:
[1, 2, 3]
Out[8]:
[1, 2, 3]
In [9]:
print([1, 2, 3])
[1, 2, 3]

반면에 문자열의 경우는 조금 다르다.

  • 값 확인: 인용 부호가 붙음
In [10]:
"파이썬이 최고에요!"
Out[10]:
'파이썬이 최고에요!'
  • 화면 출력: 인용 부호 붙지 않음
In [11]:
print("파이썬이 최고에요!")
파이썬이 최고에요!

이렇게 작동하는 이유는 문자열 클래스의 내부에서 __repr____str__ 두 메서드가 조금 다르게 정의되어 있기 때문이다. 값을 확인할 때는 __repr__ 메서드가 호출되고, 출력할 때는 __str__ 메서드가 호출된다.

In [12]:
"파이썬이 최고에요!".__repr__()
Out[12]:
"'파이썬이 최고에요!'"

작은 인용부호가 포함된 문자열이 리턴값이다. 반면에 __str__ 메서드의 경우 작은 인용부호는 포함되지 않은 문자열이 사용되었다.

In [13]:
"파이썬이 최고에요!".__str__()
Out[13]:
'파이썬이 최고에요!'

이제 jane 객체를 확인하고 출력해보자.

In [14]:
jane
Out[14]:
<__main__.Person at 0x7fa7ace4d210>
In [15]:
print(jane)
<__main__.Person object at 0x7fa7ace4d210>

두 경우 아주 조금 다르기는 하지만 기본적으로 동일한 정보를 보여준다. 보여지는 정보는 janePerson 클래스의 객체를 가리킨다는 사실과 해당 객체가 저장되어 있는 메모리의 주소이다. 이렇게 나오는 이유는 Person 클래스에서 __repr__, __str__ 모두 정의되어 있지 않기 때문이다.

이제 Person 클래스에서 __repr__, __str__ 두 메서드를 다음과 같이 재정의해보자. 두 메서드 모두 리턴값은 문자열이어야 한다.

In [16]:
class Person(object):
    def __init__(self, name, surname):
        self.name = name
        self.surname = surname

    def fullname(self):
        return f"{self.name} {self.surname}"
    
    def __str__(self):
        return f"성: {self.surname}, 이름: {self.name}"

    def __repr__(self):
        return f"Person(성: {self.surname}, 이름: {self.name})"

jane = Person("Jane", "Smith")

이제 다시 jane을 확인하고 출력해보자.

In [17]:
# __repr__ 메서드 사용됨.
jane
Out[17]:
Person(성: Smith, 이름: Jane)
In [18]:
# __str__ 메서드 사용됨.
print(jane)
성: Smith, 이름: Jane

주의: __str__ 메서드가 재정의되어 있지 않은 경우, __repr__ 메서드의 정의를 사용한다.

In [19]:
class Person(object):
    def __init__(self, name, surname):
        self.name = name
        self.surname = surname

    def fullname(self):
        return f"{self.name} {self.surname}"
    
    def __repr__(self):
        return f"Person(성: {self.surname}, 이름: {self.name})"

jane = Person("Jane", "Smith")
In [20]:
jane
Out[20]:
Person(성: Smith, 이름: Jane)
In [21]:
print(jane)
Person(성: Smith, 이름: Jane)
차이점

__repr__ 메서드와 __str__ 메서드는 기본적으로 비슷한 용도로 사용된다. 차이점은 __str__ 메서드는 객체들을 적절하게 화면에 출력하는 데에 사용되며, __repr__ 메서드는 객체들을 좀 더 형식을 갖추어 전달 할 때 사용한다. 전달 되는 값은 다른 함수 등에 의해 활용되기 때문에 적절한 정보를 담고 있는 게 좋다.

예를 들어, 시간과 관련된 datetime 모듈의 date 클래스와 datetime 클래스 객체를 이용하여 두 함수가 다르게 작동하는 것을 살펴보자. 아래 예제에서는 reprstr 두 함수를 소개하면서 두 메서드의 차이점을 설명한다. repr 함수를 호출하면 인자로 사용된 값의 __repr__ 메서드가, 반면에 str 함수를 호출하면 인자로 사용된 값의 __str__ 메서드가 호출된다.

In [22]:
import datetime

# datetime 객체: 현재 시간 정보 저장
now = datetime.datetime.now()

# date 객체: 현재 날짜 정보 저장
today = datetime.date.today()

repr 함수는 훨씬 형식을 갖춘 표현을 사용한다.

In [23]:
repr(now)
Out[23]:
'datetime.datetime(2020, 6, 2, 17, 17, 34, 413169)'
In [24]:
repr(today)
Out[24]:
'datetime.date(2020, 6, 2)'

반면에 str 함수는 간소화된 표현을 사용한다.

In [25]:
str(now)
Out[25]:
'2020-06-02 17:17:34.413169'
In [26]:
str(today)
Out[26]:
'2020-06-02'

__lt__ 메서드

코드 추상화: 클래스와 객체 1부에서 활용한 datetime 모듈의 date 클래스의 객체들에 대해 크기 비교를 하였다.

이전에 사용되었던 코드의 일부는 다음과 같으며, 1992년 3월 12일 생의 만나이를 계산하고 있다. 그런데 if 조건문에서 오늘 날짜와 올해 생일 날짜의 크기 비교를 하고 있다.

In [27]:
import datetime

birthdate = datetime.date(1992,3,12)
today = datetime.date.today()
age = today.year - birthdate.year

if today < datetime.date(today.year, birthdate.month, birthdate.day):
    age -= 1

print(f"만 {age}세")
만 28세

date 클래스의 객체들 사이의 크기 비교가 가능하며, 일반적인 날짜 기준의 크기를 사용하고 있다. 하지만 모든 클래스의 객체들의 크기 비교가 항상 가능한 것은 아니다. 해당 클래스에 __lt__ 메서드가 구현되어 있을 때만 가능하다. (lt는 less than의 줄임말임)

예를 들어, Person 클래스의 객체들은 서로 비교할 수 없다.

In [28]:
scotty = Person("Scotty", "Wing")
In [29]:
scotty.fullname()
Out[29]:
'Scotty Wing'

'scotty'와 'jane'의 크기를 비교하면 오류가 발생한다.

In [30]:
scotty < jane
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
<ipython-input-30-ea72389fb6af> in <module>
----> 1 scotty < jane

TypeError: '<' not supported between instances of 'Person' and 'Person'

이제 Person 클래스의 객체를 이름 순으로 크기비교를 하도록 만들기 위해 __lt__ 메서드를 구현하자. 크기 비교는 이름 순으로 정한다. 문자열들의 비교가 알파벳 순서로 결정되는 것을 활용한다.

  • 성을 먼저 비교.
  • 성이 같으면 이름 비교.

참고:, __lt__ 메서드는 자신과 다른 Person 클래스의 객체와의 비교이므로 self 이외에 추가로 하나 더 매개변수를 사용한다. 동일 클래스의 객체를 가리키는 변수는 관용적으로 other라는 이름을 사용한다. 물론 의무사항은 아니다.

In [31]:
class Person(object):
    def __init__(self, name, surname):
        self.name = name
        self.surname = surname

    def fullname(self):
        return f"{self.name} {self.surname}"
    
    def __repr__(self):
        return f"Person(성: {self.surname}, 이름: {self.name})"
    
    def __lt__(self, other):
        if self.surname < other.surname:
            return True
        elif self.surname == other.surname:
            if self.name < other.name:
                return True
            else:
                return False
        else:
            return False

jane = Person("Jane", "Smith")
scotty = Person("Scotty", "Wing")

이제 Person 클래스의 두 객체의 크기를 비교할 수 있다. 크기 비교는 일반적으로 알려진 < 연산자를 사용한다.

In [32]:
jane < scotty
Out[32]:
True

위 코드는 실제로 아래 코드의 일을 한다.

In [33]:
jane.__lt__(scotty)
Out[33]:
True

__lt__ 가 정의되면 > 연산자도 자동으로 사용이 가능하다. > 연산자는 __gt__ 메서드에 해당한다. (gt는 greater than의 줄임말임)

In [34]:
jane > scotty
Out[34]:
False

하지만 아직은 <=>= 는 사용할 수 없다. 이유는 두 객체 사이의 동치성(equivalence)이 정의되지 않았기 때문이다. 즉, __eq__ 메서드가 구현되어 있지 않다. 등식에 대한 설명은 아래 연습문제에서 분수 클래스를 선언할 때 자세히 살펴볼 것이다.

__class__ 속성

객체의 자료형을 확인할 때 type 함수를 사용한다. 그러면 type 함수는 해당 객체의 __class__ 속성을 확인하여 전달한다.

In [35]:
jane.__class__
Out[35]:
__main__.Person
In [36]:
type(jane)
Out[36]:
__main__.Person
In [37]:
[1, 2, 3].__class__
Out[37]:
list
In [38]:
type([1, 2, 3])
Out[38]:
list

__dict__ 속성

__dict__ 속성은 인스턴스 속성들을 사전으로 모아 둔다.

In [39]:
jane.__dict__
Out[39]:
{'name': 'Jane', 'surname': 'Smith'}

즉, __dict__는 해당 인스턴스의 모든 속성을 사전 자료형으로 포함한다. 키는 속성 변수, 값은 속성 값을 사용한다.

주의: 리스트는 인스턴스 속성을 전혀 갖지 않는다.

In [40]:
[1, 2, 3].__dict__
---------------------------------------------------------------------------
AttributeError                            Traceback (most recent call last)
<ipython-input-40-1c5e42f4d9b3> in <module>
----> 1 [1, 2, 3].__dict__

AttributeError: 'list' object has no attribute '__dict__'

__iter__ 메서드

리스트, 튜플, 문자열, 사전 등은 이터러블(iterable) 자료형에 속한다. 이터러블 자료형은 for 반복문과 함께 사용될 수 있는 자료형이라고 생각하면 쉽게 이해할 수 있다. 보다 엄밀히 말하면 __iter__ 메서드가 정의되어 있는 자료형이 이터러블 자료형이다. 실제로 모든 리스트, 튜플, 문자열은 해당 메서드를 포함하고 있다.

주의: __iter__ 메서드는 object에 포함되어 있지 않다.

In [41]:
[1, 2, 3].__iter__()
Out[41]:
<list_iterator at 0x7fa7acd74310>
In [42]:
(1, 2, 3).__iter__()
Out[42]:
<tuple_iterator at 0x7fa7acd6c2d0>
In [43]:
'1, 2, 3'.__iter__()
Out[43]:
<str_iterator at 0x7fa7acd6c450>

__iter__ 메서드의 역할은 해당 자료형을 이터레이터(iterator)로 변환시키는 일이다. 이터레이터는 __next__ 메서드를 포함하는 자료형이다. 해당 메서드는, 예를 들어, for 반목문에서 각 항목을 활용할 때 사용된다.

리스트 [1, 2, 3]을 이용하는 예를 살펴보자.

In [44]:
aList = [1, 2, 3]

iter 함수를 사용하면 __iter__ 메서드가 사용된다.

In [45]:
aIterator = iter(aList)

이제 next 함수를 호출하면 __next__ 메서드가 호출된다.

In [46]:
next(aIterator)
Out[46]:
1

next 함수를 호출할 때마다 리스트의 다음 항목이 확인된다.

In [47]:
next(aIterator)
Out[47]:
2
In [48]:
next(aIterator)
Out[48]:
3

마지막 항목이 확인된 후에는 StopIteration 오류가 발생한다.

In [49]:
next(aIterator)
---------------------------------------------------------------------------
StopIteration                             Traceback (most recent call last)
<ipython-input-49-02382458df68> in <module>
----> 1 next(aIterator)

StopIteration: 

next 함수를 다시 사용하려면 이터레이터를 다시 생성해야 한다. 이번에는 __iter____next__ 메서드를 직접 사용한다.

In [50]:
aIterator = aList.__iter__()
In [51]:
aIterator.__next__()
Out[51]:
1
In [52]:
aIterator.__next__()
Out[52]:
2
In [53]:
aIterator.__next__()
Out[53]:
3
In [54]:
aIterator.__next__()
---------------------------------------------------------------------------
StopIteration                             Traceback (most recent call last)
<ipython-input-54-31285d98d373> in <module>
----> 1 aIterator.__next__()

StopIteration: 

실제로 이터러블 자료형을 for 반복문과 함께 사용하면 내부적으로는 앞서 설명한 대로 이터레이터로 변환한 후 next 함수를 반복적으로 사용한다.

예를 들어, 아래 for 반복문을 살펴보자.

In [55]:
for item in [1, 2, 3]:
    print(item)
1
2
3

위 반복문을 파이썬 해석기가 처리하는 실제 내용은 다음과 같다.

In [56]:
aIterator = iter([1, 2, 3])

# 무한 반복문
while True:
    try:
        # 다음 항목 구하기
        item = next(aIterator)
        print(item)
    except StopIteration:
        # 더 이상 확인할 항목이 없으면 반복문 종료
        break
1
2
3

이터러블 자료형과 이터레이터 자료형에 대해서는 우선 이 정도 알아두면 좋다. 보다 많은 활용 예제에 대한 설명은 programiz.com: Python Iterators을 참고할 것을 추천한다.

__len__ 메서드

리스트, 튜플, 문자열, range 등의 자료형처럼 항목들의 순서와 중복이 허용되는 자료형을 순차 또는 시퀀스(sequence) 자료형이라 한다. 반면에 집합, 사전 등의 자료형처럼 순서를 무시하고 중복이 허용되지 않는 자료형은 단순히 모음(collection) 자료형이라 부른다. 경우에 따라 순차 자료형도 단순히 모음 자료형으로 간주하기도 한다. 순차와 모음 자료형은 모두 항목의 개수를 확인해주는 __len__ 메서드를 갖고 있다.

주의: __len__ 메서드는 object에 포함되어 있지 않다.

In [57]:
[1, 2, 3].__len__()
Out[57]:
3
In [58]:
"Python".__len__()
Out[58]:
6

jane__dict__ 속성에는 두 개의 항목이 포함되어 있다.

In [59]:
jane.__dict__.__len__()
Out[59]:
2

이와 같이 __len__ 메소드를 포함한 자료형에 대해서 len 함수를 사용하여 항목의 개수를 확인할 수 있다. 즉, len 함수를 아래와 같이 정의된 것으로 생각할 수 있다.

def len(s):
    return s.__len__()
In [60]:
len([1, 2, 3])
Out[60]:
3
In [61]:
len("Python")
Out[61]:
6
In [62]:
len(jane.__dict__)
Out[62]:
2

매직 메서드 재정의

Person 클래스에서 __init__ 메서드를 새로 정의하였다. 이와 같이 메서드를 새로 정의하는 것을 메서드 재정의 또는 메서드 오버라이딩(method overriding)이라 부른다. 따라서 모든 매직 메서드는 재정의 되든가, 아니면 부모 클래스에서 선언된 그대로 사용된다.

여기서는 앞서 다루지 않았지만 중요한 매직 메서드 몇 개를 재정의하는 것을 통해 유용한 자료형을 정의하는 방법을 소개한다.

Fraction 클래스

부동소수점을 다루는 float 자료형에 속하는 값들은 실수를 다루기 위해 구현되었다. 하지만 컴퓨터의 한계로 인해 실제로 다룰 수 있는 값들의 소수점 이하 자릿수가 제한된다. 즉, 컴퓨터는 기본적으로 제한된 범위의 유리수만 다룰 수 있다. 이런 이유로 해서 유리수 연산을 100% 정확하게 계산하기 어려운 경우가 발생하며, 이런 문제를 전문적으로 매우 조심스럽게 다루어야 하며, 많은 공학 분야에서 경우에 따라 매우 해결하기 어려운 문제를 발생시키기도 한다.

예를 들어, 아래 코드는 10의 1000승 분의 1이 0과 같다고 판단한다. 10의 1000승 분의 1이 매우 작은 수이긴 하지만 0은 아닌데 컴퓨터는 그렇다고 판단한다.

$$x = \text{1.0e-1000} = 1.0 \times 10^{-1000} = \frac{1}{10^{1000}}$$

주의: 이런 문제는 파이썬의 문제가 아니라, 컴퓨터의 기본 한계 때문에 발생하는 문제이다. 다른 프로그래밍 언어를 사용해도 비슷한 문제가 발생한다.

In [63]:
x = 1.0e-1000
x == 0
Out[63]:
True

이와 같은 문제를 어느 정도 해결하기 위해 파이썬을 유리수들의 클래스인 Fraction을 제공한다. Fraction 자료형에 속한 값은 정수들의 분수에 해당하며, 모든 연산은 분수들의 연산으로 처리된다. Fraction 클래스는 fractions 모듈에 선언되어 있다.

In [64]:
from fractions import Fraction

예를 들어 $\frac{1}{2}$, $\frac{1}{3}$, $\frac{2}{4}$에 해당하는 수는 각각 아래와 같이 표현한다.

In [65]:
F12= Fraction(1, 2)
F13= Fraction(1, 3)
F24= Fraction(2, 4)

사칙연산이 가능하다. 결과는 __repr__ 메서드의 기능을 통해 다시 Fraction 클래스의 객체로 보여진다.

In [66]:
# 1/2 + 1/3 = 5/6

F12 + F13
Out[66]:
Fraction(5, 6)
In [67]:
# 1/3 - 1/2 = -1/6

F13 - F12
Out[67]:
Fraction(-1, 6)
In [68]:
# 1/2 * 1/3 = 1/6

F12 * F13
Out[68]:
Fraction(1, 6)
In [69]:
# (1/3) / (1/2) = 2/3

F13 / F12
Out[69]:
Fraction(2, 3)

분수가 기본적으로 약분되어 처리된다. 예를 들어, 1/2 + 1/2를 2/2가 아닌 1/1, 즉 1로 처리한다.

In [70]:
F12 + F12
Out[70]:
Fraction(1, 1)

Fraction 객체와 정수들과의 연산도 가능하다.

In [71]:
Fraction(1, 6) + 2
Out[71]:
Fraction(13, 6)
In [72]:
Fraction(1, 6) * 2
Out[72]:
Fraction(1, 3)

print 함수를 사용하면 익숙한 표현으로 보여진다. 즉, __str__ 메소드가 적절하게 정의되어 있다.

In [73]:
print(F12)
1/2
In [74]:
print(F13)
1/3
In [75]:
print(F13 / F12)
2/3

크기 비교도 가능하다.

In [76]:
F12 < F13
Out[76]:
False

모든 연산은 약분을 전제로 이루어진다. 따라서 두 분수의 동일성도 쉽게 판단한다.

In [77]:
F12 == F24
Out[77]:
True

연습: 유리수 클래스 선언하기

Fraction 클래스와 유사하게 작동하는 클래스를 직접 정의하면서 다음 매직 메서드들을 재정의하는 실습을 해보자. 재정의 대상 메서드는 다음과 같다.

  • __repr__
  • __str__
  • __eq__
  • __lt__
  • __le__
  • __add__
In [78]:
import math

class myFraction:
    """분수 클래스
    정수들의 분수를 정수들의 쌍으로 구현함.
    분수는 기본적으로 약분 처리하여 저장.
    """

    def __init__(self, numerator, denominator):
        
        # 분모가 0이면 오류 발생
        if denominator == 0: 
            raise ZeroDivisionError(f"myFraction({numerator}, 0)")
            
        # 분모는 항상 양의 정수로 유지
        elif denominator < 0:
            denominator = -denominator
            numerator = -numerator

        # 최대공약수로 분모 분자 나누기
        g = math.gcd(numerator, denominator)
        numerator //= g
        denominator //= g
        
        # 인스턴스 속성: 분자와 분모
        # 약분 후 저장
        self._numerator = numerator
        self._denominator = denominator
                
    def __repr__(self):
        return f"{self.__class__.__name__}({self._numerator},{self._denominator})"
    
    def __str__(self):
        if self._denominator == 1:
            return str(self._numerator)
        else:
            return f"{self._numerator}/{self._denominator}"

    # 같다(equal)
    def __eq__(self, other):
        return (self._numerator * other._denominator) \
                == (other._numerator * self._denominator)

    # 작다(less than)
    def __lt__(self, other):
        return (self._numerator * other._denominator) \
                < (other._numerator * self._denominator)

    # 작거나 같다(less than or equal)
    def __le__(self, other):
        return self < other or self == other

    # 더하기. myFraction 객체를 생성하면서 자연스럽게 기약분수로 만듦.
    def __add__(self, other):
        numerator = self._numerator * other._denominator + \
                    self._denominator * other._numerator
        denominator = self._denominator * other._denominator
        return myFraction(numerator, denominator)

참고: 분수들의 크기 비교와 덧셈은 아래와 같이 이루어진다.

  • 크기 비교 $$\frac{b}{a}\, <\, \frac{d}{c} \quad\Longleftrightarrow\quad b\, c\, <\, a\, d$$

  • 덧셈 $$\frac{b}{a} + \frac{d}{c} \quad =\quad \frac{b\, c + a\, d}{a \,c}$$
In [79]:
R14 = myFraction(1, 4)
In [80]:
R14
Out[80]:
myFraction(1,4)
In [81]:
print(R14)
1/4
In [82]:
Rm14 = myFraction(1, -4)
In [83]:
Rm14
Out[83]:
myFraction(-1,4)
In [84]:
print(Rm14)
-1/4
In [85]:
R34 = myFraction(3, 4)
In [86]:
Rm34 = myFraction(-3, 4)
In [87]:
R28 = myFraction(2, 8)
In [88]:
print(R14)
1/4
In [89]:
print(R28)
1/4
In [90]:
R14 == R28
Out[90]:
True
In [91]:
R14 < R34
Out[91]:
True
In [92]:
R14 <= R34
Out[92]:
True
In [93]:
Rm34 < Rm14
Out[93]:
True
In [94]:
Rm34 <= Rm14
Out[94]:
True
In [95]:
Rm34 >= Rm14
Out[95]:
False
In [96]:
print(R14 + R34)
1
In [97]:
print(Rm14 + R34)
1/2

PythonTutor 활용 4

위 예제들을 PythonTutor: 유리스 클래스에서 실행하면서 확인할 수 있다.

연습문제

  1. myFraction 클래스의 객체가 사칙연산을 모두 지원하도록 아래 메서드를 구현하라. 그러면 -(부호 바꾸기), -(뺄셈), *, / 등의 연산 기호를 사용할 수 있다.
    • __neg__: 부호 바꾸기 (양수 <=> 음수)
    • __sub__: 뺄셈
    • __mul__: 곱셈
    • __truediv__: 나눗셈

  2. myFraction 클래스의 객체가 정수와의 사칙연산도 가능하도록 아래 메서드를 수정하라.
    • __add__: 덧셈
    • __sub__: 뺄셈
    • __mul__: 곱셈
    • __truediv__: 나눗셈