Python-13.Class

안녕하세요 묵호입니다.
이번엔 클래스에 대해 알아보겠습니다.

들어가면서

소프트웨어 개발 언어는 발전해오면서 시대별로 트렌드가 있었습니다. 현재는 객체 지향 프로그래밍(OOP, Object Oriented Programming)이 대세입니다.
객체 지향 프로그래밍은 컴퓨터 프로그램을 명령어의 목록으로 보는 시각에서 벗어나 여러 개의 독립된 단위인 객체들의 모임으로 파악하고자 하는 것으로, 각각의 객체는 메시지를 주고 받고 데이터를 처리할 수 있습니다. 자세한 설명은 링크를 확인해보세요.
자, 그러면 저 객체라는 것을 파이썬에서 어떻게 만들 수 있을까요? 이 객체를 만들기 위해 사용하는 것을 클래스(Class)라고 합니다. 기존에 파이썬이 제공하는 클래스와 객체는 관련된 데이터와 기능이 뭉쳐저 있는 것이라고 했는데, 우리는 우리가 직접 필요로 하는 클래스를 정의하고, 이를 기반으로 객체를 생성하는 사용자 정의 클래스/객체를 만들어 보겠습니다.

클래스와 객체 개념

우리는 지금까지 파이썬이 기본적으로 제공하는 클래스와 객체를 써왔습니다.

tempString = "Republic of Korea"
print(tempString.count(' '))
# 2

여기서 문자열이라는 클래스는 일종의 설계도이며, 우리는 설계도로 부터 구체적인 독립된 객체를 만들어 냈습니다. 문자열 클래스라는 동일한 설계도에서 만들어진 객체는 각자 다른 데이터를 가지고 있지만, 기본적으로 같은 기능과 특징을 가지고 있습니다. 즉, 같은 틀(클래스)로 만든 서로 다른 붕어빵(객체)과 같은 느낌입니다.

사용자 지정 클래스 정의하기

클래스의 가장 큰 개념은 서로 상관성이 큰 데이터(Attribute)와 함수(Methods)를 하나로 묶는 행위입니다. 이 묶는 기능을 하는 기능의 명령어가 바로 class입니다.
이름(name)과 학번(id)이 있는 학생(Student) 클래스를 정의해보겠습니다.

class Student:
    id = 0
    name = ''

네, 클래스의 정의는 이게 끝입니다. 그렇다면 이제 객체를 만들어 보겠습니다. 두 객체를 만들고, 각각의 attribute를 부여해 보겠습니다.

student1 = ()
student2 = ()

student1.id = 2016123456
student1.name = 'Mukho'

student2.id = 2017987654
student2.name = 'jiri'

객체를 만들고, 각 객체에 데이터를 넣어보았습니다. 하지만, 객체 지향 프로그래밍에서는 이렇게 데이터에 직접 대입하거나 데이터를 직접 사용하는 것을 암묵적으로 금지하고 있습니다. 대신, 함수(Methods)를 만들어 객체에 대입하고 사용합니다. 그렇다면 클래스를 수정해 보겠습니다.

class Student:
    id = 0
    name = ''

    def setId(self, Id):
        self.id = ID
    def getId(self):
        return self.id
    
    def setName(self, Name):
        self.name = Name
    def getName(self):
        return self.name

메소드를 추가했습니다. 메소드의 첫 파라메타가 self인데, 이것은 무엇일까요?
하나의 클래스로 수많은 객체들이 만들어 집니다만, 메소드는 클래스 안에 하나만 존재합니다. 즉, 객체들마다 데이터(Attribute)는 다르지만 함수(Methods)는 같습니다. 데이터는 달라도 기능은 같다는 것입니다. 따라서 클래스의 함수는 ‘내가 지금 어떤 객체의 데이터를 다뤄야 하는가?’라는 의문을 해결하기 위해 객체에 대한 정보를 갖는 self 변수를 첫번째 입력 파라메타로 받아야 합니다. 멤버 데이터를 멤버 메소드에서 활용하려면 self.를 붙이고 사용하면 됩니다. 위 클래스를 멤버 attribute를 읽고 쓰게 만든 코드입니다.

student1 = ()
student2 = ()

student1.setId(2016123456)
student1.setName('Mukho')
print(student1.getId())
print(student1.getName())

student2.setId(2017987654)
student2.setName('jiri')
print(student2.getId())
print(student2.getName())

# 2016123456
# Mukho
# 2017987654
# jiri

자, 위 코드에는 객체를 생성할 때마다 멤버 attribute 별로 함수를 호출했습니다. 하지만, 멤버 attribute가 늘어나면 늘어날수록 매우 번거로워 집니다.
우리는 객체가 만들어질때 멤버 attibute를 초기화할 것이 있으면 자동으로 하고, 객체가 만들어질때 반드시 호출해야하는 함수도 자동으로 호출하고 싶습니다. 이 때 사용하는 것이 생성자(Constructor)입니다. __init__() 명령어를 통해 만드는 생성자는 객체가 만들어질때 자동으로 호출됩니다.

Private Attribute

아, 클래스 내의 변수를 클래스 밖에서 접근할 수 없게 변수를 선언할 수 있습니다. 변수 이름 앞에 __를 붙이면 됩니다. 이는 C++에서의 private와 같습니다. 이에 접근하기 위해서는 무조건 클래스의 멤버 메소드를 통해 접근해야 합니다.
객체 지향 프로그래밍 특징 중에는 캡슐화라는 것이 있습니다. 캡슐화는 객체의 속성과 메소드를 하나로 묶고, 실제 구현 내용 일부를 내부에 감추어 은닉한다는 의미입니다. 이 중, ‘내부에 감춘다’라는 것은 언어적 측면에서 접근지정자를 두어 은닉의 정도를 기술하여 구현합니다.
이때 접근지정자는 다음과 같습니다.

  • private: 자기 클래스 내부의 메소드에서만 접근 허용
  • protected: 자기 클래스 내부 또는 상속받은 자식 클래스에서 접근 허용
  • public: 모든 접근을 허용한다.(default)
    파이썬에서는 멤버 애트리뷰트 이름 앞에 __를 붙이는 것으로 private 접근지정자를 적용할 수 있습니다. 이때, 적용한 멤버 에트리뷰트는 클래스 내의 메소드를 통해서만 접근 가능합니다.
class Student:
    def __init__(self):
        # 보통 객체 멤버 에트리뷰트를 private으로 많이 씁니다.
        self.__id = 0
        self.__name = ''

    def setId(self, Id):
        self.__id = ID
    def getId(self):
        return self.__id
    
    def setName(self, Name):
        self.__name = Name
    def getName(self):
        return self.__name

생성자를 더 편리하게 사용하려면, 모든 객체에 대해서 동일한 값이 아닌 상태로 객체를 만드는 경우입니다. 생성자에 입력 파라메터를 주게 되면 그렇게 할 수 있습니다. 입력값은 id와 name으로 멤버 attribute에 초기화해줍니다.

class Student:
    def __init__(self):
        self.__id = 0
        self.__name = ''

    # OOP의 다형성(Python에서는 지원하지 않지만, 개념 설명을 위해 이렇게 코드를 작성했습니다.)
    def __init__(self, Id, Name):
        self.__id = Id
        self.__name = Name

    def setId(self, Id):
        self.__id = ID
    def getId(self):
        return self.__id
    
    def setName(self, Name):
        self.__name = Name
    def getName(self):
        return self.__name

앞의 생성자를 토대로 객체를 생성하게 된다면, 코드가 간결해짐을 알 수 있습니다.

student1 = (2016123456, 'Mukho')
student2 = (2017987654, 'jiri')

print(student1.getId())
print(student1.getName())

print(student2.getId())
print(student2.getName())

# 2016123456
# Mukho
# 2017987654
# jiri

자, 그렇다면 우리는 의문이 생깁니다. 멤버 애트리뷰트를 꼭 하나 하나 출력해야 할까요? 아닙니다. 우리는 print() 함수 하나로 데이터를 출력하는 것과 같이, 클래스의 멤버 메소드로 __str__() 함수를 포함하고, 이 함수에서 클래스의 애트리뷰트를 한번에 출력하면 됩니다.

class Student:
    def __init__(self, Id, Name):
        self.__id = Id
        self.__name = Name

    def setId(self, Id):
        self.__id = ID
    def getId(self):
        return self.__id
    
    def setName(self, Name):
        self.__name = Name
    def getName(self):
        return self.__name

    def __str__(self):
        return 'ID: {}, Name: {}'.format(self.__id, self.__name)

student1 = Student(2016123456, 'Mukho')
student2 = Student(2017987654, 'jiri')

print(student1)
print(student2)

클래스 변수와 객체 (혹은 인스턴스) 변수 이해하기

Student 클래스의 최종 버전 기준, 클래스가 가져야 하는 애트리뷰트는 생성자 안에 있으며, self.로 시작합니다. 이렇게 표현되는 변수는 클래스로부터 만들어지는 특정 객체들에 엮입니다. Mukho 객체의 name에 Mukho가 저장되고, jiri 객체의 name에 jiri가 저장되는 꼴이죠.
하지만 가끔 클래스 차원에서의 애트리뷰트가 필요한 경우가 있습니다. 예를 들면 클래스에서 만들어지는 객체의 수를 세는 경우입니다. 이때 사용하는 변수가 클래스에 종속된 클래스 변수입니다.
클래스 변수를 사용하기 위한 메소드 위에 @classmethod를 붙여야 합니다. 이때 메소드에는 self 대신 cls를 사용합니다.

class Student:
    __studentCount = 0

    def __init__(self, Id, Name):
        self.__id = Id
        self.__name = Name
        self.addNumOfCount()

    # 소멸자
    def __def__(self):
        self.minusNumOfCount()

    def setId(self, Id):
        self.__id = ID
    def getId(self):
        return self.__id
    
    def setName(self, Name):
        self.__name = Name
    def getName(self):
        return self.__name

    def __str__(self):
        return 'ID: {}, Name: {}'.format(self.__id, self.__name)

    @classmethod
    def addNumOfCount(cls):
        cls.__studentCount += 1
    @classmethod
    def minusNumOfCount(cls):
        cls.__studentCount -= 1
    @classmethod
    def getNumOfCount(cls):
        return cls.__studentCount

studentCount변수를 정의한 위치는 멤버 메소드들의 바깥입니다. 이처럼 객체에 종속되지 않고 클래스에 연결된 변수의 선언은 멤버 메소드나 생성자가 아닌 클래스의 시작 부분에 기술합니다. 또, 이런 클래스 변수를 사용할 때는 self.가 아닌 클래스 이름인 Student.로 시작됨을 알 수 있습니다.

print(Student.getNumOfCount())
student1 = Student(2016123456, 'Mukho')
student2 = Student(2017987654, 'jiri')
print(Student.getNumOfCount())
# 0
# 2

상속(Inheritance)

상속은 부모 클래스의 멤버 변수와 메소드를 자식 클래스가 재사용하는 개념입니다. 상속을 사용하는 이유는 생산성 때문인데, 남이 만들어놓은 검증된 코드를 재사용해서 빠르고 안전하게 프로그램을 만들 수 있기 때문입니다. 남이 만들어 놓은 코드에 내가 필요한 코드를 금방 추가해 서비스를 시작할 수 있습니다.
Student 클래스를 재학생-졸업생으로 확장해보겠습니다. 아래는 Student 클래스를 상속받은 GraduatedStudent 클래스입니다.

# 졸업생
class GraduatedStudent(Student):
    def __init__(self, Id, Name, Year):
        self.__graduatedYear = Year
        super().__init__(Id, Name)
    
    def __str__(self):
        return super().__str__() + ", Graduation_Year: {}".format(self.__graduatedYear)

상속을 받기 위해서는 첫번째 줄과 같이, 괄호 안에 상속을 받을 클래스의 이름을 넣습니다. 상속을 받을 클래스를 부모 또는 Base 클래스라고 합니다. 상속을 받는 클래스는 자식 또는 Derived 클래스라고 합니다. Derived 클래스에는 Base 클래스의 모든 애트리뷰트와 메소드가 포함되어 있습니다.
GraduatedStudent 클래스의 객체 애트리뷰트로 __graduatedYear가 추가되어 있고, Derived 클래스도 생성자를 가지고 있습니다. 이때 Base 클래스에 포함된 정보는 Base 클래스 안에서 관리되므로, super()를 사용하여 Base 클래스에 접근합니다.
Derived 클래스는 Base 클래스와 같은 이름의 함수를 가질 수 있는데, 이 경우 Derived 클래스의 함수가 호출이 되면서 필요한 경우 Base 클래스의 함수를 부를 수 있으나, 필요 없으면 Derived 클래스의 함수 작업만 진행됩니다. 이와 같이 Derived 클래스가 Base 클래스와 똑같은 이름의 함수를 갖는 것을 오버라이딩(Overriding)이라고 합니다. 상속 관계에 있는 부모 클래스에서 이미 정의된 메소드를 자식 클래스에서 같은 이름을 갖는 메소드로 다시 정의하는 것이라고 할 수 있습니다. 보통의 경우, 상속받은 부모 클래스의 메소드를 재정의하여 사용하는 경우를 의미합니다.
위 코드에서는 __str()__함수를 오버라이딩 한 것입니다. 아래 코드는 함수를 오버라이딩 한 경우 부모 클래스와 자식 클래스에서의 함수 사용 예시입니다.

class Student:
    __studentCount = 0

    def __init__(self, Id, Name):
        self.__id = Id
        self.__name = Name
        self.addNumOfCount()

    # 소멸자
    def __def__(self):
        self.minusNumOfCount()

    def setId(self, Id):
        self.__id = ID
    def getId(self):
        return self.__id
    
    def setName(self, Name):
        self.__name = Name
    def getName(self):
        return self.__name

    def __str__(self):
        return 'ID: {}, Name: {}'.format(self.__id, self.__name)

    @classmethod
    def addNumOfCount(cls):
        cls.__studentCount += 1
    @classmethod
    def minusNumOfCount(cls):
        cls.__studentCount -= 1
    @classmethod
    def getNumOfCount(cls):
        return cls.__studentCount

# 졸업생
class GraduatedStudent(Student):
    def __init__(self, Id, Name, Year):
        self.__graduatedYear = Year
        super().__init__(Id, Name)
    
    def __str__(self):
        return super().__str__() + ", Graduation_Year: {}".format(self.__graduatedYear)

student1 = Student('Mukho', 2016123456)
student2 = GraduatedStudent('jiri', 2017123456, 2023)

print(student1) # Student(Base Class)
print(student2) # GraduatedStudent(Derived Class)
# ID: Mukho, Name: 2016123456
# ID: jiri, Name: 2017123456, Graduation_Year: 2023

포함 관계(has-a Relationship)

모든 클래스 들간의 관계가 반드시 상속에 의한 관계는 아닙니다.
예를 들어 학과는 학생들을 포함(has)하는 것이지, 상하관계 혹은 부모/자식 관계는 아닙니다.
이럴 경우는 학과의 객체에서 학생의 객체를 멤버 attribute로 가지면 됩니다.
아래는 개념적으로 학과 클래스에서 객체를 만들때, 해당 학과에 속한 학생들이 포함되도록 하는 has-a relationship을 간단하게 표현한 예입니다.

class Department:
    def __init__(self):
        memberStudent = Student()



이것으로 파이썬 시리즈 글은 마무리됩니다. 처음 쓴, 빠르게 쓴 글이라 내용도 부실하고 어디 부정확한 내용이 들어있을 수 있습니다. 발견시 댓글로 알려주시면 내용 수정 및 첨삭 하도록 하겠습니다.
제가 문법 한 번 더 공부할 겸 적어본 글이었지만, 누군가에게 도움이 되었으면 좋겠군요. 저도, 이 글을 볼 당신도 화이팅합시다!
그럼.. 바위^^