[패스트캠퍼스 환급챌린지]딥러닝/Chapter 3. 파이썬

[패스트캠퍼스]Chapter 3. 딥러닝을 위한 파이썬 03-13 클래스의 이해와 활용

포리셔 2023. 5. 3. 13:31

03-13 클래스의 이해와 활용

저번 포스트에서 소개한 함수와 더불어, 코드를 더욱 용이하게 사용하기 위한 수단 중 하나로 클래스(class)가 존재합니다. 잘만 사용하면 매우 유용한 기능이지만, 입문자들에게 있어서는 함수와 함께 통곡의 벽 가장 큰 장벽으로 꼽히기도 합니다. 클래스와 관련된 메서드와 그 외의 기법들도 한 두 가지가 아니지만, 여기서는 가장 기본적인 클래스의 개념과 사용 방법 위주로 소개해보겠습니다.

클래스와 객체

흔히 클래스, 객체를 설명할 때 붕어빵 또는 과자 틀에 비유해서 이야기하고는 합니다. 어떤 식이냐면...

  • 클래스: 붕어빵 (또는 과자) 틀
  • 객체(object): 틀로 찍어낸 붕어빵 (또는 과자)

객체와 인스턴스(instance)라는 용어가 함께 사용되고 있으니 참고하세요.

두 명의 사람에 관한 정보를 처리해야 하는 코드가 있다고 가정하면, 두 사람의 특징은 (높은 확률로) 각기 다를 겁니다. 가장 간단하게 나이가 다를 수도 있겠죠. 이것만 본다면 함수와 다를 바가 없지만, 나이 외에 키나 몸무게 같은 정보까지 한꺼번에 처리하려고 하면, 또한 사람의 수가 많아진다면 그 특성들에 관한 함수를 여러 개 정의하거나 수정하는 것이 생각보다 번거로울 수 있습니다. 그래서 이러한 함수나 메서드를 한 데 묶어서 붕어빵 틀처럼 사용하고자 하는 것이 클래스의 목적입니다.

앞서 예시로 든 사람의 관한 클래스를 정의해보겠습니다. 클래스를 정의할 때는 함수를 정의할 때와 비슷하게 class 구문 + 클래스 이름과 콜론(:)으로 시작합니다. 그 이후 class 문 내부에 각종 함수, 메서드를 집어넣습니다. 함수를 정의할 때와는 또다른 요소들이 몇 가지 보이는데 이는 아래에서 더 설명해보겠습니다.

class Human:
    def __init__(self):
        self.age = 0

    def old(self):
        self.age += 1

human1 = Human()
human2 = Human()

for i in range(10): # 10 years old
    human1.old()
for j in range(20): # 20 years old
    human2.old()

print(human1.age)
print(human2.age)

생성자

앞서 설명한 것과 같이 클래스는 여러 정보를 하나의 객체에 담을 때 사용 가능합니다. 이번에는 학생 관리 프로그램을 예로 들어서 이를 설명해보겠습니다.
학생에 대한 정보로 학번, 이름, 나이, 성별, 학과를 넣는다고 해보죠. 이 클래스로 만드는 모든 객체는 이 다섯 종류의 정보를 공통적으로 포함해야 합니다. 객체가 생성될 때 이 정보를 생성함과 동시에 초기화해줍니다. 이를 가능케 해주는 것이 생성자 __init__() 메서드입니다. 또한, 생성자를 포함한 모든 메서드에 self라는 인수가 들어가는 것을 알 수 있는데, 이는 인스턴스 자기 자신을 의미합니다.

class Student:
    def __init__(self, id, name, age, gender, department):
        self.id = id
        self.name = name
        self.age = age
        self.gender = gender
        self.department = department

    def show(self):
        print("======학생 정보======")
        print(f"학번: {self.id}")
        print(f"이름: {self.name}")
        print(f"나이: {self.age}")
        print(f"성별: {self.gender}")
        print(f"학과: {self.department}")

    def add_age(self, offset):
        self.age += offset

생성자 내부에서 정의되는 변수를 각각 구분할 필요가 있습니다. 예를 들어 name은 함수의 파라미터로 넘어온 name 변수이고, self.name과 같이 self가 앞에 붙는 변수들은 이 name 변수를 객체에 지정해 준 결과 생성된 변수입니다. 이렇게 지정된 변수들은 해당 객체 내에서만 유효하며, 동일한 클래스로 생성된 다른 객체에는 전혀 영향을 주지 않습니다.

메서드 호출

각 객체에 대한 show 메서드를 호출하면 아래와 같이 각 객체별로 지정된 변수들의 값이 출력됩니다.

student1 = Student("20200001", "홍길동", 20, "남성", "컴퓨터공학과")
student2 = Student("20200002", "김순자", 21, "여성", "산업디자인공학과")
student3 = Student("20200003", "임꺽정", 23, "남성", "환경공학과")

student1.show()
student2.show()
student3.show()

한편, add_age() 메서드를 실행하기 위해서는 객체가 가지고 있는 변수 외에 외부에서 또다른 변수를 인수로 넣어줘야 합니다. 이 예에서는 offset에 해당하죠. 이때, student1self에 해당되며, offset의 값(예를 들어 5라고 하면)이 offset 인수에 대응됩니다. 이 메서드를 실행한 뒤, student1 객체의 age 변수를 호출하면 기존의 20에 offset만큼이 더해진 25가 출력됩니다.

student1.add_age(5)
student1.age

클래스 변수

클래스를 사용함으로써 각 객체가 가지고 있는 변수는 서로 다르게 유지할 수 있습니다. 그런데 특정 클래스의 모든 인스턴스가 공통적으로 가지는 공유 정보가 필요하면 어떻게 할까요? 이러한 변수를 클래스 변수라고 합니다.

def) 클래스 변수: 해당 클래스에서 전체적으로 공유하는 변수
인스턴스 변수: 하나의 특정 인스턴스에서만 사용되는 변수

class Client:
    client_cnt = 0

    def __init__(self, id, name, age, gender, point):
        self.id = id
        self.name = name
        self.age = age
        self.gender = gender
        self.point = point
        Client.client_cnt += 1

    def show(self):
        print("======고객 정보======")
        print(f"고객 번호: {self.id}")
        print(f"이름: {self.name}")
        print(f"나이: {self.age}")
        print(f"성별: {self.gender}")
        print(f"포인트: {self.point}")
        print(f"현재 총 고객 수: {Client.client_cnt}")

    def __del__(self):
        Client.client_cnt += 1

세 번째 예시를 보겠습니다. Client 클래스에서 클래스 변수는 클래스의 네임스페이스, 즉 생성자 바로 윗 줄에서 지정됩니다. 반대로 인스턴스 변수들은 인스턴스의 네임스페이스, 즉 생성자 이하의 부분에 해당하는 영역에 있습니다.

client1 = Client(1, "홍길동", 20, "남성", 1200)
client2 = Client(2, "김순자", 21, "여성", 300)
client3 = Client(3, "임꺽정", 23, "남성", 700)

client1.show()
client2.show()
client3.show()
print(f"[결과] 현재 총 고객 수: {Client.client_cnt}")

인스턴스 정렬

이렇게 생성된 인스턴스들은 아무 규칙 없이 열거되어 있을 뿐입니다. 만약 이것들을 특정한 기준에 따라 정렬하려면 어떻게 할까요? 간단하게 sorted()의 key 속성의 값으로 익명 함수(람다 구문)를 넣을 수 있습니다. 이러면 point 인스턴스 변수의 값을 기준으로 정렬할 수 있습니다.

key = lambda x: x.point

현재 이 구문은 오름차순으로 정렬하기 때문에, 만약 내림차순으로 정렬하고 싶다면 reverse 키워드를 사용해주면 됩니다.
또다른 방법은 lt 함수의 사용입니다.(주의: 알파벳 소문자 l(L)과 t입니다!) 'Less than'이라는 이름에서 유래한 이 함수는 정렬 기능을 구현하게 해주며, sorted() 함수와 함께 사용되고는 합니다.
그럼 Client 클래스에 lt 메서드를 추가한 후 정렬을 해보죠. 수정된 클래스는 아래와 같습니다.

class Client:
    client_cnt = 0

    def __init__(self, id, name, age, gender, point):
        self.id = id
        self.name = name
        self.age = age
        self.gender = gender
        self.point = point
        Client.client_cnt += 1

    def show(self):
        print("======고객 정보======")
        print(f"고객 번호: {self.id}")
        print(f"이름: {self.name}")
        print(f"나이: {self.age}")
        print(f"성별: {self.gender}")
        print(f"포인트: {self.point}")
        print(f"현재 총 고객 수: {Client.client_cnt}")

    def __del__(self):
        Client.client_cnt += 1

    def __lt__(self, other):
        return self.point < other.point

여기서 결과를 정렬해 출력하면 아래와 같이 나올 겁니다.

client_list = [client1, client2, client3]

result = sorted(client_list, key=lambda x: x.point, reverse=True)
for client in result:
    client.show()

클래스의 상속

맨 처음에 함수와 변수를 일일이 지정하는 것이 번거롭기 때문에 클래스를 정의한다고 했었죠. 그런데 이러한 클래스조차도 여러 개를 쓰다 보면 아주 맥빠지고 손이 많이 가는 일입니다. 특히 그 중에서 일부 또는 전체 메서드를 이어받는 클래스를 가져오려면 그 구문을 다 가져오기는 좀 그렇죠. 따라서 체계적인 개발을 위해 상속(inheritance) 개념을 적용하게 됩니다.
맨 처음에 예로 들었던 학생 정보에 대한 클래스를 다시 떠올려봅시다. 이번에는 교사와 학생을 모두 다루는 프로그램을 짜고 싶다고 해보겠습니다. 이들은 공통적으로 이름과 나이 정보를 가지고 있다고 합니다. 거기다가 인적사항을 출력하는 메서드를 공통적으로 갖게 하고 싶다고 하네요.

class Human:
    def __init__(self, name, age):
        self.name = name
        self.age = age

    def show_human(self):
        print("=====인적 사항=====")
        print(f"이름: {self.name}")
        print(f"나이: {self.age}")

이 클래스를 교사 클래스에 그대로 적용하기 위해 정보를 상속받는 과정을 진행하겠습니다. 클래스의 상속은 정보를 상속해주는 부모 클래스와 상속받는 자식 클래스 간에 진행된다고 표현합니다. 일반적인 클래스의 정의와는 다르게, 자식 클래스를 정의할 때는 클래스 이름 뒤에 (마치 함수를 정의할 때 변수를 괄호 안에 넣는 것처럼) 부모 클래스의 이름을 괄호 안에 넣어줍니다.
여기서 super()라는 메서드가 사용됩니다. 기본적으로 이 메서드는 부모 클래스의 속성과 메서드를 가져올 때 사용합니다. 그 중에서도 super().__init__() 메서드를 호출하면, 부모 클래스의 생성자를 사용하겠다는 뜻으로 풀이됩니다.

class Teacher(Human):
    def __init__(self, name, age, teacher_id, subject, salary):
        super().__init__(name, age)
        self.teacher_id = teacher_id
        self.subject = subject
        self.salary = salary

    def show_teacher(self):
        print("=====교직원 카드=====")
        print(f"교직원 번호: {self.teacher_id}")
        print(f"담당 과목: {self.subject}")
        print(f"월급: {self.salary}")

이러면 Human 클래스에서 이름과 나이만을 상속받으면서, 교사만의 특성인 교직원 번호, 담당 과목, 월급을 출력할 수 있습니다. 그래서 이 객체를 호출하면 두 클래스의 메서드를 모두 사용할 수 있습니다.

teacher = Teacher("김순자", 40, 1, "사회", 450)
teacher.show_human()
teacher.show_teacher()

머신러닝과 클래스에 대한 잡썰

대부분의 머신러닝/딥러닝 교재는 클래스를 빡세게 정의하기보다는, 코드를 절차적으로 열거하는 정도고, 많이 나아가봤자 함수를 정의하는 정도입니다. 문제는 머신러닝/딥러닝 코드를 참고하러 깃허브 등에 들어가보면 절대다수가 클래스 형태로 정의되어 있고, 온갖 클래스가 상속되어 얽혀 있다는 점이죠. 따라서 우리가 직접 클래스를 정의하는 일이 많지 않다고 치더라도, 그 개념 숙지는 반드시 필요합니다. 이외에도 텐서플로, 파이토치 등의 머신러닝 라이브러리는 대부분의 기능을 클래스 형태로 제공하기 때문에 적절한 클래스를 상속받아 사용하게 됩니다.

여러분은 클래스 공부 열심히 하셔서 저처럼 피 보지 마세요 제발....

 

http://bit.ly/3Y34pE0

 

패스트캠퍼스 [직장인 실무교육]

프로그래밍, 영상편집, UX/UI, 마케팅, 데이터 분석, 엑셀강의, The RED, 국비지원, 기업교육, 서비스 제공.

fastcampus.co.kr

 

* 본 포스팅은 패스트캠퍼스 환급 챌린지 참여를 위해 작성되었습니다.