go.od_planter
[python] Class, 객체, __init__, self, 매직메서드(특별한 메서드), 자동 호출, 연산자 오버로딩 본문
[python] Class, 객체, __init__, self, 매직메서드(특별한 메서드), 자동 호출, 연산자 오버로딩
go.od_planter 2024. 9. 5. 21:27
1. 클래스와 객체
• 클래스(Class): 객체의 청사진(템플릿)을 정의하는 것. 예를 들어, 사람을 나타내는 클래스는 이름, 나이, 성별 등의 속성을 가질 수 있어.
• 객체(Object): 클래스를 기반으로 생성된 실제 데이터. 사람이라는 클래스를 기반으로 각각의 사람 객체를 생성할 수 있어.
2. __init__ 함수
• __init__은 클래스가 초기화될 때 자동으로 호출되는 메서드로, 객체가 생성될 때 필요한 속성을 초기화하는 역할을 해.
• self는 해당 객체를 가리키는 파라미터로, 클래스 안에서 정의된 메서드가 객체 자신의 속성에 접근할 수 있도록 해줘.
class Person:
def __init__(self, name, age): # 초기화 메서드
self.name = name # 객체의 이름 속성
self.age = age # 객체의 나이 속성
def introduce(self):
print(f"안녕하세요, 저는 {self.name}이고, {self.age}살입니다.")
위 코드를 보면:
• __init__ 메서드는 name과 age라는 두 매개변수를 받아서 self.name과 self.age에 각각 할당해. 이렇게 하면 객체가 생성될 때마다 이름과 나이를 설정할 수 있어.
• introduce 메서드는 self를 사용해서 객체의 속성에 접근하고, 이름과 나이를 출력해.
객체 생성 예시:
person1 = Person("철수", 25) # Person 클래스의 객체 생성
person2 = Person("영희", 30) # 또 다른 객체 생성
person1.introduce() # "안녕하세요, 저는 철수이고, 25살입니다." 출력
person2.introduce() # "안녕하세요, 저는 영희이고, 30살입니다." 출력
핵심 개념:
• __init__: 객체가 생성될 때 실행되는 함수로, 객체의 속성을 초기화.
• self: 객체 자기 자신을 가리키며, 클래스 내에서 그 객체의 속성이나 메서드에 접근할 때 사용.
1. 초기화가 없는 클래스도 있을까?
네, 초기화가 없는 클래스도 존재할 수 있어요. __init__ 함수는 선택 사항이기 때문에 꼭 포함하지 않아도 돼요. 만약 클래스가 속성을 초기화하거나 어떤 설정을 할 필요가 없다면, __init__ 함수 없이도 클래스를 만들 수 있어요.
class SimpleClass:
def greet(self):
print("Hello, World!")
이 클래스에는 __init__ 함수가 없어요. 그럼에도 객체를 만들고 메서드를 사용할 수 있습니다:
obj = SimpleClass()
obj.greet() # 출력: "Hello, World!"
2. 초기화 함수명은 꼭 __init__이어야만 할까?
네, 초기화 함수는 반드시 __init__이어야 해요. __init__은 Python에서 객체가 생성될 때 자동으로 호출되는 특별한 메서드 이름이기 때문에, 다른 이름으로 사용할 수는 없어요.
하지만 Python에는 다른 특별한 메서드들도 많이 있어요. 예를 들어, __str__은 객체를 문자열로 표현할 때, __del__은 객체가 삭제될 때 호출되는 함수예요. 이와 같은 특별한 메서드들은 모두 정해진 이름이 있어야 자동으로 호출됩니다.
3. self 대신 다른 이름을 쓸 수 있을까?
사실 self라는 단어는 관례일 뿐, 다른 단어를 사용할 수도 있어요. Python에서 self는 객체 자신을 가리키는 인자일 뿐이고, 반드시 “self”라는 이름이어야 하는 건 아니에요. 다만, 가독성과 관습 때문에 대부분의 Python 개발자들이 self라는 이름을 사용해요.
예시:
class Person:
def __init__(this, name, age): # 'self' 대신 'this' 사용
this.name = name
this.age = age
def introduce(this):
print(f"안녕하세요, 저는 {this.name}이고, {this.age}살입니다.")
위 코드에서 self 대신 this를 사용했어요. 이 코드도 정상적으로 작동해요:
p = Person("철수", 25)
p.introduce() # 출력: "안녕하세요, 저는 철수이고, 25살입니다."
하지만, 대부분의 Python 개발자들은 self를 사용하는 것이 규칙처럼 자리 잡혀 있기 때문에, 다른 사람과 협업하거나 코드를 공유할 때는 self를 사용하는 것이 좋습니다.
결론
• 초기화가 없는 클래스도 만들 수 있다.
• **__init__**은 반드시 그 이름으로 사용해야 한다. 다른 이름으로 대체할 수 없다.
• **self**는 다른 이름으로 사용할 수 있지만, 관습적으로 self를 사용하는 것이 좋다.
주요 매직 메서드와 그 사용법
1. __init__(self, ...):
• 객체가 생성될 때 호출되어 객체의 초기 속성을 설정.
• 예시:
class MyClass:
def __init__(self, value):
self.value = value
2. __str__(self):
• print() 함수로 객체를 출력할 때, 객체를 사람이 읽기 쉬운 문자열로 반환하는 메서드.
• 예시:
class Person:
def __init__(self, name):
self.name = name
def __str__(self):
return f"Person(name={self.name})"
p = Person("철수")
print(p) # 출력: Person(name=철수)
3. __repr__(self):
• __str__과 비슷하지만, 개발자가 이해할 수 있는 문자열을 반환하는 것이 목적. 디버깅할 때 주로 사용.
• 예시:
class Person:
def __init__(self, name):
self.name = name
def __repr__(self):
return f"Person(name={self.name})"
p = Person("철수")
print(repr(p)) # 출력: Person(name=철수)
4. __len__(self):
• 객체의 길이를 반환하는 메서드로, len() 함수 호출 시 동작.
• 예시:
class CustomList:
def __init__(self, data):
self.data = data
def __len__(self):
return len(self.data)
my_list = CustomList([1, 2, 3, 4])
print(len(my_list)) # 출력: 4
5. __getitem__(self, key):
• 객체에서 인덱스를 이용해 값을 가져올 때 사용.
• 예시:
class CustomList:
def __init__(self, data):
self.data = data
def __getitem__(self, index):
return self.data[index]
my_list = CustomList([10, 20, 30])
print(my_list[1]) # 출력: 20
6. __setitem__(self, key, value):
• 객체에서 인덱스를 통해 값을 설정할 때 호출.
• 예시:
class CustomList:
def __init__(self, data):
self.data = data
def __setitem__(self, index, value):
self.data[index] = value
my_list = CustomList([10, 20, 30])
my_list[1] = 200
print(my_list.data) # 출력: [10, 200, 30]
7. __add__(self, other):
• + 연산자를 사용할 때 호출되어 두 객체를 더하는 기능을 재정의할 수 있어요.
• 예시:
class Number:
def __init__(self, value):
self.value = value
def __add__(self, other):
return Number(self.value + other.value)
def __str__(self):
return str(self.value)
num1 = Number(10)
num2 = Number(20)
result = num1 + num2
print(result) # 출력: 30
8. __eq__(self, other):
• 두 객체가 == 연산자로 비교될 때 호출되어 비교를 재정의할 수 있어요.
• 예시:
class Person:
def __init__(self, name, age):
self.name = name
self.age = age
def __eq__(self, other):
return self.name == other.name and self.age == other.age
p1 = Person("철수", 25)
p2 = Person("철수", 25)
print(p1 == p2) # 출력: True
그 외 자주 사용하는 매직 메서드들
• __del__(self): 객체가 삭제될 때 호출.
• __call__(self, ...): 객체를 함수처럼 호출할 수 있게 해줌.
• __iter__(self): 객체를 반복 가능하게 만들어주는 메서드.
• __next__(self): 반복자(iterator)의 다음 값을 반환.
매직 메서드를 활용하는 팁
1. 클래스의 기능 확장: 매직 메서드를 통해 기본 Python 연산자(+, -, ==, !=, [] 등)를 재정의하여 클래스의 기능을 확장할 수 있어요.
2. 사용자 정의 자료형 만들기: __getitem__, __setitem__ 등을 사용하여 사용자 정의 자료형(리스트나 사전과 같은)을 쉽게 만들 수 있어요.
Python의 동작 방식
Python에서는 [](대괄호)을 사용해 객체의 특정 요소에 접근할 때, 내부적으로 __getitem__() 메서드가 호출돼요. 이 과정은 Python이 제공하는 매직 메서드의 자동 호출 덕분인데, 이는 Python이 다양한 연산자나 메서드에 대해 어떻게 반응할지를 정의한 규칙 덕분이에요.
동작 원리
1. 객체에 대괄호를 사용: 예를 들어, obj[1]이라고 쓰면 Python은 해당 객체에서 1이라는 인덱스나 키에 해당하는 값을 찾으려고 시도해요.
2. __getitem__() 호출: Python은 obj[1]이라는 구문을 보면, 그 객체에 __getitem__이라는 메서드가 정의되어 있는지 확인해요. 만약 정의되어 있다면, 그 메서드를 자동으로 호출하고, 1이라는 인덱스를 __getitem__(self, key)의 key 인자로 전달해요.
3. 결과 반환: __getitem__() 메서드는 그 인덱스에 해당하는 값을 찾아서 반환해요. 그러면 obj[1]의 결과로 그 값을 얻게 되는 거죠.
예시를 통해 자세히 살펴보자
class MyList:
def __init__(self, data):
self.data = data # 리스트 형태의 데이터를 저장
def __getitem__(self, index):
# 대괄호로 호출할 때 이 함수가 작동한다
return self.data[index]
# 객체 생성
my_list = MyList([10, 20, 30])
# 대괄호로 값 가져오기
print(my_list[1]) # 출력: 20
위 코드에서는 my_list[1]을 호출했을 때 내부적으로 다음과 같은 일이 일어나:
1. my_list[1]이 호출되면 Python은 my_list 객체의 __getitem__() 메서드를 자동으로 찾는다.
2. __getitem__() 메서드에 1이라는 인덱스 값이 전달되고, 이 메서드는 self.data[1]을 반환한다.
3. 최종적으로 my_list[1]은 20이라는 값을 반환한다.
즉, []를 사용하면 Python은 그 객체가 __getitem__ 메서드를 가지고 있는지 확인하고, 있으면 그 메서드를 호출해 값을 반환하게 되는 거야.
내부 동작 과정
• Python의 연산자 오버로딩 덕분에 [] 같은 연산자를 사용할 때 Python이 자동으로 해당 매직 메서드(__getitem__, __setitem__, __delitem__ 등)를 호출해.
• obj[key] 구문은 실제로는 obj.__getitem__(key)와 동일해.
결론
• obj[1] 같은 대괄호 표기법을 사용할 때, Python은 자동으로 __getitem__ 메서드를 호출해서 인덱스나 키에 해당하는 값을 가져와.
• 이 방식은 Python이 객체지향적으로 다양한 자료형을 다룰 수 있게 해주는 중요한 기능 중 하나야.
이걸 이해하면 리스트, 딕셔너리, 그리고 사용자 정의 객체에서도 이런 동작을 쉽게 구현할 수 있어.
• **my_list[1]**은 내부적으로 **my_list.__getitem__(1)**으로 변환되어 호출돼요.
• 이는 Python이 연산자 오버로딩을 통해 [] 연산자를 처리하는 방법을 정의했기 때문에 가능한 일이에요.
• 두 표현은 동등하게 동작하지만, 대괄호([]) 표기법이 더 간결하고 직관적이기 때문에 일반적으로 많이 사용돼요.
추가
직관적으로 보면 **my_list[1]**이 내부적으로 __getitem__을 호출하는 것처럼 보이기 때문에, 한 단계를 더 거친다고 생각할 수 있어. 하지만 실제로는 두 방법의 연산 속도는 동일해.
이유는 다음과 같아:
1. Python의 최적화:
Python은 연산자([], +, - 등)와 관련된 작업을 매우 효율적으로 처리하도록 설계돼 있어요. my_list[1]과 같은 대괄호 표기법은 Python에서 매우 빈번하게 사용되기 때문에, Python 인터프리터는 이를 효율적으로 처리할 수 있도록 내부적으로 최적화되어 있어요. 즉, my_list[1]은 my_list.__getitem__(1)을 호출하는 것과 성능 차이가 없도록 최적화된 상태입니다.
2. 컴파일 단계에서 처리:
my_list[1]은 Python 바이트코드로 컴파일될 때 바로 __getitem__() 메서드를 호출하도록 바뀌어요. Python이 실행하는 바이트코드를 보면, 두 방법 모두 결국 __getitem__ 호출로 변환됩니다. 그래서 이 차이는 실제 실행 속도에 영향을 미치지 않아요.
실제 바이트코드 확인
Python 바이트코드를 확인하면, my_list[1]과 my_list.__getitem__(1) 모두 동일한 방식으로 처리된다는 걸 알 수 있어요. 바이트코드는 Python이 코드를 실행할 때 사용하는 중간 단계 코드로, 이를 통해 Python이 두 방법을 동일하게 처리한다는 것을 확인할 수 있어요.
예시
import dis
class MyList:
def __init__(self, data):
self.data = data
def __getitem__(self, index):
return self.data[index]
my_list = MyList([10, 20, 30])
# 대괄호를 사용할 때의 바이트코드
print("my_list[1] 바이트코드")
dis.dis('my_list[1]')
# __getitem__을 직접 호출할 때의 바이트코드
print("\nmy_list.__getitem__(1) 바이트코드")
dis.dis('my_list.__getitem__(1)')
이 코드를 실행해 보면, 두 방법 모두 동일하게 __getitem__() 메서드를 호출하는 바이트코드로 변환됩니다.
결론
• **my_list[1]**과 **my_list.__getitem__(1)**은 Python에서 동일한 방식으로 처리되기 때문에, 연산 속도는 차이가 없다.
• Python은 이러한 연산자 오버로딩을 효율적으로 처리하기 때문에, 대괄호([]) 표기법을 사용하는 것이 직관적이고 편리하면서도 성능에 영향을 미치지 않아요.
1. dis.dis: Python 바이트코드 디스어셈블러
dis.dis는 디스어셈블러로, Python 코드가 실행되기 전에 컴파일되는 바이트코드를 보여주는 도구예요. Python 코드는 먼저 바이트코드로 컴파일된 후, 이 바이트코드를 Python 인터프리터가 실행하게 돼요. **dis.dis()**를 사용하면, Python 코드가 실제로 어떻게 바이트코드로 변환되는지 확인할 수 있어요.
2. 바이트코드란?
**바이트코드(bytecode)**는 Python 인터프리터가 실행하는 중간 코드 형태예요. 우리가 작성한 Python 소스 코드는 먼저 바이트코드로 변환된 후, 이 바이트코드를 Python 가상 머신(PVM)이 실행해요. 바이트코드는 사람에게는 읽기 어려운 형식이지만, Python 인터프리터가 실행하기에는 더 빠르고 효율적인 형태예요.
3. 두 바이트코드가 다른 이유
두 가지 코드를 보면 바이트코드가 약간 다르게 나오는 걸 알 수 있어요. 하지만 두 코드 모두 결과적으로는 동일한 동작을 수행해요. 그 차이는 대괄호 표기법([])과 메서드 호출(.__getitem__()) 방식의 차이에서 비롯됩니다.
첫 번째 바이트코드: my_list[1]
0 0 RESUME 0
1 2 LOAD_NAME 0 (my_list)
4 LOAD_CONST 0 (1)
6 BINARY_SUBSCR
10 RETURN_VALUE
• LOAD_NAME 0 (my_list): my_list라는 이름을 로드합니다.
• LOAD_CONST 0 (1): 인덱스 1을 로드합니다.
• BINARY_SUBSCR: 대괄호([]) 표기법을 이용한 구독 연산자로, 이것은 자동으로 __getitem__()을 호출합니다.
• RETURN_VALUE: 결과를 반환합니다.
BINARY_SUBSCR는 [] 연산자를 사용했을 때 Python이 내부적으로 __getitem__ 메서드를 호출하는 연산이에요.
두 번째 바이트코드: my_list.__getitem__(1
0 0 RESUME 0
1 2 LOAD_NAME 0 (my_list)
4 LOAD_ATTR 3 (NULL|self + __getitem__)
24 LOAD_CONST 0 (1)
26 CALL 1
34 RETURN_VALUE
• LOAD_NAME 0 (my_list): my_list라는 이름을 로드합니다.
• LOAD_ATTR 3 (NULL|self + __getitem__): my_list 객체의 __getitem__ 메서드를 로드합니다.
• LOAD_CONST 0 (1): 인덱스 1을 로드합니다.
• CALL 1: 로드된 __getitem__ 메서드를 호출합니다.
• RETURN_VALUE: 결과를 반환합니다.
여기서 **LOAD_ATTR**은 객체의 속성, 즉 __getitem__ 메서드를 가져오고, **CALL**은 그 메서드를 실행하는 동작을 나타냅니다.
4. 왜 두 바이트코드가 다른가?
• **my_list[1]**은 연산자([]) 오버로딩에 의해 동작하며, Python은 이를 최적화된 방식으로 처리하기 위해 BINARY_SUBSCR라는 바이트코드를 사용해요. 이 바이트코드는 바로 __getitem__을 호출하는 것이 아닌, 최적화된 구독 연산자로 동작합니다.
• 반면 **my_list.__getitem__(1)**은 메서드를 명시적으로 호출하는 방식이므로, LOAD_ATTR와 CALL이라는 두 개의 바이트코드를 통해 먼저 메서드를 가져온 다음에 호출하는 방식입니다.
5. 속도 차이가 있을까?
비록 바이트코드가 다르게 보이긴 하지만, 실질적으로 속도 차이는 거의 없다고 볼 수 있어요. Python 인터프리터가 [] 연산자를 사용하는 경우 매우 최적화된 방식으로 동작하므로, __getitem__()을 명시적으로 호출하는 것과 성능 차이가 거의 없어요.
그러나 이론적으로 보면 my_list[1]이 더 빠를 수는 있어요, 왜냐하면:
• BINARY_SUBSCR는 직접적으로 구독 연산을 처리하는 반면,
• my_list.__getitem__(1)은 LOAD_ATTR과 CALL을 통해 한 단계 더 거쳐야 하기 때문입니다.
하지만 이 차이는 매우 미세하기 때문에, 실제 성능 차이는 눈에 띄지 않을 가능성이 커요.
결론
• my_list[1]과 my_list.__getitem__(1)의 바이트코드는 다르게 보이지만, 결과는 동일합니다.
• [] 연산자는 Python에서 최적화된 구독 연산자로 처리되어 바이트코드에서 BINARY_SUBSCR를 사용합니다.
• 실질적인 성능 차이는 거의 없으며, 일반적인 상황에서는 []를 사용하는 것이 더 간결하고 직관적입니다.
'Language > Python' 카테고리의 다른 글
[python] 환경설정 (.env) 완전 이해하기: os.getenv, load_dotenv, kwargs 정리 (0) | 2025.03.27 |
---|---|
python의 시작(CPython)과 python으로 만든 python이란? (0) | 2024.08.23 |
Python 인터프리터 / 구현체, 인터프리터 (0) | 2024.08.23 |