본문 바로가기
프로그래밍/Python

[Python/파이썬] 클래스(Class)

by 별준 2020. 9. 6.

- 참조 문헌 및 사이트(Reference)

docs.python.org/3/tutorial/index.html

Byte of python - Swaroop C H


 

이번글에서는 파이썬에서의 클래스 Class에 대해서 한 번 알아보도록 하겠습니다.

클래스는 객체 지향 프로그래밍을 위한 도구로 사용되며, 객체를 정의하는 설계도라고 이해하면 될 것 같습니다. 

 

사용되는 용어를 먼저 알아보겠습니다.

여기서 클래스(Class)객체(Object)라는 단어가 있는데, 클래스(Class)는 사용자가 새로운 타입(형식)을 정의하는 것이며, 객체(Object)는 클래스의 인스턴스(instance), 즉, 새로운 형을 사용해서 만든 것을 의미합니다. 

만약 Person이라는 클래스가 있을 때, 이 클래스를 가지고 Jun이라는 객체를 만드는 겁니다.

 

객체는 내장된 변수들이나 함수들을 사용할 수 있습니다.. 이때, 객체 또는 클래스에 소속된 변수들을 필드(field)라고 하고, 함수들은 클래스의 메소드(method)라고 합니다. 이러한 필드와 메소드는 클래스의 속성(attribute)라고 합니다.

 

필드에는 클래스 변수객체 변수, 이렇게 두 종류가 있는데, 이 둘이 무엇인지와 차이점은 뒤에서 살펴보도록 하겠습니다.

 

클래스 정의

클래스는 아래와 같이 정의할 수 있습니다.

class ClassName:
	<statement-1>
    ...
    ...
    <statement-n>

함수 정의(def문)과 마찬가지로, 당연하겠지만 사용하기 전에 이렇게 정의를 먼저 해주어야합니다. class문을 사용해서 새로운 클래스를 생성하며, 그 아래로 들여쓰기 된 새로운 블록에서 클래스의 body를 구성합니다. 클래스의 body는 보통 변수, 함수 정의들로 구성되지만, 다른 구문들도 사용이 가능합니다.

 

클래스 정의가 시작되면, 새로운 namespace가 생성됩니다. 그리고 클래스의 이름은 아마도 전역 심볼 테이블에 연결이 될 겁니다.

아마도라고 한 이유는, 클래스의 정의가 사실 함수 안에서 정의가 될 수도 있고, if문 안에서 정의가 될 수도 있습니다. 그래서 함수 안에서 클래스가 정의된다면, 그 함수의 지역 심볼 테이블 안에서 클래스의 이름이 연결되어서 사용되기 때문입니다. 

아래의 Complex 클래스를 통해서 한 번 살펴보도록 하겠습니다.

기본적인 클래스의 정의를 통해서 전역 심볼 테이블을 한 번 살펴보죠.

class Complex:
    r = 5.0
    i = 5.0

    def print(self):
        print(f'Complex : {self.r} + {self.i}i')

print(globals())

실행결과:

위와 같이 Complex라는 클래스가 전역 심볼 테이블에 잘 연결이 되어있습니다. 

이번에는 함수 안에서 클래스를 정의하고 지역심볼테이블과 전역심볼테이블을 살펴볼까요?

def temp():
    class Complex:
        r = 5.0
        i = 5.0

        def print(self):
            print(f'Complex : {self.r} + {self.i}i')
    print('local symbol table')
    print(locals())

temp()

print('global symbol table')
print(globals())

실행결과:

함수 안에서 정의된 클래스는 전역심볼테이블이 아닌, 지역심볼테이블에 연결되어 있습니다. 따라서 함수 안에서 클래스가 정의된 경우에는 그 함수 안에서만 사용이 가능합니다.

 

객체 생성

클래스를 정의했으면, 이제 객체를 만들어봐야겠죠. 객체는 그 클래스의 성질을 갖는 일종의 변수를 만드는 것이라고 생각하면 됩니다. 위에서 정의한 Complex 클래스를 가지고 아래와 같이 생성할 수 있습니다.

class Complex:
    r = 5.0
    i = 5.0
    
    def print(self):
        print(f'Complex : {self.r} + {self.i}i')
        
# Create Object of Complex
c1 = Complex() 

함수 표기법처럼 Complex의 인스턴스를 만들어서, c1이라는 변수에 연결을 시켜주는 것이죠. 함수 표기법처럼이라는 것에서 예상했을수도 있겠지만 9 line의 Complex()에서 괄호안에 인자들을 넣어줄 수도 있습니다. 뒤에서 인스턴스를 생성하면서 초기화를 하는데 주로 사용되는데, 조금 있다가 살펴보도록 하겠습니다.

 

메소드

메소드는 클래스에 내장된 함수로, 객체는 attribute 참조에 사용되는 표준 문법으로 사용해서 Object.Method 로 메소드를 참조할 수 있으며, 클래스 객체가 만들어질 때, 클래스 namespace에 있는 모든 메소드를 참조할 수 있습니다. 위에서 정의한 Complex의 print 함수가 Complex Class의 메소드입니다. 메소드는 9 line처럼 사용해서 호출이 가능합니다.

class Complex:
    r = 5.0
    i = 5.0
    
    def print(self):
        print(f'Complex : {self.r} + {self.i}i')

c1 = Complex()
c1.print()

 

Complex 클래스에서 정의된 print함수를 살펴보면 self라는 매개변수가 추가되어 있는 것을 볼 수 있습니다. 클래스 메소드와 일반적인 함수와의 한 가지 다른 점인데, 메소드의 경우에는 매개변수의 목록에 항상 첫 번째 매개변수에 한 개의 변수가 추가되어야 한다는 점입니다. 또한, 메소드를 호출할 때 이 변수에 인자를 직접적으로 넘겨주지 않고, 파이썬 내부에서 자동으로 이 인자를 할당하게 됩니다. 이 매개변수는 현재 객체 자신의 참조가 할당됩니다.

C++이나 java/C#을 하셨으면 눈치 채셨을 수도 있겠는데, C++의 this 포인터와 java/C#의 this 참조와 동일합니다.

 

그리고 self라고 이름이 붙어있는데, self만 가능한 것은 아닙니다. 이름은 자유롭게 지어도 되지만, self라고 쓰는 것은 일종의 약속이고 강력하고 권고됩니다. 

파이썬이 자동으로 할당한다고 했는데, 9 line의 코드는 파이썬에 의해서 아래와 같은 형태로 바뀌게 되어서 호출이 됩니다.

Complex.print(c1)

 

__init__ 메소드

파이썬 클래스에서 특별한 메소드가 몇 가지 있는데, 아까 언급한 초기화를 위한 메소드를 살펴보겠습니다.

__init__ 메소드는 클래스가 인스턴스화 될 때 호출됩니다. 즉, 객체가 생성될 때, 이 메소드가 자동으로 호출되며 이러한 특성으로 초기화가 필요할 때 사용됩니다. C++에서 생성자와 동일하죠. Complex의 초기화 메소드를 작성해서 살펴보도록 합시다.

class Complex:
    r = 5.0
    i = 5.0
    
    def __init__(self, r, i):
        self.r = real
        self.i = img
    
    def print(self):
        print(f'Complex : {self.r} + {self.i}i')

c1 = Complex(1.0, 1.0)
c1.print()

여기 Complex 클래스는 객체가 생성될 때, __init__ 메소드에서 r과 i라는 매개변수를 입력받아서, 해당 값으로 객체의 변수를 초기화하고 있습니다. 물론 __init__ 메소드 또한 self 변수를 첫 번째 매개변수로 추가를 해주어야합니다.

여기서 객체의 변수 이름과 매개변수 이름을 동일하게 지정해주었는데, 객체에 내장된 변수는 self.name 의 형태로 사용되고 매개변수 r, i는 지역변수를 의미하기 때문에 파이썬에서 완전하게 구분이 가능합니다.

 

+)

추가적으로 메소드는 self인자를 통해서 다른 메소드를 호출할 수 있습니다.

class Bag:
    def __init__(self):
        self.data = []

    def add(self, x):
        self.data.append(x)

    def addtwice(self, x):
        self.add(x)
        self.add(x)

위와 같이 addtwice 메소드에서 self인자를 통해서 같은 클래스의 메소드인 add 메소드를 호출하고 있습니다.

 

클래스 변수와 객체 변수

지금까지 클래스와 객체의 기능에 해당하는 메소드에 대해서 살펴보았고, 이제 데이터를 위한 변수에 대해서 살펴보도록 하죠.

여기서 Dog라는 클래스가 있고, 두 종류의 변수가 있습니다. 하나는 모든 객체에서 동일하게 사용되는 클래스 변수이고, 다른 하나는 각 객체에서 유일하게 사용되는 객체 변수입니다. 아래 예시를 살펴봅시다.

class Dog:
    kind = 'canine'     # class variable

    def __init__(self, name):
        self.name = name    # object(instance) variable

d1 = Dog('Fido')
d2 = Dog('Buddy')

print(f" d1's name : {d1.name}")
print(f" d1's kind : {d1.kind}")
print(f" d2's name : {d2.name}")
print(f" d2's kind : {d2.kind}")

Dog 클래스를 정의하면서, kind 변수를 생성해주었습니다. 이렇게 클래스 body에 바로 생성되는 변수가 클래스 변수가 됩니다. 그리고, 초기화 메소드인 __init__을 통해서 객체 변수인 name를 생성해 매개변수 name 값으로 초기화를 해주었습니다.

여기서 클래스 변수는 모든 객체가 공유하는 변수입니다. 이 변수의 값을 변경하려면 어떻게 하면 될까요??

이 값을 변경하기 위해서는 객체가 아닌 클래스 자체의 namespace를 통해서 접근을 해야 합니다. 

위의 코드에 이어서 아래 코드를 추가해서 실행시켜 봅시다.

Dog.kind = 'poodle'
print(f" d1's kind : {d1.kind}")
print(f" d2's kind : {d2.kind}")

실행결과 :

kind 변수는 모든 객체가 공유하기 때문에, 한 번의 변경으로 모든 객체에 적용이 된 모습을 볼 수 있습니다. 

 

그렇다면 클래스 이름으로 접근해서 변경하지 않고, 객체의 이름으로 kind에 접근해서 변경을 하면 어떻게 될까요?

아래를 살펴봅시다.

class Complex:
    r = 5.0
    i = 5.0
    
    def __init__(self, real, img):
        self.r = real
        self.i = img
    
    def print(self):
        print(f'Complex : {self.r} + {self.i}i')

class Dog:
    kind = 'canine'     # class variable

    def __init__(self, name):
        self.name = name    # object(instance) variable

d1 = Dog('Fido')
d2 = Dog('Buddy')

print(f" d1's name : {d1.name}")
print(f" d1's kind : {d1.kind}")
print(f" d2's name : {d2.name}")
print(f" d2's kind : {d2.kind}")

d1.kind = 'poodle'
print(f" d1's kind : {d1.kind}")
print(f" d2's kind : {d2.kind}")

실행결과 :

결과는 d1의 kind만 변경되게 됩니다. kind는 클래스 변수로 모든 객체가 공유한다고 했는데, 어떻게 이런 결과가 나오게 됬을까요 ?

이것은 각 객체의 kind 변수의 id를 살펴보면, 조금 쉽게 이해가 될 수 있을 것 같습니다. 아래와 같이 코드는 변경해서 결과를 살펴봅시다.

class Complex:
    r = 5.0
    i = 5.0
    
    def __init__(self, real, img):
        self.r = real
        self.i = img
    
    def print(self):
        print(f'Complex : {self.r} + {self.i}i')

class Dog:
    kind = 'canine'     # class variable

    def __init__(self, name):
        self.name = name    # object(instance) variable

d1 = Dog('Fido')
d2 = Dog('Buddy')

print(f" Dog's id of kind : ", id(Dog.kind))

print(f" d1's name : {d1.name}")
print(f" d1's kind : {d1.kind}")
print(f" d1's id of kind : ", id(d1.kind))
print(f" d2's name : {d2.name}")
print(f" d2's kind : {d2.kind}")
print(f" d2's id of kind : ", id(d2.kind))

d1.kind = 'poodle'
print(f" Dog's id of kind : ", id(Dog.kind))
print(f" d1's kind : {d1.kind}")
print(f" d1's id of kind : ", id(d1.kind))
print(f" d2's kind : {d2.kind}")
print(f" d2's id of kind : ", id(d2.kind))

실행결과 :

d1.kind에 값을 변경하기 전까지 모두 같은 주소를 가지고 있으며, 공유하고 있는 것을 볼 수 있습니다. 그러나, d1.kind의 값을 변경하면, d1.kind의 주소가 변경이 되는 것을 볼 수 있습니다. 즉, d1.kind로 접근하는 순간, d1 객체에 객체 변수 kind를 생성해 버리는 것입니다. 그래서 d1 객체에는 클래스 변수 kind와 객체 변수 kind가 동시에 존재하고 있고, d1.kind를 통해서 우리는 객체 변수에 먼저 접근을 하게 됩니다.

이는 각 객체의 심볼테이블을 보면 쉽게 확인이 가능합니다. 객체의 심볼은 __dict__ 변수로 확인가능합니다. 

d1 객체에 kind 변수가 추가된 것을 확인할 수 있죠. 

위와 같은 결과로 객체가 클래스 변수를 수정을 할 수 없다라는 것을 알 수 있습니다.

 

 

클래스 변수나 객체 변수를 사용할 때, 많이 헷갈리는 것들이 많아서 한 번 알아보도록 합시다.

 

먼저 아래 코드를 한 번 살펴봅시다.

class Complex:
    r = 5.0
    i = 5.0
    
    def print(self):
        print(f'Complex : {r} + {i}i')

클래스 변수로 r과 i를 생성했고, print 메소드를 통해서 r, i를 출력합니다만.. 이걸 실행해보면 r이 정의되지 않았다는 오류를 볼 수 있습니다. C++이나 java/C#을 사용했었더라면, 이러한 오류가 발생하지 않았겠죠. 

이미 알고 계시겠지만, 이를 해결하려면 self.r과 self.i를 사용하면 해결할 수 있습니다. 단순히 메소드 안에서 r, i를 찾게되면 print함수의 지역심볼테이블만 참조하여 r과 i를 찾기 때문에 에러가 발생합니다. (파이썬은 심볼테이블이 엄격히 구분되어 있는 것 같습니다)

 

다음으로는 클래스 객체와 객체 변수가 동일한 이름으로 사용될 경우를 살펴보도록 하겠습니다. 

우선 클래스 변수만 사용되는 경우입니다.

class Complex:
    r = 5.0
    i = 5.0
    
    def print(self):
        print(f'Complex : {self.r} + {self.i}i')

c1 = Complex()

Complex.r = 10.0
Complex.i = 10.0

c1.print()

실행결과 :

위에서 살펴봤듯이 객체는 클래스 변수와 객체 변수에 모두 참조가 가능합니다. 여기서는 r, i라는 객체 변수가 없기 때문에 바로 클래스 변수 r, i로 접근을 해서 출력하고 있습니다.

 

다음의 경우에는 어떨까요?

class Complex:
    r = 5.0
    i = 5.0

    def __init__(self):
        self.r = 1.0
        self.i = 1.0
    
    def print(self):
        print(f'Complex : {self.r} + {self.i}i')

c1 = Complex()

Complex.r = 10.0
Complex.i = 10.0

c1.print()

여기서는 객체가 생성될 때, 객체 변수 r, i는 1.0으로 각각 초기화하게 됩니다. 그리고 객체를 생성하고 클래스 변수를 수정해서 출력하게 되면, c1객체에는 이제 동일한 이름의 클래스 변수와 객체 변수가 있기 때문에 객체 변수에 우선적으로 접근을 하게 됩니다.

 

class Complex:
    r = 5.0
    i = 5.0

    def print(self):
        print(f'Complex : {self.r} + {self.i}i')

    def change(self, r, i):
        self.r = r
        self.i = i

c1 = Complex()
c2 = Complex()

c1.print()
c2.print()

c1.change(10.0, 10.0)

c1.print()
c2.print()

위 코드를 실행하면 다음과 같습니다.

처음에는 두 c1, c2객체 모두 클래스 변수만을 가지고 있었지만, c1객체에서 change 메소드를 통해서 r, i의 값을 바꿔주었지만, 실제로 클래스 변수 r, i값을 바꾸는 것이 아니라 객체 변수 r, i를 생성해서 값을 대입해주게 되는 것입니다. 따라서 change 메소드에서는 객체 변수 r, i에 접근하게 되며, c1 객체는 같은 이름의 클래스 변수, 객체 변수가 있기 때문에 print 메소드에서는 이제 객체 변수에 우선적으로 접근하게 됩니다.

 

아래 코드는 위와 동일한 결과를 얻습니다.

class Complex:
    r = 5.0
    i = 5.0

    def print(self):
        print(f'Complex : {self.r} + {self.i}i')

    def change(self, r, i):
        self.r = r
        self.i = i

c1 = Complex()
c2 = Complex()

c1.print()
c2.print()

c1.r = 10.0
c1.i = 10.0

c1.print()
c2.print()

즉, 어디에서든 객체 namespace를 통해서 변수에 접근하면, 그 변수는 객체 변수로 할당이 되는 것이죠. __init__ 메소드이든, 다른 메소드에서든, 클래스 밖이든 할당되는 장소는 상관이 없습니다.

 

그리고 함수를 설명할 때도 언급했엇는데, 클래스에서도 변경이 가능한 리스트나 딕셔너리일 경우에도 주의해야합니다.

class Dog:

    tricks = []             # mistaken use of a class variable

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

    def add_trick(self, trick):
        self.tricks.append(trick)

d1 = Dog('Fido')
d2 = Dog('Buddy')
d1.add_trick('roll over')
d2.add_trick('play dead')
print(d1.tricks)

위의 출력 결과는 'roll over'만 출력해야할 것 같지만, play dead까지 출력하는 것을 볼 수 있습니다.

아래와 같이 수정되어야 각 객체에서 유니크하게 tricks 리스트를 사용할 수 있습니다.

class Dog:

    def __init__(self, name):
        self.name = name
        self.tricks = []

    def add_trick(self, trick):
        self.tricks.append(trick)

d1 = Dog('Fido')
d2 = Dog('Buddy')
d1.add_trick('roll over')
d2.add_trick('play dead')
print(d1.tricks)
print(d2.tricks)

실행결과 :

여기서는 __init__에서 tricks 리스트를 초기화해서 객체 변수로 사용될 수 있게 했지만, 아까도 언급했지만 초기화되는 장소는 상관이 없습니다. 클래스 밖에서 d1.tricks = [] 로 초기화해도 객체 변수로 사용이 가능합니다.

 

간단하게 정리를 하자면, 클래스 변수와 객체 변수가 같은 이름이라면, 객체 변수가 우선적으로 참조됩니다.

그리고 이 변수들은 일반적인 사용자에 의해서도 참조될 수 있습니다. 따라서 이러한 변수들을 참조할 때 조심해야 합니다. 사용자가 이 변수들을 건들여서 메소드에 의한 동작들이 파괴될 수 있기 때문에 클래스 변수로만 사용되는 메소드에서 사용자가 모르고 동일한 이름의 객체 변수를 할당해버리면 예기치 못한 동작이 발생할 수 있는 것이죠.

 

클래스에 대한 내용은 여기까지인데, 아직 우리에게는 상속이라는 것이 남아있습니다. 글이 너무 길어질 것 같아서, 상속에 대한 내용은 다음 글에서 다시 알아보도록 하겠습니다 !

댓글