본문 바로가기

공부방/Upstage AI Lab 4기

파이썬 클래스 연습문제 2 - 이제 조금은 알 것 같기도 하다

2024.08.02 - [공부] - 파이썬 클래스에서 2차 멘붕, 다시 개념 잡기... 에 이어서

문제3. 학교 관리 시스템

학교에는 여러 학생들이 있고, 각 학생은 다양한 과목의 성적을 가집니다. 학교 관리 시스템을 구축하여 학생들의 정보를 관리하고, 특정 기준에 따라 우수 학생을 선별할 수 있는 프로그램을 작성하세요.

요구 사항:

  1. Student 클래스를 생성합니다. 이 클래스는 학생의 이름, 학번, 그리고 성적(과목명과 점수의 딕셔너리)을 속성으로 가집니다.
  2. School 클래스를 생성합니다. 이 클래스는 학생 객체의 리스트를 관리하며, 학생을 추가하고, 전체 학생 정보를 출력하는 메서드를 포함해야 합니다.
  3. School 클래스에는 평균 점수가 특정 기준 이상인 학생들만을 선별하여 그 목록을 반환하는 메서드도 포함시킵니다. 이 메서드는 평균 점수 기준값을 매개변수로 받습니다.

아래 템플릿 코드를 사용하여, 출력 예시가 올바르게 나오도록 코드를 완성해보세요!


1. 생각의 흐름 | 어떤 구조로 짤 것인가!

Student 클래스를 만든다. 이 클래스가 생성하는 인스턴스는 이름(name), 학번(st_id), 성적(grade)를 가진다. 

School 클래스에는 전체 학생 리스트가 있다. 전체 학생 리스트에 학생을 추가하는 메서드가 있고, 전체 학생 리스트에서 학생들의 정보를 빼서 출력해주는 메서드가 있다. 또 특별 학생을 담는 리스트도 만든다. 이 리스트에는 특정 조건을 만족하는 학생만 담는다. 특정 조건을 만족하는 학생의 정보를 뽑아주는 메서드도 있다. 

2. 코드로 짜기

처음 내가 짠 Student 클래스다. 실수 투성이이지만 하나씩 되짚어가면서 외우려고 첨부. 

class Student:
    def __init__(self, name, st_id, grade):
        self.name = name
        self.st_id = st_id
        self.grade = grade

    def calculate_ave(self):
        ave = sum(self.grade.value)/len(self.grade.key) 
        return ave

객체는 아래와 같이 만든다. 그래서 grade에는 딕셔너리가 들어가고, 나중에 점수 평균을 사용해야하기 때문에 점수 평균을 구하는 메서드(calculate_ave)를 만들어준다.

student1 = Student("김철수", "A01", {"수학": 90, "과학": 85, "영어": 80})
student2 = Student("이영희", "A02", {"수학": 95, "과학": 92, "영어": 88})
student3 = Student("박민수", "A03", {"수학": 78, "과학": 80, "영어": 82})

 

그런데! calculate_ave 메서드에 문제가 있다. (혼자 깨닫지는 못했고 클로드의 도움을 받았다..ㅎ)
평균 점수(ave) = (딕셔너리 내의 value들을 모두 더한 값) / (딕셔너리의 키 개수) 이렇게 생각을 하고, 
ave = sum(self.grade.value) / len(self.grade.key) 를 했는데 계속 오류가 나는 것!! 딕셔너리에서 값들을 불러올 때에는 .values()와 .keys() 를 써야한다. 이것도 딕셔너리의 메서드이므로 괄호가 필요하다. 그리고 딕셔너리에 들어간 원소의 개수는 굳이 키 개수로 따질 필요가 없다. 그냥 len(self.grade)를 해도 된다. 이렇게 해서 수정된 코드.

class Student:
    def __init__(self, name, st_id, grade):
        self.name = name
        self.st_id = st_id
        self.grade = grade

    def calculate_ave(self):
        ave = sum(self.grade.values())/len(self.grade) #여기 나는 value를 썼는데 values 임!!!
        return ave #int(ave)로 받아도 되고, int를 안쓰면 그냥 float로 받음.

 

이제 School 클래스를 만들러 가보자.

아래도 잘못 쓴 코드인데, 어느 부분이 잘못됐는지 고치면서 공부했다. 

class School:
    def __init__(self):
        self.st_list = [ ]
        self.special_list = [ ] #이건 여기 없어도돼. good_student 메서드를 쓸 때만 임시로 만들면 되는 리스트라서. 

    def add_student(self, student): 
        self.st_list.append(student)
    
    def all_student_info(self):
        for p in st_list(student) # for p in self.st_list 로 고쳐야함. 
            print(f"이름: {p.name}, 학번: {p.st_id}, 성적: {p.grade}")

    def good_student(self, threshold):
        self.special_list = [ ] #특정 점수 이상인 학생들을 담을 리스트는 이곳에서만 쓸꺼라 여기서 정의
        for p in st_list(student) : # for p in self.st_list 로 고쳐야함. 
            stuave = p.calculate_ave 
            #stuave = p.calculate_ave() 이렇게 써야돼. 괄호를 써줘야 메서드가 호출돼. 
            #stuave는 학생의 평균 점수. 
            if stuave >= threshold :
                self.special_list.append((p.name, stuave))
                return self.special_list

1) init 메서드 초기화
일단 School 클래스 안에 전체 학생 리스트와 특정 점수 이상인 학생을 담을 리스트 이렇게 2개가 필요하다. 그래서 init 메서드 안에 두 개를 다 썼다. 전체 학생 리스트는 클래스 내에서 다양한 메서드에서 사용되기 때문에 init 메서드 안에 넣는게 맞다. 그런데 special_list는 good_student라는 메서드 외에는 굳이 쓰이지 않는다. 임시로 잠깐 쓰고 말 리스트라서 init 메서드 안에 집어넣으면 비효율적이다. 그래서 뺀다. 

2) st_list는 리스트 객체이자 속성! 함수 호출처럼 쓸 수 없다!!
def all_student_info(self)를 보면, for p in st_list(student)라고 써놨다. 내가 생각한 건 st_list 학생 전체 리스트 안에 있는 student를 하나씩 꺼내서 p에 집어넣으라는 뜻으로 썼는데. 문법적으로 오류가 있었다. st_list 는 리스트 객체이다. 호출할 수 있는 함수나 메서드가 아니기 때문에 st_list( ) 이런 식으로는 쓰지 못한다. 

⭐⭐⭐속성과 객체⭐⭐⭐

객체는 속성을 가진다. 속성은 객체의 특성이나 상태를 나타낸다. 

school = School( )
School 클래스로 school이라는 객체를 만들었다. 그러면 이제부터 school이라는 객체는 st_list(전체 학생 리스트)라는 속성을 갖는다. st_list의 입장에서 얘기하면, st_list는 School 클래스의 속성으로 사용이 된다. 그러니까 학교 클래스의 특징으로, 학교 클래스가 생성하는 인스턴스에는 전체 학생 리스트가 있다는 말과 동일하다.

이때 속성은 다양한 데이터 타입이 될 수 있다. 지금 예시에서는 st_list가 리스트 타입이며, school.st_list를 하면 School 객체의 st_list속성에 접근한다는 뜻이다. st_list는 속성이지만 리스트 타입을 가진 객체이기도 하다. 그러니까, st_list는 School 클래스가 만든 객체(school)가 가지고 있는 속성(이면서 리스트 객체. 리스트 객체는 그냥 리스트라고 이해하면 된다고..). 헷갈리면 안되는게 st_list는 School의 객체가 아니다. School 클래스가 만든 객체가 가진 객체(리스트)일 뿐.

여기서 한발 더 들어가서, 이 st_list 리스트에는 Student 클래스가 만든 객체들이 들어간다. st_list라는 속성이 다른 클래스의 객체를 포함할 수 있다. 반복해서 말하자면, st_list는 School 클래스의 객체가 가지는 속성이지만, Student라는 다른 클래스의 객체를 담을 수 있다. (원래 그럴 용도로 만든 리스트, 속성) 그리고 이것이 바로 객체 지향 프로그래밍의 강력한 특징! 

student1 = Student("Alice", "A001", {"수학": 90, "영어": 85})
school.st_list.append(student1)  # st_list 속성에 Student 객체 추가

 

내가 지금까지 헷갈렸던 이유가 속성과 객체를 헷갈려서 그랬나보다. 이제 뭔가 알 것 같은 느낌!!! 

⭐ 정리
하나의 클래스에서 생성된 각각의 객체는 여러 가지 속성을 가질 수 있다. 이 속성들은 객체의 상태나 특성을 나타낸다. 클래스의 속성은 모든 데이터 타입이 가능하다. (단순한 숫자나 문자열, 리스트, 딕셔너리 등등) 심지어 다른 클래스의 객체가 될 수도 있다. 

 

돌아와서

for p in st_list(student)는  
st_list가 속성이자 Student의 객체들을 담고 있는 리스트 객체이기 때문에 함수 호출처럼 ( ) 괄호를 쓸 수 없다. 인스턴스의 속성이나 메서드를 참조할 때에는 (대부분) self를 붙여줘야 한다.
for p in st_list(student) -> for p in self.st_list : 
이렇게 고쳐야함. 그 와중에 콜론도 빠뜨렸었네 ㅎㅎㅎ

    def all_student_info(self):
        for p in st_list(student) # for p in self.st_list 로 고쳐야함. 
            print(f"이름: {p.name}, 학번: {p.st_id}, 성적: {p.grade}")

 

그 다음 good_student 메서드를 정의하는 코드를 살펴보자. 무엇을 잘못 썼나~~~

이것도 st_list(student)로 속성을 함수 부르듯이 썼네. self.st_list로 고쳐주기. 

    def good_student(self, threshold):
        self.special_list = [ ] #특정 점수 이상인 학생들을 담을 리스트는 이곳에서만 쓸꺼라 여기서 정의
        for p in st_list(student) : # for p in self.st_list 로 고쳐야함. 
            stuave = p.calculate_ave 
            #stuave = p.calculate_ave() 이렇게 써야돼. 괄호를 써줘야 메서드가 호출돼. 
            #stuave는 학생의 평균 점수. 
            if stuave >= threshold :
                self.special_list.append((p.name, stuave))
                return self.special_list

그리고 p.calculate_ave 를 썼는데, calculate_ave는 Student 클래스에서 정의했던 메서드였다. 메서드를 호출할 때에는 ( ) 괄호를 써줘야 한다. 만약에 괄호 없이 메서드를 쓰면, 메서드 자체를 참조하게 되어 이상한 값이 나오게 된다. 진짜 이상한 값은 아니고, 실제로는 메소드의 주소나 표현이 출력된다. method3을 실행시키면 이런 식으로 이상한 값이 나옴. <bound method Example2.method1 of <__main__.Example2 object at 0x7f9d68a04a90>>

class Example:
    def method1(self):
        print("This is method1")
    
    def method2(self):
        self.method1()  # 괄호를 사용하여 method1 호출
        
    def method3(self):
        print(self.method1)  # 괄호 없이 method1 참조

넘어가서 평균 점수가 특정 점수 이상인 학생을 리스트에 담고 리스트 안의 학생을 출력하는 코드 부분을 보면, 

            if stuave >= threshold :
                self.special_list.append((p.name, stuave))
                return self.special_list

여기서 뭐가 잘못됐는지 한참 찾았다. 하하 들여쓰기가 잘못됐다. return이 if 문 안에 들어가 있엇다. 이걸 for 문 시작하는 위치로 옮겨주기!

 

전체 코드를 정리하면

class Student:
    def __init__(self, name, st_id, grade):
        self.name = name
        self.st_id = st_id
        self.grade = grade

    def calculate_ave(self):
        ave = sum(self.grade.values())/len(self.grade) 
        return ave #int(ave)로 받아도 되고, int를 안쓰면 그냥 float로 받음.
        
class School:
    def __init__(self):
        self.st_list = [ ] 

    def add_student(self, student): 
        self.st_list.append(student)
    
    def all_student_info(self):
        for p in self.st_list : 
            print(f"이름: {p.name}, 학번: {p.st_id}, 성적: {p.grade}")

    def good_student(self, threshold):
        self.special_list = [ ]
        for p in self.st_list :
            stuave = p.calculate_ave() 
            if stuave >= threshold :
                self.special_list.append((p.name, stuave))
        return self.special_list 

# 예제 실행

school = School()
student1 = Student("김철수", "A01", {"수학": 90, "과학": 85, "영어": 80})
student2 = Student("이영희", "A02", {"수학": 95, "과학": 92, "영어": 88})
student3 = Student("박민수", "A03", {"수학": 78, "과학": 80, "영어": 82})

school.add_student(student1)
school.add_student(student2)
school.add_student(student3)

print("모든 학생 정보:")
school.all_student_info()

print("\n평균 85점 이상인 학생들:")
good_students = school.good_student(85)
for name, average in good_students:
    print(f"{name}: 평균 {average}점")

실행하면 아래와 같은 결과가 나온다. 

모든 학생 정보:
이름: 김철수, 학번: A01, 성적: {'수학': 90, '과학': 85, '영어': 80}
이름: 이영희, 학번: A02, 성적: {'수학': 95, '과학': 92, '영어': 88}
이름: 박민수, 학번: A03, 성적: {'수학': 78, '과학': 80, '영어': 82}

평균 85점 이상인 학생들:
김철수: 평균 85.0점
이영희: 평균 91.66666666666667점

 

소수점 아래 두 자리만 나오게 하려면 :.2f 를 넣기.

그리고 강사쌤이 제공해준 솔루션도 첨부한다. 

# 템플릿 코드
class Student:
    def __init__(self, name, student_id, grade):
        self.name = name
        self.student_id = student_id
        self.grade = grade

    def calculate_average(self):
        return sum(self.grade.values()) / len(self.grade)


class School:
    def __init__(self):
        self.students = []

    def add_student(self, person): #student 를 person으로 바꿔봄.. 
        self.students.append(person) #student 를 person으로 바꿔봄.. 

    def print_students(self):
        print('전체 학생')
        for student in self.students: 
            print(f"{student.name}({student.student_id})")

    def get_top_students(self, threshold):
        return [student for student in self.students if student.calculate_average() >= threshold]

# 예제 사용
school = School()
school.add_student(Student("Tom", "A01", {"수학": 100, "과학": 95, "영어" : 50}))
school.add_student(Student("Bob", "A02", {"수학": 85, "과학": 95, "영어" : 66}))
school.add_student(Student("Lily", "A03", {"수학": 77, "과학": 88, "영어" : 100}))
school.add_student(Student("Kim", "A04", {"수학": 92, "과학": 100, "영어" : 70}))

school.print_students()

top_students = school.get_top_students(85)
print("\n평균 점수 85점 이상 학생:")
for student in top_students:
    print(f"{student.name}, {student.student_id}, 평균 점수: {student.calculate_average():.2f}.")