객체 지향 프로그램에 대해서 알아보자

객체 지향 프로그램에 대해서 알아보자

Reference : https://aisolab.github.io/computer%20science/2018/08/09/CS_Object-oriented-programming/ [김보섭님 블로그]

1. 프로그래밍 패러다임

프로그래밍 패러다임으로는 다음 3가지가 대표적이다.

  • 절차 지향 프로그래밍(procedural programming)
  • 객체 지향 프로그래밍(object-oriented programming)
  • 함수형 프로그래밍(fuctional programming)

2. 절차 지향 프로그래밍

절차를 의미하는 procedure는 서브 루틴, 메서드, 함수라고 불린다.
함수는 입력을 받아 연산을 하고 출력을 내보낸다. 함수를 한 번 정의해 두면 다시 호출해서 쓸 수 있고 이름으로 어떤 일을 하는지 쉽게 알 수 있다.
이처럼 함수를 사용해 프로그래밍 하는 것을 절차 지향 프로그래밍이라고 한다.

3. 절차 지향으로 학급 성적 평가 프로그램 만들기

우리가 담임 선생님이 되었다고 가정하고 엑셀에 저장된 학생들의 점수를 이용해 평균과 표준편차를 구하고 전체의 평균과 비교하여 평가하는 프로그램을 만들어 보자.

3.1 openpyxl모듈 설치하기

pip install openpyxl 을 입력한다.

3.2 openpyxl 모듈로 데이터 읽어 들이기

exam.xlsx

name score
greg 95
john 25
yang 50
timothy 15
melisa 100
thor 10
elen 25
mark 80
steve 95
anna 20

function.py

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
from openpyxl import load_workbook
from functools import reduce
import math

def get_data_from_excel(filepath):

wb = load_workbook(filename = filepath)
ws = wb.active
rows = ws.rows
raw_data = {name_cell.value : score_cell.value for name_cell, score_cell in rows}
scores = raw_data.values()
return scores

def get_average(scores):

avrg = reduce(lambda score1, score2 : score1 + score2, scores) / len(scores)
return avrg

def get_variance(scores, avrg):

tmp = 0
for score in scores:
tmp += (score - avrg)**2
else:
var = tmp / len(scores)
return var

def get_std_dev(var):

std_dev = round(math.sqrt(var),1)
return std_dev

def evaluate_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()

def main():
scores = get_data_from_excel(filepath = args.filepath)
avrg = get_average(scores = scores)
var = get_variance(scores = scores, avrg = avrg)
std_dev = get_std_dev(var = var)
evaluate_class(avrg, var, std_dev, args.total_avrg, args.total_std_dev)

if __name__ == '__main__':
main()

메인 함수에는 책과 다른점이 있다. argparse부분이다. argparse 라이브러리를 임포트해서
함수에 argument들을 넣었다. argument를 가지고 좀 더 세밀한 부분을 다뤄볼 수 있게 되었다.

이처럼 함수를 이용하면, 코드가 심플해지고, 쉽게 다시 불러와 사용할 수 있다.
프로그램이 무슨 일을 하는지 알 수 있고, 한눈에 프로그램의 실행 흐름을 파악할 수 있다.
절차 지향의 특징과 장점이라고 할 수 있겠다.

4. 객체 지향 프로그래밍

객체 지향은 ‘현실 세계에 존재하는 객체를 어떻게 모델링할 것인가?’에 대한 물음에서 시작한다.
데이터 사이언티스트들에게 익숙한 표현이 아닌가 싶다.

4.1 캡슐화

현실 세계의 객체를 나타내려면 변수와 함수만 있으면 된다. 객체가 지니는 특성 값에 해당하는 것이 변수이고,
행동 혹은 기능은 함수로 표현할 수 있다. 이처럼 현실 세계를 모델링하거나 프로그램을 구현하는 데 변수와 함수를 가진 객체를 이용하는 패러다임을 객체 지향 프로그래밍이라고 하며, 변수와 함수를 하나의 단위로 묶는 것을 캡슐화라고 한다.

4.2 클래스를 사용해 객체 만들기

객체와 함수에 대해서 사람들은 어떤 중요한 의미를 부여하게 된다. 하지만 컴퓨터의 입장에서는 어떨까?
컴퓨터는 의미가 전달이 되지 않는다. 메모리의 한 단위로만 저장될 뿐이다. 객체라는 메모리 공간을 할당한 다음 객체 안에 묶인 변수를 초기화하고 함수를 호출하는 데 필요한 것이 클래스일 뿐이다.

클래스는 객체를 생성해내는 템플릿이고(그 유명한 붕어빵 틀) 객체는 클래스를 이용해 만들어진 변수와 함수를 가진 메모리 공간이다. 둘은 서로 다른 존재이고 메모리 공간도 다르다.

객체와 매우 유사한 개념으로 인스턴스가 있다. 객체와 인스턴스의 차이점은

  • 객체는 객체 자체에 초점을 맞춘 용어이고(붕어빵)
  • 인스턴스는 이객체가 어떤 클래스에서 만들어졌는지에 초점을 맞춘 용어이다.(어떤 붕어빵 틀에서 나왔니)

사람이라는 클래스를 만들어보면서 이해해보면 쉬울 것이다.
구현 코드는 아래와 같다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class Person:
def __init__(self, name, money):
self.name = name
self.money = money

def give_money(self, other, money):
other.get_money(money)
self.money -= money

def get_money(self, money):
self.money += money

def __str__(self):
return 'name : {}, money : {}'.format(self.name, self.money)
1
2
greg = Person('greg', 5000)
john = Person('john', 2000)
1
2
3
4
5
print(greg, john)
name : greg, money : 5000 name : john, money : 2000
greg.give_money(john, 2000)
print(greg, john)
name : greg, money : 3000 name : john, money : 4000

4.3 파이썬의 클래스

4.2에서 구현한 클래스를 가져와서 살펴보자

1
2
3
4
5
6
7
8
type(Person.__init__)
= <class 'function'>
type(Person.give_momey)
= <class 'function'>
type(Person.get_money)
= <class 'function'>
type(Person.show)
= <class 'function'>

모두 함수라는 충격적인 결과가 나온다. 이번에는 객체가 가진 메서드를 살펴보자

1
2
3
4
5
6
type(g.give_money)
= <class 'method'>
type(g.get_meney)
= <class 'method'>
type(g.show)
= <class 'method'>

객체 g의 메서드는 메서드인 것을 알 수 있다. 비슷한 것 같은데 둘의 차이는 무엇일까?

1
2
3
4
5
6
7
8
dir(g.give_money)

g.give_money.__func__

g.give_money.__self__

g.give_money.__self__ is g

위의 코드를 실행 시켜보면 차이를 확인해 볼 수 있다.
g가 가진 메서드의 속성을 dir을 통해 확인해보면, __func__, __self__가 등장하는 것을 볼 수 있다.

__self__를 확인해 보면 Person객체라고 나온다. __func__는 또한 Person클래스의 give_money()함수라는 것을 확인할 수 있고, __self__가 이 메소드를 가진 객체 자신을 참조하고 있다는 것도 알 수 있다.

객체에서 메서드를 호출할 때 self를 전달하지 않아도 되는 이유를 여기서 알 수 있게 된다. 메서드 내부에 함수와 객체의 참조를 가지고 있으므로, 함수에 직접 객체의 참조를 전달할 수 있기 때문이다.

[혼자서하는 괴발개발 블로그]
https://aisolab.github.io/computer%20science/2018/08/09/CS_Object-oriented-programming/
classmethod와 staticmethod를 한눈에 정리가능한 코드가 있어서
가져와봤다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
class Person:

# 여기에 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
def check_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

def get_money(self, money):
self.money += money

def give_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)

print(Person.check_population())
print(Person._Person__whole_population)
print(mark._Person__whole_population)
print(greg._Person__whole_population)
print(steve._Person__whole_population)

steve._Person__birth()

print(mark._Person__whole_population)
print(greg._Person__whole_population)
print(steve._Person__whole_population)
1
2
3
4
5
6
7
8
3
3
3
3
3
4
4
4

마크와 그렉 스티브가 인스턴스로 만들어졌다. 만들어지자마자 클래스메서드의 __birth가 실행되어서
인구가 총 3명이 된다.
스티브 인스턴스를 통해 클래스 메서드로의 접근이 가능해서
인구가 4가 되었고
인스턴스로 접근해 전역변수 __whole_population을 요청하면 4가 나오게 된다.

4.4 객체 지향으로 은행 입출금 프로그램 만들기

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
class Account:
__num_acnt = 0

@staticmethod
def get_num_acnt():
return Account.__num_acnt

def __init__(self, name, money):
self._user = name
self._balance = money

Account.__num_acnt += 1

def deposit(self, money):
assert money > 0, '금액이 음수입니다.'
self._balance += money

def withdraw(self, money):
assert money > 0, '금액이 음수입니다.'

if self._balance >= money:
self._balance -= money
else:
pass

def transfer(self, other, money):
assert money > 0, '금액이 음수입니다.'
self.withdraw(money)
if self._balance >= 0:
other.deposit(money)
return True
else:
return False

def __str__(self):
return 'user : {}, balance :{}'.format(self._user, self._balance)

4.5 정보 은닉

결론부터 말하자면, 파이썬은 정보 은닉을 지원하지 않는다. 정보은닉은 캡슐화할때 사용된다. 캡슐화하는 과정에서 어떤 멤버와 메서드는 공개해서 유저 프로그래머가 사용할 수 있게 해야하고, 어떤 멤버와 메서드는 숨겨서, 접근하지 못하도록 해야한다. 캡슐화는 그래서 정보 은닉까지 포함하는 개념이다.

파이썬이 그나마 제공하는 방법은 두 가지이다.

  • 숨기려는 멤버 앞에 언더바 두개 붙이기(name mangling)
  • 프로퍼티 기법

첫번째 방법을 사용해보자

1
2
3
4
5
6
7
8
9
10
11
12
class Account:
def __init__(self, name, money):
self.__name = name
self.__balance = money

def get_balance(self):
return self.__balance

def set_balance(self, new_bal):
if new_bal < 0:
return
self.__balance = new_bal
1
2
my_acnt = Account(name = 'hyubyy', money = 5000)
print(my_acnt.__dict__)
1
{'_Account__name': 'hyubyy', '_Account__balance': 5000}

내 계좌에 5000을 넣어놨다.

1
2
my_acnt.__balance = -5000
print(my_acnt.get_balance())

내 계좌에 직접 접근해서 -5000을 하는 코드이다. 그런데
print를 하게되면 결과는 5000이 나오게된다.
어? 정보 은닉이 된게 아닐까?

1
print(my_acnt.__dict__)
1
{'_Account__name': 'hyubyy', '_Account__balance': 5000, '__balance': -5000}

-5000은 __balance라는 형태로 저장되어 있다. 숨겨진 형태로 저장될 때
_Account__balance로 원래의 5000이 따로 저장된다. 클래스 안에서 멤버 앞에 언더바를 두 개 붙이면 이 멤버는 객체가 만들어질 때 이름이 변한다. 하지만 __dict__로 확인이 가능해서, 언제든지 접근해서 변경할 수 있다.

1
my_acnt._Account__balance = 8888
1
print(my_acnt.__dict__)
1
{'_Account__name': 'hyubyy', '_Account__balance': 8888, '__balance': -5000}

다음은 프로퍼티 기법이다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class Account:
def __init__(self, name, money):
self.__name = name
self.balance = money

@property
def balance(self): # getter function
return self.balance

@balance.setter
def balance(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값은 변경되지 않는다.

하지만 프로퍼티 기법 역시 유저가 접근하는 것을 막을 수는 없다.
마찬가지로

1
my_acnt._balance = -3000

으로 바꿔버리면 그만이기 때문이다.
이처럼 파이썬은 완벽한 정보 은닉을 제공하지 않는다.

5. 객체지향으로 다시 만드는 학급 성적 평가 프로그램

이제 사용자 프로그램을 더 심플하게 작성할 수 있게 되었다.
statistics.py

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
from functools import reduce
import math

class Stat:

def get_average(self, scores):

avrg = reduce(lambda score1, score2 : score1 + score2, scores) / len(scores)
return avrg

def get_variance(self, scores, avrg):

tmp = 0
for score in scores:
tmp += (score - avrg)**2
else:
var = tmp / len(scores)
return var

def get_std_dev(self, var):

std_dev = round(math.sqrt(var),1)
return std_dev

datahandler.py

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
from openpyxl import load_workbook
from statistics import Stat

class DataHandler:
evaluator = Stat()

@classmethod
def get_data_from_excel(cls, filepath):

wb = load_workbook(filename = filepath)
ws = wb.active
rows = ws.rows
raw_data = {name_cell.value : score_cell.value for name_cell, score_cell in rows}
scores = raw_data.values()
return scores

def __init__(self, filepath):
self.scores = DataHandler.get_data_from_excel(filepath = filepath)
self.cache = {'scores' : self.scores}

def get_average(self):

if 'average' not in self.cache.keys():
self.cache.update({'average' : DataHandler.evaluator.get_average(self.cache.get('scores'))})
return self.cache.get('average')
else:
return self.cache.get('average')

def get_variance(self):

if 'variance' not in self.cache.keys():
self.cache.update({'variance' : DataHandler.evaluator.get_variance(self.cache.get('scores'), self.get_average())})
return self.cache.get('variance')
else:
return self.cache.get('variance')

def get_std_dev(self):

if 'std_dev' not in self.cache.keys():
self.cache.update({'std_dev' : DataHandler.evaluator.get_std_dev(self.get_variance())})
return self.cache.get('std_dev')
else:
return self.cache.get('std_dev')

def evaluate_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()

def main():
datahandler = DataHandler(filepath = args.filepath)
datahandler.evaluate_class(total_avrg = args.total_avrg, total_std_dev = args.total_std_dev)

if __name__ == '__main__':
main()`

유저 프로그램인 main.py script를 실행시키면 아래와 같다.

1
2
3
4
5
6
7
8
9
10
11
12
$ python main.py --help
usage: 평가프로그램 [-h] filepath total_avrg total_std_dev

엑셀에 저장된 학생들의 점수를 가져와 평균과 표준편차를 구하고, 학년 전체 평균과 비교하는 프로그램

positional arguments:
filepath 엑셀파일 저장경로
total_avrg 학년평균
total_std_dev 학년표준편차

optional arguments:
-h, --help show this help message and exit
1
$ python main.py ./class_1.xlsx 50 25
1
2
평균:51.5, 분산:1240.25, 표준편차:35.2
성적은 평균 이상이지만 학생들의 실력 차이가 크다. 주의 요망!

객체 지향 프로그램에 대해서 알아보자

http://tkdguq05.github.io/2019/01/28/oop/

Author

SangHyub Lee, Jose

Posted on

2019-01-28

Updated on

2023-12-08

Licensed under

Comments