github이나 tech blog에서 python 코드를 보다보면 class를 활용한 코드 작성을 많이 보게되는데, 이와 관련해서 그때 그때 내용을 읽어보아도 제대로 정리해두지 않으니 자꾸 까먹게 된다. 그래서 비교적 알기쉽게 정리가 잘 되어 있는 조대표의 ‘파이썬으로 배우는 알고리즘 트레이딩’ 책에서 class 관련 내용과 예시를 발췌하여 정리해 두기로 한다. >

1. class의 정의

(1) 개괄

이 책에서는 객체와 class를 명함첩을 만드는 프로그램의 예시를 통해 설명하고 있는데, 아래에서는 그 흐름을 따라가 본다.

우선 고객의 이름, e-mail, 주소 등의 정보를 담고 있는 명함을 제작한다고 생각해보자. 그럼 아래 코드와 같은 함수를 만들어 명함을 출력하는 방식을 생각해 볼 수 있다.

1
2
3
4
5
6
7
8
9
10
11
12
# 이름, email, 주소 등을 저장
name = 'Youngsoo Kim'
email ='ysk@naver.com'
addr = 'Incheon'

# 명함 출력 함수
def print_business_card(name, email, addr):
    print('--------------------------')
    print('Name: %s' % name)
    print('E-mail: %s' % email)
    print('Address: %s' % addr)
    print('--------------------------')

그런데 회원수가 김영수 1명에서 2명, 3명 계속 늘어나는 상황을 가정해보자. 그럼 일일이 각 고객의 이름, email, 주소를 저장하고 ‘print_business_card’ 함수에 각 정보를 인자로 전달하여 명함을 출력하는 방식을 생각해볼 수 있다.

위와 같은 처리방식을 진행하기 위해서는 name_list, email_list, addr_list 등의 list를 만들어 각 고객의 정보를 name_list[0], email_list[0], addr_list[0] 형태로 저장하고 이를 ‘print_business_card’ 함수에 인자를 주어 명함을 출력할 것이다. 이를 그림으로 도식화하면 아래와 같다.


그림1 (출처: ‘파이썬으로 배우는 알고리즘 트레이딩’)


위의 그림과 같이 데이터와 데이터를 처리하는 함수가 분리되어 있고 함수를 순차적으로 호출하면서 데이터를 조작하는 프로그래밍 방식을 절차지향 프로그래밍이라 한다.

이와 달리 객체지향 프로그래밍객체(object)를 정의하는 것에서 시작한다. 그렇다면 객체는 또 무엇인가??

위의 명함만들기 예제에서 이름, email, 주소 등과 같은 명함을 구성하는 데이터와 명함을 출력하는 ‘print_business’ 함수를 묶은 것을 객체라 한다.


그림2 (출처: ‘파이썬으로 배우는 알고리즘 트레이딩’)


객체는 정수, 실수, 문자열과 마찬가지로 하나의 타입으로 인식되기 때문에 명함으로 만들어야할 고객의 명단이 여러명이더라도 쉽게 관리할 수 있다. 마치 5개의 정수값을 변수로 바인딩하는 것처럼 5개의 명함 값을 변수로 바인딩하거나 리스트에 명함이라는 객체를 저장하면 된다.

이번 포스팅의 핵심인 class는 객체를 찍어내는 틀이라 생각하면 이해가 그나마(?) 직관적이다. 여기까지만 봐서는 아직 이해가 쉽지 않지만 앞의 명함 만들기 프로젝트를 예시로 내용을 좀더 전개해보면 class에 대한 개념이 어느정도 자리잡을 것이다.

(2) python에서 class 정의하기

python에서 함수를 정의할때 def 라는 키워드를 썼던 것처럼 class를 정의하려면 class라는 키워드를 사용한다. class는 곧 객체를 찍어내는 틀이고 객체는 데이터와 함수의 묶어놓은 하나의 새로운 타입이니 class에 변수와 함수를 포함시켜 정의가 가능하다.

class를 정의하는 것은 객체를 찍어내는 틀을 만들었다는 것이니 이를 실제로 사용하려면 인스턴스라는 것을 생성해야한다. 붕어빵 기계에 비유하면 class는 붕어빵을 만드는 기계이고 인스턴스는 기계에 반죽, 팥 등을 넣어 만든 붕어빵에 해당한다.(개인적으로 이 책에서 제시한 위 비유가 클래스와 인스턴스 개념을 이해하는데 제일 도움이 많이 된 직관적인 비유라 생각한다)

python에서 정의된 class를 이용해 인스턴스를 생성하려면 class 뒤에 ()을 넣으면 된다.(함수를 호출하는 것과 비슷하다)

아래의 예시를 보면 BusinessCard class를 통해 card1이라는 인스턴스를 생성하면 메모리의 0x7f50910e57c0 위치에 해당 인스턴스가 생성되고 card1이라는 변수가 이를 바인딩 했음을 알 수 있다. 그리고 card1의 타입을 확인해보면 BusinessCard 클래스임을 알 수 있다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
## BusinessCard라는 class를 생성
class BusinessCard:
    pass

## card1 이라는 인스턴스를 생성
card1 = BusinessCard()

## card1 호출
>>> card1
<__main__.BusinessCard object at 0x7f50910e57c0>

## card1 타입 확인
>>> type(card1)
<class '__main__.BusinessCard'>

(3) class에 method 추가하기

앞에서 정의한 BusinessCard class는 내부에 데이터와 함수가 없는 빈 껍데기였다. 그래서 딱히 인스턴스를 생성해도 이를 통해 할 수 있는 기능이 없었다. 여기에서는 BusinessCard class에 사용자로부터 데이터를 입력받고 이를 저장하는 기능을 수행하는 set_info 함수를 추가해본다. 이처럼 class 내부에 정의되어 있는 함수를 특별히 method(메서드)라 한다.

1
2
3
4
5
class BusinessCard:
    def set_info(self, name, email, addr):
        self.name = name
        self.email = email
        self.addr = addr

위에서 set_info의 인자중 name, email, addr은 각각 사용자의 이름, email, 주소 정보를 method로 전달하는 인자인데 문제는 self 라는 인자인다. 뒤에서 자세히 설명하겠지만 self 인자는 인스턴스 그 자체를 의미한다.

class는 결국 인스턴스라는 붕어빵을 만들기위한 틀로서 기능하고, 우리는 생성된 인스턴스에서 set_info와 같은 method를 활용할 것이다. 이때 만약 우리가 member1이라는 인스턴스를 생성하여 set_info method를 통해 사용자의 이름, 주소 등의 정보를 저장하고자 한다면 이 정보들은 class인 BUsinessCard의 name, email, addr이 아닌 card1 인스턴스의 name, email, addr에 바인딩 되기를 바랄 것이다. 그렇기 때문에 class의 method의 인자로 인스턴스 그 자체인 self가 항상 들어가고 self.name, self.email 등이 이름, 주소 등의 정보를 비인딩하는 것이다. 다만 후에 찍어낼 붕어빵(인스턴스)의 이름이 뭐가 될지 안정해졌기 때문에 self라는 단어를 대신 사용하는 것이다.(뒤에서 좀 더 보충 설명이 나오니 이해가 어렵더라도 일단 넘어가자)


그림3. 변수의 바인딩 (출처: ‘파이썬으로 배우는 알고리즘 트레이딩’)


이제 새롭게 정의한 BusinessCard class로부터 member1이라는 인스턴스를 생성하고 set_info method를 사용하여 사용자의 정보를 저장해보자. member1 인스턴스는 set_info method를 가진 class로부터 생성되었으므로 당연히 member1 인스턴스 또한 set_info method를 호출할 수 있다.

이때 set_info method는 처음 정의 시 self를 포함해서 인자가 4개 들어가는 함수로 정의했지만 self 인자는 인스턴스 자체를 의미하므로 여기에서는 이름, email, 주소 3가지 인자만 method에 전달하면 된다.

1
2
3
4
5
## 인스턴스 생성
member1 = BusinessCard()

## set_info method 호출
member1.set_info('yuna kim', 'yunakim@naver.com', 'Seoul')

최초 BusinessCard class에서 self.name, self.addr 등이 바인딩하는 정보는 인스턴스.name, 인스턴스.addr 등을 통해 접근가능하다.

1
2
3
4
>>> member1.name
'yuna Kim'
>>> member1.email
'yunakim@naver.com

붕어빵 기계 틀인 BusinessCard class를 통해 또 다른 붕어빵인 member2를 만드는 것도 가능하다.

1
2
3
4
5
6
7
8
9
10
## 인스턴스 생성
member2 = BusinessCard()

## set_info method 호출
member2.set_info('sarang lee', 'sarang.lee@naver.com', 'Kyunggi')

>>> member2.name
'sarang lee'
>>> member1.email
'sarang.lee@naver.com

member1과 member2는 서로 동일한 인스턴스 변수인 name, email, addr을 갖고 있지만 각각 다른 데이터를 바인딩하고 있다.


그림4. member1, member2가 바인딩하는 정보 (출처: ‘파이썬으로 배우는 알고리즘 트레이딩’)


이제 BusinessCard class에 명함을 출력하는 method를 추가해본다. 맨 처음 명함만들기 예제에서 사용한 print_business_card 함수와 동일한 함수이다. 이때 인자로는 인스턴스 자체를 의미하는 self만을 사용한다. 왜냐하면 명함 출력에 필요한 이름, email, 주소 정보는 set_info method를 통해 이미 self.name과 같은 인스턴스 변수에 저장되어 있어서 이를 활용하기만 하면 되기 때문이다.

1
2
3
4
5
6
7
8
9
10
11
12
class BusinessCard:
    def set_info(self, name, email, addr):
        self.name = name
        self.email = email
        self.addr = addr

    def print_info(self):
        print('--------------------------')
        print('Name: %s' % self.name)
        print('E-mail: %s' % self.email)
        print('Address: %s' % self.addr)
        print('--------------------------')

새로운 BusinessCard class로부터 member1 인스턴스를 정의하고 명함을 출력해본다.

1
2
3
4
5
6
7
8
9
10
11
12
## 인스턴스 생성
member1 = BusinessCard()

## set_info method 호출
member1.set_info('yuna kim', 'yunakim@naver.com', 'Seoul')

>>> member1.print_info()
--------------------------
Name: yuna kim
E-mail: yunakim@naver.com
Address: Seoul
--------------------------

2. class 생성자

위의 예시에서 아래의 코드는 class로 부터 우선 인스턴스를 생성하고 method를 통해 인스턴스에 정보를 바인딩하는 방식이었다.

1
member1.set_info('yuna kim', 'yunakim@naver.com', 'Seoul')

즉, 붕어빵 기계로 붕어빵을 만들고 그 이후에 팥을 채워넣은 셈이다. 그런데 붕어빵을 만들면서 바로 그 안에 팥을 채워넣을 수 없을까?

python에서는 인스턴스 생성과 동시에 자동으로 호출되는 method인 생성자가 존재한다. ‘init(self)’ 와 같은 방식으로 생성자를 활용할 수 있는데 이러한 생성자는 인스턴스 생성과 동시에 호출된다. 아래의 예시를 보자.

1
2
3
4
5
6
7
class MyClass:
    def __init__(self):
        print('객체가 생성됐습니다.')

## 인스턴스 생성시 자동으로 생성자 method가 호출
>>> inst1 = MyClass()
객체가 생성됐습니다.

생성자 method를 이용하면 위의 명함 예시에서 인스턴스의 생성과 동시에 명함에 필요한 정보를 입력받도록 class를 정의할 수 있다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
## 기존의 set_info method를 생성자 method로 대체
class BusinessCard:
    def __init__(self, name, email, addr):
        self.name = name
        self.email = email
        self.addr = addr

    def print_info(self):
        print('--------------------------')
        print('Name: %s' % self.name)
        print('E-mail: %s' % self.email)
        print('Address: %s' % self.addr)
        print('--------------------------')

## 새롭게 정의된 class에서는 인스턴스 생성시 바로 생성자 method가 호출되므로 생성자 method에 필요한 3가지 인자(name, email, addr)가 전달되지 않으면 오류 발생
>>> member1 = BusinessCard()
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: __init__() missing 3 required positional arguments: 'name', 'email', and 'addr'

## 필요한 인자를 전달하여 인스턴스 생성하여 print_info method를 호출하면 정상으로 작동
member1 = BusinessCard('Kangsan Lee', 'kslee@naver.com', 'Busan')

>>> member1.print_info()
--------------------------
Name: Kangsan Lee
E-mail: kslee@naver.com
Address: Busan
--------------------------

3. self 인자 이해하기

여기에서는 위에서 간략히 설명하고 넘어간 self 인자에 대해 자세히 알아본다. 위에서 class 내의 method의 첫 번째 인자가 self여야 한다고 설명한 부분은 사실 틀린 개념이다. 아래의 예를 보자.

1
2
3
4
5
6
class Foo:
    def func1():
        print('function 1')

    def func2(self):
        print('function 2')

Foo라는 class 안에 func1, func2 두개의 method를 정의했다. 그런데 차이는 func1은 아무런 인자를 받지 않고, func2는 self 인자를 받도록 하였다. 이제 Foo class의 인스턴스를 만들어 두개의 method를 호출해보자.

1
2
3
4
5
6
7
8
9
10
11
## 인스턴스 생성
f = Foo()

# method 호출
>>> f.func1()
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: func1() takes 0 positional arguments but 1 was given

>>> f.func2()
function 2

위와 결과를 보면 func1 method를 호출시에는 ‘func1이 아무 인자를 갖지 않지만 1개의 인자를 전달받았다.’는 내용의 에러가 발생하는 반면, func2 method 호출시에는 정상적으로 함수가 작동한 것을 볼 수 있다.

이는 인스턴스 내 method 호출시 첫 번째 인자로 인스턴스(self) 자체가 전달되기 때문이다. class 정의 시 func1 method의 경우 self를 인자로 주지 않고 정의하였는데 f라는 인스턴스를 생성후 func1를 호출할 때 f 인스턴스가 self로 func1에 인자로 주어지기 때문에 에러가 발생하는 것이다. func2 method의 경우 정의 시에 self를 인자로 주었기 때문에 호출 시 에러가 발생하지 않는다.

이제 self의 정체를 좀 더 명확히 밝히기 위해 python 내장함수인 id()를 이용해 인스턴스가 메모리에 할당된 주소값을 확인해보자.

1
2
3
4
5
6
7
8
## func2에 self의 주소값을 print하는 코드를 추가
class Foo:
    def func1():
        print('function1')

    def func2(self):
        print(id(self))
        print('function 2')

우선 위 class를 활용하여 인스턴스를 생성하고 인스턴스가 할당된 메모리의 주소값을 보자.

1
2
3
4
5
## 인스턴스 생성
f = Foo()
## f의 주소값 확인
>>> id(f)
140358611962080

이제 func2 method를 통해 self의 주소를 확인해보자.

1
2
3
4
## func2 method 호출
>>> f.func2()
140358611962080
function 2

func2 내 print(id(self)) 코드를 통해 반환된 메모리 주소는 위에서 id(f)를 통해 반환된 인스턴스의 주소와 동일하다. 즉, class 내에서 정의된 self 인자는 class를 통해 생성해 내는 인스턴스 그 자체와 동일함을 확인할 수 있다.

4. class 네임스페이스

python에서 class와 인스턴스의 차이를 정확히 이해하는 것은 매우 중요하다. 이를 위해서는 우선 네임스페이스라는 개념을 알아야한다. 네임스페이스의 정의는 변수가 객체를 바인딩할 때 그 둘 사이의 관계를 저장하고 있는 공간을 의미한다. 가령 ‘a = 2’라고 할때 a라는 변수가 2라는 객체가 저장된 주소를 갖고 있는데 이 연결 관계가 저장된 공간이 네임스페이스이다.

예를 들어 보기 위해 아래와 같이 ‘Stock’ class를 정의한다.

1
2
class Stock:
    market = 'kospi'

python에서 class가 정의되면 그림5와 같이 하나의 독립적인 네임스페이스가 생성되고 class 내에 정의된 변수나 method는 해당 네임스페이스 안에 python 딕셔너리 타입으로 저장된다. Stock class 예시에서는 네임스페이스 안에 market = ‘kospi’ 형태의 딕셔너리를 포함하고 있는 것을 볼 수 있다.


그림5. class내 네임스페이스의 개념도 (출처: ‘파이썬으로 배우는 알고리즘 트레이딩’)


‘Stock’ class의 네임스페이스를 파이썬 코드로 확인하려면 class의 dict 속성을 확인하면 된다. 또한 class가 독립적인 네임스페이스를 가지고 class 내의 변수나 method를 네임스페이스에 저장하고 있으므로 class 내 변수에 아래와 같은 방법으로 접근하는 것도 가능하다.

1
2
3
4
5
>>> Stock.__dict__
mappingproxy({'__module__': '__main__', 'market': 'kospi', '__dict__': <attribute '__dict__' of 'Stock' objects>, '__weakref__': <attribute '__weakref__' of 'Stock' objects>, '__doc__': None})

>>> Stock.market
'kospi'

python에서는 class를 통해 인스턴스를 생성하면 인스턴스별로 별도의 네임스페이스를 유지한다. 아래의 예시를 통해 보자.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
## s1, s2 인스턴스 생성
s1 = Stock()
s2 = Stock()

## s1 인스턴스에만 market 변수를 추가
s1.market = 'kosdaq'

## s1, s2 메모리 주소값 확인
>>> id(s1)
140358601742224
>>> id(s2)
140358611962752

## s1, s2의 네임스페이스 확인
>>> s1.__dict__
{'market': 'kosdaq'}

>>> s2.__dict__
{}

위에서 보듯 s1, s2 인스턴스는 각각 서로 다른 메모리에 위치하고 있고, s1은 market이라는 변수를 갖고 있고, s2의 네임스페이스는 비어있다. 이를 도식화하면 그림6과 같다.


그림6. class, 인스턴스들의 네임스페이스 개념도 (출처: ‘파이썬으로 배우는 알고리즘 트레이딩’)


이때 s1과 s2 인스턴스에서 각각 market이라는 변수에 접근하면 어떻게 될까? 위 그림 상으로는 s1 인스턴스의 네임스페이스에는 market:’kosdaq’이라는 키:값 쌍이 존재하니 ‘kosdaq’이라는 값을 반환할 것으로 예상되나 s2의 네임스페이스는 비어있으니 따로 반환하는 값이 없을 것으로 예상된다. 그러나 결과는??

1
2
3
4
5
## 각각 인스턴스에서 market 변수 접근
>>> s1.market
'kosdaq'
>>> s2.market
'kospi'

위 코드와 같이 s2의 market 변수에 대한 반환값은 Stock class내 market변수에 바인딩 된 ‘kospi’값이 나온다. 이는 python에서 인스턴스를 통해 변수에 접근하면 해당 인스턴스의 네임스페이스 내에서 해당 변수가 존재하는지 우선적으로 찾고, 존재하지 않는다면 class의 네임스페이스에서 해당 변수를 찾아 값을 반환하기 때문이다.

5. class 변수와 인스턴스 변수

python class에서 생성자 method(init)가 인스턴스의 생성 시 자동으로 호출되는 함수라면 소멸자 method(del)는 인스턴스의 소멸 시 자동으로 호출되는 함수이다.

은행 계좌를 class로 표현한 아래의 예시를 보자.

1
2
3
4
5
6
7
8
9
10
11
class Account:
    num_accounts = 0

    ## 생성자 정의
    def __init__(self, name):
        self.name = name
        Account.num_accounts += 1

    ## 소멸자 정의
    def __del__(self):
        Account.num_accounts -= 1

Account class에는 num_accounts와 self.name이라는 두 종류의 변수가 있다. 이중 num_accounts는 class 내부에 선언된 변수로서 이를 class 변수라고 하며, self.name은 인스턴스 내에 정의된 인스턴스 변수이다.

class 변수는 Account class의 네임스페이스에 위치하며, self.name과 같은 인스턴스 변수는 인스턴스의 네임스페이스에 위치한다.

그러면 어떤 상황에서 class 변수를 사용하고, 인스턴스 변수를 사용하는가? 위의 Account class 예시에서 이제 두 명의 고객이 계좌를 개설하는 아래의 상황을 예시로 보자.

1
2
kim = Account('kim')
lee = Account('lee')

위에서 생성한 kim, lee 인스턴스에서 계좌의 수를 의미하는 num_accounts 변수에 접근하면 개수가 몇개로 나올까? 답은 2개이다. 위의 네임스페이스에서 배운 내용을 떠올려보면 kim이나 lee 같은 인스턴스에는 num_accounts 변수가 없으므로 class의 네임스페이스에서 num_accounts 변수가 갖고 있는 값을 반환하기 때문이다.

1
2
3
4
5
>>> kim.num_accounts
2

>>> lee.num_accounts
2

이처럼 여러 인스턴스 간에 서로 공유해야하는 값은 class 변수를 통해 바인딩해야 한다. class의 네임스페이스에 있는 변수는 인스턴스에게도 공유되기 때문이다.

위의 내용을 도식화하여 정리하면 아래 그림 7과 같다.
그림7. (출처: ‘파이썬으로 배우는 알고리즘 트레이딩’)

6. class 상속

객체지향 프로그래밍에서는 class에서 상속 기능을 지원한다. 여기서의 상속이란 우리가 부모로부터 재산을 물려받는 것과 비슷한 개념이다. 하나의 class에 이미 구현된 method나 속성을 다른 class가 상속받아서 그대로 사용할 수 있는 개념이다.

예시로 아래와 같은 부모(parent) class를 정의해보자. 이 class 안에는 노래를 부르는 method가 정의되어 있다.

1
2
3
class Parent:
    def can_sing(self):
        print('Sing a song')

위 class로 부터 father라는 인스턴스를 생성하고 method를 호출해보자. 제대로 작동한다.

1
2
3
4
father = Parent()

>>> father.can_sing()
Sing a song

이제 Parent class로부터 상속받은 자식 class를 정의해보자. class를 정의할 때 다른 class로부터 상속받고자 한다면 새로 정의할 class 이름 다음에 괄호를 사용해 상속받고자 하는 class의 이름을 지정하면 된다. 만약 여러개의 class를 상속 받고 싶다면 괄호 안에 여러개의 class의 이름을 넣으면 된다.

1
2
3
class Luckychild(Parent):
    # pass는 아무런 method나 변수를 정의하지 않을 때 사용
    pass

Parent class로부터 상속을 받은 Luckychild class로부터 인스턴스를 생성하면 이 인스턴스는 노래를 부를 수 있을까? 답은 ‘가능하다’이다. 아래를 통해 확인해보자.

1
2
3
4
5
child1 = Luckychild()

>>> child1 = Luckychild()
>>> child1.can_sing()
Sing a song

다른 class로부터 상속을 받은 class에 새로운 method를 정의하는 것도 가능하다. 아래에서는 기존 Parent class로부터 상속을 받고, 춤을 출 수 있는 method를 자체적으로 정의한 class를 정의하였다.

1
2
3
class Luckychild2(Parent):
    def can_dance(self):
        print('Shuffle Dance')

Luckychild2로부터 생성된 인스턴스는 노래도 부를 수 있고 춤도 출 수 있게 된다.

1
2
3
4
>>> child2.can_sing()
Sing a song
>>> child2.can_dance()
Shuffle Dance

이처럼 상속 기능을 사용하면 최소한의 코드로 부모 class에 구현된 method를 바로 이용할 수 있는 장점이 있다.

Leave a comment