1. 클래스, 인스턴스, 객체#

주요 내용

파이썬과 같은 객체 지향 프로그래밍 언어의 가장 큰 장점 중의 하나는 필요한 자료형을 클래스로 정의하고 객체를 생성하여 활용할 수 있다는 점이다.

여기서는 fractions 모듈에 포함된 Fraction 클래스를 직접 정의하는 과정을 다루면서 클래스, 인스턴, 객체 개념을 소개한다. Fraction 클래스는 1/2, 2/7 처럼 기약분수들의 자료형 역할을 수행하고 분수들의 덧셈, 크기 비교 등을 지원하도록 할 것이다.

슬라이드

본문 내용을 요약한 슬라이드를 다운로드할 수 있다.

1.1. 클래스 선언과 생성자#

Fraction 클래스를 완성시키는 과정을 이용하여 클래스를 선언하고 인스턴스를 생성하여 활용하는 과정을 살펴본다. 이를 위해 Fraction 클래스를 간단하게 정의한 다음에 필요에 따라 유용한 기능을 추가하는 방식으로 Fraction 클래스의 기능을 확장한다.

1.1.1. 클래스 선언#

분수들의 클래스를 정의해보자. 클래스 정의는 다음 형식으로 이루어진다.

class 클래스명:
    ...
    # 속성 지정 및 메서드 선언

Fraction 클래스의 속성으로 “임의의 정수인 분자와 0이 아닌 임의의 정수인 분모”에 대한 정보가 저장된다. Fraction 클래스가 제공해야 하는 메서드로 예를 들어 분수의 사칙연산, 크기 비교 등이 포함된다.

1.1.2. 생성자#

모든 파이썬 클래스는 인스턴스instance를 생성하는 생성자__init__() 메서드를 포함해야 한다. 생성자는 생성되는 인스턴스(객체)의 속성attributes으로 저장될 값을 지정하거나 저장된 값들을 처리하는 데에 필요한 기타 정보를 인자로 받아 저장한다.

Fraction 클래스는 1/2, 2/5 등과 같은 기약 분수 객체를 인스턴스로 생성하는 기능을 수행해야 하기에 Fraction 클래스의 생성자는 기약 분수의 분자와 분모에 해당하는 값을 인자로 받아 저장한다.

class Fraction:
    def __init__(self, top, bottom):
        """생성자 메서드
        top: 분자
        bottom: 분모
        """

        self.top = top
        self.bottom = bottom

인스턴스 초기 설정

엄밀히 말하면 __init__() 메서드는 객체를 생성하는 게 아니라 생성되는 객체의 초기 설정을 담당하지만 생성자라는 표현이 일반적으로 사용된다.

1.2. 인스턴스와 객체#

객체는 특정 클래스의 인스턴스로 생성된다.

1.2.1. 인스턴스 생성#

Fraction 클래스의 인스턴스instance는 하나의 기약 분수처럼 작동하도록 해야 한다. 예를 들어 아래 코드는 3/5 에 해당하는 객체object를 가리키는 f35 변수를 선언한다.

f35 = Fraction(3, 5)

메모리 상에서 f35 변수가 가리키는 분수 객체는 아래와 같이 저장된다.

self의 기능

f35 = Fraction(3, 5) 방식으로 변수 할당이 실행되면 파이썬 실행기interpreter는 다음을 싱행한다.

>>> __init__(f35, 3, 5)

즉, self 매개변수는 현재 생성되는 객체를 인자로 받는다.

1.2.2. 인스턴스 변수와 속성#

인스턴스 변수instance variable는 클래스 내부에서 self와 함께 선언된 변수를 가리킨다. Fraction 클래스의 인스턴스 변수는 topbottom 두 개다. 인스턴스 변수는 클래스 내부에서만, 따라서 생성되는 객체 고유의 속성을 가리키는 역할을 수행한다.

이런 의미에서 인스턴스 변수가 가리키는 값을 인스턴스 속성instance attribute이라 부른다. 인스턴스 속성은 따라서 클래스의 인스턴스가 생성되어야만 의미를 갖는다.

인스턴스 변수 이외에 self 를 사용하지 않는 클래스 변수class variable도 있지만 여기서는 다루지 않는다.

1.3. 매직 메서드#

클래스 내부에서 선언된 함수를 메서드method라 부른다. 이중에 첫째 매개변수로 self 를 사용하는 메서드가 인스턴스 메서드다. 인스턴스 메서드는 인스턴스 속성처럼 클래스의 인스턴스가 생성되어야만 활용될 수 있다. 인스턴스 메서드 이외에 self를 첫째 인자로 사용하지 않는 클래스 메서드class method, 정적 메서드static method 등이 있지만 여기서는 다루지 않는다.

매직 메서드magic method는 파이썬의 모든 클래스에서 선언되는 인스턴스 메서드를 가리킨다. 매직 메서드는 파이썬 객체가 기본적으로 갖춰야 하는 기능을 지정하는 특별한 메서드이며 메서드의 이름이 밑줄 두 개로 감싸진다.

클래스에 기본으로 포함된 매직 메서드를 dir() 함수를 이용하여 확인할 수 있다. 예를 들어 Fraction 클래스가 지원하는 매직 메서드 목록은 다음과 같다.

dir(Fraction)
['__class__',
 '__delattr__',
 '__dict__',
 '__dir__',
 '__doc__',
 '__eq__',
 '__format__',
 '__ge__',
 '__getattribute__',
 '__getstate__',
 '__gt__',
 '__hash__',
 '__init__',
 '__init_subclass__',
 '__le__',
 '__lt__',
 '__module__',
 '__ne__',
 '__new__',
 '__reduce__',
 '__reduce_ex__',
 '__repr__',
 '__setattr__',
 '__sizeof__',
 '__str__',
 '__subclasshook__',
 '__weakref__']

클래스의 매직 메서드 종류는 지원하는 기능에 따라 달라질 수 있다. 여기서는 앞으로 사용할 매직 메서드 몇 개만 살펴본다.

1.3.1. 객체 출력#

__str__() 매직 메서드

f35 는 ‘3/5’에 해당하는 분수를 가리켜야 한다. 그런데 print() 함수를 이용하여 이 사실을 확인하려면 이해할 수 없는 결과가 출력된다.

print(f35)
<__main__.Fraction object at 0x7f3bf07a1730>

위 실행결과는 f35 변수가 현재 코드 파일에서 선된 Fraction 클래스의 인스턴스로 생성된 객체를 가리키고 있다는 내용을 담고 있다. 이상한 형식의 숫자는 객체가 저장된 메모리상의 주소를 가리킨다.

이와같이 출력된 이유는 Fraction 클래스 고유의 출력방식이 아닌 Fraction 클래스에 기본적으로 포함된 __str()__ 함수가 정의된 대로 작동하기 때문이다. 클래스의 인스턴스가 원하는 대로 출력되도록 하려면 __str__() 메서드를 사용자가 직접 재정의overriding해야 한다.

Fraction 클래스를 선언할 때 __str__() 메서드를 아래처럼 재정의해보자.

class Fraction:
    """Fraction 클래스"""

    def __init__(self, top, bottom):
        """생성자 메서드
        top: 분자
        bottom: 분모
        """
        self.top = top
        self.bottom = bottom

    def __str__(self):
        return f"{self.top}/{self.bottom}"  # 3/5, 1/2 형식으로 출력

아래 코드는 3/51/2 해당하는 두 개의 객체를 생성한다.

f35 = Fraction(3, 5)
f12 = Fraction(1, 2)

클래스 재정의

클래스의 본문을 수정하면 이전에 선언된 인스턴스 또한 새롭게 생성해야만 해당 객체가 수정된 기능을 수행할 수 있다.

이제 print() 함수를 실행하면 3/5, 1/2 등의 형식으로 출력한다.

print(f35)
3/5
print(f12)
1/2
print(f"피자의 {f35}를 먹었다.")
피자의 3/5를 먹었다.
print(f"피자의 {f12}을 먹었다.")
피자의 1/2을 먹었다.

print() 함수를 호출하면 실제로는 인자 객체의 __str__() 메서드가 호출된다.

f35.__str__()
'3/5'

__repr__() 매직 메서드

print() 함수는 잘 작동한다. 하지만 그냥 f35 를 확인하려하면 여전히 제대로 보여지지 않는다.

f35
<__main__.Fraction at 0x7f3bf076b110>

이를 해결하려면 아래처럼 __repr__() 메서드 또한 재정의해야 한다.

class Fraction:
    """Fraction 클래스"""

    def __init__(self, top, bottom):
        """생성자 메서드
        top: 분자
        bottom: 분모
        """
        self.top = top
        self.bottom = bottom

    def __str__(self):
        return f"{self.top}/{self.bottom}"  # 3/5, 1/2 형식으로 출력
    
    def __repr__(self):
        return f"{self.top}/{self.bottom}"  # 3/5, 1/2 형식으로 출력

그러면 굳이 print() 함수를 사용하지 않아도 원하는 대로 보여진다.

f35 = Fraction(3, 5)
f35
3/5

__repr__()__str__()

__repr__()__str__()은 원래 각자의 기능이 다르다. 따라서 필요에 따라 두 메서드의 기능을 다르게 정의할 수 있다. 예를 들어 아래 코드에서 __repr()__ 메서드는 한국어로 분수를 표현하도록 선언된다.

class Fraction:
    """Fraction 클래스"""

    def __init__(self, top, bottom):
        """생성자 메서드
        top: 분자
        bottom: 분모
        """
        self.top = top
        self.bottom = bottom

    def __str__(self):
        return f"{self.top}/{self.bottom}"  # 3/5, 1/2 형식으로 출력
    
    def __repr__(self):
        return f"{self.bottom}분의 {self.top}"  # 5분의 3 형식으로 출력
f35 = Fraction(3, 5)
f35
5분의 3

반면에 __str__() 메서드는 일반적인 분수로 표현된다.

print(f35)
3/5

일반적으로 __repr__()__str__()를 구분하지만 꼭 그럴 필요는 없다. 그럴 때는 __repr__() 메서드만 재정의해도 된다.

class Fraction:
    """Fraction 클래스"""

    def __init__(self, top, bottom):
        """생성자 메서드
        top: 분자
        bottom: 분모
        """
        self.top = top
        self.bottom = bottom

#     def __str__(self):
#         return f"{self.top}/{self.bottom}"  # 3/5, 1/2 형식으로 출력
    
    def __repr__(self):
        return f"{self.top}/{self.bottom}"  # 3/5, 1/2 형식으로 출력
f35 = Fraction(3, 5)
f35
3/5
print(f35)
3/5

1.3.2. 인스턴스 연산#

두 개의 분수 객체를 이용하여 분수의 덧셈이 가능한지 확인해보자. 예를 들어 1/4 + 1/2을 계산해보자.

f14 = Fraction(1, 4)
f12 = Fraction(1, 2)

그런데 분수의 덧셈을 시도하면 오류가 발생한다.

f14 + f12
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
Cell In[25], line 1
----> 1 f14 + f12

TypeError: unsupported operand type(s) for +: 'Fraction' and 'Fraction'

이유는 덧셈 연산자 +Fraction 클래스의 인스턴스에 대해 지원되지 않기 때문이다. 그리고 덧셈, 뺄셈 등 사칙연산에 대해 일반적으로 사용되는 기호를 사용하려면 각각의 기호에 해당하는 매직 메서드를 선언해야 한다.

__add__() 매직 메서드

분수 객체의 덧셈을 위해 + 연산자를 사용하려면 Fraction 클래스에 __add__() 메서드가 아래 코드에서처럼 적절하게 정의되어 있어야 한다. 아래 코드에서 사용된 분수의 덧셈은 아래 수식을 구현한다.

\[\frac {a}{b} + \frac {c}{d} = \frac{ad+cb}{bd}\]

__add__() 메서의 반환값은 두 분수의 합을 가리키는 Fraction 클래스의 인스턴스로 선언된다.

class Fraction:
    """Fraction 클래스"""

    def __init__(self, top, bottom):
        """생성자 메서드
        top: 분자
        bottom: 분모
        """
        self.top = top
        self.bottom = bottom

    def __repr__(self):
        return f"{self.top}/{self.bottom}"

    def __add__(self, other):
        new_top = self.top * other.bottom + self.bottom * other.top
        new_bottom = self.bottom * other.bottom

        return Fraction(new_top, new_bottom)

이제 두 분수 객체의 덧셈 결과가 제대로 계산된다.

f14 = Fraction(1, 4)
f12 = Fraction(1, 2)

f14 + f12
6/8

그런데 덧셈의 결과가 기약분수의 형태가 아니다. 따라서 __add__() 메서드가 반환하는 Fraction() 클래스의 인스턴스가 덧셈 결과를 최대공약수로 약분한 분수를 표현하는 객체가 되도록 해야 한다.

분모, 분자의 최대공약수(gcd)는 유클리드 호제법을 이용한다.

유클리드 호제법

두 개의 정수 \(m\)\(n\)의 최대공약수를 가장 빠르고 효율적으로 계산하는 기법은 유클리드 호제법이다.

  • \(m\)\(n\)으로 나눌 수 있으면 \(n\)이 최대공약수이다.

  • 그렇지 않으면 \(n\)\(m\,\%\, n\)의 최대공약수가 원하는 최대공약수이다.

아래 gcd() 함수는 유클리드 호제법을 구현한다.

def gcd(m, n):
    while m % n != 0:
        m, n = n, m % n
    return n

6과 14의 최대공약수는 2다.

gcd(6, 14)
2

8과 20의 최대공약수는 2다.

gcd(8, 20)
4

gcd() 함수를 __add__() 함수의 정의에 활용하자.

class Fraction:
    """Fraction 클래스"""

    def __init__(self, top, bottom):
        """생성자 메서드
        top: 분자
        bottom: 분모
        """
        self.top = top
        self.bottom = bottom

    def __repr__(self):
        return f"{self.top}/{self.bottom}"

    def __add__(self, other):
        new_top = self.top * other.bottom + \
                     self.bottom * other.top
        new_bottom = self.bottom * other.bottom
        common = gcd(new_top, new_bottom)  # 분모, 분자의 최대공약수
        
        return Fraction(new_top // common, new_bottom // common) # 기약분수 객체

1/4 더하기 1/2의 결과가 이제는 3/4로 표현된다.

f14 = Fraction(1, 4)
f12 = Fraction(1, 2)

f14 + f12
3/4

연산 실행: selfother

f14 + f12 표현식이 실행되면 실제로는 f14 객체의 __add__() 메서드가 다음과 같이 호출된다.

f14.__add__(f12)

즉, f14 를 기준(self)으로 다른 값(other) f12와의 덧셈이 실행된다.

1.3.3. 인스턴스 동일성/동등성#

동일성 대 동등성

객체의 동일성identity은 두 객체가 동일한 메모리 주소에 저장되었는가에 따라 결정된다. 반면에 메모리의 주소가 아니라 객체가 표현하는 값의 동일성 여부는 두 객체의 동등성equality으로 판단된다.

is 연산자

아래처럼 두 변수가 가리키는 객체를 동일하게 선언하면 두 변수가 동일한 객체를 가리킨다고 확인된다. 두 객체의 동일성 여부는 is 연산자로 확인한다.

f1 = Fraction(1, 2)
f2 = f1
f1 is f2 # 동일성
True

== 연산자

단순히 메모리상의 주소가 아닌 두 객체가 동일한 값을 가리키는지 여부인 두 객체의 동등성은 == 연산자로 판단한다. 그리고 동일한 두 객체는 당연히 동등한 객체로 판정된다.

예를 들어 앞서 선언된 두 변수가 동일한 객체를 가리키기에 두 변수가 가리키는 객체의 동등성도 참으로 판정된다.

f1 == f2 # 동등성
True

그런데 아래 코드에서 생성된 두 객체는 모두 1/2을 가리키지만 서로 독립적으로 선언되었기에 저장된 메모리 주소가 다르다. 따라서 두 변수 f1f2는 동일하지 않은 객체를 가리킨다.

f1 = Fraction(1, 2)
f2 = Fraction(1, 2)
f1 is f2 # 동일성 판단
False

동등성 또한 거짓으로 판정된다.

f1 == f2
False

메모리상에서 두 변수가 가리키는 객체가 서로 다름을 아래 이미지에서 확인할 수 있다.

__eq__() 매직 메서드

그럼에도 불구하고 f1f2 모두 1/2를 가리키는 객체라는 사실을 반영할 수 있어야 하는데 이를 위해 __eq__() 매직 메서드를 이용한다.

아래 코드는 분수의 동등성을 구현한 __eq__() 메서드를 Fraction 클래스에 추가한다. 참고로 두 분수의 동등성은 아래와 같이 정의된다.

\[\frac {a}{b} = \frac {c}{d} \Longleftrightarrow ad = bc\]
class Fraction:
    """Fraction 클래스"""

    def __init__(self, top, bottom):
        """생성자 메서드
        top: 분자
        bottom: 분모
        """
        self.top = top
        self.bottom = bottom

    def __repr__(self):
        return f"{self.top}/{self.bottom}"

    def __add__(self, other):
        new_top = self.top * other.bottom + \
                     self.bottom * other.top
        new_bottom = self.bottom * other.bottom
        common = gcd(new_top, new_bottom)
        
        return Fraction(new_top // common, new_bottom // common)

    def __eq__(self, other):  # 분수 객체의 동등성
        first_top = self.top * other.bottom
        second_top = other.top * self.bottom

        return first_top == second_top

f1f2를 다시 선언하자.

f1 = Fraction(1, 2)
f2 = Fraction(1, 2)

이제 동등성이 의도한대로 작동한다.

f1 == f2
True

실제로 f1 == f2를 실행하면 __eq__() 메서드가 다음과 같이 호출된다.

f1.__eq__(f2)
True

물론 여전히 두 객체는 동일하지 않다고 판단된다. 즉, 동등성과 동일성은 서로 다른 개념임에 주의한다.

f1 is f2
False

1.4. 인스턴스 메서드#

매직 메서드 이외에 클래스에 다른 기능을 제공하는 인스턴스 메서드를 선언할 수 있다. 여기서는 다음 두 기능을 지원하는 메서드를 구현한다.

  • 분모와 분자 추출

  • 분수 객체를 부동소수점으로 변환

분모와 분자 추출

아래 코드는 Fraction 클래스의 인스턴스로부터 분자와 분모를 반환하는 numerator() 메서드와 denominator() 메서드를 함께 구현한다.

class Fraction:
    """Fraction 클래스"""

    def __init__(self, top, bottom):
        """생성자 메서드
        top: 분자
        bottom: 분모
        """
        self.top = top
        self.bottom = bottom

    def __repr__(self):
        return f"{self.top}/{self.bottom}"

    def __add__(self, other):
        new_top = self.top * other.bottom + \
                     self.bottom * other.top
        new_bottom = self.bottom * other.bottom
        common = gcd(new_top, new_bottom)
        
        return Fraction(new_top // common, new_bottom // common)

    def __eq__(self, other):
        first_top = self.top * other.bottom
        second_top = other.top * self.bottom

        return first_top == second_top
    
    def numerator(self):
        return self.top     # 분자 반환

    def denominator(self):
        return self.bottom  # 분모 반환

예를 들어 2/3의 분모는 3, 분자는 2임을 아래 코드가 확인해준다.

f3 = Fraction(2, 3)

print(f"{f3}의 분자:", f3.numerator())
print(f"{f3}의 분모:", f3.denominator())
2/3의 분자: 2
2/3의 분모: 3

부동소수점으로의 변환

아래 코드는 분수를 부동소수점으로 변환하는 to_float() 메서드를 구현한다. to_float() 메서드의 반환값은 인자로 입력된 분수 객체의 분자를 분모로 나눈 결과를 반환한다. 분자와 분모의 나눗셈은 파이썬의 기본 나눗셈 연산자를 활용한다.

class Fraction:
    """Fraction 클래스"""

    def __init__(self, top, bottom):
        """생성자 메서드
        top: 분자
        bottom: 분모
        """
        self.top = top
        self.bottom = bottom

    def __repr__(self):
        return f"{self.top}/{self.bottom}"

    def __add__(self, other):
        new_top = self.top * other.bottom + \
                     self.bottom * other.top
        new_bottom = self.bottom * other.bottom
        common = gcd(new_top, new_bottom)
        
        return Fraction(new_top // common, new_bottom // common)

    def __eq__(self, other):
        first_top = self.top * other.bottom
        second_top = other.top * self.bottom

        return first_top == second_top
    
    def numerator(self):
        return self.top

    def denominator(self):
        return self.bottom
    
    def to_float(self):
        return self.numerator() / self.denominator() # 부동소수점으로 변환

Fraction 클래스의 인스턴스는 아래 이미지에서 언급된 모든 메서드를 활용할 수 있다.

예를 들어 2/3을 부동소수점으로 변환하면 다음과 같다.

f3 = Fraction(2, 3)
f3.to_float()
0.6666666666666666

1.5. 연습문제#

참고: (실습) 클래스, 인스턴스, 객체