파이썬 강좌 – 특별 메소드와 연산자 ~ 파이썬의 내부 동작 이해하기

  1. 프롤로그
  2. 개발 첫걸음
    1. 컴퓨터 구성요소 – 컴퓨터는 어떤 걸 할 수 있나?
    2. 개발과 관련된 용어
    3. 파이썬의 선택 – 왜 파이썬인가?
    4. 파이썬 설치 – Hello World 출력하기
    5. Visual Studio Code 의 편리한 기능
    6. REPL과 콘솔 창 – 파이썬 동작시키기
  3. 파이썬 기초
    1. 기초 입출력 – 소통하기
    2. 변수와 대입 – 기억하기
    3. 연산자 – 계산하기
    4. 조건문 – 분기를 만들기
    5. 반복문 – 비슷한 작업을 반복하기
    6. 반복문 코딩하기
    7. 변수와 리스트 – 비슷한 변수들을 묶기
    8. for, range – 리스트의 항목을 다루기
    9. 함수와 메소드의 호출 – 편리한 기능 이용하기
    10. 모듈 설치와 사용 – 유용한 기능 끌어다 쓰기
    11. 문자열 – 텍스트 다루기
  4. 파이썬 중급
    1. 함수를 직접 만들기 – 자주 쓰는 기능을 묶기
    2. 딕셔너리, 튜플, 세트 – 변수를 다양한 방법으로 묶기
    3. 클래스와 객체 – 변수를 사람으로 진화시키기
    1. 상속 – 클래스를 확장하기
    2. 정체성과 동질성 – 객체의 성질
    3. 특별 메소드와 연산자 – 파이썬의 내부 작동방식 이해하기
    4. 다양한 함수 인수 – 유연한 함수 만들기
    5. 슬라이싱 – 리스트 간편하게 접근하기
    6. 지능형 리스트(List Comprehension) – 리스트 갖고 놀기
    7. namedtuple - 데이터 묶음 손쉽게 만들기
    8. 조건 표현식 (Conditional Expression) - 간단한 분기 나타내기
    9. 코드 스타일 - 코드의 일관성 유지하기
    10. 명령문, 표현식 – 문법을 이루는 것들
    11. 본격적인 검색 해보기
  5. 파이썬 고급
    1. 일급 함수 다루기
    2. NotImplementedError와 NotImplemented
    3. 어노테이션 – 수월하게 프로그래밍하기
    1. 내장 함수 톺아보기
    2. 예외와 에러 – 예상치 못한 상황에 대응하기 (v0.1)
    3. 변수의 범위 – 이름 검색의 범위
  6. 파이썬 심화
    1. 시퀀스와 반복자 – 반복과 순회를 자유자재로 다루기
    2. 데코레이터 – 함수의 기능을 강화하기
    3. 프로퍼티
    4. 제너레이터
    5. async와 await
    6. 객체로서의 클래스 – 클래스를 동적으로 정의하기
  7. 파이썬 프로젝트 실습
    1. 원카드 게임 만들기 (1)
    2. 원카드 게임 만들기 (2)
    3. 원카드 게임 만들기 (3) (작성중)
    4. 턴제 자동 전투 게임 만들기 (작성중)
  8. 실전 (파이썬 외적인 것들)
    1. 정규표현식 – 문자열을 검색하고 치환하기 (작성중)
    2. 유니코드 – 컴퓨터에서 문자를 표기하는 방법
    3. html, css, 인터넷 – 자동화 첫 걸음 내딛기
    4. 네트워크 – 인터넷으로 통신하는 방법
    5. 문서 – 문맥을 읽어보기

몬스터를 예쁘게 출력하기

우리는 클래스를 통해 객체가 어떤 속성과 메소드를 가지고 있을지 정의내릴 수 있습니다. 하지만 지금까지의 방법으론 우리가 만든 클래스는 썩 그렇게 편하지는 않습니다. 어떤 객체에게 변화를 주거나 값에 접근하기 위해서는 속성과 메소드를 반드시 거쳐야 하기 때문입니다.

이를테면, 간단히 이름을 속성으로만 가지고 있는 Monster 클래스를 상상해보세요. 다음과 같이 구현하실 수 있을 것입니다.

class Monster:
    def __init__(self, name):
        self.name = name

어느 순간, Monster가 어떤 객체인지 알아보고 싶어서 print를 이용해보자구요.

class Monster:
    def __init__(self, name):
        self.name = name

mon1 = Monster('골렘')
print(mon1)
<__main__.Monster object at 0x000001B0DDCFA760>

아이고, 이상한 말들이 등장했습니다. __main__ 실행 환경의 Monster 객체이며, 이 객체가 메모리의 0x000001B0DDCFA760라는 주소에서 거주하고 있다는 것을 알 수 있군요. 하지만 이 정보들은 그렇게 유용하지 않습니다. 뭐 Monster 객체라는 것은 알 수 있으니 좋지만, __main__이나 메모리 정보를 굳이 알아야 할까요. 다른 케이스를 한번 생각해봅시다. 우리가 리스트를 만들어서 출력하고자 할 때 안의 내용물이 전부 출력되지, list 객체이며 메모리가 어딘가에 있다는 정보는 출력하지 않습니다. 어쩔 수 없는 걸까요? 일단 printInformation라는 메소드를 만들어 이용해봅시다.

class Monster:
    def __init__(self, name):
        self.name = name
    
    def printInformation(self):
        print(f"Monster(name='{self.name}')")

mon1 = Monster('골렘')
mon1.printInformation()
Monster(name='골렘')

유용한 정보만 딱 골라내어 출력하는 메소드를 구현해보았습니다. 하지만 이러한 방법은 한 가지 문제점을 안고 있습니다. 유용한 정보를 출력해보는 로직은 흔하게 사용되지만 구현하는 방법은 클래스마다 제각각일 수 있습니다. 예를 들어 지금은 printInformation이라는 이름을 지었지만, 어떤 클래스에는 print라고 간단하게 이름을 지을 수도 있고, 어떤 곳에는 show라고 완전히 다른 단어로 메소드를 구현할 수 있습니다. 그래서, 클래스 설계자는 이러한 부분에 대해 확실한 설명을 적어두어야 할 의무가 생기며, 마찬가지로 클래스를 이용하는 사람들에게도 그러한 설명을 찾아봐야 하는 수고가 듭니다. 한두 개의 클래스만 다룬다면 상관이 없겠지만, 클래스가 점점 많아진다면 쉴 새 없이 밀려드는 메소드와 속성에 진절머리가 나게 될 것입니다!

잠깐, 이전 시간에 list에 관해 배웠을 때를 떠올리세요. 여기서 다음 개념이 기억나시나요?

중요한 점은, 우리가 리스트를 예쁘게 보여주는 법이 어떤 식으로 구현되어 있는지도 모르고, 심지어 어떻게 쓰는지도 알 필요가 없다는 점입니다. 이 비기를 이용하는 주체는 손가락으로 열심히 영단어를 타이핑치는 우리가 아니라 바로 print입니다. print가 알아서 A부터 Z까지 다 해주므로 우리는 print를 쓰는 방법만 제대로 익히면 됩니다.

요약하면, 리스트는 예쁘게 보여주는 법을 구현해놓고 있고, 기본 내장 함수인 print는 그것을 갖다 쓴다고 했습니다. 정답은 나온 것 같네요. 우리가 만든 Monster 클래스도, print가 쓸 수 있게 예쁘게 보여주는 법을 구현하면 됩니다!

class Monster:
    def __init__(self, name):
        self.name = name
    
    def __repr__(self):
        return f"Monster(name='{self.name}')"

mon1 = Monster('골렘')
print(mon1)
Monster(name='골렘')

__repr__이라는 새로운 특별 메소드를 정의했습니다. 언더바 두 개가 앞뒤로 있는 메소드는 예전 시간에 언급했듯이 특별 메소드 입니다. 이 메소드 또한 생성자처럼 파이썬에서 특별하게 취급한다는 이야기입니다. repr는 representation의 약자입니다. 이 메소드는 이 객체를 설명하는 공식적인(official) 문자열을 반환해야 하는 임무를 가지고 있습니다.

__repr__ 메소드를 구현했다는 뜻은, 이 객체를 예쁘게 보여주는 방법을 어느정도 구현했다는 뜻입니다. printmon1의 타입을 검사하여 Monster인지 알아내고, __repr__ 등의 특별 메소드를 구현해 놓았는지 체크합니다. 만약 발견했다면, print는 머리를 탁 치며 그 메소드를 이용하여 print를 실행합니다.

graph TD mon1["print(mon1)"]-->check["mon1의 타입을 체크한다<br> ... Monster이다!"] check-->b1{"Monster에<br>__str__ 메소드가<br>구현되어 있는가?"} b1-->|yes|str["__str__ 메소드를 호출하여 그 반환값을 출력한다."] b1-->|no|hasprint{"Monster에<br>__repr__ 메소드가<br>구현되어 있는가?"} hasprint-->|no|en["가장 기본적인 형태로 출력한다.<br>(__main__.Monster object 후략)"] hasprint-->|Yes|print["__repr__ 메소드를<br>호출하여 그 반환값을<br>출력한다"] class hasprint,print em

print의 여행

이로써 아까 언급했던, 출력 메소드를 임의로 정의하여 사용하던 때의 단점이 완전히 커버됩니다. __repr__를 구현함으로써 우리는 파이썬의 int, list, set와 같은 내장형과 print의 끈끈한 유대관계를 흉내낼 수 있습니다. 우리는 클래스 사용자들이 더 쉽게 출력하도록 하는 방법을 일절 고민할 필요없이 __repr__ 메소드만 구현하면 됩니다. 사용하는 사람 입장에서도 별 다른 고민없이 print를 이용하면 됩니다.


흉내내기

리스트 흉내내기

이제, 몬스터가 서식하는 땅을 클래스로서 만들어보려고 합니다. 이 클래스는 Field라는 이름을 지니고 있고, _monsters 속성을 가지고 있습니다. 이 속성이 진짜 리스트이며, 여기에는 현재 서식중인 몬스터들이 저장됩니다.

class Monster:
    def __init__(self, name):
        self.name = name
    
    def __repr__(self):
        return f"Monster(name='{self.name}')"

class Field:
    def __init__(self):
        self._monsters = []

    def append(self, monster):
        self._monsters.append(monster)

    def __getitem__(self, position):
        return self._monsters[position]

field = Field()

for name in ['오크', '들개', '오우거', '골렘', '피카츄', '박쥐']:
    field.append(Monster(name))

# []로 항목 접근
print(field[3])

# 슬라이싱
print(field[2:5])

# for로 항목 접근
for monster in field:
    print(f"필드에서 {monster.name}이(가) 발견되었다!")
Monster(name='골렘')
[Monster(name='오우거'), Monster(name='골렘'), Monster(name='피카츄')]
필드에서 오크이(가) 발견되었다!
필드에서 들개이(가) 발견되었다!
필드에서 오우거이(가) 발견되었다!
필드에서 골렘이(가) 발견되었다!
필드에서 피카츄이(가) 발견되었다!
필드에서 박쥐이(가) 발견되었다!

Field 클래스에서는 3개의 메소드를 정의했습니다. 생성자인 __init__, 몬스터를 추가시켜주기 위해 만든 append, 그리고 파이썬에서 리스트처럼 동작할 수 있게 해주는 __getitem__ 입니다.

__getitem__ 메소드를 추가시켜준 것 만으로도 []를 통해 항목을 가져오거나 슬라이싱을 할 수 있게 되었습니다. 뿐만 아니라 for에서도 아주 쉽게 사용할 수 있게 되었습니다! 정말 근사하지 않나요?

만약 Field 클래스에 __len__ 메소드를 추가한다면 이 클래스는 표준 파이썬 시퀀스처럼 작동하기 때문에 랜덤으로 아이템을 뽑아오는 random.choice, 역순 기능인 reserved 등의 함수도 이용할 수 있게 됩니다.

import random

class Monster:
    def __init__(self, name):
        self.name = name
    
    def __repr__(self):
        return f"Monster(name='{self.name}')"

class Field:
    def __init__(self):
        self._monsters = []

    def append(self, monster):
        self._monsters.append(monster)

    def __getitem__(self, position):
        return self._monsters[position]

    def __len__(self):
        return len(self._monsters)

field = Field()

for name in ['오크', '들개', '오우거', '골렘', '피카츄', '박쥐']:
    field.append(Monster(name))

print(f"랜덤 뽑기! >> {random.choice(field)}")

print(list(reversed(field)))
랜덤 뽑기! >> Monster(name='박쥐')
[Monster(name='박쥐'), Monster(name='피카츄'), Monster(name='골렘'), Monster(name='오우거'), Monster(name='들개'), Monster(name='오크')]

수치형 흉내내기

완전히 새로운 예제로 넘어갑시다. 어떤 슬라임이 있습니다. 이 슬라임은 다른 슬라임과 합쳐질 때마다 사이즈가 10% 증가합니다. 그러니까, 크기가 60이고 40인 두 슬라임이 합쳐지면 100이 되고, 거기에 10% 증가하여 110이 됩니다. 이러한 특성을 충족시키기 위해 다음과 같은 클래스를 만들어보았습니다.

class Slime:
    def __init__(self, size):
        self.size = float(size)
    
    def __repr__(self):
        return f"Slime(size={self.size})"

    def combineWith(self, other):
        size = self.size + other.size
        size += size / 10
        return Slime(size)
    
blue = Slime(60)
red = Slime(40)
pink = blue.combineWith(red)

print(pink)
Slime(size=110.0)

Slime 클래스에는 size 속성 하나가 있습니다. 그리고 combineWith 메소드를 두어 두 슬라임을 합치는 기능을 넣었습니다. 사이즈 6040이 합쳐 110이 나옴을 확인해주세요. 우리의 의도대로 잘 작동하고 있습니다.


여기서 두 가지만 바꿔봅시다. combineWith 메소드의 이름을 __add__로 수정하고, combineWith 메소드 호출 부분을 그냥 + 연산자로 해봅시다.

class Slime:
    def __init__(self, size):
        self.size = float(size)
    
    def __repr__(self):
        return f"Slime(size={self.size})"

    def __add__(self, other):
        size = self.size + other.size
        size += size / 10
        return Slime(size)
    

blue = Slime(60)
red = Slime(40)
pink = blue + red

print(pink)
Slime(size=110.0)

헉! +는 기본형에만 동작하는 것이 아닌가요? intfloat은 값이 더해지고, str은 문자열이 이어지는 연산자였습니다만, 이제 Slime 객체에도 동작하게 되었습니다! 이렇듯 +와 같은 연산자의 본래 기능을 확장하거나 재정의하는 개념을 일컬어 연산자 오버로딩(operator overloading) 이라고 합니다.


다른 것들 흉내내기

우리가 만든 클래스를 파이썬의 로직에 녹일 수 있는 특별 메소드는 종류가 굉장히 많습니다. 공식 문서에서 필요한 것을 뽑아 쓸 수 있도록 합니다.


연산자 오버로딩(operator overloading)

파이썬에서의 연산자 오버로딩은 각 연산자에 대응되는 특별 메소드를 구현함으로써 실현할 수 있습니다. 앞서 살펴보았듯 + 연산자에는 __add__ 메소드를 구현해주면 됩니다. *, -, / 등의 연산자도 물론 오버로딩할 수 있습니다. 우선 기초적인 연산자 오버로딩의 특징에 대해 알아봅시다.

  • 내장 자료형끼리 작동하는 연산자에 대해서는 우리가 새롭게 기능을 정의해줄 수 없습니다. 즉 3 + 8이나 'abc' + 'def'에 대해서 미리 정의된 기능을 우리가 수정할 수 없다는 뜻입니다.
  • <=> 같은 새로운 연산자를 생성할 수 없으며, 기존에 사용되는 연산자만 오버로딩할 수 있습니다. 오버로딩할 수 있는 연산자는 다음과 같습니다.
    • 우리가 흔히 사용하는 +, -, *, /, **, //, %
    • @, &, |, >>, <<, ^, ~ 등 자주는 사용되지 않는 연산자
    • 복합 대입 연산자
    • ==, =!, <, >, <=, >= 등의 비교 연산자
  • 다음 연산자는 오버로딩이 불가능한 연산자입니다 : is, and, or, not, =

각 연산자에 대한 특별 메소드의 이름은 공식 문서를 참조해주세요.

그리고 다음은 이번 시간에 더 자세히 알아볼 내용입니다.

  • 중위 연산자는 왼쪽 요소에 대해서 관련 특별 메소드를 검사하며, 만약 실패하면 오른쪽 요소에 대해서 역순 특별 메소드를 검사합니다.
  • 구현되지 않을 부분에 대해서는 NotImplemented 값을 리턴하여 의도를 명백히 합니다.
  • 연산자 오버로딩은 적재적소에 쓰여야 합니다.

중위 연산자가 작동하는 방식

중위 연산자란, 연산자의 양쪽에 값이 오는 형태를 뜻합니다. +, - 등 거의 모든 형태의 연산자가 이러한 형태를 취하고 있지요. +가 실제로 파이썬에서는 다음과 같은 순서로 작동합니다.

graph TD l1("a + b") --> add1{"a에 <br> __add__가<br> 있는가?"} add1-->|"있다"| add1do["a.__add__(b)<br>를 실행하여<br>결과 값을<br> 구한다."] add1do-->impl1["결과 값이<br>NotImplemented<br>인가?"] impl1-->|"그렇다"| add2 impl1-->|"아니다"| result["결과 값을<br>최종적으로<br>반환한다."] add1-->|"없다"| add2{"b에 <br> __radd__가<br> 있는가?"} add2-->|"있다"| add2do["b.__radd__(a)<br>를 실행하여<br>결과 값을<br> 구한다."] add2do-->impl2["결과 값이<br>NotImplemented<br>인가?"] impl2-->|"그렇다"| error impl2-->|"아니다"| result["결과 값을<br>최종적으로<br>반환한다."] add2-->|"없다"|error["TypeError<br>를 발생시킨다."]

a + b 가 작동하는 방식

여기서 NotImplemented는 아직 구현되지 않았다는 뜻을 담은 특별한 값이자 이름이며, 잠시 뒤에 다시 설명합니다.

역순 특별 메소드는 오버로딩하고자 하는 메소드의 이름 제일 앞에 r이 들어가 있습니다. 여기서 r은 뜻을 딱히 엄격하게 두지는 않았지만 reserved, reflected, right 등의 뜻으로 해석할 수 있습니다. 즉 연산자의 오른쪽 값이 이용해봄직한 메소드라는 뜻이지요.

그렇다면 왜 이렇게 두 가지 버전을 만들어놓았을까요? 우리가 만든 클래스와 내장형 값을 자연스럽게 조화시키기 위함입니다. 아래 예제를 보면서 이야기를 이어가도록 하겠습니다.


역순 메소드가 존재하는 이유

다음 예제를 참고해주세요. Cup 클래스는 현재 가지고 있는 물의 양을 나타내는 water 속성을 가지고 있습니다. 그리고 이 클래스로 만든 객체와 숫자를 +로 더하면 그만큼 water를 증가시켜 보고자 합니다.

class Cup:
    def __init__(self, water):
        self.water = water
    
    def __repr__(self):
        return f"Cup(water='{self.water}')"

    def __add__(self, other):
        return Cup(self.water + other)

basic = Cup(30)
advanced = basic + 40
print(advanced)
Cup(water='70')

잘 작동합니다. 하지만 basic40의 순서를 바꾸면 어떻게 될까요?

class Cup:
    def __init__(self, water):
        self.water = water
    
    def __repr__(self):
        return f"Cup(water='{self.water}')"

    def __add__(self, other):
        return Cup(self.water + other)

basic = Cup(30)
advanced = 40 + basic
print(advanced)
Traceback (most recent call last):
  File "c:/Users/tooth/Desktop/test2.py", line 12, in <module>
    advanced = 40 + basic
TypeError: unsupported operand type(s) for +: 'int' and 'Cup'

하하.. 에러가 발생했습니다. 우리가 Cup에게 __add__ 메소드를 구현했지만 파이썬 인터프리터는 int에게서 Cup과 상호작용할 수 있는 __add__ 메소드가 존재하는지를 먼저 검사합니다. 하지만 없습니다. 이럴 때의 해결 방법은 Cup 클래스에서 __radd__ 메소드를 구현하는 것입니다!

class Cup:
    def __init__(self, water):
        self.water = water
    
    def __repr__(self):
        return f"Cup(water='{self.water}')"

    def __add__(self, other):
        return Cup(self.water + other)

    def __radd__(self, other):
        return self + other

basic = Cup(30)
advanced = 40 + basic
print(advanced)
Cup(water='70')

훌륭하게 작동하게 되었습니다! __radd__ 에서는 단순히 self + other 이라고 적은 것에 주목해주세요. 이렇게 하면 자연스레 Cup__add__를 사용하게 됩니다.

비교 연산자는 이러한 중위 연산자가 작동하는 방식과 살짝 다릅니다. 궁금하시다면, 이는 공식 문서에 설명을 넘기도록 하겠습니다.


구현되지 않는 부분 명백히 하기

앞서 __add__가 어떻게 작동하는지에 대한 순서도를 볼 때 NotImplemented 라는 값이 등장했습니다. 이것은 None, True, False와 같이 키워드이고, 구현되지 않았음을 알리는 용도입니다.

Cup 예제를 그대로 갖고 와보겠습니다. 달라진 점은 basic40을 더하는 게 아니라 뜬금없는 'abc'를 더하고 있습니다. 이는 의도된 행동이 아니므로 에러가 일어나야 하겠지요.

class Cup:
    def __init__(self, water):
        self.water = water
    
    def __repr__(self):
        return f"Cup(water='{self.water}')"

    def __add__(self, other):
        return Cup(self.water + other)

    def __radd__(self, other):
        return self + other

basic = Cup(30)
advanced = basic + 'abc'
print(advanced)
Traceback (most recent call last):
  File "c:/Users/tooth/Desktop/test2.py", line 15, in <module>
    advanced = basic + 'abc'
  File "c:/Users/tooth/Desktop/test2.py", line 9, in __add__
    return Cup(self.water + other)
TypeError: unsupported operand type(s) for +: 'int' and 'str'

이 에러 메세지를 접하고 Cup에 무언가를 더하려면 int와 호환되는 어떤 Cup을 이용하는 사람 입장에서는 다소 곤혹스러울 수 있겠습니다.

class Cup:
    def __init__(self, water):
        self.water = water
    
    def __repr__(self):
        return f"Cup(water='{self.water}')"

    def __add__(self, other):
        try:
            return Cup(self.water + other)
        except TypeError:
            return NotImplemented

    def __radd__(self, other):
        return self + other

basic = Cup(30)
advanced = basic + 'abc'
print(advanced)
Traceback (most recent call last):
  File "c:/Users/tooth/Desktop/test2.py", line 18, in <module>
    advanced = basic + 'abc'
TypeError: unsupported operand type(s) for +: 'Cup' and 'str'

연산자 오버로딩은 적재적소에 쓰여야 합니다.

연산자 오버로딩은 여러모로 양날의 검입니다. 한 눈에 보기 쉬워서 편리해질 수도 있지만, 남용하게 된다면 있느니만 못하는 기능이 되어버릴 수도 있지요. 아래는 전문가를 위한 파이썬의 글을 인용한 것입니다.

연산자 오버로딩을 혐오하는 사람도 많다. 이 언어 기능은 남용되거나, 프로그래머를 혼란스럽게 만들거나, 버그를 만들거나, 예상치 못한 성능상의 병목이 될 수도 있다(지금까지 그래왔다). 그렇지만 잘 사용하면 코드의 가독성이 향상되고 만족스러운 API를 구현할 수 있다. 파이썬은 다음과 같은 제한을 두어 융통성, 사용성, 안전성을 적절히 유지한다.

(중략)

개발자 중 20~30퍼센트는 연산자 오버로딩을 악마의 자식이라고 생각하는 것 같다. 예를 들어 자신이 + 연산자를 오버로딩해서 사용하려는 용도와 다르게, 다른 개발자가 리스트 삽입 연산을 구현하도록 이미 + 연산자를 오버로딩했다면 정말 짜증난다. 오버로딩할 수 있는 연산자는 6개 정도 밖에 안 되지만, 이 연산자를 사람들이 수백만 가지 방식으로 사용하고 싶어 하기 때문에 이러한 문제가 발생한다. 따라서 여러분의 직관에 따라 연산자를 선택할 수 없는 경우가 종종 있다.

우리는 Slime 객체에 대해 연산자 오버로딩을 구현했습니다. 하지만 이는 썩 좋지 못한 생각일 수 있습니다. Slime은 수치형도 아니고 어떠한 상태를 나타낸다는 용도의 클래스도 아니기 때문에 +가 직관적으로 어떠한 역할을 하는지 알기 어렵습니다. 차라리 처음에 시도했던 것처럼 combineWith 메소드로 기능을 구현하는 것이 의도를 명백히 보여주기 때문에 훨씬 나을 수 있습니다.

연산자 오버로딩에서 흔히 사용되는 예시 중 하나는 수학적인 개념의 벡터(vector)입니다. 벡터는 수치형으로 이야기할 수 있고, 차원 만큼의 속성을 가지고 있어야 하기 때문에 클래스로 표현하기 용이하며, 무엇보다 +, * 등의 의미가 명확합니다.


특별 메소드를 직접 호출하지 마세요

특별 메소드는 파이썬 인터프리터가 적절하게 이용하기 위한 메소드입니다. 우리는 우리가 만든 클래스가 파이썬 내부에서 잘 사용되기를 바라기 때문에 그러한 특별 메소드를 정의합니다. 만약 파이썬 인터프리터가 아닌 단순한 사용자가 사용하기를 기대하는 기능을 만든다면, 일반 메소드를 정의하면 됩니다. 특별 메소드의 기능을 사용하고 싶다면 특별 메소드를 직접 호출하는 것이 아니라, 그 로직을 이용하는 파이썬 문법을 사용하면 됩니다. __getitem__ 메소드를 직접 호출하기보다, []를 통해 항목에 접근하세요. 아래 표는 똑같은 말입니다.

~~ 한다면 ~~ 하세요
파이썬의 로직에 우리가 만든 클래스를 녹이고 싶다면 특별 메소드를 정의하세요
사용자 코드에서 사용될 기능을 만든다면 일반 메소드를 정의하세요
특별 메소드의 기능을 직접 사용하고 싶다면 특별 메소드를 직접 호출하지 말고 파이썬의 로직을 이용하세요

단, 사용자 코드에서 특별 메소드를 호출하는 경우가 한 가지 있는데, 바로 __init__ 메소드 안에서 상위 클래스의 __init__을 호출할 때입니다.


연습 문제

  • 연산자 오버로딩의 뜻은 무엇인가?
  • 우리가 만든 클래스를 예쁘게 보여주려면 어떤 특별 메소드를 구현해야 하는가?
  • 우리가 만든 클래스가 리스트처럼 기능하기 위하여 어떤 특별 메소드를 구현해야 하는가?
  • 파이썬 인터프리터가 a + b 를 맞닥뜨렸을 때 어떻게 동작하는지 설명하라.
  • 내장 함수인 abs의 기능을 커스터마이징할 수 있는 특별 메소드의 이름을 검색하여 찾아보라.

프로그래밍 문제

  1. SpecialStr 클래스를 만드세요. 이 클래스의 목적은 + 연산시 수치형 및 문자형의 포괄적인 호환성 구현입니다. 클래스는 문자열 속성 하나를 가지고 있으며, 생성자의 인수 하나를 받아서 문자열 속성을 초기화시킵니다. 다음 코드를 실행시켰을 때 잘 출력되어야 합니다.
sp = SpecialStr(‘abc’)
sp += ‘def’
sp = 56 + sp
sp += 3.14
print(sp)
56abcdef3.14

  1. 프롤로그
  2. 개발 첫걸음
    1. 컴퓨터 구성요소 – 컴퓨터는 어떤 걸 할 수 있나?
    2. 개발과 관련된 용어
    3. 파이썬의 선택 – 왜 파이썬인가?
    4. 파이썬 설치 – Hello World 출력하기
    5. Visual Studio Code 의 편리한 기능
    6. REPL과 콘솔 창 – 파이썬 동작시키기
  3. 파이썬 기초
    1. 기초 입출력 – 소통하기
    2. 변수와 대입 – 기억하기
    3. 연산자 – 계산하기
    4. 조건문 – 분기를 만들기
    5. 반복문 – 비슷한 작업을 반복하기
    6. 반복문 코딩하기
    7. 변수와 리스트 – 비슷한 변수들을 묶기
    8. for, range – 리스트의 항목을 다루기
    9. 함수와 메소드의 호출 – 편리한 기능 이용하기
    10. 모듈 설치와 사용 – 유용한 기능 끌어다 쓰기
    11. 문자열 – 텍스트 다루기
  4. 파이썬 중급
    1. 함수를 직접 만들기 – 자주 쓰는 기능을 묶기
    2. 딕셔너리, 튜플, 세트 – 변수를 다양한 방법으로 묶기
    3. 클래스와 객체 – 변수를 사람으로 진화시키기
    1. 상속 – 클래스를 확장하기
    2. 정체성과 동질성 – 객체의 성질
    3. 특별 메소드와 연산자 – 파이썬의 내부 작동방식 이해하기
    4. 다양한 함수 인수 – 유연한 함수 만들기
    5. 슬라이싱 – 리스트 간편하게 접근하기
    6. 지능형 리스트(List Comprehension) – 리스트 갖고 놀기
    7. namedtuple - 데이터 묶음 손쉽게 만들기
    8. 조건 표현식 (Conditional Expression) - 간단한 분기 나타내기
    9. 코드 스타일 - 코드의 일관성 유지하기
    10. 명령문, 표현식 – 문법을 이루는 것들
    11. 본격적인 검색 해보기
  5. 파이썬 고급
    1. 일급 함수 다루기
    2. NotImplementedError와 NotImplemented
    3. 어노테이션 – 수월하게 프로그래밍하기
    1. 내장 함수 톺아보기
    2. 예외와 에러 – 예상치 못한 상황에 대응하기 (v0.1)
    3. 변수의 범위 – 이름 검색의 범위
  6. 파이썬 심화
    1. 시퀀스와 반복자 – 반복과 순회를 자유자재로 다루기
    2. 데코레이터 – 함수의 기능을 강화하기
    3. 프로퍼티
    4. 제너레이터
    5. async와 await
    6. 객체로서의 클래스 – 클래스를 동적으로 정의하기
  7. 파이썬 프로젝트 실습
    1. 원카드 게임 만들기 (1)
    2. 원카드 게임 만들기 (2)
    3. 원카드 게임 만들기 (3) (작성중)
    4. 턴제 자동 전투 게임 만들기 (작성중)
  8. 실전 (파이썬 외적인 것들)
    1. 정규표현식 – 문자열을 검색하고 치환하기 (작성중)
    2. 유니코드 – 컴퓨터에서 문자를 표기하는 방법
    3. html, css, 인터넷 – 자동화 첫 걸음 내딛기
    4. 네트워크 – 인터넷으로 통신하는 방법
    5. 문서 – 문맥을 읽어보기

2 thoughts on “파이썬 강좌 – 특별 메소드와 연산자 ~ 파이썬의 내부 동작 이해하기

답글 남기기

이메일 주소는 공개되지 않습니다. 필수 항목은 *(으)로 표시합니다

Scroll to top