절차를 의미하는 procedure는 서브 루틴, 메서드, 함수라고 불린다. 함수는 입력을 받아 연산을 하고 출력을 내보낸다. 함수를 한 번 정의해 두면 다시 호출해서 쓸 수 있고 이름으로 어떤 일을 하는지 쉽게 알 수 있다. 이처럼 함수를 사용해 프로그래밍 하는 것을 절차 지향 프로그래밍이라고 한다.
3. 절차 지향으로 학급 성적 평가 프로그램 만들기
우리가 담임 선생님이 되었다고 가정하고 엑셀에 저장된 학생들의 점수를 이용해 평균과 표준편차를 구하고 전체의 평균과 비교하여 평가하는 프로그램을 만들어 보자.
defevaluate_class(avrg, var, std_dev, total_avrg, total_std_dev): """ evaluate_class(avrg, var, std_dev, total_avrg, total_std_dev) -> None Args: avrg : 반평균 var : 반분산 std_dev : 반표준편차 total_avrg : 학년평균 total_std_dev : 학년분산 """ print("평균:{}, 분산:{}, 표준편차:{}".format(avrg, var, std_dev)) if avrg < total_avrg and std_dev > total_std_dev: print('성적이 너무 저조하고 학생들의 실력 차이가 너무 크다.') elif avrg > total_avrg and std_dev > total_std_dev: print('성적은 평균 이상이지만 학생들의 실력 차이가 크다. 주의 요망!') elif avrg < total_avrg and std_dev < total_std_dev: print('학생들의 실력 차이는 크지 않지만 성적이 너무 저조하다. 주의 요망!') elif avrg > total_avrg and std_dev < total_std_dev: print('성적도 평균 이상이고 학생들의 실력 차이도 크지 않다.')
main.py
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
from functions import * import argparse parser = argparse.ArgumentParser(prog = '평가프로그램', description = '엑셀에 저장된 학생들의 점수를 가져와 평균과 표준편차를 구하고, 학년 전체 평균과 비교하는 프로그램') parser.add_argument('filepath', type = str, help = '엑셀파일 저장경로') parser.add_argument('total_avrg', type = float, help = '학년평균') parser.add_argument('total_std_dev', type = float, help = '학년표준편차') args = parser.parse_args()
메인 함수에는 책과 다른점이 있다. argparse부분이다. argparse 라이브러리를 임포트해서 함수에 argument들을 넣었다. argument를 가지고 좀 더 세밀한 부분을 다뤄볼 수 있게 되었다.
이처럼 함수를 이용하면, 코드가 심플해지고, 쉽게 다시 불러와 사용할 수 있다. 프로그램이 무슨 일을 하는지 알 수 있고, 한눈에 프로그램의 실행 흐름을 파악할 수 있다. 절차 지향의 특징과 장점이라고 할 수 있겠다.
4. 객체 지향 프로그래밍
객체 지향은 ‘현실 세계에 존재하는 객체를 어떻게 모델링할 것인가?’에 대한 물음에서 시작한다. 데이터 사이언티스트들에게 익숙한 표현이 아닌가 싶다.
4.1 캡슐화
현실 세계의 객체를 나타내려면 변수와 함수만 있으면 된다. 객체가 지니는 특성 값에 해당하는 것이 변수이고, 행동 혹은 기능은 함수로 표현할 수 있다. 이처럼 현실 세계를 모델링하거나 프로그램을 구현하는 데 변수와 함수를 가진 객체를 이용하는 패러다임을 객체 지향 프로그래밍이라고 하며, 변수와 함수를 하나의 단위로 묶는 것을 캡슐화라고 한다.
4.2 클래스를 사용해 객체 만들기
객체와 함수에 대해서 사람들은 어떤 중요한 의미를 부여하게 된다. 하지만 컴퓨터의 입장에서는 어떨까? 컴퓨터는 의미가 전달이 되지 않는다. 메모리의 한 단위로만 저장될 뿐이다. 객체라는 메모리 공간을 할당한 다음 객체 안에 묶인 변수를 초기화하고 함수를 호출하는 데 필요한 것이 클래스일 뿐이다.
클래스는 객체를 생성해내는 템플릿이고(그 유명한 붕어빵 틀) 객체는 클래스를 이용해 만들어진 변수와 함수를 가진 메모리 공간이다. 둘은 서로 다른 존재이고 메모리 공간도 다르다.
객체와 매우 유사한 개념으로 인스턴스가 있다. 객체와 인스턴스의 차이점은
객체는 객체 자체에 초점을 맞춘 용어이고(붕어빵)
인스턴스는 이객체가 어떤 클래스에서 만들어졌는지에 초점을 맞춘 용어이다.(어떤 붕어빵 틀에서 나왔니)
classPerson: # 여기에 class variable (또는 class member) # instance 모두가 공유하는 동일한 값 # instance를 생성하지않고도, class만 선언한 상태에서 호출이 가능하다. # oop에서 global variable을 대체하기위하여 사용 __whole_population = 0 # name mangling technique 사용, 외부에서 ## Person.__whole_population으로 접근 불가 ## Person._Person__whole_population으로 접근 가능 # class method # oop에서 global에 선언된 function을 대체하기위해 사용 # 대체 생성자를 만들 때, 더 많이씀 (여긴 대체생성자 구현하지 않음) @classmethod def__birth(cls): cls.__whole_population += 1 @staticmethod defcheck_population(): return Person.__whole_population # instance method def__init__(self, name, money): # 생성자(constructor) Person.__birth() # instance variable (또는 instance member) # instance마다 값이 다른 변수, instance가 가지는 고유한 값 # 여기에서는 self.name, self.age self.name = name self.money = money
defget_money(self, money): self.money += money defgive_money(self, other, money): # message passing # 다른 인스턴스(객체)랑 상호작용을 할 때, 상대 인스턴스(객체)의 인스턴스 변수를 바꿔야한다면 other.get_money(money) # 이렇게하세요 # other.money += money 이렇게하지마세요 self.money -= money def__str__(self): return'{} : {}'.format(self.name, self.money)
주석처리도 너무 잘되어 있기 때문에 쭉 보고 따라 쳐보면서 이해하면 아주 좋을 것 같다.
클래스 메서드의 특징 중 하나는 인스턴스를 만들지 않고도 불러낼 수 있다는 것이다.
1 2
# 클래스 메소드는 인스턴스를 생성하지않고도 호출할 수 있다. print(Person.check_population())
1
0
만들어놓은 클래스를 사용해보자.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
# 클래스 변수는 인스턴스간에 모두 공유한다. # 인스턴스를 통해서도 클래스 변수나 클래스 메소드를 호출할 수 있다. mark = Person('mark', 5000) greg = Person('greg', 3000) steve = Person('steve', 2000)
마크와 그렉 스티브가 인스턴스로 만들어졌다. 만들어지자마자 클래스메서드의 __birth가 실행되어서 인구가 총 3명이 된다. 스티브 인스턴스를 통해 클래스 메서드로의 접근이 가능해서 인구가 4가 되었고 인스턴스로 접근해 전역변수 __whole_population을 요청하면 4가 나오게 된다.
결론부터 말하자면, 파이썬은 정보 은닉을 지원하지 않는다. 정보은닉은 캡슐화할때 사용된다. 캡슐화하는 과정에서 어떤 멤버와 메서드는 공개해서 유저 프로그래머가 사용할 수 있게 해야하고, 어떤 멤버와 메서드는 숨겨서, 접근하지 못하도록 해야한다. 캡슐화는 그래서 정보 은닉까지 포함하는 개념이다.
-5000은 __balance라는 형태로 저장되어 있다. 숨겨진 형태로 저장될 때 _Account__balance로 원래의 5000이 따로 저장된다. 클래스 안에서 멤버 앞에 언더바를 두 개 붙이면 이 멤버는 객체가 만들어질 때 이름이 변한다. 하지만 __dict__로 확인이 가능해서, 언제든지 접근해서 변경할 수 있다.
classAccount: def__init__(self, name, money): self.__name = name self.balance = money
@property defbalance(self): # getter function return self.balance @balance.setter defbalance(self, money): # setter function if money < 0: return self._balance = money if __name__ == '__main__': my_acnt = Account('greg', 5000) my_acnt.balance = -3000 print(my_acnt.balance)
실행 결과는 5000이다. 놀랍게도 balance가 2000으로 나오지 않았다.
위의 코드에서 특이한 점이 있는데 @property와 @balance.setter라는 부분이다. @property를 붙여주면 이 함수는 getter 함수가 되며, @balance.setter는 setter함수로 사용된다. 따라서, my_acnt 객체에는 balance라는 멤버가 없다. balance라는 이름의 getter와 setter밖에 존재 하지 않는다. my_acnt.balance = -3000은 값을 변경하는 것처럼 보이지만, 실제로는 setter가 실행되고 그 결과로 _balance값은 변경되지 않는다.
defevaluate_class(self, total_avrg, total_std_dev): avrg = self.get_average() var = self.get_variance() std_dev = self.get_std_dev() print("평균:{}, 분산:{}, 표준편차:{}".format(avrg, var, std_dev)) if avrg < total_avrg and std_dev > total_std_dev: print('성적이 너무 저조하고 학생들의 실력 차이가 너무 크다.') elif avrg > total_avrg and std_dev > total_std_dev: print('성적은 평균 이상이지만 학생들의 실력 차이가 크다. 주의 요망!') elif avrg < total_avrg and std_dev < total_std_dev: print('학생들의 실력 차이는 크지 않지만 성적이 너무 저조하다. 주의 요망!') elif avrg > total_avrg and std_dev < total_std_dev: print('성적도 평균 이상이고 학생들의 실력 차이도 크지 않다.')
main.py
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
from datahandler import DataHandler import argparse parser = argparse.ArgumentParser(prog = '평가프로그램', description = '엑셀에 저장된 학생들의 점수를 가져와 평균과 표준편차를 구하고, 학년 전체 평균과 비교하는 프로그램') parser.add_argument('filepath', type = str, help = '엑셀파일 저장경로') parser.add_argument('total_avrg', type = float, help = '학년평균') parser.add_argument('total_std_dev', type = float, help = '학년표준편차') args = parser.parse_args()