19. 클래스, 인스턴스, 객체#
객체 지향 프로그래밍 언어의 가장 큰 장점 중의 하나는 필요한 자료형을 클래스로 정의하고 객체를 생성하여 활용할 수 있다는 점이다.
여기서는 fractions 모듈에 포함된 Fraction 클래스처럼
작동하는 클래스를 직접 정의하면서 클래스, 인스턴스, 객체 개념을 구체적으로 소개한다.
Fraction 클래스는 1/2, 2/7 처럼 기약 분수들의 자료형 역할을 수행하고
분수들의 덧셈, 크기 비교 등을 지원한다.
19.1. 클래스#
Fraction 클래스를 완성시키는 과정을 이용하여 클래스 선언과 인스턴스 생성 과정을 살펴본다.
먼저 Fraction 클래스를 단순하게 정의한 다음에 필요에 따라
속성과 메서드 함수를 추가하는 방식으로 Fraction 클래스의
기능을 확장한다.
19.1.1. 클래스 선언#
클래스 선언의 기본 형식은 다음과 같다.
class 클래스명:
# 속성과 메서드 선언
이어지는 장에서 클래스 상속을 다룰 때 좀 더 일반화된 클래스 선언 형식을 소개한다.
속성, 인스턴스 변수, 메서드
속성attributes은 클래스의 인스턴스에 저장되는 값을, 메서드methods는 클래스의 인스턴스가 활용할 수 있는 함수를 가리킨다. 속성을 가리키는 변수는 인스턴스 변수라 부른다.
메서드와 인스턴스 변수는 각각 클래스 본문에서 선언된 함수와 변수를 가리키며 각각 특별한 형식을 따른다.
인스턴스 변수의 형식:
self.변수명메서드의 형식: 함수의 첫째 매개 변수가
self
예제: Fraction 클래스
Fraction 클래스는 속성으로 분모와 분자로 사용되는 두 개의 정수를 저장하며,
분수의 분수의 사칙연산, 크기 비교 등의 기능을 메서드로 제공한다.
아래 코드는 이번 장에서 최종적으로 선언되는 Fraction 클래스를 보여준다.
class Fraction:
def __init__(self, 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 to_float(self, digits=2):
return round(self.top_ / self.bottom_, digits)
def numerator(self):
return self.top_
def denominator(self):
return self.bottom_
Fraction클래스에서 선언된 인스턴스 변수와 속성
인스턴스 변수 |
속성 |
|---|---|
|
분수 객체의 분자 |
|
분수 객체의 분모 |
Fraction클래스에서 선언된 메서드
메서드 |
기능 |
|---|---|
|
생성자. 인스턴스 초기화. |
|
객체 출력 ( |
|
두 분수 객체의 덧셈 |
|
두 분수 객체의 동치성 판단 |
|
분수 객체의 분자 반환 |
|
분수 객체의 분모 반환 |
|
분수 객체를 부동소수점으로 변환 |
Fraction클래스에서 사용되었지만 인스턴스 변수도 메서드도 아닌 변수와 함수
기능 |
변수 또는 함수 |
|---|---|
매개 변수 |
|
지역 변수 |
|
전역 함수 |
|
19.1.2. 클래스의 인스턴스#
클래스를 선언하는 이유는 공통 속성과 공통 기능을 갖는 값을 반복적으로 손쉽게 생성하기 위해서다.
클래스를 이용하여 생성된 값이 해당 클래스의 인스턴스instances며, 특정 클래스의 인스턴스를 일반적으로 객체objects라 부른다. 18장에서 설명하였듯이 파이썬에서 언급되는 모든 값은 객체, 즉 특정 클래스의 인스턴스로 선언된다.
예를 들어, 리스트 객체, 사전 객체, 튜플 객체는 각각
대괄호, 중괄호, 소괄호를 사용하여 원하는 만큼 쉽게 생성할 수 있다.
반면에 여러 마리의 거북이 객체는 turtle.Turtle 클래스를
함수 호출 방식을 이용하여 반복해서 활용하면 된다.
거북이 객체와 달리 리스트, 튜플, 사전 등은 클래스 이름을 사용하지 않는 이유는 가장 기본으로 사용되는 값들이기에 특별한 생성 방식이 지원되기 때문이다.
리스트 인스턴스 생성: 생성된 모든 리스트는 리스트 자료형이 제공하는 모든 메서드를 동일한 방식으로 활용할 수 있다.
list1 = [1, 2, 3]
list2 = ['a', 'b', 'c', 'd']
list1.append(4)
list2.append('e')
사전 인스턴스 생성: 생성된 모든 사전은 사전 자료형이 제공하는 모든 메서드를 동일한 방식으로 활용할 수 있다.
dict1 = {1:'Python', 2:'Java', 3:'C++'}
dict2 = {'a':'A', 'b':'B', 'c':'C', 'd':'D'}
dict1.items()
dict2.items()
튜플 인스턴스 생성: 생성된 모든 튜플은 튜플 자료형이 제공하는 모든 메서드를 동일한 방식으로 활용할 수 있다.
tuple1 = (1, 2, 3)
tuple2 = ('a', 'b', 'c', 'd')
tuple1.count(1)
tuple2.count('b')
거북이 인스턴스 생성:
tina와tommy거북이 모두 동일한 방식으로 움직이고 활용될 수 있다.
import turtle
tina = turtle.Turtle()
tommy = turtle.Turtle()
tina.forward(30)
tommy.forward(30)
Fraction 클래스 정의 1
이제부터 앞서 언급된 Fraction 클래스의 본문에 포함된 메서드와 인스턴스 변수를 하나씩
추가하면서 클래스의 주요 요소와 기능을 살펴 본다.
먼저 훨씬 단순한 Fraction 클래스를 다음과 같이 선언한다.
class Fraction:
def __init__(self, top, bottom):
self.top_ = top
self.bottom_ = bottom
def to_float(self, digits=2):
return round(self.top_ / self.bottom_, digits)
인스턴스 생성
클래스의 인스턴스는 일반적으로 클래스를 함수처럼 호출하는 방식으로 생성된다.
예를 들어 Fraction 클래스의 인스턴스 아래 형식으로 생성된다.
Fraction(a, b)
위 표현식에서 마치 함수의 인자처럼 사용된 a 와 b 는 각각
바로 이어서 설명할 __init__() 메서드의 top과 bottom 두 매개 변수에 전달되는 값이다.
self 매개 변수에 대한 인자는 지정하지 않음에 주의한다.
self 매개 변수는 특별한 기능을 수행하며 잠시 뒤에 자세히 설명한다.
클래스의 인스턴스 생성에 필요한 인자
거북이 객체를 생성할 때 tina = turtle.Turtle()처럼 아무런 인자를 지정하지 않아도 된다.
하지만 사실은 tutle.Turtle 클래스의 인스턴스를 생성하는
생성자 메서드가 키워드 인자를 사용되었기 때문이다.
이처럼 클래스의 인스턴스를 생성할 때 필요한 인자는 클래스에 따라 달라진다. 정확히는 클래스의 생성자 메서드에 의존한다. 곧이어 생성자에 대해 설명할 때 보다 자세히 설명한다.
Fraction 클래스의 인스턴스 예제
아래 코드에서 f35 와 f53 은 각각 3/5과 5/3에 해당하는 객체를 생성한다.
f35 = Fraction(3, 5)
f53 = Fraction(5, 3)
생성된 두 객체 모두 Fraction 클래스의 인스턴스로서
to_float() 메서드를 활용할 수 있다.
이를 이용하여 두 객체가 각각 3/5와 5/3를 가리키는 값임을 확인할 수 있다.
참고로 3/5는 0.6, 5/3는 약 1.67이며, 메서드는 언제나 처럼 아래 방식으로 호출된다.
객체.메서드(인자1, 인자2, ...)
print(f"f35가 가리키는 값: {f35.to_float(3)}")
f35가 가리키는 값: 0.6
print(f"f53가 가리키는 값: {f53.to_float(3)}")
f53가 가리키는 값: 1.667
to_float() 메서드의 키워드 인자를 지정하지 않으면 물론 기본값 2가 대신 사용되어
소숫점 이하 둘째 자리까지 출력된다.
print(f"f35가 가리키는 값: {f35.to_float()}")
f35가 가리키는 값: 0.6
print(f"f53가 가리키는 값: {f53.to_float()}")
f53가 가리키는 값: 1.67
to_float() 메서드 또한 self 매개 변수에 대한 인자는 절대로 지정하지 않는다.
self 매개 변수 vs. 키워드 인자 매개 변수
함수를 호출할 때 키워드 인자가 지정된 매개 변수에 대한 인자는 지정하지 않아도 된다. 이유는 기본값으로 지정된 인자가 자동으로 인자로 사용되기 때문이다.
반면에 self 매개 변수에 대한 인자는 절대로 지정하지 않아야 한다.
이유는 self 매개 변수는 인스턴스가 생성될 때마다
클래스에 따라 다른 적절한 값이 대신 인자로 사용되기 때문이다.
19.2. 생성자#
파이썬의 클래스에는 인스턴스 생성을 담당하는 __init__() 메서드가 기본으로 포함된다.
__init__() 메서드는 보통 생성자라고 불리며,
주로 생성되는 인스턴스의 속성을 지정하는 일을 수행한다.
위 Fraction 클래스의 생성자는
3/5, 5/3 등과 같은 기약 분수 객체를 인스턴스로
생성하기 위해
기약 분수의 분자와 분모에 해당하는 값을
각각 self.top_과 self.bottom_ 인스턴스 변수에 할당한다.
예를 들어, 아래와 같이 Fraction 클래스의 인스턴스를 선언하자.
f35 = Fraction(3, 5)
그러면 파이썬 실행기는 Fraction 클래스의 __init__() 메서드를
다음과 같이 호출한다.
__init__(f35, 3, 5)
호출 과정에 사용되는 매개 변수별 인자는 다음과 같다.
self=f35: 생성되는 객체를 가리키는 변수top=3: 분자로 지정되어야 하는 값bottom=5: 분모로 지정되어야 하는 값
호출된 생성자는 self.top_과 self.bottom_ 인스턴스 변수에
각각 3과 5를 할당한다.
그 결과 메모리 상에서 Fraction 클래스의 인스턴스 객체 Fraction(3, 5) 가
다음과 같이 생성된다.

생성자 메서드 이름
파이썬 클래스의 생성자는 모두 __init__ 로 정의된다.
반면에 Java, C++, C# 등 많은 다른 OOP 언어에서는 생성자 메서드의 이름과
클래스 이름이 동일한 경우가 많다.
언어마다 생성자를 부르는 방식이 다른 이유가 있지만 여기서는
더이상 언급하지 않는다.
self의 기능
여기서 소개하는 클래스의 본문에서 선언된 메서드와 인스턴스 변수는 모두 self 키워드를 사용한다.
이는 파이썬 언어만의 특징이며 Java, C++, C# 등 다른 프로그래밍언어에서는 일반적으로 사용되지 않는다.
self의 기능은 다양하다.
첫째, self.top_과 self.bottom_은 클래스 내부 전체에서 사용될 수 있는 권한을 갖는다.
두 변수는 __init__() 메서드, 즉 함수 안에서 선언되었지만
to_float() 메서드, 즉 다른 함수 내부에서 사용된다.
이처럼 self.변수명 형식으로 클래스 본문 어딘가에서 선언된 변수는
클래스 어디에서도 활용될 수 있다.
즉, 인스턴스 변수의 활동 영역scope은 클래스 본문 전체다.
둘째, 메서드의 첫째 매개 변수로 self가 사용된다.
self 매개 변수는 현재 생성되는 객체를 인자로 받는다.
따라서 f35.to_float() 처럼 객체의 메서드로 호출될 때는 self 매개 변수에 대한 인자는 지정하지 않는다.
이유는 파이썬 실행기가 self 매개 변수에 메서드가 속한 객체인 f35를 인자로
지정하기 때문이다.
즉, 파이썬 실행기에 의해 아래 코드가 대신 실행된다.
to_float(f35, 3, 5)
메서드의 첫째 매개 변수로 self를 사용하는 메서드를 인스턴스 메서드라고 부른다.
하지만 여기서는 다른 종류의 메서드를 다루지 않기에 그냥 메서드라고 한다.
self를 사용하지 않는 변수와 메서드
클래스 본문에 self 를 사용하지 않는 변수와 메서드를 선언할 수 있다.
그런 변수와 메서드는 여기서 다루는 인스턴스 변수와 메서드와 용도와 활용법이 다르다.
자세한 내용은 더이상 다루지 않는 대신 아래 두 링크를 참고할 것을 추천한다.
19.3. 매직 메서드#
매직 메서드magic methods는 생성되는 클래스의 인스턴스가 기본적으로 갖춰야 하는 기능을 지정하는 특별한 메서드이며 메서드의 이름이 밑줄 두 개로 감싸진다.
정의된 클래스에 기본으로 포함된 매직 메서드의 목록은
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__',
'to_float']
위 목록 중에서 밑줄 기호 두 개로 감싸인 메서드가 모두 매직 메서드다.
모든 매직 메서드는 각자 고유의 기능을 갖는다.
여기서는 Fraction 클래스를 분수들의 클래스로서
제대로 작동하기 위해 필요한 매직 메서드 몇 개를 살펴 보면서
매직 메서드의 활용법을 살펴본다.
19.3.1. 객체 출력#
f35 는 ‘3/5’에 해당하는 분수를 가리켜야 한다.
그런데 print() 함수를 이용하여 값을 확인하면
화면에 3/5 대신 Fraction 클래스의 인스턴스 생성된 객체의 주소 정보가 출력된다.
print(f35)
<__main__.Fraction object at 0x7fbcac3062d0>
이와같이 출력된 이유는 __str()__ 메서드의 반환값이 그렇게 정의되어 있기 때문이다.
클래스의 인스턴스가 다른 방식으로 출력되도록 하려면 __str__() 메서드를
사용자가 직접 재정의해야 한다.
__str__() 매직 메서드 재정의
__str__() 등 앞서 언급된 모든 매직 메서드는 클래스가 선언되는 순간
정의된 채로 클래스의 메서드로 자동 지정된다.
하지만 필요에 따라 매직 메서드의 정의를 변경할 수 있으며
이를 메서드 재정의라 부른다.
영어로는 메서드 오버라이딩method overriding이라 한다.
아래 코드는 분수 객체를 3/5, 1/2 등으로 출력되도록 만들기 위해
Fraction 클래스를 선언할 때 __str__() 메서드의 재정의를 포함시킨다.
class Fraction:
def __init__(self, top, bottom):
self.top_ = top
self.bottom_ = bottom
def __str__(self):
return f"{self.top_}/{self.bottom_}"
def to_float(self, digits=2):
return round(self.top_ / self.bottom_, digits)
아래 코드는 3/5와 1/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'
f12.__str__()
'1/2'
__repr__() 매직 메서드 재정의
print() 함수는 잘 작동한다.
하지만 그냥 f35 를 확인하려하면 여전히 제대로 보여지지 않는다.
f35
<__main__.Fraction at 0x7fbcac3785f0>
리스트 항목인 경우에도 마찬가지로 제대로 원하는 대로 출력되지 않는다.
print(['a', f35, 1])
['a', <__main__.Fraction object at 0x7fbcac3785f0>, 1]
이를 해결하려면 아래처럼 __repr__() 매직 메서드 또한 재정의해야 한다.
class Fraction:
def __init__(self, top, bottom):
self.top_ = top
self.bottom_ = bottom
def __str__(self):
return f"{self.top_}/{self.bottom_}"
def __repr__(self):
return f"{self.top_}/{self.bottom_}"
def to_float(self, digits=2):
return round(self.top_ / self.bottom_, digits)
그러면 굳이 print() 함수를 사용하지 않아도 원하는 대로 보여진다.
f35 = Fraction(3, 5)
f35
3/5
리스트에 대해서도 잘 작동한다.
print(['a', f35, 1])
['a', 3/5, 1]
__repr__() 대 __str__()
__repr__()와 __str__() 두 메서드는 원래 각자의 기능이 다르며,
필요에 따라 두 메서드의 기능을 다르게 정의할 수 있다.
예를 들어 아래 코드에서 __repr()__ 메서드는 한국어로 분수를 표현하도록 선언된다.
class Fraction:
def __init__(self, top, bottom):
self.top_ = top
self.bottom_ = bottom
def __str__(self):
return f"{self.top_}/{self.bottom_}"
def __repr__(self):
return f"{self.bottom_}분의 {self.top_}"
def to_float(self, digits=2):
return round(self.top_ / self.bottom_, digits)
클래스를 수정하였기에 다시 인스턴스를 생성한다.
f35 = Fraction(3, 5)
이제 print() 함수를 사용하지 않아도 지정된 방식으로 출력된다.
f35
5분의 3
반면에 __str__() 메서드는 일반적인 분수로 표현된다.
print(f35)
3/5
리스트 항목에 대해서는 __repr__() 메서드가 작동한다.
print(['a', f35, 1])
['a', 5분의 3, 1]
일반적으로 __repr__()와 __str__()를 구분하지만 꼭 그럴 필요는 없다.
그럴 때는 __repr__() 메서드만 재정의해도 된다.
class Fraction:
def __init__(self, top, bottom):
self.top_ = top
self.bottom_ = bottom
# def __str__(self):
# return f"{self.top_}/{self.bottom_}"
def __repr__(self):
return f"{self.top_}/{self.bottom_}"
def to_float(self, digits=2):
return round(self.top_ / self.bottom_, digits)
그러면 __str__() 메서드를 대신해 __repr__() 메서드가 사용된다.
f35 = Fraction(3, 5)
f35
3/5
print(['a', f35, 1])
['a', 3/5, 1]
__str__() 메서드가 선언되지 않았음에도 불구하고 지원된다.
엄밀히 말하면 __repr__() 메서드를 활용하도록 설정되어 있다.
print(f35) # __repr__() 대신 사용됨
3/5
f35.__str__()
'3/5'
19.3.2. 인스턴스 연산#
두 개의 분수 객체를 이용하여 분수의 덧셈이 가능한지 확인해보자. 예를 들어 1/4 + 1/2을 계산해보자.
f14 = Fraction(1, 4)
f12 = Fraction(1, 2)
그런데 분수의 덧셈을 시도하면 오류가 발생한다.
f14 + f12
---------------------------------------------------------------------------
TypeError Traceback (most recent call last)
Cell In[36], line 1
----> 1 f14 + f12
TypeError: unsupported operand type(s) for +: 'Fraction' and 'Fraction'
이유는 덧셈 연산자 +가 Fraction 클래스의 인스턴스에 대해 지원되지 않기 때문이다.
덧셈, 뺄셈 등 사칙연산에 대해 일반적으로 사용되는 기호를 사용하려면
각각의 기호에 해당하는 매직 메서드를 선언해야 한다.
__add__() 매직 메서드
분수 객체의 덧셈을 위해 + 연산자를 사용하려면
아래 코드에서처럼 Fraction 클래스에 __add__() 메서드가 적절하게 정의되어 있어야 한다.
아래 코드에서 사용된 분수의 덧셈은 아래 수식을 따라한다.
즉, a/b + c/d 의 분자는 ad+cb, 분모는 bd이고.
두 값을 이용하여 생성된 Fraction 클래스의 인스턴스가
__add__() 메서드의 반환값이다.
class Fraction:
def __init__(self, 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)
def to_float(self, digits=2):
return round(self.top_ / self.bottom_, digits)
이제 두 분수 객체의 덧셈 결과가 제대로 계산된다.
f14 = Fraction(1, 4)
f12 = Fraction(1, 2)
f14 + f12
6/8
그런데 덧셈의 결과가 기약 분수의 형태가 아니다.
이유는 두 기약 분수의 합이 반드시 기약 분수 형식으로 계산되지는 않기 때문이다.
따라서 __add__() 메서드가 반환하는 Fraction() 클래스의 인스턴스가
사용하는 분자와 분모의 최대공약수가 1이 되도록 약분해서
Fraction 클래스의 인스턴스를 생성해야 한다.
이를 위해 분모, 분자의 최대공약수(gcd)를 유클리드 호제법 알고리즘을 이용하여 계산하는
함수 gcd()를 활용한다.
def gcd(m, n):
while m % n != 0:
m, n = n, m % n
return n
유클리드 호제법
두 개의 정수 m과 n의 최대공약수를 구하기 위해
아래 과정을 반복 적용한다.
m % n = 0인 경우:n이 최대공약수m % n > 0인 경우:n과m % n의 최대공약수 계산
6과 14의 최대공약수는 2다.
gcd(6, 14)
2
8과 20의 최대공약수는 4다.
gcd(8, 20)
4
gcd() 함수를 __add__() 함수의 정의에 활용하자.
class Fraction:
def __init__(self, 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 to_float(self, digits=2):
return round(self.top_ / self.bottom_, digits)
1/4 더하기 1/2의 결과가 이제는 3/4로 표현된다.
f14 = Fraction(1, 4)
f12 = Fraction(1, 2)
f14 + f12
3/4
self와 other
__add__() 매직 메서드가 추가되면서 덧셈 연산자 + 가 지원되기 시작했다.
파이썬 실행기 내부에서 아래 내용이 차례대로 진행된다.
f14 + f12표현식 실행f14객체의__add__()메서드가 다음과 같이 호출됨:f14.__add__(f12)
내부적으로 파이썬 실행기는
self매개 변수에f14를,other매개 변수에f12를 지정하면서__add__()함수 호출:__add__(f14, f12)
f14.__add__(f12)는 실제로 실행되는 표현식이다.
f14.__add__(f12)
3/4
참고로 other는 동일 클래스의 다른 인스턴스를 가리키는 매개 변수로 관용적으로 많이 사용된다.
예를 들어 __add__() 메서드처럼 동일한 클래스의 두 인스턴스를 인자로 사용하는 메서드에서
자주 사용된다.
19.3.3. 인스턴스 동일성/동등성#
두 객체의 동일성identity은 두 객체가 동일한 메모리 주소에 저장되었는가에 따라 결정된다. 반면에 메모리의 주소가 아니라 객체가 표현하는 값의 동일성 여부는 두 객체의 동등성equality으로 판단된다.
동일성 판단: is 연산자
두 변수가 가리키는 객체를 동일하게 선언하면
두 변수가 동일한 객체를 가리킨다고 확인된다.
두 객체의 동일성 여부는 is 연산자로 확인한다.
f1 = Fraction(1, 2)
f2 = f1
f1 is f2
True

동등성 판단: == 연산자
단순히 메모리상의 주소가 아닌 두 객체가 동일한 값을 가리키는지 여부인
두 객체의 동등성은 == 연산자로 판단한다.
동일한 두 객체는 당연히 동등한 객체로 판정된다. 예를 들어 앞서 선언된 두 변수가 동일한 객체를 가리키기에 두 변수가 가리키는 객체의 동등성도 참으로 판정된다.
f1 == f2
True
그런데 아래 코드에서 생성된 두 객체는 모두 1/2을 가리키지만 서로 독립적으로 선언되었기에
저장된 메모리 주소가 다르다.
따라서 두 변수 f1와 f2는 동일하지 않은 객체를 가리킨다.
f3 = Fraction(1, 2)
f1 is f3
False
동등성 또한 거짓으로 판정된다.
f1 == f3
False
메모리상에서 두 변수가 가리키는 객체가 서로 다름을 아래 그림에서 확인할 수 있다.

동등성 지정: __eq__() 매직 메서드
f1과 f3 모두 분수로써 1/2를 가리킨다는 점을 동등성에 반영할 수 있으며,
이를 위해 __eq__() 매직 메서드를 이용한다.
아래 코드는 분수의 동등성을 구현한 __eq__() 메서드를 Fraction 클래스에 추가한다.
참고로 두 분수의 동등성은 아래와 같이 정의된다.
즉, 두 분수의 분모의 곱을 두 분수에 곱한 값이 동일하면 두 분수는 동일한 값을 가리킨다는 사실을 이용한다.
class Fraction:
def __init__(self, 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 to_float(self, digits=2):
return round(self.top_ / self.bottom_, digits)
f1과 f2를 다시 선언하자.
f1 = Fraction(1, 2)
f2 = Fraction(1, 2)
이제 동등성이 의도한대로 작동한다.
f1 == f2
True
실제로 f1 == f2를 실행하면 __eq__() 메서드가 다음과 같이 호출된다.
f1.__eq__(f2)
True
물론 여전히 두 객체는 동일하지 않다고 판단된다. 즉, 동등성과 동일성은 서로 다른 개념이다.
f1 is f2
False
19.4. 인스턴스 메서드#
매직 메서드의 특성
파이썬 클래스의 매직 메서드는 각지 고유의 역할이 있으며
역할 수행 방식이 필요에 따라 적절히 재정의될 수 있다.
예를 들어 __str___() 메서드는 임의의 객체가 사용할 수 있다.
f1.__str__()
'1/2'
[1, 2, 3].__str__()
'[1, 2, 3]'
'abc'.__str__()
'abc'
심지어 정수와 부동소수점에 대해서도 지원된다.
단 소괄호로 감싸야 함에 주의한다.
그렇지 않으면 점 기호 .가
값과 메서드를 구분하는 구분점 기호가 아닌 소수점으로 잘못 인식되기 때문이다.
(17).__str__()
'17'
(17.1).__str__()
'17.1'
사용자 정의 메서드
클래스 고유의 기능은 사용자가 직접 정의한 메서드와 속성에 의해 결정된다.
이렇듯 사용자가 직접 선언하는 메서드를
인스턴스 메서드instance methods라 부른다.
인스턴스 메서드는 클래스 본문에서 정의되는 함수이며,
첫째 매개 변수로 항상 self 가 사용된다.
예를 들어,
Fraction 클래스가 처음 정의될 때부터 포함된 to_float() 함수가 인스턴스 메서드다.
to_float() 메서드는 매직 메서드와는 달리 Fraction 클래스의
인스턴스만 사용할 수 있는 메서드다.
인스턴스 메서드 호출은 리스트, 사전 등의 메서드 호출과 동일한 방식을 따른다.
아래 코드는 f1 객체가 가리키는 값, 즉 1/2에 해당하는 분수 객체를
부동소수점으로 변환한다.
앞서 설명하였듯이 self 매개 변수에 대한 인자는 지정하지 않기에
to_float() 메서드를 인자 없이 호출한다.
f1.to_float()
0.5
매직 메서드와는 달리 to_float() 메서드는 다른 클래스의 인스턴스,
즉 다른 자료형의 값과는 함께 사용될 수 없다.
예를 들어 리스트에 대해 to_float() 메서드를 호출하면
지원되지 않는 메서드이기에 AttributeError 오류가 발생한다.
[1, 2, 3].to_float()
---------------------------------------------------------------------------
AttributeError Traceback (most recent call last)
Cell In[62], line 1
----> 1 [1, 2, 3].to_float()
AttributeError: 'list' object has no attribute 'to_float'
예제: 분모와 분자 추출 인스턴스 메서드
아래 코드의 Fraction 클래스는
생성된 인스턴스로부터 각각 분자와 분모를 추출하는 numerator() 메서드와 denominator() 메서드를
인스턴스 메서드로 포함한다.
class Fraction:
def __init__(self, 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 to_float(self, digits=2):
return round(self.top_ / self.bottom_, digits)
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
19.5. 예제#
datetime 모듈
datetime 모듈은 날짜와 시간을 다루는 클래스를 제공한다.
import datetime
datetime 모듈의 date 클래스는 날짜 정보를 (년, 월, 일) 형식으로 저장한다.
예를 들어 2025년 5월 30일을 아래와 같이 저장하면
변수 d는 언급된 날짜의 정보를 date 클래스의 객체로 저장한다.
d = datetime.date(2025, 5, 30)
d
datetime.date(2025, 5, 30)
date 객체에 저장된 년, 월, 일 정보는 각각 year, month, day 속성으로 확인된다.
print(f"변수 d에 저장된 날짜: {d.year}년 {d.month}월 {d.day}일")
변수 d에 저장된 날짜: 2025년 5월 30일
date 클래스의 today() 메서드는 호출되는 순간의 날짜를 저장한 date 객체를 생성한다.
today = datetime.date.today()
today
datetime.date(2025, 6, 18)
print(f"오늘 날짜: {today.year}년 {today.month}월 {today.day}일")
오늘 날짜: 2025년 6월 18일
참고: today() 메서드는 클래스의 인스턴스를 생성하지 않아도 사용할 수 있는 메서드다.
이런 메서드를 클래스 메서드라 부른다.
클래스 메서드와 인스턴스 메서드와의 자세한 차이점과 활용법은 아래 링크를 참고한다.
Person 클래스
아래 코드는 개인정보를 저장하는 클래스를 선언한다.
class Person:
def __init__(self, name, birthdate, address, telephone, email):
self.name_ = name
self.birthdate_ = birthdate # datetime.date 객체 사용
self.address = address
self.telephone_ = telephone
self.email_ = email
def age(self): # 나이 계산
today = datetime.date.today()
age_ = today.year - self.birthdate_.year
if today < datetime.date(today.year, self.birthdate_.month, self.birthdate_.day):
age_ -= 1
return age_
Person 클래스의 인스턴스 선언
아래와 같이 ‘김강현’이라는 사람의 개인 정보를 담은 객체를 생성하여
kgh 변수에 할당한다.
생년월일에 필요한 정보는 datetime 모듈의 date 클래스의 인스턴스를 이용한다.
kgh = Person("김강현",
datetime.date(2005, 3, 12),
"경기도 안성시 중앙로 327",
"010-1234-5678",
"kgh@python_class.com")
예제 1
kgh 객체를 생성할 때 __init__() 생성자 메서드가 실제로 호출되는 방식을 설명하라.
답:
실제로는 다음처럼 호출된다.
__init__(kgh,
"김강현",
datetime.date(2005, 3, 12),
"경기도 안성시 중앙로 327",
"010-1234-5678",
"kgh@python_class.com")
예제 2
kgh의 이메일 주소를 확인하는 코드를 작성하라.
답:
print("이메일:", kgh.email_)
이메일: kgh@python_class.com
예제 3
김강현의 나이를 확인하는 코드를 작성하라.
답:
print("나이:", kgh.age())
나이: 20
예제 4
변수의 활동 영역scope은 해당 변수의 의미가 인정되는 코드 영역을 가리킨다. 예를 들어 전역 변수는 프로그램 전체에서 의미가 있지만 함수 내부에서 선언된 지역 변수는 함수 밖에서는 의미를 갖지 않는다. 클래스 내부에서 선언된 변수 또한 자신만의 활동 영역을 갖는다.
다음 클래스 또는 변수들의 역할과 활동영역(scope)을 설명하라.
Personnameself.name_age(함수이름)age_(age함수 내부에서 선언된 변수)self.email_
답:
Person: 클래스 이름. 프로그램 전역에서 사용 가능kgh:Person클래스의 인스턴스 이름. 전역변수.name:__init__함수의 매개변수.__init__함수 본체에서만 사용되는 지역변수.self.name_: 클래스 내부에서만 사용되는 인스턴스 변수. 객체 속성으로는 전역변수로 사용될 수 있음.age(함수이름):Person클래스의 메서드 이름.Person클래스 내부에서만 사용되는 함수. 객체의 매서드로는 프로그램 전체에서 활용 가능.age_(age함수 내부에서 선언된 변수)age메서드 내부에서만 사용되는 지역변수.self.email_: 클래스 내부에서만 사용되 인스턴스 변수. 객체 속성으로는 전역변수로 사용될 수 있음.
예제 5
아래 Person 클래스는 다른 사람의 전화번호를 저장하는 기능을 포함한다.
self.phonebook_인스턴스 변수: 이름과 전화번호를 저장하는 사전 객체phoneNumberAdd()인스턴스 메서드: 상대방의 이름과 전화번호 추가
class Person:
def __init__(self, name, birthdate, address, telephone, email):
self.name_ = name
self.birthdate_ = birthdate # datetime.date 객체 사용
self.address = address
self.telephone_ = telephone
self.email_ = email
self.phonebook_ = dict()
def age(self): # 나이 계산
today = datetime.date.today()
age_ = today.year - self.birthdate_.year
if today < datetime.date(today.year, self.birthdate_.month, self.birthdate_.day):
age_ -= 1
return age_
def phoneNumberAdd(self, other):
self.phonebook_[other.name_] = other.telephone_
아래 두 코드는 새롭게 정의된 Person 클래스를 이용하여
김강현과 함중아 두 사람의 개인 정보를 담은 객체를 생성한다.
kgh = Person("김강현",
datetime.date(2005, 3, 12),
"경기도 안성시 중앙로 327",
"010-1234-5678",
"kgh@python_class.com")
hja = Person("함중아",
datetime.date(2002, 9, 21),
"경기도 안성시 중앙로 327",
"010-5678-1234",
"hja@python_class.com")
김강현이 함중아의 전환번호를 저장한 다음에 저장된 함중아의 전화번호를 확인하는 코드를 작성하라.
답:
아래 코드는 김강현이 자신의 전화번호부에 함중아의 전화번호를 저장한다.
kgh.phoneNumberAdd(hja)
다음은 저장된 함중아의 전화번호를 확인한다.
print("함중아의 전화번호:", kgh.phonebook_['함중아'])
함중아의 전화번호: 010-5678-1234
반면에 아래 코드는 함중아가 김강현의 전환번호를 저장한 다음에 저장된 번호를 확인한다. 이전 코드와는 다음 두 가지 면엣 다르다.
phoneNumber()메서드를 호출하는 주체가 다름.phonebook_인스턴스 변수에 저장된 값을 활용하는 주체도 다름
hja.phoneNumberAdd(kgh)
print("김강현의 전화번호:", hja.phonebook_['김강현'])
김강현의 전화번호: 010-1234-5678