TigerCow.Door


안녕하세요. 문범우입니다.

이번 포스팅에서는 SOLID 원칙에 대해서 알아봅니다. SOLID는 5가지 원칙을 줄여서 말하는 내용인데, 한번에 5가지 모두를 알아보지 않고, 각 원칙에 대해서 코드로 함께 살펴보며 총 5개의 포스팅으로 나누어 설명드릴 예정입니다.



0. SOLID란?


우선 솔리드, SOLID는 객체 지향 프로그래밍 및 설계의 다섯가지 기본 원칙을 이야기한다. 프로그램이 시간이 지나도 유지 보수 및 확장이 쉬울 수 있도록 하기 위한 원칙이며, 우리가 학습하고자 했던 클린코드를 위한 원칙이기도 하다. 


조금 더 구체적으로, SOLID는 아래와 같은 5가지 원칙을 이야기 한다.


1. Single Responsibility Principle(단일 책임 원칙)


2. Open/Closed Principle(개방/폐쇄 원칙)


3. Liskov Substitution Principle(리스코프 치환 원칙)


4. Interface Segregation Principle(인터페이스 분리 원칙)


5. Dependency Inversion Principle(의존관계 역전 원칙)


이제 우리는 각 원칙에서 강조하는 것과, 실제로 그것을 코드로서 간단하게 살펴보며 이해를 해보도록 하겠다.




1. SRP(Single Responsibility Principle)


단일 책임 원칙(SRP)이란 하나의 클래스는 하나의 책임만 가지며, 그 책임에 대해 완전히 캡슐화 해야 함을 말한다. 

클래스가 하나의 책임만 가진다는 것은, 하나의 클래스는 하나의 일을 담당하고 있으며, 클래스를 변경해야 할 이유는 오직 한가지가 존재한다는 것으로 생각할 수 있다. 이러한 원칙은, 코드의 응집력을 높이는데 도움을 준다. 


간혹 하나의 클래스가 다양한 책임을 지고 있는 경우가 있는데, 이러한 클래스(객체)를 일컬어 신(god) 객체라고 이야기 한다. 이러한 신 객체가 존재하면 시스템에 대한 유지보수가 어려워진다.


어떤 면에서 단일 책임 원칙은, 관계형 데이터베이스 설계에서의 정규화 개념과 유사하다고 생각할 수 있다. 만약 객체의 속성이나 메서드 중에서 서로 특성이 다른 요소(그룹)가 발견되면 그러한 것들을 적절히 분리시켜야 한다.


이제 실제로 코드를 통해 너무 많은 책임을 가진 클래스와, 이를 단일 책임 원칙에 따라 분리한 예제를 살펴보도록 하자.


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
class Course:
    """너무 많은 책임을 가진 Course 클래스"""
    
    def __init__(self, code, name, schedule, pf):
        self.code = code
        self.name = name
        self.schedule = schedule
        self.pf = pf
    
    def connect(self, con)->bool:
        """DB connecting"""
        pass
    
    def close(self)->bool:
        """DB close"""
        pass
    
    def get_course_by_pf(self, professor)->list:
        """professor로 강의 찾기 함수"""
        pass
    
    def save_course(self, Course)->bool:
        """DB에 course 저장하기"""
        pass
    
    def update_course(self, code)->bool:
        """특정 code를 가진 course를 DB update 하기"""
        pass
cs

code: https://github.com/doorBW/python_clean_code


먼저 위의 Course 클래스를 살펴보자.

해당 Course 클래스는 너무 많은 책임을 가지고 있다. 클래스의 생성자 __init__함수가 정의되어 있으면서 DB접근과 관련된 함수들도 함께 정의되어 있다.

즉, Course 객체의 속성들에 대한 관리를 하고 있으면서, Course의 DB 내용에 대한 관리도 하고 있다. 이러한 경우 DB에 대한 시스템 변경이 있을 때나, Course의 속성에 대한 변경이 있을 때 동일한 클래스를 수정하고 서로 영향이 없도록 신경써야 한다.

따라서 위와 같은 클래스는 아래와 같이 분리할 수 있다.


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
class Course:
    """SRP원칙을 준수한 Course 클래스"""
    
    def __init__(self, code, name, schedule, pf):
        self.code = code
        self.name = name
        self.schedule = schedule
        self.pf = pf
 
class CourseDB:
    """SRP원칙을 준수한 CourseDB 클래스"""
    def __init__(self, con):
        self.con = con
    
    def connect(self)->bool:
        """DB connecting"""
        pass
    
    def close(self)->bool:
        """DB closing"""
        pass
    
    def get_course_by_pf(self, professor)->list:
        """professor로 강의 찾기 함수"""
        pass
    
    def save_course(self, Course)->bool:
        """DB에 course 저장하기"""
        pass
    
    def update_course(self, code)->bool:
        """특정 code를 가진 course를 DB update 하기"""
        pass
cs

code: https://github.com/doorBW/python_clean_code


이전과 달리 Course 클래스와 CourseDB 클래스로 나누어, DB를 관리하는 클래스를 따로 분리하였다. 

이렇게 됨으로써 각 클래스가 자신이 담당한 하나의 일에 대해서만 책임을 지게 된다.


이번에는 다른 예제를 살펴보자.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class Human:
    """추상화 과정이 필요한 Human 클래스"""
    def __init__(self, name, sex):
        self.name = name
        self.sex = sex
        
    def go_restroom(self):
        """화장실 가는 함수"""
        if(self.sex == '남자'):
            print("남자화장실로 간다.")
        elif(self.sex == '여자'):
            print("여자화장실로 간다.")
        else:
            print("성별을 지정해주세요.")
 
cs

code: https://github.com/doorBW/python_clean_code


위의 Human 클래스에서는 객체 생성시에 성별을 정의하게 되어있다. 그리고 go_restroom에서는 성별에 따라 분기처리하여 로직을 처리한다. 즉, Human클래스는 성별이 다른 두가지 케이스에 대한 책임을 동시에 가지고 있다. 이러한 경우 Human 클래스를 다음과 같이 추상화시킴으로써 해결할 수 있다.


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
from abc import *
 
class HumanBase(metaclass=ABCMeta):
    def __init__(self, name):
        self.name = name
    
    @abstractmethod
    def go_restroom(self):
        pass
    
class Male(HumanBase):
    def __init__(self, name):
        super().__init__(name)
        self.sex = "남자"
    
    def go_restroom(self):
        print("남자화장실로 간다.")
        
class Female(HumanBase):
    def __init__(self, name):
        super().__init__(name)
        self.sex = "여자"
        
    def go_restroom(self):
        print("여자화장실로 간다.")
cs

code: https://github.com/doorBW/python_clean_code


위와 같이 추상클래스, HumanBase 클래스를 만들어 주었다. 기본적으로 생성시에는 이름만 받고 이를 상속받은 클래스들은 go_restroom을 구현하게끔 하였다.

그리고 HumanBase를 상속받는 Male 클래스와 Female 클래스를 정의함으로써, 성별이 다른 경우에 대해 각 클래스가 유일한 책임을 가지도록 하였다.


이렇게 우리가 SOLID에서 단일 책임 원칙에 대해 알아보면서 대표적인 두가지 예제를 살펴보았다.

첫번째, Course 클래스에서는 단일 클래스에서 너무 많은 액션, 업무를 담당하고 있어 이를 분리함으로써 단일 클래스가 하나의 책임만 가지도록 하였다.

두번째, Human 클래스에서는 보다 높은 추상과 과정을 통해서 각 클래스가 하나의 책임만 가지도록 설정하였다.


이렇게 단일 책임 원칙을 지킨 경우에 클래스에 대한 외부 영향도를 최소화 할 수 있으며, 결국 유지보수나 확장 면에서 보다 효율적일 수 있다.


블로그 이미지

Tigercow.Door

Back-end / Python / Database / AI / Algorithm / DeepLearning / etc

댓글을 달아 주세요


안녕하세요. 문범우입니다.

이번 포스팅에서는 파이썬에서의 인자(Argument)에 대해서 알아보도록 하겠습니다.

파이썬에서 인자가 어떻게 작동하는지, 그리고 가변인자와 같은 개념들도 함께 알아보도록 하겠습니다.



0. 인자(Argument)와 매개변수(Parameter)


본격적으로 인자에 대해서 알아보기전에, 자주 헷갈릴 수 있는 인자와 매개변수에 대해서 잠깐 짚고 넘어가도록 하자. 물론 이는 파이썬언어 뿐만이 아니라 다른 언어에서도 혼동되서 사용될 수 있는 개념이다.


1
2
3
4
5
def func1(param1, param2):
    print(f"param1:{param1}, param2:{param2}")
 
func1("AA","B")
# param1:AA, param2:B
cs


위의 코드를 살펴보며 이야기해보자.

함수 func1에는 param1과 param2가 전달되도록 정의되어있다. 이렇게 함수가 정의되는 내용에 포함되는 특성을 매개변수(Parameter)라고 한다. 즉, 함수 func1는 2개의 매개변수, param1과 param2를 전달받아서 이를 출력하는 기능을 하는 것이다.

그리고 4번 라인에서는 func1에 "AA"라는 값과 "B"라는 값을 전달하며 함수를 호출하고 있다. 이때 함수에 전달하는 값을 인자(Argument)라고 한다.


이러한 차이에 의해서 매개변수(Parameter)는 변수(Variable)로 보아야 하며, 인자(Argument)는 값(Value)로 보아야한다. 두 개념이 비슷하다고 생각할 수 있으나, 함수에 대해서 이야기를 할때에는 구분해서 사용해야 혼동되지 않을 수 있다.




1. 파이썬에서 인자의 전달방식


앞에서 인자에 대해서 설명할 때, 함수를 호출하며 값으로 전달되는 것이라고 설명했다. 하지만 사실 함수에 '값(Value)'로 전달되는 것은 특정 상황에서만이다.

함수에 인자가 전달될 때에는, 실제로 그 값이 넘어가는 "값에 의한 호출(Call by Value)"와 값이 참조하고 있는 참조값이 넘어가는 "참조에 의한 호출(Call by Reference)" 두가지가 존재한다.

(만약 당신이 C언어를 했다면 포인터를 공부하면서 이 개념을 접했을 수도 있다.)


각각의 호출방법이 어떤 차이가 있는지 먼저 코드로 살펴보자.


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
def call_by_value(param1:int)->None:
    param1 += 1
    print(f"call by value: {param1}")
    
def call_by_ref(param2:list)->None:
    param2[0+= 1
    print(f"call by ref: {param2}")
    
val1 = 3
val2 = [3]
 
print(f"before val1:{val1}")
call_by_value(val1)
print(f"after val1:{val1}")
print("- - - - - - - - - - ")
print(f"before val2:{val2}")
call_by_ref(val2)
print(f"after val2:{val2}")
# before val1:3
# call by value: 4
# after val1:3
# - - - - - - - - - - 
# before val2:[3]
# call by ref: [4]
# after val2:[4]
cs


위의 코드에서는 call_by_value 함수와 call_by_ref함수를 정의하여 사용하고 있다. 각각의 함수가 받는 매개변수의 타입은 다르지만, 그 이유에 대해서는 뒤에서 이야기하겠다. 우선 "값에 의한 호출"과 "참조에 의한 호출"에 대한 차이를 이해해보자.


우리가 주목해야 할 것은 19번줄부터 25번줄까지의 결과이다. val1과 val2 모두 함수를 거치기 이전에 출력을 한 다음, 함수 호출시 함수 내부에서 출력한 후에, 함수 호출이 끝난 후에 출력을 한다.

먼저 val1의 값을 보면, 함수 호출 이전에 3이란 값을 출력하고, 함수 내부에서 출력될 때에는 1을 더한 4가 출력되고 있다. 그리고 함수가 끝난 다음에는 값이 변화하지 않고 그대로 3을 출력한다. 즉, 함수에서 더한 1이 val1에 영향을 미치지 않았다.

그럼 이번에는 val2의 값을 살펴보자. 당연히 함수 호출 이전에는 3을 출력했다. 그리고 함수 내부에서 출력될 때에는 1을 더한 4가 출력되었다. 여기까지는 val1과 같았지만, 함수 호출이 끝난 뒤 val2를 출력했을 때에는, val1과 달리 기존의 val2=3값에서 변화가 되어 4를 출력하였음을 볼 수 있다.


즉, val1은 함수에서 내부적으로 1을 더했지만, 기존의 값에 영향을 미치지 않았고, val2는 함수에서 더한 1이 실제 값이 영향을 미쳤다.

이것이 바로 "값에 의한 호출"과 "참조에 의한 호출"에 대한 차이로 발생한 것이다.


값에 의한 호출을 할 때에는, 함수로 인자가 전달될 때 동일한 "값"을 가진 객체를 복사하여 함수에 전달한다. 즉, 위의 코드에서 val1이 call_by_value함수에 전달될 때에는 사실 val1자체가 전달된 것이 아니고, val1이 가진 값을 동일하게 가진 또다른 객체가 함수로 전달된 것이다. 그리고 함수에서 1을 더한것은 val1이 아니고 val1과 똑같은 값을 가진 "val1과 다른 객체"에 더한 것이다. 이로 인해 함수에서 전달받은 값에 대해 변화를 시도해도 기존의 val1값은 변화가 없는 것이다.


이와 달리, 참조에 의한 호출을 할 때에는, 함수로 인자가 전달될 때 실제로 인자가 가진 "참조 값"을 전달한다. 참조 값을 전달한다는 것은 실제로 인자 객체를 그대로 전달한다고 생각해도 된다. 즉, 값에 의한 호출에서와 달리 인자를 복사한 객체를 전달하는 것이 아니라, 말 그대로 인자 그 자체를 전달하는 것이다. 따라서 함수내부에서 전달받은 인자에 대해 변화를 주면, 실제로 그 인자에 영향이 있게 된다.


이것이 "값에 의한 호출"과 "참조에 의한 호출"의 차이이다.


C언어의 경우에는 인자를 값에 의한 호출로 전달할 것인지, 참조에 의한 호출로 전달할 것인지를 명시적으로 정해줄 수 있다. 하지만 파이썬 같은 경우는 이를 명시적으로 나타내지 않는다.

그럼 파이썬에서는 "값에 의한 호출"과 "참조에 의한 호출"을 어떻게 구분할 수 있을까?

위의 코드를 통해 짐작했을 수도 있다. 파이썬에서는 따로 호출에 대한 명시적인 구분을 두지 않고, 함수에 전달되는 인자의 타입(type)에 의해서 결정된다.

변수 타입에는 불변형(immutable) 객체와, 가변형(mutable) 객체가 있다. 즉 값의 수정이 허용되지 않는 변수 타입이 있으며, 값의 수정이 허용되는 변수 타입이 있다.

이를 통해, 함수에 전달되는 인자가 불변형 객체, 값의 수정이 허용되지 않는 변수타입이라면 이는 "값에 의한 호출"로 함수에 전달된다. 하지만 함수에 전달되는 인자가 가변형 객체, 값의 수정이 허용되는 변수타입이라면 이는 "참조에 의한 호출"로 함수에 전달된다.

때문에 이를 파이썬 공식 문서에서는 call by value나 call by reference라는 설명이 아닌, call by assignment라고 설명하고 있다. 함수에 할당되는 변수의 타입에 따라서 그 방식이 달라지기 때문이다.

파이썬에서 불변형객체, 가변형객체는 아래와 같이 나뉜다.


* 불변형(immutable) 객체

int, float, str, tuples 등


* 가변형(mutable) 객체

list, set, dict 등




2. 가변 인자


파이썬에서는 다른 언어와 같이, 가변인자 함수를 지원한다. 가변인자 함수라 함은 인자의 개수가 정해지지 않은 함수라고 생각하면 된다.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
def args_test_func1(a, b, c):
    print("* call args_test_func1")
    print(f"a:{a}, b:{b}, c:{c}, a+b+c:{a+b+c}")
    print("* END args_test_func1", end='\n\n')
    
def args_test_func2(a, b, *args):
    print("* call args_test_func2")
    print(f"a:{a}, b:{b}, args:{args}, a+b+c:{a+b+sum(args)}")
    print("* END args_test_func2", end='\n\n')
    
args_test_func1(1,2,3)
args_test_func2(1,2,3)
args_test_func2(1,2,3,4,5,6)
# * call args_test_func1
# a:1, b:2, c:3, a+b+c:6
# * END args_test_func1
 
# * call args_test_func2
# a:1, b:2, args:(3,), a+b+c:6
# * END args_test_func2
 
# * call args_test_func2
# a:1, b:2, args:(3, 4, 5, 6), a+b+c:21
# * END args_test_func2
cs


위의 코드에서 2개의 함수를 정의했다. args_test_func1 함수는 우리가 기존에 보던 함수처럼 a,b,c 3개의 매개변수를 정의한 함수이다. 아래 결과에서도 3개의 인자를 전달해 함수가 올바르게 기능했다.

이와 달리 args_test_func2 함수에서는 a,b라는 매개변수와 함께, *args 라는 매개변수를 정의하였다. 이렇게 *를 이용한 매개변수는 가변인자를 받을 수 있는 것으로 해석된다. 즉, 개수가 정해지지 않은 인자를 args라는 이름으로 받아서 처리하겠다는 것이다.

실제로 13번 줄에서 3개 이상의 인자를 함수에 전달했는데, 3부터 6까지는 args라는 이름으로 받아서 처리한 것을 볼 수 있다. 약간의 차이점은 args로 받은 인자는 튜플로 받아서 처리했다는 점이다.


이렇게 *를 사용하는 것을 패킹(packing)한다고 말하기도 하는데, 다음의 코드를 보면 그 의미가 더 와닿을 것이다.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
def packing_func1(a, b, c):
    print(f"a:{a}, b:{b}, c:{c}, a+b+c:{a+b+c}")
    
packing_list = [1,2,3]
packing_func1(packing_list)
# ---------------------------------------------------------------------------
# TypeError                                 Traceback (most recent call last)
# <ipython-input-15-aaa44dbee827> in <module>
#       3 
#       4 packing_list = [1,2,3]
# ----> 5 packing_func1(packing_list)
 
# TypeError: packing_func1() missing 2 required positional arguments: 'b' and 'c'
 
packing_func1(*packing_list)
# a:1, b:2, c:3, a+b+c:6
 
cs


packing_func1은 이전와 같이 3개의 매개변수를 정의한 함수이다. 그리고 이 함수를 호출하기 위해 3개의 int값을 가지는 리스트, packing_list를 정의했다. 그리고 해당 리스트를 그대로 인자로 전달하면 에러가 발생한다. 타입의 맞고 틀림을 떠나서, packing_func1에서는 3개의 인자가 필요한데, 1개의 인자만 전달했기 때문이다.

이를 해결하기 위해서는 파이썬의 *를 이용한 패킹 기법을 사용하면 된다. 즉, 15번 줄과 같이 packing_list를 *를 통해 패킹함으로써 3개의 인자에 대응하는 값을 전달할 수 있다.




3. 가변 키워드 인자


가변 키워드 인자는 위에서 알아본 가변 인자와 비슷하다. 가변인자에서는 별표( * )를 하나 사용했지만, 가변 키워드 인자에서는 별표( * )를 2개 사용한다.


1
2
3
4
5
def kwargs_test_func1(**kwagrs):
    print(f"kwagrs:{kwagrs}")
    
kwargs_test_func1(key="value", test="wow")
# kwagrs:{'key': 'value', 'test': 'wow'}
cs


가변 키워드 인자가 가변 인자 개념과 다른 점은, **kwagrs로 정의된 매개변수는 인자로 받은 값을 "딕셔너리"형태로 패킹한다는 것이다.

따라서 **가 붙은 매개변수는 딕셔너리형태로 함수 내에서 활용할 수 있다.

또한 가변 인자에서와 같이, 기존의 딕셔너리 자료형을 함수에 전달할 때, 다음과 같이 사용할 수 있다.


1
2
3
4
5
6
7
8
9
def kwargs_test_func2(a, b, c):
    print(f"a:{a}, b:{b}, c:{c}, a+b+c:{a+b+c}")
    
kwagrs_dict = {
    'a'1'b'2'c'3
}
 
kwargs_test_func2(**kwagrs_dict)
# a:1, b:2, c:3, a+b+c:6
cs



블로그 이미지

Tigercow.Door

Back-end / Python / Database / AI / Algorithm / DeepLearning / etc

댓글을 달아 주세요



안녕하세요. 문범우입니다.

이번 포스팅에서는 상속과 컴포지션에 대해서 알아보도록 하겠습니다.

어떤 경우에 상속을 쓰는 것이 올바른 것인지, 그리고 파이썬에서 상속과 파이썬을 어떻게 사용하는지에 대해서 코드로 함께 살펴보겠습니다.



1. 상속(Inheritance)


상속이란 기본적으로 자신이 가진 속성과 메소드를 물려주는 클래스(부모클래스, Parent class, Super class)와 이를 물려받는 클래스(자식클래스, Child class, Sub class)로 이루어진다.

객체 지향적 소프트웨어를 디자인하고 구현할 때 상속은 중요한 개념으로, 또 자주 사용된다. 하지만 이를 다시 생각해보자. 상속개념을 활용하면 부모클래스와 자식클래스간에 강력한 결합력(Coupling)이 발생하게 된다.

좋은 코드, 유지보수를 위한 코드를 생각할 때 결합력을 낮추고, 응집도는 높여야 한다고 했는데, 이러한 점을 고려하면 상속이 무조건적으로 좋은 방안은 아닐 수 있다.

즉, 상속이란 것이 좋은 경우와 그렇지 않은 경우를 잘 선별해가며 활용할 수 있어야한다.


만약 새로운 자식클래스를 만들었을 때 해당 자식클래스가 올바르게 정의된 클래스인지 확인하기 위해서는 상속받은 모든 메서드를 실제로 자식클래스에서 사용하는지를 살펴보아야 한다. 만약 대부분의 메소드를 실제로 필요로 하지 않는다면 다음과 같은 이유로 설계상의 실수일 수 있다.


- 부모클래스가 잘 정의된 인터페이스 대신 막연한 정의와 너무 많은 책임을 가진 경우

- 자식클래스가 확장하려고 하는 부모클래스의 적절한 세분화가 아닌경우


따라서, 상속을 쓰는 올바른 경우는 새로 정의하고자 하는 자식클래스가 부모클래스의 기능을 그대로 물려받으면서 이중 특정 기능을 수정하고자 하거나, 추가적인 기능을 구현하고자 할 때이다.


또한 상속에서도, 인터페이스 정의는 또 다른 상속의 올바른 경우로 생각할 수 있다. 어떤 객체에 인터페이스 방식을 강제하고자 할 때, 실제 기능을 구현하지 않은 추상클래스를 만들고, 이를 상속받는 자식클래스에서 실제로 기능을 구현하게 한다. 


그럼 올바르지 않은 상속의 경우는 어떠한 케이스가 있을까? 바로 예시 코드를 살펴보자.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import collections
from datetime import datetime
 
class TransactionalPolicy(collections.UserDict):
    """잘못된 상속의 예시"""
    
    def change_in_policy(self, customer_id, **new_policy_data):
        self[customer_id].update(**new_policy_data)
 
 
policy = TransactionalPolicy({
    "client001":{
        "fee"1000.0,
        "expiration_date": datetime(2020,1,3),
    }
})
 
policy["client001"]
# {'fee': 1000.0, 'expiration_date': datetime.datetime(2020, 1, 3, 0, 0)}
 
policy.change_in_policy("client001",expiration_date=datetime(2020,1,4))
policy["client001"]
# {'fee': 1000.0, 'expiration_date': datetime.datetime(2020, 1, 4, 0, 0)}
 
cs


위의 코드를 살펴보면 TransactionalPolicy 클래스는 파이썬 내장 collections의 사전자료형을 상속받아 구현하였다. 그리고 TransactionalPolicy 클래스에서 사용하고자 하는 change_in_policy 함수를 구현하였다.


이후 policy 라는 TransactionalPolicy 객체를 만들어 추가한 함수를 사용하였다. 이처럼 원하는 기능을 수행하는 클래스를 구현하였지만, 과연 이 클래스가 사전자료형을 상속하는 것이 올바른 것일까? 파이썬에서 객체가 사용가능한 메서드를 출력해주는 dir함수를 이용하여 policy객체가 이용가능한 메서드를 살펴보자.


1
2
3
4
5
6
7
8
dir(policy)
# ['_MutableMapping__marker', '__abstractmethods__', '__class__', '__contains__', '__copy__', '__delattr__',
# '__delitem__', '__dict__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__getitem__',
# '__gt__', '__hash__', '__init__', '__init_subclass__', '__iter__', '__le__', '__len__', '__lt__', '__module__',
# '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__reversed__', '__setattr__', '__setitem__',
# '__sizeof__', '__slots__', '__str__', '__subclasshook__', '__weakref__', '_abc_impl',
# 'change_in_policy', 'clear', 'copy', 'data', 'fromkeys', 'get', 'items',
# 'keys', 'pop', 'popitem', 'setdefault', 'update', 'values']
cs


policy 객체를 보면 우리가 추가로 구현한 change_in_policy 라는 함수 이외에도 다양한 함수가 있다. 정말 해당 객체에서 pop, popitem 등의 함수가 필요로 한 것일까? 필요하지 않을텐데 포함되어 있다. 더군다나 해당 함수들은 public 메서드이기 때문에 이를 사용하게 될 가능성도 있다. 이러한 것을 살펴보아, TransactionalPolicy 클래스가 사전자료형을 상속받은 것은 올바르지 않다고 볼 수 있다.


더군다나, 의미적으로 보았을 때 부모클래스로부터 상속을 받은 자식클래스는 개념적으로 확장되거나, 세부적이라는 것을 의미해야 한다. 하지만 TransactionalPolicy 클래스가 사전자료형에 대해서 개념적으로 확장되거나 세부적이라는 것이라고 생각할 수 있을까?


그럼, 우리는 TransactionalPolicy 클래스를 정의하기 위해서 어떻게 해야할까?


이러한 경우 컴포지션이라는 개념을 활용해볼 수 있다.



2. 컴포지션(Composition)


컴포지션이란, 상속과 다르게 단순히 사용한다는 개념이다. 즉, 기존의 상속 개념에서의 자식클래스가 부모클래스의 모든 속성을 물려받는게 아니라, 자식클래스가 필요한 속성만 부모클래스로부터 가져와 사용하는 것이다.

일반적으로 상속은 암시적 선언이라고 하며, 컴포지션은 명시적 선언이라고 한다.


그럼 바로 컴포지션은 어떻게 사용되는지 위에서의 예시를 가져와 살펴보자.


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
class NewTransactionalPolicy:
    """컴포지션을 활용한 예시"""
    
    def __init__(self, policy_data, **extra_data):
        self._data = {**policy_data, **extra_data}
    
    def change_in_policy(self, customer_id, **new_policy_data):
        self._data[customer_id].update(**new_policy_data)
        
    def __getitem__(self, customer_id):
        return self._data[customer_id]
    
    def __len__(self):
        return len(self._data)
 
new_policy = NewTransactionalPolicy({
    "client001":{
        "fee"1000.0,
        "expiration_date": datetime(2020,1,3),
    }
})
 
new_policy["client001"]
# {'fee': 1000.0, 'expiration_date': datetime.datetime(2020, 1, 3, 0, 0)}
 
new_policy.change_in_policy("client001",expiration_date=datetime(2020,1,4))
new_policy["client001"]
# {'fee': 1000.0, 'expiration_date': datetime.datetime(2020, 1, 4, 0, 0)}
cs


이전의 예시와 달리 NewTransactionalPolicy 클래스에서는 사전자료형을 상속받지 않았다.

하지만 __init__함수에서 _data를 private로 선언하면서 사전자료형의 데이터로 초기화한다. 그리고 이후 우리가 사용하고자 하는 change_in_policy함수와 __getitem__함수에서 _data, 즉 사전 자료형을 활용하여 구현하고자 하는 기능을 구현하였다.


이렇게 직접 사전자료형을 상속받지 않고, 단순히 사전자료형을 가져와서 사용하였다.

이를 통해 추후 사전자료형에서 변경이 발생하더라도 그 인터페이스만 잘 유지된다면 NewTransactionalPolicy에서는 별도의 변경이 필요없다. 더군다나 이전에 dir함수를 통해 policy 객체가 사용가능한 메서드를 살펴본 것 처럼 new_policy 객체를 살펴보면 다음과 같다.


1
2
3
4
5
6
dir(new_policy)
# ['__class__', '__delattr__', '__dict__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__',
#  '__getattribute__', '__getitem__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__le__',
#  '__len__', '__lt__', '__module__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__',
#  '__setattr__', '__sizeof__', '__str__', '__subclasshook__', '__weakref__',
#  '_data', 'change_in_policy']
cs


확실히 상속을 받았던 policy 객체에 비해, 사용 가능하지만 필요하지 않았던 함수가 사라진 것을 확인할 수 있다.



이렇게 상속과 컴포지션은 단순히 별개의 것이 아니라, 기존의 클래스를 재사용하는 상황에 있어서, 상속을 사용해야 할지 컴포지션을 사용해야 할 지 신중히 판단해야 한다. 단순히 현상황만을 고려한다기 보다는 상속과 컴포지션의 특성과, 장단점을 잘 생각하고 활용해야 새롭게 정의하는 클래스가 추후 가져올 문제를 최소화 할 수 있다.



블로그 이미지

Tigercow.Door

Back-end / Python / Database / AI / Algorithm / DeepLearning / etc

댓글을 달아 주세요


안녕하세요. 문범우입니다.

이번 포스팅에서는 개발 분야에서 사용되는 약어(Abbreviations)에 대해서 알아보려 합니다.

물론, 파이썬이라는 언어에서만 해당되거나 염두에 두어야 할 내용이라기 보다는, 프로그래밍에 있어서 어떠한 언어를 사용하던 각 약어들을 염두해두고 프로그래밍을 한다면 보다 좋은 코드를 작성할 수 있고, 특정 상황과 코드에 보다 적합한 아이디어를 얻을 수 있을 것 입니다.


1. DRY / OAOO


DRY(Do not Repeat Yourself)와 OAOO(Once And Only Once)는 강조하고자 하는 의미가 비슷하므로 함께 다루어보자. 두개의 약어는, '중복을 피하라'라는 의미를 가지고 있다.

즉, 특정 기능과 역할을 하는 것은 코드에 단 한 곳에 정의되어 있어야 하고 중복되지 않아야 한다. 그리고 이를 통해 코드를 변경하고자 할 때 수정이 필요한 곳은 단 한 군데 존재해야 한다.


코드의 중복이 발생한다는 건 유지보수를 하는데에 있어서 직접적인 영향을 미칠 수 있다는 것이다. 다양한 문제가 있을 수 있지만 축약해보면 다음과 같은 3가지 문제가 대표적이다.


- 오류가 발생하기 쉽다.

특정 계산 로직이 코드 전체 여러곳에 동일하게 분포되어 있을 때, 계산 로직에 대한 변경사항이 발생하면 코드의 모든 곳을 찾아 변경해주어야 하는데 이때 하나라도 빠뜨리면 오류가 발생하기 쉬워진다.


- 비용이 발생한다.

동일한 기능에 대한 반복 수정이 이루어져야 하기 때문에, 당연히 1회의 수정보다 다수의 수정에 있어서 비용적으로 손해가 발생한다.


- 신뢰성이 떨어진다.

동일한 기능이 코드 여러 곳에 분포되어 있을 때, 모든 곳을 찾아서 수정해야 한다. 물론 언어적 기능과 도구의 도움을 받을 수도 있지만, 모든 곳을 정확히 기억하지 못할 수 있다는 점 때문에 시스템 전체의 신뢰성이 보다 떨어질 수 있다.


간단하게 나마 코드의 중복이 발생할 수 있는 예시와 적절히 조치 된 예시를 살펴보자.


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
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
#-*- coding:utf-8 -*-
# DRY / OAOO
 
user_math_score_dic = {
    'A'90,
    'B'93,
    'C'30,
    'D'100,
    'E'31,
    'F'82,
    'G'79,
}
 
user_eng_score_dic = {
    'A'30,
    'B'63,
    'C'39,
    'D'94,
    'E'10,
    'F'49,
    'G'68,
}
 
# Danger code
def get_user_score_list(user_math_score_dic, user_eng_score_dic):
    """
    input: 유저의 이름을 key로, 점수를 value로 가지는 dict형 자료형 2개
    output: 종합 점수 계산에 따라 내림차순으로 정렬한 유저의 이름 list
    """
    
    user_sum_score_dic = {}
    # 종합 점수 계산 (math*2 + eng)
    for k, math_score in user_math_score_dic.items():
        sum_score = math_score*2
        sum_score += user_eng_score_dic[k]
        user_sum_score_dic[k] = sum_score
 
    # 종합 점수에 따라 내림차순 정렬
    sorted_user = sorted(user_sum_score_dic.keys(), key=lambda x: user_sum_score_dic[x])
    return sorted_user
 
print("# Danger code")
print(get_user_score_list(user_math_score_dic, user_eng_score_dic))
 
 
# Good code
def calc_user_sum_score(user_math_score_dic, user_eng_score_dic):
    """
    input: 유저의 이름을 key로, 점수를 value로 가지는 dict형 자료형 2개
    output: 종합 점수 계산이 된 dict 자료형
    """
    user_sum_score_dic = {}
    # 종합 점수 계산 (math*2 + eng)
    for k, math_score in user_math_score_dic.items():
        sum_score = math_score*2
        sum_score += user_eng_score_dic[k]
        user_sum_score_dic[k] = sum_score
    return user_sum_score_dic
 
def get_user_score_list2(user_math_score_dic, user_eng_score_dic):
    """
    input: 유저의 이름을 key로, 점수를 value로 가지는 dict형 자료형 2개
    output: 종합 점수 계산에 따라 내림차순으로 정렬한 유저의 이름 list
    """
    user_sum_score_dic = calc_user_sum_score(user_math_score_dic, user_eng_score_dic)
 
    # 종합 점수에 따라 내림차순 정렬
    sorted_user = sorted(user_sum_score_dic.keys(), key=lambda x: user_sum_score_dic[x])
    return sorted_user
 
print("# Good code")
print(get_user_score_list(user_math_score_dic, user_eng_score_dic))
 
cs


위의 코드를 보면 기존에 정의된 get_user_score_list함수에서는 내부적으로 종합 점수에 대한 계산이 진행되고 있다. 만약 그러한 계산 로직이 다른 곳에서도 필요하면 어떻게 될까? 따로 함수화가 되어 있지 않기 때문에 동일 로직을 중복시켜야 한다. 하지만 아래와 같이 calc_user_sum_score라는 함수를 분리해두면, 추후 동일 로직이 필요할 때 해당 함수를 이용할 수 있을 것이다.



2. YAGNI / KIS


YAGNI( You Aren't Gonna Need It)와 KIS(Keep It Simple) 또한 의미하는 바가 비슷하므로 함께 다루도록 하자. 두 약어가 의미하는 것은 '현재 주어진 문제에 적합한, 간단한 코드를 작성하라'이다.


YAGNI에서 보다 강조하는 것은, 과잉된 프로그래밍을 하지 말라는 것이다. 우리는 결론적으로 시스템에 대한 확장성과 유지보수 등을 위해 보다 좋은 코드를 작성하려고 한다. 하지만 그 목표가 코드를 작성하는 시점에 특정 미래적 상황을 예측해야 한다는 것은 아니다. 필자가 참고하고 있는 서적에서는 이렇게 이야기 한다.


유지보수가 가능한 소프트웨어를 만드는 것은 미래의 요구 사항을 예측하는 것이 아니다.

- 파이썬 클린 코드


위에서의 말대로, 우리가 확장성과 유지보수 등을 위한 소프트웨어를 만들어야 한다는 것은, 다가오지 않은 미래에 대해 특정 상황을 예측해야 한다는 것은 아니다. 오히려 그랬다면 코드적인 학습보다, 미래학자와 같은 학습을 해야하지 않을까 싶다. 

따라서, 프로그래밍 시점에서는 현재의 요구사항을 잘 해결하기 위한 소프트웨어를 작성하되, 이때에 보다 수정가능하고, 높은 응집도와 낮은 결합력을 가지는 프로그래밍을 해야한다. 미래에 ~이 필요할거야, 나중에 ~가 고려되지 않을까, 라는 생각에 현재의 요구사항을 넘어서는, 과잉 프로그래밍을 하면 안된다.


KIS에서 조금 더 강조하는 점은 현재에 선택한 솔루션이 최선의, 최소한의 솔루션이어야 한다는 것이다. 문제를 해결하는데에 있어서 화려하고 어려운 기술은 필수요소가 아니다. 항상 보다 간결하고 최소한의 솔루션으로 문제를 해결해야 한다.

단순하게 해결될 수 있는 문제를 보다 복잡하게 해결하게 되면 추후 해당하는 함수, 클래스, 데이터에 대한 수정에 있어 더 큰 어려움이 내포될 수 있다.


더군다나, 파이썬의 철학에서는 '단순한 것이 복잡한 것보다 낫다.' 라고 이야기 하고 있다.



3. EAFP / LBYL


EAFP(Easier to Ask Forgiveness than Permission)와 LBYL(Look Before You Leap)는 상대적인 의미를 지니고 있는 약어이다.


우선 EAFP는 허락보다 용서를 구하는 것이 쉽다는 말인데 이 의미는 일단 코드가 실행되도록 두고 동작하지 않을 경우를 대응한다는 의미이다. 일반적으로는 코드가 실행되도로 하고 발생할 수 있는 에러에 대해서 catch, except문을 이용해 조치하는 코드를 의미한다.


이에 반해 LBYL은 도약하기 전에 살피라는 말이며, 의미적으로는 코드가 실행되기 이전에 확인/검토를 하라는 의미이다. 간단하게는 if문 등을 이용한 체크 정도로 생각하면 된다.


아래 코드는 파일을 사용하기 이전에 LBYL에 따른 코드와, EAFP에 따른 코드를 나타내고 있다.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# LBYL
if os.path.exists(filename):
    with open(filename) as f:
        ...
        
 
# EAFP
try:
    with open(filename) as f:
        ...
        
except FileNotFoundError as e:
    logger.error(e)
    ...
 
cs




블로그 이미지

Tigercow.Door

Back-end / Python / Database / AI / Algorithm / DeepLearning / etc

댓글을 달아 주세요


안녕하세요. 문범우입니다.

이번 포스팅에서는, 파이썬에서 제공되는 annotation에 대해서 알아보도록 하겠습니다.



1. Annotation 이란?


우선 Annotation의 사전적 정의는 "주석"이다. 즉, 쉽게 말해서 코드에 대한 추가적인 설명을 이야기하는 무언가를 의미한다.

대표적으로 Java언어에서 함수나 클래스 상단에 @를 통해 annotation을 표시한다.



위는 java의 spring 프레임워크를 사용한 코드 중 일부인데, 10번, 13번, 16번 라인을 보면 @를 통해 annotation을 사용하고 있음을 볼 수 있다. 물론 자바에서의 annotation과 파이썬에서의 annotation은 차이가 있을 수 있다. 하지만 기본적으로 로직이 되는 코드를 "도와주는" 의미에서는 크게 다르지 않다고 볼 수 있다.

(사실 java의 annotation과 같은 것을 파이썬에서는 decorator로 나타내기 때문에 엄밀히 말했을 때, 자바의 annotation과 파이썬의 annotation은 다르다고 볼 수 있다.)


보다 자세히, 파이썬에서의 annotation에 대해서 알아보자.

사실 파이썬에서는 annotation에 대한 강제성이 전혀 없다. 우리가 파이썬에서 #을 이용하거나, 지난 포스팅에서 알아본 docstring과 같이 안써도 되지만, 보다 좋은 코드가 될 수 있도록 추가적으로 관리해주는 것 중 하나일 뿐이다.

파이썬에서 사용하는 annotation의 기본 아이디어는, 코드를 사용하는 이에게 함수나 클래스에 대해 그 인자값의 형태 또는 반환값을 알려주자는 것이다.


함수에 대해서는 함수의 인자에 대한 타입에 대한 힌트를 적거나, 함수의 return값에 대한 타입을 알려줄 수 있다. 또한 파이썬 3.6이후로는 변수에 대해서도 직접 annotation을 달 수 있다. 즉, 클래스에서 사용되는 변수값에 대해 그 타입을 적어둘 수 있다는 것이다.



2. Annotation 사용하기


백문이 불여일견, 우선 Annotation을 사용한 파이썬 코드를 살펴보자.


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
#-*- coding:utf-8 -*-
class AnnotationClassExample:
    """
    Annotation에 대한 예시를 확인하기 위한 class입니다.
    __annotation__ 속성을 통해
    class할당되는 first_param과 second_param에 대한 타입을 확인할 수 있습니다.
    """
    first_param: str
    second_param: int
 
    def set_first_param(self, value: str-> None:
        """
        AnnotationClassExample 클래스의
        first_param 값을 바인딩합니다.
        함수의 반환은 없습니다.
        """
        self.first_param = value
 
    def set_second_param(self, value: int-> bool:
        """
        AnnotationClassExample 클래스의
        second_param 값을 바인딩합니다.
        함수의 반환은 True or False 입니다.
        """
        if type(value) == int:
            self.second_param = value
            return True
        else:
            self.second_param = 0
            return False
 
def main():
    print("Annotation 만들어보기")
    new_class = AnnotationClassExample()
    print("\n* AnnotationClassExample 클래스의 annotations")
    print(new_class.__annotations__)
    print("\n* set_first_param 함수의 annotations")
    print(new_class.set_first_param.__annotations__)
    print("\n* set_second_param 함수의 annotations")
    print(new_class.set_second_param.__annotations__)
 
if __name__ == '__main__':
    main()
cs


https://github.com/doorBW/python_clean_code


위의 코드에서는 AnnotationClassExample 클래스와 그 내부에 2개의 변수를 가지고 있으며, 클래스 내부에 2가지 함수를 추가로 구현해두었다. 


우선 함수에 대한 annotation을 살펴보자.

11번, 19번 라인의 함수선언부를 살펴보면 함수의 인자에 대한 annotation과 함수의 return에 대한 annotation이 적용되었다. 이를 통해 함수를 사용하고자 하는 이는 함수의 인자가 어떤 타입을 가져야하는지, 그리고 함수를 통해 얻게되는 값의 타입은 무엇인지 보다 쉽게 알 수 있다.

또한 파이썬 3.6부터 변수에 대한 annotation이 가능하다고 했는데, 이는 8번, 9번 라인과 같이 클래스 내부의 변수에 대한 annotation으로 사용할 수 있다. 


이렇게 annotation을 적용하면, 그 개체에 대해 __annotations__ 이라는 속성이 생긴다. 그리고 해당 속성을 통해 우리가 적용해둔 annotation 값을 볼 수 있는 것이다.

실제로 위의 코드를 실행시켜 보면 main함수내에서 각 클래스와 함수에 대해 __annotations__ 속성을 호출하고, 그 결과는 다음과 같다.



우리가 코드에서 적용시켜준 annotation들이 출력되는 것을 확인할 수 있다.


위와 같이 annotation을 통해 함수나 변수 등에 미리 타입에 대한 힌트를 적어둘 수 있다.

물론 이 또한 파이썬에서 강제성이 있거나, 지켜야 한다는 것은 아니다. annotation은 말 그대로 '힌트'를 주는 것에 불과하다.





블로그 이미지

Tigercow.Door

Back-end / Python / Database / AI / Algorithm / DeepLearning / etc

댓글을 달아 주세요


안녕하세요. 문범우입니다.


파이썬 언어를 활용하는데 보다 좋은 코드, 유지보수가 가능한 코드, 팀원들과 함께 협업할 수 있는 코드를 작성하기 위해 다양한 방법을 알아보고자 합니다. 흔히들 이야기 하듯, 클린코드를 작성하는 방법에 대해서 파이썬에서는 어떻게 구현될까를 함께 알아볼 것 입니다.

물론 클린코드라는 것에 정확한 방법과 기준은 없습니다. 하지만 함께 알아보는 내용들을 통해 그를 위한 다양한 방법과 방향에 대해서는 알아볼 수 있을 것 입니다. 

하나씩 공부해보며 자신의 상황과 프로젝트에 맞게 적용시키면 좋을 것 같네요.


이번 포스팅에서는 그 첫번째로, 파이썬의 docstring에 대해서 알아보도록 하겠습니다.



0. 문서화


Docstring에 대해서 알아보기전에 문서화에 대해서 한번 짚어보자.

프로젝트를 진행해보거나, 다른 개발자와 함께 협업을 진행해본 개발자라면 '문서화'가 왜 필요한지 어느정도 느꼈을 수 있다.


여러분들은 지금 개발하고 있는 프로젝트, 지금 작성한 코드를 한달, 일년뒤에 보았을 때 코드를 작성할 때와 같이 부드럽게 코드리딩이 가능한가? 사실 한달, 일년도 아니다. 복잡한 로직과 다양한 처리를 진행하는 개발을 진행할 때면 당장 내일에 그 코드를 정확히 기억하기 힘든 경우도 많을 것이다.



물론 그렇게 리딩이 어렵다는 것은 로직 자체가 깔끔하지 못하다는 문제이지 않을까란 생각을 할 수도 있을 수 있다. 당연히 그 또한 중요하겠지만, 만약 내가 작성한 코드를 다른 동료 개발자가 본다면 어떠할까? 로직이 깔끔하지만 수많은 함수와 클래스들의 코드를 직접 리딩해가며 모든 로직을 '코드'만으로 이해하고 받아들인다는건 상상하는 것보다 매우 힘든 일이 될 수 있다.


하지만 만약 각각의 함수와 클래스, 기타 등등이 어떤 기능을 하는 것인지 알아볼 수 있도록 '문서화'를 해두었다면 어떨까? 내가 작성한 코드를 매우 오랜만에 보더라도 해당 함수의 매개변수는 어떤 타입인지, 해당 함수가 어떤 기능을 하는 것인지, 그리고 반환 값은 무엇인지 한번에 알아 볼 수 있어 해당 함수를 이용하거나 받아들이는데 매우 편리할 것이다. 

즉, 문서화를 통해서 구현된 코드에 대해 보다 명확하고 편리하게 설명할 수 있으며 특정 함수나 클래스 등을 다른 곳에서 사용하고자 할 때 그것들에 대해 손쉽고 명확하게 이해하고 사용할 수 있어 추가적인 개발에 있어 사전에 버그를 방지할 수 있을 것이다.


물론 위에서 이야기한 내용보다 '문서화'가 필요하고 중요한 이유는 더 다양하고 더 중요한 이유들이 있을 수 있지만 이정도라면 개발자 누구나 '문서화'가 왜 필요한지 어느정도 스스로 생각해 볼 수 있는 계기가 될 것이라 생각한다.



1. Docstring 이란?


그럼 왜 필자는 Docstring에 대한 이야기에 앞서 문서화를 이야기했을까?

Docstring은 쉽게 생각했을 때, 코드에 포함된 문서(Document)이다. 즉, 코드에 대한 문서화를 코드 밖에, 워드나 한글 파일, 엑셀을 이용해 따로 하는 것이 아니라 코드 내부에 문서화를 실시한다는 것이다.

특히나 파이썬과 같은 동적 타입의 언어이기 때문에 코드내부에 문서를 포함시키는 docstring 이 매우 좋다. 파이썬에서는 파라미터의 타입을 체크하거나 강제하지 않는다. 헌데 특정 함수나 클래스를 이용하거나 수정하고자 할때 그에 대한 설명이 명확하게 나와있다면 그러한 이용이나 수정이 매우 간편하게 진행될 것이다.


파이썬에서 docstring은 함수나 클래스 모듈등에 정의할 수 있다. 그리고 작성한 내용을 확인하기 위해서는 해당 객체의 __doc__ 라는 속성을 이용하면 된다. (더블언더바)


docstring에 대한 사용법을 알아보기 이전에 실제로 docstring이 정의된 예시를 확인해보자.



위의 사진은 파이썬에서 개체의 길이를 구하는데 사용하는 len함수의 docstring을 출력한 사진이다. 또 다른 예시는 dictionary개체의 docstring을 살펴보자.


 


위와 같이 dictionary 개체의 docstring을 보니, 어떻게 사용하는지도 설명을 첨부하였다.

만약 우리가 dictionary가 무엇인지 궁금했다면 이와 같이 docstring을 통해서 간략하게나마 확인할 수 있을 것이다.



2. Docstring 사용하기


그럼 직접 우리가 클래스나 함수에 대해서 Docstring을 만들어서 사용해보도록 하자.


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
class DocstringClassExample():
    """
    DocstringClassExample() 예제 클래스
    class에 대한 설명을 함께 적어준다.
    """
 
    def docstring_func_example():
        """
        Return 0 always
        """
        print("ocstring_func_example 함수를 실행하였습니다.")
        return 0
 
def main():
    print("Docstring 만들어보기")
    new_doc = DocstringClassExample()
    print("Class docstring start")
    print(new_doc.__doc__)
    print("Class docstring end")
    print()
    print("Function docstring start")
    print(new_doc.docstring_func_example.__doc__)
    print("Function docstring end")
 
 
if __name__ == '__main__':
    main()
cs


https://github.com/doorBW/python_clean_code


위와 같이 간단한 Class 하나와 함수하나를 만들었다.

docstring은 위와 같이 클래스나 함수 선언 바로 하단부에 따옴표 세개를 이용하면 된다.(쌍따옴표, 홀따옴표 무관하다.)

이렇게 만든 코드를 실행시켜보면 다음과 같이 결과가 출력된다.



docstring 자체를 만들어내는 것, 이를 사용하는 것은 함께 알아본 것 처럼 전혀 어렵지 않고 너무 간단하다.

하지만 보다 중요한 것은 이러한 기능을 놓치지 않고 이용하면서 보다 유지보수 좋은 코드를 지속적으로 만들어 나가는 것, 그리고 기존에 있던 docstring을 최신으로 유지하는 노력일 것이다.




블로그 이미지

Tigercow.Door

Back-end / Python / Database / AI / Algorithm / DeepLearning / etc

댓글을 달아 주세요


안녕하세요. 문범우입니다.

이번 포스팅에서는 파이썬 패키지를 배포하는 방법에 대해서 함께 살펴보도록 하겠습니다.



1. pip: 파이썬 패키지 관리자


파이썬 패키지를 배포하는 방법에 대해 설명드리기에 앞서 간단하게 pip, 파이썬 패키지 관리자에 대해 짚고 넘어가보겠습니다.

파이썬을 공부하고 어느정도 사용을 해본 분들이라면 자연스럽게 pip를 사용해 보셨을 것이라고 생각합니다.


가령, 데이터 분석을 위해서 주로 numpy나 pandas, 웹 개발을 할 때에는 django, flask 등을 이용하기 위해 아래와 같이 pip를 이용하여 필요한 라이브러리를 다운받아 사용하셨을 겁니다.


1
pip install <라이브러리 이름>
cs


이때 우리가 사용하는 pip는 무엇일까요?

pip란, Python Package Index(PyPI)라는 저장소에서 제공되는 파이썬 패키지 소프트웨어를 설치 및 관리하는 패키지 관리 시스템입니다.

즉 우리가 그 동안 pip를 통해 설치한 다양한 라이브러리(패키지)들은 모두 PyPI라는 곳에 저장되어 있으며 실제로 아래 PyPI사이트에서 검색을 통해 확인해볼 수 있습니다.


https://pypi.org/


그리고 위의 PyPI사이트에 일정한 템플릿을 맞추어 자신의 패키지를 어렵지 않게 등록할 수 있습니다.

별도의 승인과정이나 절차가 없으며 단순히 특정 파일들만 잘 셋팅하면 어렵지 않게 자신만의 라이브러리(패키지)를 등록하여, pip로 설치할 수 있게 되는 것 입니다.


자신이 구현한 알고리즘이나, 특정 기능을 하는 함수를 더 많은 사람들에게 공유하고, 기회가 된다면 피드백을 받아 보다 좋은 코드로 발전시키는 것은 언제나 중요하고 보람찬 일이라고 생각합니다.


그럼 이제, 어떻게 PyPI 사이트에 자신의 코드를 등록할 수 있는지 살펴보도록 하겠습니다.



2. 준비 단계


2-1. PyPI 회원가입


제일 먼저 PyPI에 회원가입을 진행합니다.

아래 사이트에서 우측 상단의 Register를 클릭하고 이름과 이메일, 비밀번호를 입력 후 이메일 인증만 진행하면 됩니다.

추후 패키지를 등록하고자 할 때 PyPI의 계정이 필요하니 미리 가입을 해두는 것을 추천드립니다.


https://pypi.org/



2-2. 패키지 이름 중복 확인


가입이 완료되었다면, 위의 사이트에서 search를 통해 자신이 등록하고자 하는 패키지의 이름의 있는지 확인합니다. 패키지 이름은 추후 사용해야 할 곳이 많으니 자신이 쓰고자 하는 패키지의 이름이 중복되지 않는지를 먼저 확인 후 이후 과정을 진행하시는 것이 편리합니다.

만약, 이름이 중복된다면 다양한 것들을 수정해야 할 수 있습니다.


저는 doorbw-test 라는 이름으로 패키지가 없는 것을 확인하였기에 해당 이름으로 패키지 생성 및 등록을 진행해보도록 하겠습니다.



2-3. 등록하고자 하는 파일(함수) 구현


또한 제가 등록하고자 하는 함수는 test_function()으로써 아래와 같이 코드를 작성하였습니다.


1
2
3
4
def test_function(input_str):
    print("Hello, I'm beomwoo.moon")
    print("Your input string is,",input_str)
    print("Bye!")
cs


위의 코드를 test.py라는 파일로 저장하였습니다.

추후 우리가 doorbw_test라는 이름으로 패키지를 등록하면, 위의 함수를 사용하기 위해서 doorbw-test를 pip로 설치한 후에 다음과 같이 호출해야 합니다.


(패키지 등록시 대시('-')가 아닌 언더바('_')를 사용해야 합니다.

파이썬에서 import할때 대시를 포함한 라이브러리를 호출하려면 다른 작업이 필요하기 때문입니다. 하지만 언더바를 사용하더라도 PyPI에서는 대시로 나타나니 혼동하지 않기 바랍니다.)


1
2
3
from doorbw_test import test
 
test.test_function("[pip deploy test]")
cs



우리가 등록하는 패키지 이름으로부터 test.py를 import하고 test안에 있는 test_function을 실행하는 모습입니다.



2-4. github repository 구성


마지막으로는 해당 코드를 공유할 github repository를 만들어 줍니다.

해당 과정은 필수는 아닙니다.

일반적으로 배포하고자 하는 패키지이름과 동일하게 github repository를 만들어주지만, 저는 설명을 위한 배포이기 때문에 repository 이름은 'pypi_deploy_test'로 진행하였습니다.



이제 위에서 준비한 내용들을 바탕으로 PyPI에 등록해보도록 하겠습니다.



3. 등록 단계


3-1. 폴더 구성


처음에도 말씀드렸듯이, PyPI에 패키지를 등록하려면 별도의 승인과정 같은 것은 없지만 일정한 템플릿을 구성해야 한다고 말씀드렸습니다.

먼저 패키지를 등록할 폴더를 만듭니다. 패키지를 등록할 폴더는 패키지 이름과 동일해야 합니다.

따라서 저같은 경우는 doorbw_test라는 폴더를 만들었으며, 해당 폴더안에 동일한 이름의 폴더를 하나 더 만들고 위에서 작성한 test.py파일을 넣어줍니다.

현재까지의 디렉토리 상태를 트리구조로 본다면 다음과 같습니다.


doorbw_test

   - doorbw_test

      - test.py



3-2. setup.py 파일 구성


위와 같은 상태에서 두번째 doorbw_test와 같은 경로상에 setup.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
from setuptools import setup, find_packages
 
setup(
    # 배포할 패키지의 이름을 적어줍니다. setup.py파일을 가지는 폴더 이름과 동일하게 합니다.
    name                = 'doorbw_test',
    # 배포할 패키지의 버전을 적어줍니다. 첫 등록이므로 0.1 또는 0.0.1을 사용합니다.
    version             = '0.1',
    # 배포할 패키지에 대한 설명을 작성합니다.
    description         = 'for explain about pypi deploy',
    # 배포하는 사람의 이름을 작성합니다.
    author              = 'beomwoo.moon',
    # 배포하는 사람의 메일주소를 작성합니다.
    author_email        = 'doorbw@outlook.com',
    # 배포하는 패키지의 url을 적어줍니다. 보통 github 링크를 적습니다.
    url                 = 'https://github.com/doorBW/pypi_deploy_test',
    # 배포하는 패키지의 다운로드 url을 적어줍니다.
    download_url        = 'https://github.com/doorBW/pypi_deploy_test/archive/master.zip',
    # 해당 패키지를 사용하기 위해 필요한 패키지를 적어줍니다. ex. install_requires= ['numpy', 'django']
    # 여기에 적어준 패키지는 현재 패키지를 install할때 함께 install됩니다.
    install_requires    =  [],
    # 등록하고자 하는 패키지를 적는 곳입니다.
    # 우리는 find_packages 라이브러리를 이용하기 때문에 아래와 같이 적어줍니다.
    # 만약 제외하고자 하는 파일이 있다면 exclude에 적어줍니다.
    packages            = find_packages(exclude = []),
    # 패키지의 키워드를 적습니다.
    keywords            = ['pypi deploy'],
    # 해당 패키지를 사용하기 위해 필요한 파이썬 버전을 적습니다.
    python_requires     = '>=3',
    # 파이썬 파일이 아닌 다른 파일을 포함시키고 싶다면 package_data에 포함시켜야 합니다.
    package_data        = {},
    # 위의 package_data에 대한 설정을 하였다면 zip_safe설정도 해주어야 합니다.
    zip_safe            = False,
    # PyPI에 등록될 메타 데이터를 설정합니다.
    # 이는 단순히 PyPI에 등록되는 메타 데이터일 뿐이고, 실제 빌드에는 영향을 주지 않습니다.
    classifiers         = [
        'Programming Language :: Python :: 3',
        'Programming Language :: Python :: 3.2',
        'Programming Language :: Python :: 3.3',
        'Programming Language :: Python :: 3.4',
        'Programming Language :: Python :: 3.5',
        'Programming Language :: Python :: 3.6',
        'Programming Language :: Python :: 3.7',
    ],
)
cs


각각에 대한 설명은 주석으로 달아두었습니다.



3-3. __init__.py / README.md /

setup.cfg / .gitignore / git init


이번에는 5개의 작업을 진행합니다.


먼저 우리가 앞에서 만들었던 test.py 파일과 같은 경로에 __init__.py 파일을 만들어 줍니다.

해당 파일 안에는 비워두셔도 되고 단순히 print문을 입력하셔도 됩니다.


이후 setup.py 파일과 동일한 경로에 README.md 파일을 만들어 패키지에 대한 간략한 설명을 작성해 줍니다.


1
2
3
4
5
# HELLO
This is just explain for PyPI deploy  
 
doorbw@outlook.com  
 
cs


위와 같이 README.md를 작성하였다면 동일한 경로에 setup.cfg 파일을 만들어 줍니다.


1
2
[metadata]
description-file = README.md
cs


위와 같이 setup.cfg를 만들어 주었다면 github등록 전 마지막으로 .gitignore파일을 아래와 같이 만들어 줍니다.


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
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
# vscode
.vscode/
 
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
*$py.class
 
# C extensions
*.so
 
# Distribution / packaging
.Python
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
pip-wheel-metadata/
share/python-wheels/
*.egg-info/
.installed.cfg
*.egg
MANIFEST
 
# PyInstaller
#  Usually these files are written by a python script from a template
#  before PyInstaller builds the exe, so as to inject date/other infos into it.
*.manifest
*.spec
 
# Installer logs
pip-log.txt
pip-delete-this-directory.txt
 
# Unit test / coverage reports
htmlcov/
.tox/
.nox/
.coverage
.coverage.*
.cache
nosetests.xml
coverage.xml
*.cover
.hypothesis/
.pytest_cache/
 
# Translations
*.mo
*.pot
 
# Django stuff:
*.log
local_settings.py
db.sqlite3
 
# Flask stuff:
instance/
.webassets-cache
 
# Scrapy stuff:
.scrapy
 
# Sphinx documentation
docs/_build/
 
# PyBuilder
target/
 
# Jupyter Notebook
.ipynb_checkpoints
 
# IPython
profile_default/
ipython_config.py
 
# pyenv
.python-version
 
# celery beat schedule file
celerybeat-schedule
 
# SageMath parsed files
*.sage.py
 
# Environments
.env
.venv
env/
venv/
ENV/
env.bak/
venv.bak/
 
# Spyder project settings
.spyderproject
.spyproject
 
# Rope project settings
.ropeproject
 
# mkdocs documentation
/site
 
# mypy
.mypy_cache/
.dmypy.json
dmypy.json
 
# Pyre type checker
.pyre/
cs


이제 해당 폴더를 앞에서 만든 github repository에 등록시켜주도록 합시다.

현재까지의 디렉토리 상태는 아래의 트리구조와 같습니다.


doorbw_test

   - doorbw_test

      - __init__.py

      - test.py

   - .gitignore

   - README.md

   - setup.cfg

   - setup.py


cmd또는 터미널에서 해당 폴더를 이전에 만든 github repository에 올려줍니다.




3-4. 필요한 라이브러리 설치 및 빌드


이제 우리가 만든 패키지를 배포하기 위한 마지막 작업으로 필요한 라이브러리를 설치합니다.

우선 setup.py파일에서 사용한 setuptools,

그리고 빌드시에 사용할 wheel,

배포시에 사용할 twine

총 3개를 아래의 명령어로 설치합니다.


1
pip install setuptools wheel twine
cs


이제 앞에서 구성한 setup.py을 통해 패키지 빌드를 시작합니다.

setup.py파일이 있는 경로상에서 아래와 같이 명령어를 입력합니다.


1
python setup.py bdist_wheel
cs


위의 명령어를 입력하고 나면 폴더에 build, dist, <자신의 패키지이름>.egg-info 이름의 3개의 폴더가 생성되었을 것입니다.

이 중에서 dist 폴더 내부에 있는 파일이름을 확장자까지 모두 복사합니다.



3-5. 등록하기


dist 폴더 내부에 있는 확장자를 포함한 파일 이름을 복사하셨다면 아래와 같이 명령어를 실행합니다.


1
2
# dist 폴더 아래에 있는 파일명이 doorbw_test-0.1-py3-none-any.whl 일때,
twine upload dist/doorbw_test-0.1-py3-none-any.whl
cs


위의 명령어를 입력하면 자신의 이름을 입력하라고 나옵니다.

이때 제일 처음에 가입했던 PyPI의 이름을 입력하시고 이어서 비밀번호를 입력하시면 됩니다.



위와 같이 결과가 출력된다면 정상적으로 등록된 것 입니다.

이제 PyPI에 가서 자신의 계정으로 로그인 후에 자신이 등록한 패키지를 확인할 수 있습니다.


이후 아래와 같이 실제로 자신이 등록한 패키지를 pip install로 다운받아서 사용하실 수 있습니다.





추가적으로 문의사항이 있으시거나 잘 해결되지 않는 점이 있다면

주저하지 마시고 언제든지 이메일 또는 카카오톡으로 연락주시면 빠르게 도움드리도록 하겠습니다.

감사합니다.

블로그 이미지

Tigercow.Door

Back-end / Python / Database / AI / Algorithm / DeepLearning / etc

댓글을 달아 주세요

안녕하세요. 문범우입니다.

오랜만에 파이썬으로 필요한 프로그램을 만들어 공유하려고 글 작성합니다.

소개해드리고자 하는 프로그램은


중고나라에서 키워드 검색후 게시글 크롤링 및 메일 발송

기능을 가진 프로그램입니다.


모든 소스코드는 아래의 github주소에 공개되어있습니다.

https://github.com/doorBW/joonggonara_crawl


간단하게 활용 스택 및 배운점 등을 공유하겠습니다.


1. 활용 스택


- Python 3.7.1

- pyenv 및 virtualenv를 통한 가상환경설정

- Selenium / BeautifulSoup / smtplib / dotenv 라이브러리 활용

- ubuntu crontab 활용


위와 같은 언어 및 라이브러리를 활용하였습니다.

crawl.py 에서 중고나라 사이트에 접속하여 사전에 설정해놓은 키워드로 검색을 하고

50개씩 보기를 선택한 후에 중복 글쓴이를 제외한 나머지 게시글을 리스트로 만들어서 메일로 발송합니다.

해당 파일은 aws의 개인서버에 두고 crontab을 활용하여 정해진 시각에 실행되도록 설정하였습니다.



2. 배운 점


과거에 selenium을 활용해본적은 있으나, 이번에 보다 많이 활용해본 것 같습니다.

네이버 카페에서 게시글이 나오는 쪽이 iframe으로 설정되어 있어 selenium에서 frame도 바꿔주어야 했고,

50개씩 보기를 선택하면서 보다 활용도가 높았던 것 같습니다.

활용할수록 매우 재미있는 라이브러리 같아서, 여름쯤 시간내서 한번 더 깊게 공부해보면 재미있을 것 같습니다.

관련되서 재밌는 프로젝트들에 대해서 아시는게 있다면 알려주세요 :)

이 외에 다른 라이브러리나 ubuntu crontab 활용에 대해서는 과거에 사용해봤던 것과 동일했기 때문에 따로 언급하지 않겠습니다.



3. 더 공부해볼 점


사실상 게시글 제목을 중심으로 크롤링을 진행했지만, 그러한 제목에는 상품의 가격이 제대로 나와있지 않습니다.

가격 또한 쉽게 보려면 게시글에 들어가서 내용을 검토해야 하는데, 그렇게 하지 않은 이유는 게시글 내용에서도 가격이 정확하게 나와 있지 않기 때문입니다.

중고나라에서 가격을 적는 칸이 있지만, 사실 대다수가 지키지 않고 있는 실정입니다.

이에 따라서 게시글 내용을 text로 반환 받아 그 내용 중 상품 가격을 추출할 수 있다면 매우 활용적일 수 있습니다.

인공지능 분야에서도 자연어처리 분야에 대해 지식이 필요할 것 같은데, 혹시 관련되서 내용을 아시거나, 다른 방법으로 해당 문제를 해결할 수 있는 방안에 대해서 아신다면 댓글 부탁드립니다!


블로그 이미지

Tigercow.Door

Back-end / Python / Database / AI / Algorithm / DeepLearning / etc

댓글을 달아 주세요

파이썬 클린 코드

[해당 리뷰는 터닝포인트 출판사에서 서적을 지원받고 작성하게 되었습니다.]

파이썬 클린코드

기초적인 파이썬 내용을 넘어, 보다 전문적인 파이썬 개발자가 되고자 하는 분

 

안녕하세요. 문범우입니다.

최근 개인적인 사정으로 인해 블로그에 글을 많이 못올리는데,

오랜만에 IT 도서리뷰로 포스팅을 하게 되었습니다.

이번에 소개해드릴 서적은, 터닝포인트 출판사에서 나온 '파이썬 클린코드'라는 서적입니다.

그럼 간단하게 나마 해당 책에 대한 소개를 드려보도록 하겠습니다.

 

누가 읽으면 좋을까?

사실 이 책에 대해서 이야기를 할 때, 대상 독자에 대한 고려가 제일 필요하다고 생각됩니다.

솔직하게 말씀드려서 아직 파이썬이라는 프로그래밍 언어가 낯설거나, 처음이신분들, 유지보수에 대해서 아직 정확히 파악하지 못했거나 소프트웨어 공학에 대해서 전혀 알지 못하시는 분들에게는 절대 추천드리지 않습니다.

그런 분들께서는 오히려 책의 내용이 지루할 뿐만 아니라, 하나하나 이해하기도 힘들 수 있을 것이라 생각됩니다.

반대로, 파이썬 언어에 대해서 익숙하신 분들, 팀 프로젝트를 하며 코드의 유지보수성에 대해서 고민하신 분들에게는 적극적으로 추천드리고자 하는 책 입니다.

 

소프트웨어 공학의 중요 개념과 파이썬의 연결고리

사실 저는 소프트웨어 공학에 대해서 학습하면서, 당연히 이론적인 내용도 중요했지만 실제로 코드에서 그것들이 어떻게 반영되는지를 무척이나 궁금해했습니다. 하지만 대학에서 이에 대해서 학습할 때에는 자바(Java)언어에 대해 한정적으로 배울 수 있었습니다.

물론 해당 내용이 이론적으로 잘 이해하고 공부한다면 언어는 단순히 도구적으로 사용될 뿐이지만, 개인적으로 파이썬을 좋아하는 개발자로써 파이썬에서의 소프트웨어 공학의 이론들은, 개념들은 어떻게 적용되고 있을까, 실제로 파이썬 언어만의 특징을 어떻게 더 살려낼 수 있을까? 에 대해서 고민한 적이 많습니다.

그리고 이번에 소개시켜드리는 책에서는 그러한 부분들을 많이 해소시킬 수 있었습니다.

책의 목차에서도 그렇듯, '파이썬 클린코드' 서적에서는 우리가 그동안 배웠던 소프트웨어 공학의 내용들과 파이썬 언어만의 특징을 잘 살려내서 설명해주고 있습니다.

 

아쉬웠지만 극복할 수 있는 키워드 설명

책을 읽으면서 아쉬웠던 점은, 가끔 특정 키워드나 개념에 대해 충분한 설명없이 넘어가는 점들이 있습니다.

하지만 이를 '극복할 수 있는' 이라고 말씀드리는 것은, 그러한 것들이 엄청나게 많다거나, 그러한 것들 때문에 다른 내용이 읽히지 않는 정도는 아니기 때문입니다.

오히려 저는 책의 중간중간 포스트잍을 붙여가면서 모르는 키워드에 대해서 정리하고, 그것을 기반으로 해당 챕터의 내용을 다시 상기할 수 있게끔 하는 방법을 사용했는데, 더 공부한 느낌도 나고 정리도 잘 되는 것 같아서 내심 뿌듯했습니다 :)

 


이렇게 '파이썬 클린코드' 서적에 대해 개인적으로 생각하는 특징을 바탕으로 짧게나마 정리해보았습니다.

사실 무엇보다 개인적으로는, 최근 파이썬 언어가 핫해지면서 기초적인 책들이 많이 나오고 있는데, 그 와중에 오랜만에 적당히 깊이있고 잘 정리되어 있는 책이 나와서 너무나 반갑고 재미있게 읽은 책인 것 같습니다.

 

자신이 파이썬 언어에 대해서 보다 깊이 공부하고 싶고, 어느정도 자신이 있다면 꼭 한번 읽어보시기를 적극적으로 추천드립니다.

블로그 이미지

Tigercow.Door

Back-end / Python / Database / AI / Algorithm / DeepLearning / etc

댓글을 달아 주세요


안녕하세요. 문범우입니다.

오랜만에 파이썬 관련 내용을 포스팅하게 되었습니다.

최근 자바 언어에 대해 다시 공부하면서 멀티 쓰레딩 개념을 학습중인데, 파이썬에서 해당 내용을 다뤄보지 않은 것 같아 간략하게나마 공부하고 이를 정리해보았습니다.


즉, 이번 글에서는 파이썬에서의 멀티 프로세싱, 멀티 쓰레딩에 대해서 알아보도록 하겠습니다.



글에 앞서서, 멀티 프로세싱, 멀티 쓰레딩 등, 동시성 프로그래밍에 대한 개념적인 내용은 아래 글을 참고하시면 되겠습니다.

https://doorbw.tistory.com/26


먼저 전체적인 코드입니다.


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
from functools import partial 
from threading import Thread
import multiprocessing
import time
 
def singleCount(cnt,name):
    for i in range(1,10000001):
        cnt += 1
        if(i%2500000 == 0):
            print(name,":",i)
 
lists = ['1','2','3','4']
# single process start
cnt = 0
print(" # # SINGLE PROCESSING # # ")
start_time = time.time()
for each in lists:
    singleCount(cnt,each)
print("SINGLE PROCESSING TIME : %s\n" %(time.time()-start_time))
 
# multi process start
cnt = 0
print(" # # MULTI PROCESSING # # ")
start_time = time.time()
pool = multiprocessing.Pool(processes=4)
func = partial(singleCount, cnt)
pool.map(func, lists)
pool.close()
pool.join()
print("MULTI PROCESSING TIME : %s\n" %(time.time()-start_time))
 
#multi threading start
cnt = 0
print(" # # MULTI THREADING # # ")
start_time = time.time()
th1 = Thread(target=singleCount, args=(cnt,"1"))
th1.start()
th1.join()
th2 = Thread(target=singleCount, args=(cnt,"2"))
th2.start()
th2.join()
th3 = Thread(target=singleCount, args=(cnt,"3"))
th3.start()
th3.join()
th4 = Thread(target=singleCount, args=(cnt,"4"))
th4.start()
th4.join()
print("MULTI THREADING TIME : %s\n" %(time.time()-start_time))
 
cs


코드에서는 싱글 프로세싱, 멀티 프로세싱, 멀티 쓰레딩 총 3개의 로직이 구현되어 있으며 이에 대한 결과는 아래와 같습니다.


* 싱글 프로세싱


* 멀티 프로세싱


* 멀티 쓰레딩


위의 결과를 보시면 사실상 싱글 프로세싱과 멀티 쓰레딩의 시간차이는 크게 없고, 멀티 프로세싱에서만 시간 효율이 존재함을 알 수 있습니다.


파이썬에서는 GIL(Global Interpreter Lock)이라는 동작때문에 사실상 여러개의 스레드가 동일한 자원에 대해 접근하지 못합니다.

즉, 우리가 기대한 것과 달리 하나의 스레드가 종료함에 따라 다른 스레드가 진행되는 것이죠. 이러한 GIL때문에 오히려 멀티 쓰레딩이 싱글 쓰레딩보다 I/O작업이 많아 짐에 따라 시간소요가 커질 수 있기도 합니다.

이에 대해 보다 자세한 내용은 아래 링크에서 확인하실 수 있습니다.

https://medium.com/@mjhans83/python-gil-f940eac0bef9


그리고, 멀티 프로세싱은 개념적으로 공부한 것과 같이 다수의 프로세스를 띄워 작업을 처리하기 때문에 당연히 싱글 프로세싱보다 처리 시간이 단축되는 것을 볼 수 있습니다.



오랜만에 다시금 동시성 프로그래밍 개념들에 대해 공부하다보니 또 헷갈리는 내용들과 질문들이 생기게 되었습니다..

위의 글에서 설명이 부족한 이유는 아마 아직 해결되지 못한 궁금증과 질문들 때문일겁니다.. 따로 위에는 적어두지 않았지만 추후에 보다 깊이 이해하게 된다면 한번더 제대로 정리하고 싶어지네요 :-(


잘못된 점이나 궁금하신 점들 언제든지 연락주시면 저도 많이 부족하지만 같이 이야기해보면서 답을 찾아나가면 좋을 것 같습니다 :)

블로그 이미지

Tigercow.Door

Back-end / Python / Database / AI / Algorithm / DeepLearning / etc

댓글을 달아 주세요