파이썬을 배워보자 13일차 - 클래스1 (클래스 기본)

0

Python

목록 보기
13/18

점프 투 파이썬 : https://wikidocs.net/book/1
파이썬 기본을 갈고 닦자 : https://wikidocs.net/16031
코딩 도장 : https://dojang.io/mod/page/view.php?id=2378

클래스

거의 모든 프로그래밍 언어에는 클래스가 있는데(GO, C 제외) 파이썬에도 클래스가 있다.

클래스는 자신과 관련된 데이터와 연산들을 모아놓은 하나의 타입이라고 보면 된다.

https://brilliant.org/wiki/classes-oop/

OOP에서의 클래스는 하나의 이며 하나의 타입이다. 이를 통해 만들어낸 것들을 객체라고 한다. 객체instance, object라고도 불린다.

클래스는 상태(state)와 행동(behavior)로 이루어져있는데, 상태(state) 또는 속성는 하나의 맴버 변수 또는 attributes이고, 행동(behavior)은 하나의 메서드 또는 함수를 말한다.

클래스(상태, 행동)

즉 클래스는 다음과 같이 정의할 수 있는 것이다. 그리고 클래스를 통해 만들어진 객체들은 클래스의 상태메서드를 갖게된다.

이를 파이썬 코드로 표현하면 다음과 같다.

class Calculator:
    def __init__(self):
        self.result  = 0
    
    def add(self, num):
        self.result += num
        return self.result
    
cal1 = Calculator()
cal2 = Calculator()

print(cal1.add(3)) # 3
print(cal1.add(4)) # 7 
print(cal2.add(23)) # 23
print(cal2.add(-4)) # 19

아직 문법에 대해 모르는 게 많으니, 위에서 언급한 것만 캐치해보자

class Calculator:

클래스의 선언 부분이다. class 이름:을 적고 본문이 나온다.

def __init__(self):
    self.result = 0

def는 클래스의 메서드를 말한다. 즉, behavior를 일컫는다. 여기서 __init__클래스 생성자로 클래스가 인스턴스를 만들 때 맨 첨에 호출되는 함수이다.

self.result = 0클래스의 맴버변수(상태, 속성)을 말한다. 이 맴버변수는 클래스 안에서의 스코프(유효 범위)를 가지며 클래스 안의 메서드들은 모두 접근가능하지만, 클래스 밖에서는 접근이 불가능하다.

def add(self, num):
    self.result += num
    return self.result

우리가 선언한 메서드이다. __init__은 특별한 메서드인 것과 달리 add는 우리가 만든 메서드로 이러한 형식으로 얼마든지 만들 수 있다. 참고로 self는 어떤 메서드 이건간에 class안의 메서드라면 꼭 넣어주어야 한다.

cal1 = Calculator()
cal2 = Calculator()

클래스를 통해, 인스턴스(객체)를 생성하는 모습이다. 마치 함수를 호출하듯이 클래스를 호출하면 된다.

print(cal1.add(3)) # 3
print(cal1.add(4)) # 7 
print(cal2.add(23)) # 23
print(cal2.add(-4)) # 19

인스턴스를 통해 메서드를 호출한 것이다. 클래스에 만들어놓은 메서드를 호출할 수 있고, 매개변수 또한 넣을 수 있다.

그럼 이제 본격적으로 파이썬의 클래스를 어떻게 사용하고 만드는 지 알아보도록 하자

1. 클래스 기본

위에 언급했듯이 클래스는 상태(속성), 행동(메서드)로 구성된다. 이에 따라 클래스를 만들고 사용하는 방식은 다음과 같다.

class 클래스이름:
    def 메서드1(self, 매개변수):
        self.맴버변수 = 초기값
        self.맴버변수 = 초기값
    
    def 메서드2(self, 매개변수):
        본문
        return 반환값

객체 = 클래스이름(인자)
객체.메서드2(인자)

뭔가 복잡해보이지만 클래스의 핵심 두 가지를 생각하면 별로 어려운게 없다. 상태(속성), 행동(메서드)만이 클래스의 전부이기 때문이다.

참고로 맴버변수self.맴버변수를 통해 등록하고 메서드에서 사용할 수 있게 된다.

그럼 예제를 통해 만들어보도록 하자

간단한 계산기를 만들다고 하자, 그러면 계산에 필요한 함수들이 있을 것이다.

  1. 데이터 넣기
  2. 덧셈
  3. 뺄셈
  4. 곱셈
  5. 나눗셈

위 함수들을 하나씩 만들어보자

datas = []

def setData(num1, num2):
    datas.append(num1)
    datas.append(num2)

def add():
    return datas[0] + datas[1]

def sub():
    return datas[0] - datas[1]

def mul():
    return datas[0] * datas[1]

def div():
    return datas[0] // datas[1]

setData(1,2)
print(add())
print(sub())
print(mul())
print(div())

함수들을 만들고보니, 5가지 함수들은 사실 하나의 계산기에 필요한 함수들이다. 이렇게 번잡하게 써놓으면 이 함수가 어디에 속하는 함수이고 어디에 쓰이는 지도 알기 쉽지 않다. 또한 관리하기도 쉽지 않다.

그래서 Caculator라는 클래스에 이 함수들을 행동(메서드)로 담을 것이다.

참고로 메서드와 함수의 차이는 클래스의 맴버함수로 쓰이면 메서드, 그냥 일반함수로 쓰이면 함수인 것이다. 용어상의 차이이지 실제 코드 상에서 메서드라는 키워드가 따로 있는 것은 아니다. 그렇기 때문에 굳이 엄격하게 구분하진 않아도 된다.

class Calculator:
    datas = []
    def setData(num1, num2):
        datas.append(num1)
        datas.append(num2)

    def add():
        return datas[0] + datas[1]

    def sub():
        return datas[0] - datas[1]

    def mul():
        return datas[0] * datas[1]

    def div():
        return datas[0] // datas[1]

클래스로 함수를 옮겨 Calculator의 메서드로 만들었다. 그런데 datas를 메서드들이 읽어오지 못하는 문제가 생긴다.

클래스 안에 쓰이는 맴버변수는 일반적인 변수 선언으로 사용할 것이 아니라, self라는 것에 엮어주어야 한다. 그리고 메서드들도 self에 접근하여 맴버변수를 호출할 수 있도록 self를 매개변수로 가져야 한다.

즉, self는 클래스가 생성할 인스턴스 자기 자신을 가리키는 것이다

class Calculator:
    def setData(self, num1, num2):
        self.datas = []
        self.datas.append(num1)
        self.datas.append(num2)

    def add(self):
        return self.datas[0] + self.datas[1]

    def sub(self):
        return self.datas[0] - self.datas[1]

    def mul(self):
        return self.datas[0] * self.datas[1]

    def div(self):
        return self.datas[0] // self.datas[1]

calc = Calculator()
calc.setData(1,2)
print(calc.add())
print(calc.sub())
print(calc.mul())
print(calc.div())

다음과 같이 메서드마다 self를 넣어주고 self에 맴버변수 datas를 엮어주면 메서드에서도 맴버변수 datas에 접근할 수 있게된다.

3
-1
2
0

이제 깔끔하게 정리해보도록 하자

class Calculator:
    def setData(self, num1, num2):
        self.num1 = num1
        self.num2 = num2

    def add(self):
        return self.num1 + self.num2

    def sub(self):
        return self.num1 - self.num2

    def mul(self):
        return self.num1 * self.num2

    def div(self):
        return self.num1 // self.num2

datas list로 받는 것이 아닌, 값들로 받아 처리하도록 하였다. 재밌는 것은 이제 우리는 클래스 내부의 것들을 내부에서 self를 통해 접근할 수 있다.

add메서드에서 sub메서드를 호출해보도록 하자

def add(self):
    sub() # NameError: name 'sub' is not defined. Did you mean: 'sum'?
    return self.num1 + self.num2

다음과 같이 변경해놓고 실행하면 NameError가 발생한다. 왜 그럴까??

sub가 어디에 있는 지 찾지를 못해서 그런다. 즉, 클래스 외부의 sub함수를 찾다가 없어서 에러를 뱉은 것이다. 그래서 우리는 ``self통해 sub를 찾아내면 된다. self```는 클래스 안의 정보를 담고있기 때문이다.

def add(self):
    print(self.sub())
    return self.num1 + self.num2

다음과 같이 self.메서드로 클래스 내부에서 메서드를 호출할 수 있다.

재밌는 것은 self.맴버변수로 엮어진 맴버변수를 인스턴스를 통해 접근이 가능하다. 즉, 인스턴스.메서드()로 호출하듯이 인스턴스.맴버변수로 호출이 가능하다.

print(calc.num1, calc.num2) # 1 2

이렇게 호출이 가능하다.

이러한 맴버 변수는 인스턴스 별로 따로 관리된다. 다음과 같이 인스턴스를 두 개 만들어보자

calc1 = Calculator()
calc2 = Calculator()
calc1.setData(1,2)
calc2.setData(10,20)

print(calc1.num1, calc1.num2) # 1 2
print(calc2.num1, calc2.num2) # 10 20

즉, 클래스의 요소인 상태(속성, 맴버변수)는 각 인스턴스마다 관리되는 것이지 클래스 자체에 있는 것은 아니다. 이것은 정적 맴버변수라고해서 클래스 자체에 고유한 변수가 있도록 할 수는 있지만 `맴버 변수는 아니다.

1.1 맴버변수를 클래스 안에서만 사용하고 싶다면??

그런데, 이런 문제가 있다. 맴버변수를 밖에 공개하고 싶지않다면 어떻게 해야할까?? 기존 파이썬을 공부하신 분들은 _맴버변수로 하면 된다고 알고 계시는 분들이 있다.

class Calculator:
    def setData(self, num1, num2):
        self._num1 = num1
        self._num2 = num2

    def add(self):
        return self._num1 + self._num2

    def sub(self):
        return self._num1 - self._num2

    def mul(self):
        return self._num1 * self._num2

    def div(self):
        return self._num1 // self._num2

calc1 = Calculator()
calc2 = Calculator()
calc1.setData(1,2)
calc2.setData(10,20)

print(calc1._num1, calc1._num2) # 1 2
print(calc2._num1, calc2._num2) # 10 20

그러나 막상 외부에서도 클래스의 맴버 변수에 접근하는 것이 가능하다. 사실 _맴버변수는 외부에 접근하는 것을 막는 것이 아닌, 그냥 컨벤션(관습)으로 맴버변수 앞에 _를 붙이면 우린 그걸 내부에서만 사용하는 변수로 알자! 라고 했을 뿐이다. 때문에 이것이 정말 다른 언어의 private한 효과를 주진 못한다. 즉, 맴버변수를 외부에서 접근하지 못하도록 할 수가 없다.

그래서 __맴버변수로 바꾸면 외부에서 접근을 못하도록 하는 기능이 있다.

class Calculator:
    def setData(self, num1, num2):
        self.__num1 = num1
        self.__num2 = num2

    def add(self):
        return self.__num1 + self.__num2

    def sub(self):
        return self.__num1 - self.__num2

    def mul(self):
        return self.__num1 * self.__num2

    def div(self):
        return self.__num1 // self.__num2

calc1 = Calculator()
calc2 = Calculator()
calc1.setData(1,2)
calc2.setData(10,20)

print(calc1.add()) # 3
print(calc2.add()) # 30

print(calc1.__num1, calc1.__num2) # 'Calculator' object has no attribute '__num1'
print(calc2.__num1, calc2.__num2) # 'Calculator' object has no attribute '__num2'

__맴버변수로 선언하여 외부에서 클래스의 맴버변수에 접근하지 못하도록 할 수 있다.

1.2 빈 클래스

본문이 없는 빈 클래스를 만들 수 있다.

class Test:
    pass

1.3 특정 클래스의 인스턴스 인지 확인하기

isinstance를 사용하면 객체의 자료형을 판단할 수 있다. True, False로 결과가 반환된다.

isinstance(객체, 클래스) 

위의 Calculator을 예시로 들어보면

print(isinstance(calc1, Calculator)) # True
print(isinstance(calc1, int)) # False

다음과 같다.

1.4 특정 속성만 추가할 수 있도록 하기(__slots__)

파이썬 클래스의 문제점 중 하나는 인스턴스에서 자유롭게 맴버변수를 추가할 수 있다는 것이다.

calc1 = Calculator()
calc2 = Calculator()
calc1.random = 23

print(calc1.random) # 23
print(calc2.random) # AttributeError: 'Calculator' object has no attribute 'random'

이 처럼 클래스 내부가 아닌, 인스턴스에서도 속성을 추가할 수 있다. 이렇게되면 문제는 해당 속성은 인스턴스에 귀속되므로 클래스에는 영향을 미치지 않는다. 때문에 다른 인스턴스는 해당 속성을 가질 수 없다.

위에서도 calc1.random = 23으로 정의하였는데 calc2.random은 없다고 나온다. 이유는 calc1.randomcalc1인스턴스에만 속성이 추가되었기 때문이다.

그래서 특별한 지시자가 있는데 그것이 바로 __slots__이다. 리스트 형식으로 문자열을 넣어주면 해당 문자열들만 클래스가 아닌 인스턴스에서 속성을 추가할 수 있도록 한다.

class Calculator:
    __slots__ = ['random', 'age']
    def setData(self, num1, num2):
        self.__num1 = num1
        self.__num2 = num2

    def add(self):
        return self.__num1 + self.__num2

    def sub(self):
        return self.__num1 - self.__num2

    def mul(self):
        return self.__num1 * self.__num2

    def div(self):
        return self.__num1 // self.__num2

calc1 = Calculator()
calc1.random = 23
calc1.age = 20
calc1.name ="hello" # AttributeError: 'Calculator' object has no attribute 'name'

2. 생성자(constructor)

그럼 맨 처음에 봤던 __init__메서드는 무엇일까? 이것은 특별한 메서드로 생성자(constructor)이라고 한다. 이들은 클래스를 통해 객체를 생성할 때 단 한 번 호출되는 메서드이다.

자동으로 호출되기 때문에 따로 호출해주지 않아도 된다.

class Calculator:
    def __init__(self):
        print("instance start hello!")
    
    def setData(self, num1, num2):
        self.num1 = num1
        self.num2 = num2

    def add(self):
        print(self.sub())
        return self.num1 + self.num2

    def sub(self):
        return self.num1 - self.num2

    def mul(self):
        return self.num1 * self.num2

    def div(self):
        return self.num1 // self.num2

calc1 = Calculator() # instance start hello!
calc2 = Calculator() # instance start hello!
calc1.setData(1,2)
calc2.setData(10,20)

print(calc1.num1, calc1.num2) # 1 2
print(calc2.num1, calc2.num2) # 10 20

class에 __init__하나만 추가했을 뿐인데 객체 생성 시, 자동으로 instance start hello! 로그를 찍은 것을 확인할 수 있다.

__init__ 역시도 self을 써주어 class의 맴버함수임을 알려주어야 한다. 그래야 에러가 발생하지 않는다.

또한, __init__ 생성자도 매개변수를 받을 수 있다. 매개변수를 받고, 맴버변수의 초기값을 설정해줄 수 있다.

class Calculator:
    def __init__(self, num1, num2):
        self.num1 = num1
        self.num2 = num2
        print("instance start hello!")
    
    def setData(self, num1, num2):
        self.num1 = num1
        self.num2 = num2

    def add(self):
        print(self.sub())
        return self.num1 + self.num2

    def sub(self):
        return self.num1 - self.num2

    def mul(self):
        return self.num1 * self.num2

    def div(self):
        return self.num1 // self.num2

calc1 = Calculator(1,2) # instance start hello!
calc2 = Calculator(10,20) # instance start hello!

print(calc1.num1, calc1.num2) # 1 2
print(calc2.num1, calc2.num2) # 10 20

setData 메서드를 사용하지 않고도 처음에 생성자를 통해 맴버 변수를 초기화시킬 수 있게 되었다.

사실 __init__은 이름만 생정자 메서드라고 불리지 일반 함수의 특성은 모두 가진다. 따라서 *args나 **kwargs도 모두 가능하다.

3. 파이썬에는 메서드 오버로딩이 없다.

메서드 오버로딩(overroading)은 메서드 이름이 같지만 파라미터 수와 타입이 다르면 다른 메서드처럼 다룰 수 있는 기능을 말한다.

가령 java의 경우는 다음의 경우를 허용한다.

class Calculator{
    int add(int a, int b){
        return a+ b;
    }
    int add(float a, float b){
        return a+ b;
    }
    int add(int a, int b, int c){
        return a + b + c;
    }
}

다음과 같이 메서드 이름이 add로 같지만 파라미터 수는 2개와 3개로 다르기도 하고 타입도 다르다. 따라서 이 3개의 메서드는 다른 메서드로 취급받게 된다. 이를 메서드 오버로딩이라고 한다.

그러나, 파이썬에서는 이것이 불가능하다.

class Calculator:    
    def add(self, num1 , num2):
        return num1 + num2
    
    def add(self, num1, num2 , num3):
        return num1 + num2 + num3
    
calc = Calculator()
print(calc.add(1,2,3)) # 6
print(calc.add(1,2)) # TypeError: Calculator.add() missing 1 required positional argument: 'num3'

인자 2개 메서드 add가 인자 3개 메서드 add에 덮어씌여지고 사라진다. 이는 오버로딩이 안된다는 것을 알려준다.

0개의 댓글