5.객체지향 설계


클래스와 객체

객체지향 프로그래밍의 원리

디자인 패턴


원을 나타내는 객체를 정의하고 싶다고 가정해보자. collections 모듈의네임드 튜플을 사용했던 것을 기억할 것이다.('2.3.3 네임드 튜플')

PS D:\Mastering-Python-Design-Patterns-Second-Edition\algo\Abstract_Data_Type> python
Python 3.7.4 (tags/v3.7.4:e09359112e, Jul  8 2019, 19:29:22) [MSC v.1916 32 bit (Intel)] on win32
Type "help", "copyright", "credits" or "license" for more information.
>>> import collections
>>> circle = collections.namedtuple("Circle", "x y radius")
>>> circle
<class '__main__.Circle'>
>>> circle = circle(13,84,9)
>>> circle
Circle(x=13, y=84, radius=9)

하지만 이코드에서는 고려하지 않은 점이 많다. 첫째, 사용자가 원의 반지름(radius)을 입력할 때, 음수 등 유효하지 않은 값을 입력할 수도 있다. 둘째, 코드에서 원 넓이(area)와 둘레(perimeter)를 구하고 싶다면 어떻게 할까?

첫째 문제의 경우, 객체를 만들 때 유효성 검사를 할 수 없다는 것은 순수한 절차적 프로그래밍 방식의 매우 좋지 않은 측면임을 알 수 있다. 잘못된 입력에 대해 많은 예외 처리가 있다고 하더라도, 실제 목적에 맞게 유효성 검증을 할 수 없는 입력 데이터가 존재할 수 있다. 이 예에서 네임드 튜플 대신 리스트를 골랐다고 상상해보자. 리스트의 정렬 속성은 어떻게 다뤄야 할까?

이 예에서 배울 수 있는 점은 명확하다. 오직 우리가 기대하는 속성만 가진 객체를 만들어야 한다. 즉, 데이터를 패키지화하고, 메서드를 제한해야 한다. 이것이 바로 객체지향 프로그래밍이다. 이 예에서는 원을 나타낼 자신만의 고유한 데이터 타입, 즉 클래스를 만들어야 한다.

5.1 클래스와 객체

클래스(class)는 사전에 정의된 특별한 데이터와 메서드의 집합이다. 클래스에 선언된 모양 그대로 생성된 실체를 객체(Object)라고 한다. 객체가 소프트웨어에 실체화될 떄(메모리에 할당되어 사용될 떄), 이 실체를 인스턴스(instance)라고 한다. 객체는 인스턴스를 포함할 수 있으며, 포괄적인 의미를 지닌다. 파이썬에서 가장 간단한 형태의 클래스는 다음과 같다.

class ClassName:
      # 문장1
      # ...
      # 문장 n
      pass
      
>>> x = ClassName() 클래스 정의에 따라 인스턴스 생성
>>> x

5.1.1 클래스 인스턴스 생성

클래스 인스턴스 생성(class instantiation)은 함수 표기법을 사용하여 초기 상태의 객체를 생성하는 일이다. 인스턴스 생성작업은 어떤 특징을 가진 빈 객체를 만드는 것이다. (여러 범위의 ) 여러 이름을 같은 객체에 바인딩(binding)(또는 에일리어싱 aliasing) 할 수 있다. Hello 라는 클래스가 있다고 하자. 그러면 Hello()를 호출하여 객체를 생성하는데, 이떄 Hello()를 생성자(constructor)라고 한다. 생성자를 호출하면 Hello._new_() 라는 특수 메서드가 호출되어 객체가 할당되고 그 다음 Hello.__init__()메서드가 객체를 초기화한다. 


속성
객체에는 데이터(data)와 메서드(method)로 이루어지는 클래스 속성(attribute)이 있다. 메서드 속성은 함수인데, 그 첫 번째 인수는 호출된 인스턴스 자신이다.(파이썬에서는 이를 셀프(self)라고 한다. )

속성은 점(.)뒤에 나오는 모든 이름이다. 모듈 내 모든 이름의 참조는 속성 참조다. 모듈명.함수명과 같은 표현식에서 모듈명은 모듈 객체이고, 함수명은 객체의 속성 중 하나다. 속성은 읽기 전용일 수도 있고 쓰기 가능할 수도 있다. 쓰기 가능한 속성은 del문으로 삭제할 수 있다.


네임스페이스
네임스페이스(namespace)는 이름을 객체로 매핑(mapping)하는 것이다. 대부분 네임스페이스는 파이썬 딕셔너리로 구현되어 있다. 네임스페이스의 예로는 내장된 이름 셋, 모듈의 전역이름, 함수의 지역 이름 등이 있다.
스크립트 파일이나 대화식 인터프리터의 최상위 호출에 의해 실행되는 명령문은 __main__ 이라는 모듈의 일부로 간주되어, 고유의 전역 네임스페이스를 갖는다.


스코프
스코프(scope)는 네임스페이스에 직접 접근할 수 있는 파이썬 프로그램의 텍스트영역(textual region)이다. 스코프는 정적으로 결정되지만, 동적으로 사용된다. 즉 스코프는  텍스트에 따라 결정된다. 즉 한 모듈에 정의된 함수의 전역 스코프는 해당 모듈의 네임스페이스다. 클래스 정의가 실행되면, 새로운 네임스페이스가 만들어지고, 지역 스코프로 사용된다. 

5.2 객체지향 프로그래밍의 원리


5.2.1 특수화

**특수화**는 슈퍼(super) 클래스(부모(parent) 또는 베이스(base) 클래스라고도 한다)의 모든 속성을 상속(inheritance)하여 새 클래스를 만드는 절차다. 모든 메서드는 서브(sub)클래스(자식 클래스)에서 재정의(override), 재구현(re-implemented)될 수 있다. (파이썬에서 모든 메서드는 가상(virtual)이다.) 상속은 is-a관계다. 사람클래스와 이를 상속 받는 학생 클래스가 있다고 하자. 이 때 "모든 학생은 사람이다"라는 명제가 성립하며 이것이 is-a 관계다. 반대로 "모든 사람은 학생이다"는 성립하지 않는다. 구글 파이썬 가이드에서는 한 클래스가 다른 클래스를 상속 받지 않으면, 파이썬의 최상위 클래스인 object를 명시적으로 표기하는 것을 권장한다.

즉 좋은 예는 다음과 같다.

class SampleClass(object):
    pass

class OuterClass(object):
    class InnerClass(object):
        pass

class ChildClass(ParentClass):
    """부모 클래스 상속"""

반면 다음은 나쁜 예이다.

class SampleClass:
    pass

class OuterClass:
    class InnerClass:
        pass

5.2.2 다형성

**다형성(polymorphism)**(또는 동적 메서드 바인딩)은 메서드가 서브 클래스 내에서 재정의 될 수 있다는 원리다. 즉, 서브 클래스 객체에서 슈퍼 클래스와 동명의 메서드를 호출하면, 파이썬은 서브 클래스에 정의된 메서드를 사용한다는 뜻이다. 슈퍼 클래스의 메서드를 호출해야 한다면, 내장된 super() 메서드를 사용하여 쉽게 호출할 수 있다.

예를 들어 파이썬에서 사용자 정의 클래스의 모든 객체는 기본적으로 **해시가능(hashable)**하다. 객체가 해시 가능하다는 것은 hash() 속성을 호출할 수 있다는 뜻이며 불변 객체임을 의미한다. 다음 예제를 살펴보자.

class Symbol(object):
    def __init__(self,value):
        self.value = value



if __name__ == "__main__":
    x = Symbol("Py")
    y = Symbol("Py")


    symbols = set()
    symbols.add(x)
    symbols.add(y)

    print(x is y )
    print( x==y)
    print(len(symbols))
FalseFalse

두 변수 x,y의 참조가 다르므로 첫 번째 결과(x is y)는 예상대로 False가 나왔다. 그런데, x,y의 값이 같으니 두 번째 조건(x == y)은 True가 되어야 할 것 같지만 결과는 False다. 세 번째 결과 역시 셋은 중복 항목이 없으므로 길이가 1이 나와야 할 것 같지만 2가 나왔다.

두 번째와 세 번째 결과를 고치기 위해 객체의 비교를 담당하는 &#95;&#95;eq&#95;&#95;() 메서드를 재정의해보자.

class Symbol(object):
    def __init__(self,value):
        self.value = value

    def __eq__(self, other):
        if isinstance(self, other.__class__):
            return self.value == other.value
        else:
            return NotImplemented



if __name__ == "__main__":
    x = Symbol("Py")
    y = Symbol("Py")


    symbols = set()
    symbols.add(x)
    symbols.add(y)

    print(x is y )
    print( x==y)
    print(len(symbols))

결과

PS D:\Mastering-Python-Design-Patterns-Second-Edition\algo\Object_Design> python .\2_hash_and_eq_NO.py
Traceback (most recent call last):
  File ".\2_hash_and_eq_NO.py", line 19, in <module>
    symbols.add(x)
TypeError: unhashable type: 'Symbol'

&#95;&#95;eq&#95;&#95;() 메서드를 재정의하자 Symbol 클래스가 해시 가능하지 않다고 (unhashable ) 에러가 발생한다. 객체가 해시 가능하지 않다는 것은 가변 객체임을 의미하는데, 셋은 불변 객체다. 에러를 고치기 위해 &#95;&#95;hash&#95;&#95;() 메서드를 추가한다.

class Symbol(object):    def __init__(self,value):        self.value = value    def __eq__(self, other):        if isinstance(self, other.__class__):            return self.value == other.value        else:            return NotImplemented    def __hash__(self):        return hash(self.value)if __name__ == "__main__":    x = Symbol("Py")    y = Symbol("Py")    symbols = set()    symbols.add(x)    symbols.add(y)    print(x is y )    print( x==y)    print(len(symbols))

결과

PS D:\Mastering-Python-Design-Patterns-Second-Edition\algo\Object_Design> python .\3_hash_and_eq_OK.pyFalseTrue1

이제 예상대로 결과가 나오는 것을 확인할 수 있다.

5.2.3 합성과 집합화

**합성(composition) 그리고 집합화(aggregation)**은 한 클래스에서 다른 클래스의 인스턴스 변수를 포함하는 것을 말하며, 클래스 간의 관계를 나타낸다. 파이썬의 모든 클래스는 상속을 사용한다(object 베이스 클래스로부터 상속받는다.) 대부분 클래스는 다양한 타입의 인스턴스 변수를 가지며, 합성과 집합화를 사용한다. 두 클래스 A,B가 있다고 가정한다. 합성은 A와 B가 강한 연관 관계를 맺으며, 강한 생명주기(strong lifecycle)를 갖는다. 즉, 의존성이 강하다. 예를 들어 집 클래스는 방 클래스를 갖는다. 집이 있으면 방(공간)이 있다.

집합화는 A와 B가 연관 관계가 있지만, 생명주기가 약하며 독립적이다. 예를 들어 학생 클래스는 미술, 음악 등의 과목 클래스를 갖는다. 한 학생은 미술, 음악 두 과목을 수강할 수 있고, 그중 한 과목 또는 두 과목 모두 수강하지 않을 수 있다.

5.2.4 클래스 예제

이번 장 앞에서 네임드 튜플로 구현한 원 클래스를 객체지향 설계로 다시 구현해보자. 즉 원의 데이터 컨테이너를 만들 것이다. 먼저, 일반적인 데이터와 메서드 속성을 가진 점(Point) 클래스를 구현하고, 그다음 상속을 사용하여 Circle 서브 클래스를 구현했다.

#클래스 예제import math"""hypot 쓰인 함수 설명math.hypot(*coordinates)유클리드 크기(norm) sqrt(sum(x**2 for x in coordinates))를 반환합니다. 원점에서 coordinates로 지정된 점까지의 벡터의 길이입니다.2차원 점 (x, y)의 경우, 피타고라스 정리를 사용하여 직각 삼각형의 빗변(hypotenuse)을 계산하는 것과 동등합니다, sqrt(x*x + y*y).버전 3.8에서 변경: n 차원 점에 대한 지원이 추가되었습니다. 이전에는, 2차원인 경우만 지원되었습니다.math.hypot(x, y)	가로 x 세로 y인 직각삼각형의 빗면의 유클리드 거리를 반환합니다.	root(x^2 + y^2)x*x + y*y에서 루트씌웠다고 생각하면 됨"""#주클래스 점#서브클래스 원은 점클래스를 상속받는다.class Point(object):    def __init__(self, x=0, y=0):        self.x = x #데이터 속성(attribute)        self.y = y    def distance_From_origin(self): #메서드 속성        return math.hypot(self.x,self.y)    def __eq__(self, other):        return self.x == other.x and self.y == other.y    def __repr__(self):        return "point ({0.x!r}, {0.y!r}".format(self)    def __str__(self):        return "({0.x!r}, {0.y!r}".format(self)class Circle(Point):    def __init__(self,radius,x=0, y=0):        super().__init__(x,y) #생성 및 초기화        self.radius = radius    def edge_distance_from_origin(self):        return abs(self.distance_From_origin()- self.radius)    def area(self):        return math.pi*(self.radius**2)    def circumference(self):        return 2*math.pi*self.radius    def __eq__(self,other):        return self.radius == other.radius and super().__eq__(other)    def __repr__(self):        return "circle ({0.radius!r}, {0.x!r})".format(self)    def __str__(self):        return repr(self)        

5.3 디자인패턴


**디자인 패턴(design pattern)**은 잘 설계된 구조의 형식적 정의를 소프트웨어 엔지니어링으로 옮긴 것이다. 다양한 디자인 패턴이 있고 이들을 사용하여 서로 다른 문제를 해결할 수 있다.

5.3.1 데커레이터 패턴

테커레이터 패턴은 @표기를 사용해 함수 또는 메서드의 변환을 우아하게 지정해주는 도구다. 데커레이터 패턴은 함수의 객체와 함수를 변경하는 다른 객체의 래핑(wrapping)을 허용한다. 구글 파이썬 스타일 가이드의 코드 예제를 살펴보자.

Class C(object):    @my_decorator    def method(self):        #메서드 내용

위 코드가 뜻하는 바는 아래와 같다.

class C(object):    def method(self):          # 메서드 내용    method = my_decorator(method)

데커레이터를 사용하여 리스트에 임의의 값을 넣는 함수를 벤치마킹하는 코드 예제는 다음과 같다.

import random
import time


def benchmark(func):
    def wrapper(*args, **kwargs):
        t = time.perf_counter()
        print("t",t)
        res = func(*args, **kwargs)
        print("{0} {1}".format(func.__name__, time.perf_counter()-t))
        return res

    return wrapper

#파이썬에서 코드 실행시간을 측정하는 방법을 찾아 테스트해봤습니다.
#파이썬 3.3 이상부터 perf_counter와 process_time을 사용할 수 있는데 차이점은 다음과 같습니다.

# perf_counter는 sleep 함수를 호출하여 대기한  시간을 포함하여 측정합니다. 
# process_time는 실제로 연산하는데 걸린 시간만 측정합니다. 

@benchmark
def random_tree(n):
    temp = [ n for n in range(n)]
    for i in range(n+1):
        temp[random.choice(temp)] = random.choice(temp)
    return temp


if __name__ == "__main__":
    random_tree(10000)

결과

PS D:\Mastering-Python-Design-Patterns-Second-Edition\algo\Object_Design> python .\4_benchmark_decorator.py
t 0.2078034
random_tree 0.07035290000000002

파이썬에서 일반적으로 사용하는 데커레이터는 @classmethod와 @static-method가 있다. 이들은 각각 메서드를 클래스와 정적 메서드로 변환한다. 다음 코드에서 두 데커레이터의 차이점을 살펴보자. @classmethod는 첫 번째 인수로 클래스(cls)를 사용하고, @staticmethod는 첫 번째 인수에 self 혹은 cls가 없다. 클래스 내 변수에 접근하려면 @classmethod의 첫 번째 인수를 사용할 수 있다.

class A(object):
    _hello = True

    def foo(self, x):
        print("foo{0} {1} 실행".format(self,x))

    @classmethod
    def class_foo(cls, x):
        print("class_foo({0}, {1}) 실행: {2}".format(cls,x, cls._hello))

    @staticmethod
    def static_foo(x):
        print("static_foo({0}) 실행".format(x))

if __name__ == "__main__":
    a = A() 
    a.foo(1)
    a.class_foo(2)
    A.class_foo(2)
    a.static_foo(3)
    A.static_foo(3)
    

결과

PS D:\Mastering-Python-Design-Patterns-Second-Edition\algo\Object_Design> python .\5_class_and_static_decorator.pyfoo<__main__.A object at 0x019A42D0> 1 실행class_foo(<class '__main__.A'>, 2) 실행: Trueclass_foo(<class '__main__.A'>, 2) 실행: Truestatic_foo(3) 실행static_foo(3) 실행

5.3.2 옵서버 패턴

옵서버(observer) 패턴은 특정 값을 유지하는 핵심 객체를 갖고, 직렬화된 객체의 복사본을 생성하는 일부 옵서버(관찰자)가 있는 경우 유용하다. 즉, 객체와 일대다(one-to-many) 의존 관계에서 한 객체의 상태가 변경되면 그 객체에 종속된 모든 객체에 그 내용을 통지하여 자동으로 상태를 갱신하는 방식이다. 옵서버 패턴은 @property 데커레이터를 사용하여 구현할 수 있다. 예를 들어 속성(property)을 읽기 전용으로 설정하는 것과 같은 속성 접근을 제어할 수 있다, 속성은 접근자(access)나 getter/setter 메서드 대신 사용된다. 먼저 간단한 속성 예제를 살펴보자.

class C:    def __init__(self,name):        self._name = name    @property    def name(self):        return self._name    @name.setter    def name(self, new_name):        self._name = "{0} >> {1}".format(self._name, new_name)c= C("진")print(c._name)print(c.name)c.name="아스틴"print(c._name)

결과

PS D:\Mastering-Python-Design-Patterns-Second-Edition\algo\Object_Design> python .\test.py진진진 >> 아스틴
파이썬의 옵서버 패턴은 다른 컴퓨터 언어와조금 다른 방식으로 구현된다. 다음은 속성을 사용한 옵서버 패턴의 구현 내용과 예제다. [Dive Into Design Pattern](SourceMaking.com, 2018)을 참조했다. 유튜브 InfoQ 채널의 Tutorial: The Observer Pattern in Python도 참조했다.
class Subcriber(object):    def __init__(self, name):        self.name=name    def update(self,message):        print("{0}, {1}".format(self.name, message))class Publisher(object):    def __init__(self):        self.subscribers=set()    def register(self,who):        self.subscribers.add(who)    def unregister(self, who):        self.subscribers.discard(who)    def dispatch(self,message):        for subscriber in self.subscribers:            subscriber.update(message)if __name__ == "__main__":    pub = Publisher()    astin = Subcriber("아스틴")    james = Subcriber("제임스")    jeff = Subcriber("제프")    pub.register(astin)    pub.register(james)    pub.register(jeff)    pub.dispatch("점심시간입니다.")    pub.unregister(jeff)    pub.dispatch("퇴근시간입니다.")    

결과

PS D:\Mastering-Python-Design-Patterns-Second-Edition\algo\Object_Design> python .\6_observer_pattern_with_set.py아스틴, 점심시간입니다.제프, 점심시간입니다.  제임스, 점심시간입니다.아스틴, 퇴근시간입니다.제임스, 퇴근시간입니다.

Publisher 클래스에서 셋을 사용하여 옵서버 패턴을 구현해봤다. 다음코드에서는 딕셔너리를 사용해보자.

class SubscriberOne(object):    def __init__(self,name):        self.name=name    def update(self,message):        print("{0}, {1}".format(self.name, message))class SubscriberTwo(object):    def __init__(self,name):        self.name = name    def receive(self,message):        print("{0}, {1}".format(self.name, message))class Publisher(object):    def __init__(self):        self.subscribers = dict()        def register(self,who,callback=None):        if callback is None:            callback = getattr(who,'update')        self.subscribers[who] = callback    def unregister(self,who):        del self.subscribers[who]    def dispatch(self,message):        for subscriber,callback in self.subscribers.items():            callback(message)if __name__ == "__main__":    pub = Publisher()    astin = SubscriberOne("아스틴")    james = SubscriberTwo("제임스")    jeff = SubscriberOne("제프")    pub.register(astin, astin.update)    pub.register(james, james.receive)    pub.register(jeff)    pub.dispatch("점심시간입니다.")    pub.unregister(jeff)    pub.dispatch("퇴근시간입니다.")
PS D:\Mastering-Python-Design-Patterns-Second-Edition\algo\Object_Design> python .\7_observer_pattern_with_dict.py아스틴, 점심시간입니다.제임스, 점심시간입니다.제프, 점심시간입니다.  아스틴, 퇴근시간입니다.제임스, 퇴근시간입니다.

Subscriber 클래스, 즉 구독자의 형태가 다양하면 이전 코드보다 조금 더 유연하게 구현할 수 있다.(SubscriberOne 클래스, SubscriberTwo 클래스). 마지막으로 이벤트 기반의 옵서버 패턴을 살펴보자.

class Subscriber(object):    def __init__(self,name):        self.name = name    def update(self,message):        print("{0}, {1}".format(self.name, message))class Publisher(object):    def __init__(self,events):        self.subscribers = {event: dict() for event in events}        print('__init__ events',events)    def get_subscribers(self,event):        return self.subscribers[event]    def register(self,event,who,callback=None):        if callback is None:            callback = getattr(who, 'update')        self.get_subscribers(event)[who] = callback        print("event",event)        print("who",who)        print("callback",callback)         print("self",self)    def unregister(self, event, who):        del self.get_subscribers(event)[who]    def dispatch(self, event, message):        for subscriber, callback in self.get_subscribers(event).items():            callback(message)       if __name__ == "__main__":    pub = Publisher(["점심", "퇴근"])    astin = Subscriber("아스틴")    james = Subscriber("제임스")    jeff = Subscriber("제프")    pub.register("점심", astin)    pub.register("퇴근",astin)    pub.register("퇴근",james)    pub.register("점심",jeff)    pub.dispatch("점심", "점심시간입니다.")    pub.dispatch("퇴근","저녁시간입니다.")

코드결과

PS D:\Mastering-Python-Design-Patterns-Second-Edition\algo\Object_Design> python .\8_observer_pattern_with_event.py
__init__ events ['점심', '퇴근']
event 점심
who <__main__.Subscriber object at 0x00A61D30>
callback <bound method Subscriber.update of <__main__.Subscriber object at 0x00A61D30>>
self <__main__.Publisher object at 0x00A61CD0>
event 퇴근
who <__main__.Subscriber object at 0x00A61D30>
callback <bound method Subscriber.update of <__main__.Subscriber object at 0x00A61D30>>
self <__main__.Publisher object at 0x00A61CD0>
event 퇴근
who <__main__.Subscriber object at 0x016D4690>
callback <bound method Subscriber.update of <__main__.Subscriber object at 0x016D4690>>
self <__main__.Publisher object at 0x00A61CD0>
event 점심
who <__main__.Subscriber object at 0x016D4670>
callback <bound method Subscriber.update of <__main__.Subscriber object at 0x016D4670>>
self <__main__.Publisher object at 0x00A61CD0>
아스틴, 점심시간입니다.
제프, 점심시간입니다.
아스틴, 저녁시간입니다.
제임스, 저녁시간입니다.

5.3.3 싱글턴 패턴

초기화된 객체의 인스턴스를 전역에서 사용하기 위해서는 싱글턴(singleton) 패턴을 사용한다. 이 객체의 인스턴스는 하나만 존재한다. 파이썬에는 private 접근 제한자가 없기 때문에 &#95;&#95;name&#95;&#95;() 클래스 메서드를 가지고 하나의 인스턴스만 생성되도록 구현해야 한다. 먼저 싱글턴 인스턴스가 생성되었는지 확인한다. (이미 싱글턴 인스턴스가 생성되었는데, 또 다시 생성을 시도했는지 확인한다). 싱글턴 인스턴스가 없다면, 슈퍼 클래스를 호출하여 싱글턴 인스턴스를 생성한다.

class SinEx:
    _sing =None

    def __new__(self, *args, **kwargs):
        if not self._sing:
            self._sing = super(SinEx, self).__new__(self, *args, **kwargs)
        return self._sing


x = SinEx()
print("x",x)

y = SinEx()
print("x == y check: ",x==y)
print("y",y)
PS D:\Mastering-Python-Design-Patterns-Second-Edition\algo\Object_Design> python .\9_singleton.py
x <__main__.SinEx object at 0x016D42F0>
x == y check True
y <__main__.SinEx object at 0x016D42F0>

위 예제에서 두 객체의 주소는 같으므로, 두 객체는 같다. 사실 디자인 패턴에 대해서는 다양한 의견이 있다. 파이콘 스웨덴에서 발표한 파이썬 디자인 패턴에 대한 "Design Patterns in Python by Peter Ullrich"유튜브 영상도 참고할 만하다.

CHAPTER 04

구조와 모듈

모듈

제어문

파일 처리

오류처리

4.1 모듈


파이썬에서 모듈(module)은 def를 사용하여 정의한다. def가 실행되면, 함수의 객체와 참조가 같이 생성된다.</br>

반환값을 정의하지 않으면, 파이썬은 자동으로 None을 반환한다. C언어와 마찬가지로, 아무런 값을 반환하지 않는 함수는 프로시저(procedure)라고 부른다.</br>


4.1.1 스택과 활성화 레코드

함수가 호출될 때마다 활성화 레코드(activation record)가 생성된다. 활성화 레코드에는 함수의 정보(반환값, 매개변수, 지역 변수, 반환값, 반환 주소 등)가 기록되며, 이를 스택(stack1)에 저장한다. 활성화 레코드는 다음과 같은 순서로 처리된다.

  1. 함수의 실제 매개변수를 스택에 저장(push)한다.
  2. 반환 주소를 스택에 저장한다.
  3. 스택의 최상위 인덱스를 함수의 지역 변수에 필요한 총량만큼 늘린다.
  4. 함수로 건너뛴다.(jump)

활성화 레코드를 풀어내는 unwinding 절차는 다음과 같다.

  1. 스택의 최상위 인덱스는 함수에 소비된 총 메모리양(지역 변수)만큼 감소한다.
  2. 반환 주소를 스택에서 빼낸다.(pop)
  3. 스택의 최상위 인덱스는 함수의 실제 매개변수만큼 감소한다.

4.1.2 모듈의 기본값

모듈을 생성할 때, 함수 또는 메서드에서 가변 객체를 기본값으로 사용해선 안된다. 나쁜 예와 좋은 예를 살펴보자. 먼저 나쁜예다.


  1. 역자주 여기서는 콜 스택, 실행 스택, 제어 스택, 런타임 스택, 기계 스택 등의 맥락에 해당하는 스택을 말한다. 주로 현재 실행중인 서브루틴을 실행한 다음 어디로 돌아가야할지등을 저장하는 데 쓰인다.(https://ko.wikipedia.org/wiki/콜_스택 참조)

코드

def append(number,number_list=[]):
    number_list.append(number)
    return number_list

print("예상결과[5]",append(5)) 
print("예상결과[5,2]",append(7)) 
print("예상결과[5,2,7]",append(2)) 

결과

PS D:\Mastering-Python-Design-Patterns-Second-Edition\algo\module_structure> python .\test.py
예상결과[5] [5]
예상결과[5,2] [5, 7]
예상결과[5,2,7] [5, 7, 2]

좋은 예는 다음과 같다. 초기화 로직을 넣었다.

코드

# def append(number,number_list=[]):#     number_list.append(number)#     return number_list# print("예상결과[5]",append(5)) # print("예상결과[5,2]",append(7)) # print("예상결과[5,2,7]",append(2)) def append(number,number_list=None):    if number_list is None:        number_list = []    number_list.append(number)    return number_listprint("append[5]",append(5))print("append[7]",append(7))print("append[2]",append(2))

결과

PS D:\Mastering-Python-Design-Patterns-Second-Edition\algo\module_structure> python .\test.pyappend[5] [5]append[7] [7]append[2] [2]

4.1.3 __\__init\__.py 파일

패키지(package)는 모듈과 \__init\__.py 파일이 있는 디렉터리다. 파이썬은 \__init\__.py 파일이 있는 디렉터리를 패키지로 취급한다. 모듈 검색 경로 중 string과 같이 흔한 이름의 디렉터리에 유효한 모듈이 들어 있는 경우 이러한 모듈이 검색되지 않는 문제를 방지하기 위해서다.

import 폴더이름.파일모듈명

__\____init\___.py 파일은 빈 파일일 수도 있지만, 패키지의 초기화 코드를 실행하거나, \____all\__ 변수를 정의할 수도 있다.

__all__ = ["파일",....]

실제 파일 이름은 확장자가 .py겠지만, 여기서 작성할 때는 .py를 붙이지 않는다. 다음 명령문을 살펴보자.

from 폴더이름 import *

위 코드는 이름이 __\__ __로 시작하는 모듈을 제외한 모듈의 모든 객체를 불러온다. \____all\___ 변수가 있는 경우, 해당 리스트의 객체를 불러온다.

터미널에서 특정 모듈이 있는지 간단하게 확인하려면, **python -c import 모듈** 명령을 사용하면된다.

PS D:\Mastering-Python-Design-Patterns-Second-Edition> python -c "import astin"Traceback (most recent call last):  File "<string>", line 1, in <module>      ModuleNotFoundError: No module named 'astin'

4.1.4 \___name\___ 변수

파이썬은 모듈을 임포트(import) 할 때마다 __\__name\__이라는 변수를 만들고, 모듈 이름을 저장한다. 이해를 돕기 위해 다음과 같이 hello.py 파일을 저장한 후 , 그 위치에서 대화식 인터프리터를 실행하여 hello 모듈을 임포트해보자.

hello = "hello"def world():    return "World"if __name__ == "__main__":    print("{0} 직접 실행됨".format(__name__))else:    print("{0} 임포트됨".format(__name__))    
Python 3.7.4 (tags/v3.7.4:e09359112e, Jul  8 2019, 19:29:22) [MSC v.1916 32 bit (Intel)] on win32Type "help", "copyright", "credits" or "license" for more information.>>> import hellohello 임포트됨>>> hello.hello'hello'>>>    >>> hello.world()'World'>>> __name__'__main__'

대화식 인터프리터 또는 .py파일을 직접 실행하면 파이썬은 _\__name_\__\__main\__ 으로 설정하므로, 위 코드 조건문에서 참에 해당하는 코드를 실행한다. 이번에는 hello.py를 직접 실행해보자. 차이를 알 수 있을 것이다.

PS D:\Mastering-Python-Design-Patterns-Second-Edition\algo\module_structure> python .\hello.py__main__ 직접 실행됨

4.1.5 컴파일된 바이트코드 모듈

컴파일러가 사용하는 바이트 컴파일 코드(byte-complied code)는 표준 모듈을 많이 사용하는 프로그램의 시작시간(로딩 시간)을 줄이기 위한것이다. -0 플래그를 사용하여 파이썬 인터프리터를 호출하면, 최적화된 코드가 생성되어 .pyo파일에 저장된다. (최적화 과정에서 assert문은 제거된다. 파이썬 3.5부터는 .pyo대신 .pyc를 사용한다. 이렇게 만든 파일은 리버스 엔지니어링이 까다로우므로 라이브러리로 배포하는 데에도 사용할 수 있다. 파이썬 플래그에 대한 정보를 확인하고 싶다면 다음과같이 --help를 붙여 파이썬을 실행해보자.)

PS D:\Mastering-Python-Design-Patterns-Second-Edition\algo\module_structure> python --helpusage: C:\Program Files (x86)\Python37-32\python.exe [option] ... [-c cmd | -m mod | file | -] [arg] ...Options and arguments (and corresponding environment variables):-b     : issue warnings about str(bytes_instance), str(bytearray_instance)         and comparing bytes/bytearray with str. (-bb: issue errors)-B     : don't write .pyc files on import; also PYTHONDONTWRITEBYTECODE=x-c cmd : program passed in as string (terminates option list)-d     : debug output from parser; also PYTHONDEBUG=x-E     : ignore PYTHON* environment variables (such as PYTHONPATH)-h     : print this help message and exit (also --help)-i     : inspect interactively after running script; forces a prompt even         if stdin does not appear to be a terminal; also PYTHONINSPECT=x-I     : isolate Python from the user's environment (implies -E and -s)-m mod : run library module as a script (terminates option list)-O     : remove assert and __debug__-dependent statements; add .opt-1 before         .pyc extension; also PYTHONOPTIMIZE=x-OO    : do -O changes and also discard docstrings; add .opt-2 before         .pyc extension-q     : don't print version and copyright messages on interactive startup-s     : don't add user site directory to sys.path; also PYTHONNOUSERSITE-S     : don't imply 'import site' on initialization-u     : force the stdout and stderr streams to be unbuffered;         this option has no effect on stdin; also PYTHONUNBUFFERED=x-v     : verbose (trace import statements); also PYTHONVERBOSE=x         can be supplied multiple times to increase verbosity-V     : print the Python version number and exit (also --version)         when given twice, print more information about the build-W arg : warning control; arg is action:message:category:module:lineno         also PYTHONWARNINGS=arg-x     : skip first line of source, allowing use of non-Unix forms of #!cmd-X opt : set implementation-specific option--check-hash-based-pycs always|default|never:    control how Python invalidates hash-based .pyc filesfile   : program read from script file-      : program read from stdin (default; interactive mode if a tty)arg ...: arguments passed to program in sys.argv[1:]Other environment variables:PYTHONSTARTUP: file executed on interactive startup (no default)PYTHONPATH   : ';'-separated list of directories prefixed to the               default module search path.  The result is sys.path.PYTHONHOME   : alternate <prefix> directory (or <prefix>;<exec_prefix>).               The default module search path uses <prefix>\python{major}{minor}.PYTHONCASEOK : ignore case in 'import' statements (Windows).PYTHONIOENCODING: Encoding[:errors] used for stdin/stdout/stderr.PYTHONFAULTHANDLER: dump the Python traceback on fatal errors.PYTHONHASHSEED: if this variable is set to 'random', a random value is used   to seed the hashes of str, bytes and datetime objects.  It can also be   set to an integer in the range [0,4294967295] to get hash values with a   predictable seed.PYTHONMALLOC: set the Python memory allocators and/or install debug hooks   on Python memory allocators. Use PYTHONMALLOC=debug to install debug   hooks.PYTHONCOERCECLOCALE: if this variable is set to 0, it disables the locale   coercion behavior. Use PYTHONCOERCECLOCALE=warn to request display of   locale coercion and locale compatibility warnings on stderr.PYTHONBREAKPOINT: if this variable is set to 0, it disables the default   debugger. It can be set to the callable of your debugger of choice.PYTHONDEVMODE: enable the development mode.

4.1.6 sys 모듈

sys.path는 인터프리터가 모듈을 검색할 경로를 담은 문자열 리스트다. sys.path 변수는 PYTHONPATH 환경변수 또는 내장된 기본값 경로로 초기화된다. 환경변수를 수정하면 모듈 경로를 추가하거나 임시로 모듈 경로를 추가할 수 있다.

Python 3.7.4 (tags/v3.7.4:e09359112e, Jul  8 2019, 19:29:22) [MSC v.1916 32 bit (Intel)] on win32Type "help", "copyright", "credits" or "license" for more information.>>> import sys>>> sys.path['', 'C:\\Program Files (x86)\\Python37-32\\python37.zip', 'C:\\Program Files (x86)\\Python37-32\\DLLs', 'C:\\Program Files (x86)\\Python37-32\\lib', 'C:\\Program Files (x86)\\Python37-32', 'C:\\Users\\smart\\AppData\\Roaming\\Python\\Python37\\site-packages', 'C:\\Program Files (x86)\\Python37-32\\lib\\site-packages']

sys.ps1과 sys.ps2 변수는 파이썬 대화식 인터프리터의 기본 및 보조 프롬프트(prompt) 문자열을 정의한다.(기본 값은 각각 >>> 및 ...이다.).

이미 앞에서도 사용했지만, sys.argv 변수를 사용하면 명령 줄에 전달된 인수를 프로그램 내에서 사용할 수 있다.

PS D:\Mastering-Python-Design-Patterns-Second-Edition\algo\module_structure> python .\test.py 로미오 줄리엣 로미오줄리엣

dir() 내장 함수는 모듈이 정의하는 모든 유형의 이름(모듈, 변수, 함수)을 찾는 데 사용된다. 이름 기준으로 정렬된 문자열 리스트를 반환한다.

>>> import sys>>> dir (sys)['__breakpointhook__', '__displayhook__', '__doc__', '__excepthook__', '__interactivehook__', '__loader__', '__name__', '__package__', '__spec__', '__stderr__', '__stdin__', '__stdout__', '_base_executable', '_clear_type_cache', '_current_frames', '_debugmallocstats', '_enablelegacywindowsfsencoding', '_framework', '_getframe', '_git', '_home', '_xoptions', 'api_version', 'argv', 'base_exec_prefix', 'base_prefix', 'breakpointhook', 'builtin_module_names', 'byteorder', 'call_tracing', 'callstats', 'copyright', 'displayhook', 'dllhandle', 'dont_write_bytecode', 'exc_info', 'excepthook', 'exec_prefix', 'executable', 'exit', 'flags', 'float_info', 'float_repr_style', 'get_asyncgen_hooks', 'get_coroutine_origin_tracking_depth', 'get_coroutine_wrapper', 'getallocatedblocks', 'getcheckinterval', 'getdefaultencoding', 'getfilesystemencodeerrors', 'getfilesystemencoding', 'getprofile', 'getrecursionlimit', 'getrefcount', 'getsizeof', 'getswitchinterval', 'gettrace', 'getwindowsversion', 'hash_info', 'hexversion', 'implementation', 'int_info', 'intern', 'is_finalizing', 'last_traceback', 'last_type', 'last_value', 'maxsize', 'maxunicode', 'meta_path', 'modules', 'path', 'path_hooks', 'path_importer_cache', 'platform', 'prefix', 'ps1', 'ps2', 'set_asyncgen_hooks', 'set_coroutine_origin_tracking_depth', 'set_coroutine_wrapper', 'setcheckinterval', 'setprofile', 'setrecursionlimit', 'setswitchinterval', 'settrace', 'stderr', 'stdin', 'stdout', 'thread_info', 'version', 'version_info', 'warnoptions', 'winver']

dir()함수는 내장 함수 및 변수의 이름까지는 나열하지 않는다. 객체의 모든 메서드나 속성을 찾는 데 유용하다.

4.2 제어문

4.2.1 if문

파이썬 if문은 다른 언어의 switch문 또는 case문을 대체한다. 다음 예제를 살펴보자.

x = int(input("숫자를 입력하세요:"))if x < 0:    x =0     print("음수를 입력하여 x를 0으로 변경했습니다.")elif x == 0:    print("0이 입력되었습니다.")elif x == 1:    print("1이 입력되었습니다.")else:    print("2이상의 숫자가 입력되었습니다.")

결과

PS D:\Mastering-Python-Design-Patterns-Second-Edition\algo\module_structure> python .\test.py숫자를 입력하세요:11이 입력되었습니다.

4.2.2 for문

파이썬 for문은 C나 파스칼 언어와 다르다. 파스칼처럼 숫자의 산술 진행을 반복하거나, C처럼 사용자가 반복 단계와 조건을 모두 정의할 수 있도록 하는 대신, 파이썬의 모든 for문은 모든 시퀸스 항목(리스트, 문자열 등)을 순서대로 순회한다.

for name in names:    print(name)

4.2.3 참과 거짓

거짓(False)는 사전 정의된 상수 False 또는 숫자 0, 특수 객체 None, 빈 컬렉션 시퀸스(빈 문자열 '', 빈 리스트[], 빈 튜플() , 빈 딕셔너리{}에 의해 정의된다. 여기에 속하지 않은 값은 모두 참(True)이다.) 비교 또는 다른 불리언 표현식의 결과를 변수에 할당할 수 있다.

>>> string1, string2, string3 = '', '괴물', '외계인'>>> non_null = string1 or string2 or string3>>> non_null'괴물'>>> non_null'괴물'>>> non_null'괴물'>>> non_null'괴물'>>> non_null'괴물'>>> non_null'괴물'

구글 파이썬 스타일 가이드에서는 암묵적인(implicit) False 사용에 대해 다음과 같은 기준을 세워뒀다.

  • == 또는 != 연산자를 사용하여 내장 변수 None 같은 싱글턴(singleton)을 비교하지 않는다. 대신 is 또는 is not을 사용한다.
  • if x is not None과 if x 를 잘 구분해서 사용한다.
  • ==를 사용하여 불리언 변수를 False와 비교하지 않는다. 대신 if not x를 사용한다. None과 False를 구별할 필요가 있는 경우, if not x and x is not None과 같은 연결 표현식을 사용한다.
  • 시퀸스(문자열,리스트,튜플)의 경우, 빈 시퀸스는 False다. if len(시퀸스) 또는 if not len(시퀸스)보다는 if not 시퀸스 또는 if 시퀸스를 사용하는 것이 좋다.
  • 정수를 처리할 때 뜻하지 않게 None을 0으로 잘못 처리하는 것처럼 , 암묵적 False를 사용하는 것은 위험하다.

좋은 예와 나쁜 예를 살펴보겠다. 먼저 좋은 예다.

if not users:    print("사용자가 없습니다.")if foo ==0:    handle_zero()if i % 10 == 0:    handle_multiple_of_ten()

다음은 나쁜 예다.

if len(users) == 0:    print("사용자가 없습니다.")if foo is not None and not foo:    handle_zero()if not i % 10:    handle_multiple_of_ten()

4.2.4 return 대 yield

파이썬에서 제너레이터(**generator**)는 이터레이터(**iterator**)를 작성하는 편리한 방법이다. 객체에 \___iter_\___() 와 \___next\___() 메서드를 둘 다 정의하면 이터레이터 프로토콜을 구현한 셈이다. 이 때 yield 키워드를 사용하면 편리하다.

호출자가 메서드를 호출할 떄, return 키워드는 반환값을 반환하고 메서드를 종료한 후, 호출자에게 제어를 반환한다. 반면 yield 키워드는 각 반환값을 호출자에게 반환하고, 반환값이 모두 소진되었을 때에만 메서드가 종료된다.

이터레이터는 파이썬의 강력한 기능이다. 이터레이터는 이터레이터 프로토콜을 구현하는 컨테이너 객체라고 할 수 있는데, 컨테이너의 다음 값을 반환하는 \___next\___() 메서드와 이터레이터 자신을 반환하는 \___iter\___() 메서드를 기반으로 한다.

yield 키워드는 제너레이터 맥락에서 이터레이터를 만드는 아주 강력한 도구다. 제너레이터는 최종값을 반환하지만, 이터레이터는 yield 키워드를 사용하여 코드 실행 중에 값을 반환한다. 즉, \__next\___()메서드를 호출할 때마다 어떤 값 하나를 추출한 후 해당 yield 표현식의 값을 반환한다. 이렇게 이터레이터는 StopIteration 예외가 발생할 때까지 값을 반환한다.

a = [1,2,3]def f(a):    while a:        print("a.pop()",a.pop())        yield a.pop()

 

제너레이터는 매우 강력하고 효율적이다. 시퀸스를 반환하거나 반복문을 사용하는 함수를 다룰 때, 제너레이터를 고려할 수 있다. 다음 코드는 이터레이터를 사용하여 피보나치 수열을 구현한다.(출력 부분을 빼면 "1.7.4 피보나치 수열"에서 살펴봤던 코드와 같다.)

def fib_generator():    a,b = 0,1    while True:        yield b        a, b = b, a+bif __name__ == "__main__":    fib = fib_generator()    print(next(fib))    print(next(fib))    print(next(fib))    print(next(fib))    print(next(fib))    print(next(fib))    print(next(fib))

출력 결과

PS D:\Mastering-Python-Design-Patterns-Second-Edition\algo\module_structure> python .\test.py11235813

4.2.5 break 대 continue

반복문 (for 또는 while)에서 break 키워드를 만나면, 바로 반복문을 빠져나간다. 반복문에서 continue 키워드를 만나면, 반복문의 다음 단계로 전환한다. (반복문의 다음 반복을 계속한다.)

반복문에는 else 절을 사용할 수 있는데, 이는 반복문이 종료되었을 때(for문에서 리스트의 항목을 모두 순회했거나, while문에서 False가 되었을 때 실행된다. 다만 break문으로 반복문이 종료되는 경우에는 실행되지 않는다.)

코드

for i in range(10):    if i == 4:        break    print(i)else:    print("for 문 종료")

결과

PS D:\Mastering-Python-Design-Patterns-Second-Edition\algo\module_structure> python .\test.py0123

코드

for i in range(10):    if i % 2 ==0:        continue    print(i)else:    print("for 문 종료!")

결과

PS D:\Mastering-Python-Design-Patterns-Second-Edition\algo\module_structure> python .\test.py13579for 문 종료!

4.2.6 range()

range()메서드는 숫자 리스트를 생성한다. 숫자 시퀸스를 순회할 떄 유용하다.

a = range(10)b = range(4,10)c = range(0,10,3)print("a",a)print("b",b)print("c",c)

결과

a range(0, 10)b range(4, 10)c range(0, 10, 3)

4.2.7 enumerate()

enumerate() 메서드는 반복 가능한 객체의 인덱스 값과 항목 값의 튜플을 반환한다. 예를 들어 파일을 가져와서 특정 단어가 나타나는 위치를 출력하는 나만의 **grep** 함수를 만들 수 있다. 명령 줄에서 실행 시 단어와 파일을 모두 지정해야 한다.

import sysdef grep_word_from_files():    word = sys.argv[1]    for filename in sys.argv[2:]:        with open(filename) as file:            for lino, line in enumerate(file,start=1):                if word in line:                    print("{0}:{1}:{2:.40}".format(                        filename, lino, line.rstrip()))if __name__ == "__main__":    if len(sys.argv) < 2:        print("Usage: python {0} [word] [file ...]".format(sys.argv[0]))        sys.exit()    else:        grep_word_from_files()

결과

PS D:\Mastering-Python-Design-Patterns-Second-Edition\algo\module_structure> python .\3_grep_word_from_files.py  for  .\3_grep_word_from_files.py.\3_grep_word_from_files.py:5:    for filename in sys.argv[2:]:.\3_grep_word_from_files.py:7:            for lino, line in enumerate( .\3_grep_word_from_files.py:9:                    print("{0}:{1}:{2:.4 .\3_grep_word_from_files.py:15:        print("Usage: python {0} [word] 

4.2.8 zip()

zip()메서드는 2개 이상의 시퀸스를 인수로 취하여, 짧은 길이의 시퀸스를 기준으로 각 항목이 순서대로 1:1 대응하는 새로운 튜플 시퀸스를 만든다.

>>> a = [1,2,3,4,5]>>> b = ['a','b','c','d']>>> zip(a,b)<zip object at 0x02333FA8>>>> list(zip(a,b))[(1, 'a'), (2, 'b'), (3, 'c'), (4, 'd')]

4.2.10 map()

map(function, list) 메서드는 시퀸스의 모든 항목에 함수를 적용한 결과 리스트를 반환한다.

def cube(x): return x*x*xprint("a: ",list(map(cube,range(1,11))))seq = range(8)def square(x): return x*xprint("b:" ,list(zip(seq,map(square,seq))))

4.2.11 람다 함수

**람다(lambda)** 함수를 쓰면 코드 내에서 함수를 간결하게(compact) 동적으로 사용할 수 있다. 아래의 예제 코드를 살펴보자.

def area(b,h):    return 0.5 * b * hprint("Area",area(5,4))area2= lambda b,  h : 0.5 * b *hprint("Area2",area2(5,4))
PS D:\Mastering-Python-Design-Patterns-Second-Edition\algo\module_structure> python .\test.pyArea 10.0Area2 10.0

람다 함수는 defaultdict에서 키 생성 시 매우 유용하다.(누락된 키에 대한 기본 값 설정 시).

코드

import collections minus_one_dict = collections.defaultdict(lambda:-1)point_zero_dict =  collections.defaultdict(lambda: (0,0))message_dict = collections.defaultdict(lambda:"No message")print("minus_one_dict",minus_one_dict)print("point_zero_dict",point_zero_dict)print("message_dict",message_dict)

결과

minus_one_dict defaultdict(<function <lambda> at 0x01C208E8>, {})point_zero_dict defaultdict(<function <lambda> at 0x021AF390>, {})message_dict defaultdict(<function <lambda> at 0x021AF468>, {})   

4.3 파일 처리


파이썬에서 파일 처리는 매우 쉽고 편하다. 파일을 읽어서 모든 빈 줄을 제거하는 코드를 살펴보자.

from os import remove
import sys

def read_data(filename):
    lines = []
    fh = None
    try:
        fh = open(filename)
        for line in fh:
            if line.strip():
                lines.append(line)
    except (IOError,OSError) as err:
        print(err)
    finally:
        if fh is not None:
            fh.close()
    return lines

def write_data(lines,filename):
    fh = None
    try:
        fh = open(filename,"w")
        for line in lines:
            fh.write(line)
    except (EnvironmentError) as err:
        print(err)
    finally:
        if fh is not None:
            fh.close()

def remove_blank_lines():
    if len(sys.argv) < 2:
        print("Usage python {0} [file ...]".format(sys.argv[0]))
    
    for filename in sys.argv[1:]:
        lines = read_data(filename)
        if lines:
            write_data(lines,filename)

if __name__ == "__main__":
    remove_blank_lines()

빈 줄 제거 전 내용을 확인해보자. 사용할 파일은 "4.1.4 __name__ 변수"에서 작성한 hello.py다.

hello = "hello"

def world():
    return "World"

if __name__ == "__main__":
    print("{0} 직접 실행됨".format(__name__))
else:
    print("{0} 임포트됨".format(__name__))

빈 줄을 제거한 결과는 다음과 같다.

root@DESKTOP-JPGB0S5:/mnt/d/Mastering-Python-Design-Patterns-Second-Edition/algo/module_structure# python3.6 4_remove_blank_lines.py  hello.py
root@DESKTOP-JPGB0S5:/mnt/d/Mastering-Python-Design-Patterns-Second-Edition/algo/module_structure# cat hello.py
hello = "hello"
def world():
    return "World"
if __name__ == "__main__":
    print("{0} 직접 실행됨".format(__name__))
else:
    print("{0} 임포트됨".format(__name__))root@DESKTOP-JPGB0S5:/mnt/d/Mastering-Python-Design-Patterns-Second-Edition/algo/module_structure#

코드에서 쓰인 함수

strip([chars]) : 인자로 전달된 문자를 String의 왼쪽과 오른쪽에서 제거합니다.lstrip([chars]) : 인자로 전달된 문자를 String의 왼쪽에서 제거합니다.rstrip([chars]) : 인자로 전달된 문자를 String의 오른쪽에서 제거합니다.아래는 write 함수를 쓸경우 파일에서 쓰기가 실행된다.# writedata.pyf = open("C:/doit/새파일.txt", 'w')for i in range(1, 11):    data = "%d번째 줄입니다.\n" % i    f.write(data)f.close()

다음과 같이 with문을 사용할 수도 있다. 명시적으로 close()로 연 파일을 닫지 않아도 되므로 더 선호되기도 한다,

import sysdef read_data(filename):    lines = []    with open(filename) as fh:        for line in fh:            if line.strip():                lines.append(line)        return linesdef write_data(lines, filename):    fh =None    with open(filename, "w") as fh:        for line in lines:            fh.write(line)def remove_blank_lines():    if len(sys.argv) < 2:        print("Usage: python {0} [file ...]".format(sys.argv[0]))    for filename in sys.argv[1:]:        lines = read_data(filename)        if lines:            write_data(lines, filename)if __name__ == "__main__":    remove_blank_lines()    

실행결과

root@DESKTOP-JPGB0S5:/mnt/d/Mastering-Python-Design-Patterns-Second-Edition/algo/module_structure# cat hello.py hello = "hello"   def world():          return "World"if __name__ == "__main__":    print("{0} 직접 실행됨".format(__name__))else:    print("{0} 임포트됨".format(__name__))

4.3.1 파일 처리 메서드

**open()**

open(filename,mode,encoding) 메서드는 파일 객체를 반환한다. 모드와 인코딩 인수는 옵션이며, 생략하면 텍스트 읽기 모드와 시스템 기본형식 인코딩이 적용된다. 모드는 문자열로 지정하며 종류는 다음과 같다.

r: 읽기모드

w: 쓰기모드 (동명파일이 이미 있다면, 그 파일을 지운 후 내용을 새로 쓴다.)

a: 추가모드(동명 파일이 있다면 그 파일끝에 내용을 추가한다.)

r+:읽기와 쓰기 모드

t: 텍스트 모드

b:바이너리 모드

fin = open(filename,encoding="utf8")fout = open(filename, "w", encoding="utf8")

read(size) : 메서드는 파일에서 size만큼의 내용을 읽고 문자열을 반환한다. readline() : 파일에서 한줄을 읽는다. readlines() : 파일의 모든 데이터 행을 포함한 리스트를 반환한다. write() : 데이터를 파일에쓰고,None을 반환한다. tell(), seek() : tell()메서드는 파일의 현재 위치를 나타내는 정수를 반환한다. close() : 파일을 닫고, 열린 파일이 차지하는 시스템 자원을 해제한다. 파일을 성공적으로 닫으면 True를 반환한다. input() : 함수는 사용자의 입력을 받는다. peek() 메서드는 파일 포인터 위치를 이동하지 않고, n바이트를 반환한다. fileno() 파일 서술자를 반환한다.(파일 서술자를 가진 파일 객체에서만 사용 가능하다.)

4.3.2 shutil 모듈

shutil 모듈은 시스템에서 파일을 조작할 때 유용하다. 다음 코드는 터미널에서 파일 및 확장자를 지정하면 새 확장자의 이름으로 복사본을 만든다.

import os
import sys
import shutil

"""
root@DESKTOP-JPGB0S5:/mnt/d/Mastering-Python-Design-Patterns-Second-Edition/algo/module_structure# python3.6 6_change_ext_file.py hello.py txt
hello.txt
"""

def change_file_ext():
    if len(sys.argv) < 2:
        print("Usage: python {0} filename.old_ext 'new_ext'".format(sys.argv[0]))
        sys.exit()

    name = os.path.splitext(sys.argv[1])[0] + "." + sys.argv[2]
    print(name)

    try:
        shutil.copyfile(sys.argv[1],name)
    except OSError as err:
        print(err)

if __name__ == "__main__":
    change_file_ext()

결과

root@DESKTOP-JPGB0S5:/mnt/d/Mastering-Python-Design-Patterns-Second-Edition/algo/module_structure# cat hello.txt 

hello = "hello"
def world():
    return "World"
if __name__ == "__main__":
    print("{0} 직접 실행됨".format(__name__))
else:
    print("{0} 임포트됨".format(__name__))

CHAPTER 03

컬렉션 자료구조

딕셔너리

파이썬 컬렉션 데이터 타입

연습문제


2장에서는 시퀸스 자료구조로 데이터를 슬라이싱하거나 정렬했다. 컬렉션(collection)자료구조는 시퀸스 자료구조와 달리, 데이터를 서로 연관시키지(relating) 않고 모아두는 컨테이너다(container). 컬렉션 자료구조는 시퀸스 자료구조에서 봤던 속성 중 세 가지 속성을 지닌다.

  • 멤버십 연산자 : in
  • 크기 함수 : len(seq)
  • 반복성 : 반복문의 데이터를 순회한다.

파이썬의 내장 컬렉션 데이터 타입에는 딕셔너리가 있다. 이 장의 마지막 절에서는 collections 모듈이 제공하는 다른 유용한 컬렉션 데이터 타입들도 살펴본다.

3.1 셋

파이썬의 셋(집합,set)은 반복 가능하고, 가변적이며, 중복 요소가 없고, 정렬되지 않은 컬렉션 데이터 타입이다.

인덱스 연산은 할 수 없다. 셋은 멤버십 테스트중복항목 제거에 사용된다. 셋의 삽입 시간복잡도는 O(1)이고, 합집합(union)의 시간 복잡도는 O(m+n)이다. 교집합(intersection)의 경우, 두 셋 중에서 더 작은 셋에 대해서만 계싼하면 되므로, 시간복잡도는 O(n)이다.

NOTE_ 프로즌 셋(frozen set)은 셋과 달리 불변 객체이며, 셋에서 사용할 수 있는 일부 메서드를 사용할 수 없다. 곧, 프로즌 셋의 요소를 변경하는 메서드를 사용할 수 없다. frozenset()으로 생성한다.Python 3.7.4 (tags/v3.7.4:e09359112e, Jul  8 2019, 19:29:22) [MSC v.1916 32 bit (Intel)] on win32Type "help", "copyright", "credits" or "license" for more information.>>> dir(set())['__and__', '__class__', '__contains__', '__delattr__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__gt__', '__hash__', '__iand__', '__init__', '__init_subclass__', '__ior__', '__isub__', '__iter__', '__ixor__', '__le__', '__len__', '__lt__', '__ne__', '__new__', '__or__', '__rand__', '__reduce__', '__reduce_ex__', '__repr__', '__ror__', '__rsub__', '__rxor__', '__setattr__', '__sizeof__', '__str__', '__sub__', '__subclasshook__', '__xor__', 'add', 'clear', 'copy', 'difference', 'difference_update', 'discard', 'intersection', 'intersection_update', 'isdisjoint', 'issubset', 'issuperset', 'pop', 'remove', 'symmetric_difference', 'symmetric_difference_update', 'union', 'update']>>> >>> >>> dir(frozenset())['__and__', '__class__', '__contains__', '__delattr__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__iter__', '__le__', '__len__', '__lt__', '__ne__', '__new__', '__or__', '__rand__', '__reduce__', '__reduce_ex__', '__repr__', '__ror__', '__rsub__', '__rxor__', '__setattr__', '__sizeof__', '__str__', '__sub__', '__subclasshook__', '__xor__', 'copy', 'difference', 'intersection', 'isdisjoint', 'issubset', 'issuperset', 'symmetric_difference', 'union']>>> >>> >>> fs = frozenset((0,1,2,3,4))>>> 2 in fsTrue>>> len(fs)5

add()

A.add(x)는 셋A에 x가 없는 경우 x를 추가한다.

>>> people = {"버피", "에인절", "자일스"}>>> people.add("윌로")>>> people{'에인절', '버피', '자일스', '윌로'}

update()와 != 연산자

A.update(B)와 혹은 A |= B는 A를 B에 추가한다.(합집합)

>>> people = {"버피", "에인절", "자일스"} >>> people.update({"로미오", "줄리엣", "에인절"})>>> people{'로미오', '에인절', '버피', '자일스', '줄리엣'}>>>>>> people |= {"리키","유진"}>>> people{'로미오', '유진', '에인절', '리키', '버피', '자일스', '줄리엣

union()과 | 연산자

A.union(B)와 A와 | B는 앞에서 본 update() 메서드와 같지만, 연산 결과를 복사본으로 반환한다.

>>> people = {"버피", "에인절", "자일스"}>>> people.union({"로미오", "줄리엣"}){'로미오', '에인절', '버피', '자일스', '줄리엣'}>>>>>> people{'에인절', '버피', '자일스'}>>>>>> people | {"브라이언"}{'브라이언', '에인절', '버피', '자일스'}>>>>>> 

 

intersection()과 &연산자

A.intersection(B)와 A & B는 A와 B의 교집합의 복사본을 반환한다.

>>> people = {"버피", "에인절", "자일스", "이안"}>>> vampires= {"에인절", "자일스","윌로"}>>> people.intersection(vampires){'에인절', '자일스'}>>> people & vampires{'에인절', '자일스'}>>>

difference()와 연산자

A.difference(B)와 A-B와 A와 B의 차집합의 복사본을 반환한다.

>>> people = { "버피", "에인절", "자일스", "아영"}>>> vampires = {"스파이크","에인절", "상민"}>>> people.difference(vampires){'자일스', '버피', '아영'}>>> people - vampires{'자일스', '버피', '아영'}>>>

clear()

A.clear()는 A의 모든 항목을 제거한다.

>>> people={"버피","자일스","에인절"}>>> people.clear()>>> peopleset()

discard(),remove(),pop()

A.discard(x)는 A의 항목 x를 제거하며 반환값은 없다. A.remove()는 A.discard()와 같지만, 항목 X가 없을 경우 KeyError 예외를 발생시킨다.

A.pop()는 A에서 한 항목을 무작위로 제거하고 그 항목을 반환한다. 셋이 비어있으면 KeyError 예외를 발생시킨다.

>>> countries={"프랑스", "스페인", "영국"}>>> countries.discard("한국")>>> countries.remove("일본")Traceback (most recent call last):  File "<stdin>", line 1, in <module>KeyError: '일본'>>>>>> countries.pop() #무작위'스페인'>>> countries.discard("스페인")>>> countries.remove("영국")>>> countries.pop()'프랑스'>>> countries.pop()Traceback (most recent call last):  File "<stdin>", line 1, in <module>KeyError: 'pop from an empty set'>>>KeyboardInterrupt

3.1.2 셋과 리스트

리스트 타입은 셋 타입으로 변환(casting)할 수 있다. 다음 예제를 살펴보자.

def remove_dup(l1):    '''리스트의 중복된 항목을 제거한 후 반환한다.'''    print("remove_dup l1",list(set(l1)))    return list(set(l1))def intersection(l1, l2):    '''교집합 결과를 반환한다.'''    print("intersection l1",list(set(l1) & set(l2)))    return list(set(l1) & set(l2))def union(l1, l2):    '''합집합 결과를 반환한다.'''    print("union set(l1), set(l2)",list(set(l1) |  set(l2) ))    return list(set(l1) |  set(l2) )def test_sets_operation_with_lists():    l1 = [1, 2, 3, 4, 5, 5, 9, 11, 11, 15]    l2 = [4, 5, 6, 7, 8]    l3 = []    assert(remove_dup(l1) == [1, 2, 3, 4, 5, 9, 11, 15])    assert(intersection(l1, l2) == [4,5])    assert(union(l1, l2) == [1, 2, 3, 4, 5, 6, 7, 8, 9, 11, 15])    assert(remove_dup(l3) == [])    assert(intersection(l3, l2) == l3)    assert(sorted(union(l3, l2)) == sorted(l2))    print("테스트 통과")if __name__ == '__main__':   test_sets_operation_with_lists()    

결과

PS D:\Mastering-Python-Design-Patterns-Second-Edition\algo\collection_data_Structure> python .\1_set_operations_with_lists.pyremove_dup l1 [1, 2, 3, 4, 5, 9, 11, 15]intersection l1 [4, 5]union set(l1), set(l2) [1, 2, 3, 4, 5, 6, 7, 8, 9, 11, 15]remove_dup l1 []intersection l1 []union set(l1), set(l2) [4, 5, 6, 7, 8]테스트 통과

딕셔너리에서도 셋 속성을 사용할 수 있다. 딕셔너리는 바로 다음 절에서 자세히 살펴본다.

def set_operations_with_dict():    pairs = [("a",1), ("b",2), ("c",3)]    d1 = dict(pairs)    print("딕셔너리1\t:{0}".format(d1))    d2 = {"a":1, "c":2, "d":3, "e":4}    print("딕셔너리2\t: {0}".format(d2))    intersection = d1.keys() & d2.keys()    print("d1 n d2 (키)\t: {0}".format(intersection))    intersection_items = d1.items() & d2.items()    print("d1 n d2 (키,값)\t: {0}".format(intersection_items) )    subtraction1 = d1.keys() - d2.keys()    print("d1 - d2 (키)\t: {0}".format(subtraction1))    subtraction2 = d2.keys() - d1.keys()    print("d2 - d1 (키)\t:{0}".format(subtraction2))    subtraction_items = d1.items() -d2.items()    print("d1 - d2 (키,값)\t: {0}".format(subtraction_items))    """딕셔너리의 특정 키를 제외한다."""    d3 = {key: d2[key] for key in d2.keys() - {"c","d"}}    print("d2 - {{c,d}}\t: {0}".format(d3))if __name__ == "__main__":    set_operations_with_dict()

결과

딕셔너리1       :{'a': 1, 'b': 2, 'c': 3}딕셔너리2       : {'a': 1, 'c': 2, 'd': 3, 'e': 4}d1 n d2 (키)    : {'c', 'a'}d1 n d2 (키,값) : {('a', 1)}d1 - d2 (키)    : {'b'}d2 - d1 (키)    :{'d', 'e'}d1 - d2 (키,값) : {('c', 3), ('b', 2)}d2 - {c,d}      : {'a': 1, 'e': 4}

3.2.1 딕셔너리 메서드

setdefault()

setdefault() 메서드는 딕셔너리에서 키의 존재 여부를 모른 채 접근할 때 사용된다. (딕셔너리에 존재하지 않는 키에접근하면 예외가 발생한다) A.setdefault(key,default)를 사용하면 딕셔너리 A에 key가 존재할 경우 키에 해당하는 값을 얻을 수 있고, key가 존재하지 않는다면, 새 키와 기본값 default가 딕셔너리에 저장된다.

def usual_dict(dict_data):    """dict[key]사용"""    newdata={}    for k, v in dict_data:        print("k",k)        print("v",v)        print("dict_data",dict_data)        if k in newdata:                        newdata[k].append(v)            print("if newdata[k]",newdata[k])        else:            newdata[k] = [v]            print("else newdata[k]",newdata[k])    return newdatadef setdefault_dict(dict_data):    """ setdefault()메서드 사용"""    newdata = {}    for k, v in dict_data:        newdata.setdefault(k,[]).append(v)    return newdatadef test_setdef():    dict_data = (("key1", "value1"),                ("key1","value2"),                ("key2", "value3"),                ("key2", " value4"),                ("key2", "value5"),)    print(usual_dict(dict_data))    print(setdefault_dict(dict_data))if __name__ == "__main__":    test_setdef()

결과

PS D:\Mastering-Python-Design-Patterns-Second-Edition\algo\collection_data_Structure> python .\3_setdefault_example.pyk key1v value1dict_data (('key1', 'value1'), ('key1', 'value2'), ('key2', 'value3'), ('key2', ' value4'), ('key2', 'value5'))else newdata[k] ['value1']k key1v value2dict_data (('key1', 'value1'), ('key1', 'value2'), ('key2', 'value3'), ('key2', ' value4'), ('key2', 'value5'))if newdata[k] ['value1', 'value2']k key2v value3dict_data (('key1', 'value1'), ('key1', 'value2'), ('key2', 'value3'), ('key2', ' value4'), ('key2', 'value5'))else newdata[k] ['value3']k key2v  value4dict_data (('key1', 'value1'), ('key1', 'value2'), ('key2', 'value3'), ('key2', ' value4'), ('key2', 'value5'))if newdata[k] ['value3', ' value4']k key2v value5dict_data (('key1', 'value1'), ('key1', 'value2'), ('key2', 'value3'), ('key2', ' value4'), ('key2', 'value5'))if newdata[k] ['value3', ' value4', 'value5']{'key1': ['value1', 'value2'], 'key2': ['value3', ' value4', 'value5']}{'key1': ['value1', 'value2'], 'key2': ['value3', ' value4', 'value5']}

A.update(B)는 딕셔너리 A에 딕셔너리 B의 키가 존재한다면, 기존 A의 (키, 값)을 B의 (키,값)으로 갱신한다. B의 키가 A에 존재하지 않는다면, B의 (키,값)을 A에 추가한다.

>>> d = {'a':1, 'b': 2}>>> d.update({'b':10})>>> d{'a': 1, 'b': 10}>>> d.update({'c':100})>>> d{'a': 1, 'b': 10, 'c': 100}>>> 

get()

A.get(key)는 딕셔너리A의 key값을 반환한다. key가 존재하지 않으면 아무것도 반환하지 않는다.

>>> sunnydale = dict(name="잰더", age=17, hobby='게임')>>> sunnydale.get("hobby")'게임'>>> sunnydale['hobby']'게임'>>> sunnydale.get("hello") >>> sunnydale['hello']     Traceback (most recent call last):     File "<stdin>", line 1, in <module>KeyError: 'hello'

items(), values(),keys()

items(), keys(), values() 메서드는 딕셔너리 뷰(view)다. 딕셔너리 뷰란 딕셔너리의 항목(키 또는 값)을 조회하는 읽기 전용의 반복 가능한 객체다.

>>> sunnydale = dict(name="잰더", age=17, hobby='게임')>>> sunnydale.items()                                  dict_items([('name', '잰더'), ('age', 17), ('hobby', '게임')])>>> sunnydale.values()dict_values(['잰더', 17, '게임'])>>> sunnydale.keys()   dict_keys(['name', 'age', 'hobby'])>>> sunnydale_copy=sunnydale.items()>>> sunnydale_copy['address'] = "서울"Traceback (most recent call last):  File "<stdin>", line 1, in <module>TypeError: 'dict_items' object does not support item assignment>>> sunnydale['address'] = "서울">>> sunnydale{'name': '잰더', 'age': 17, 'hobby': '게임', 'address': '서울'}

pop(), popitem()

A.pop(key)는 딕셔너리 A의 key 항목을 제거한 후, 그 값을 반환한다.

A.popitem()은 딕셔너리 A에서 항목(키와 값)을 제거한 후, 그 키와 항목을 반환한다.

>>> sunnydale = dict(name="잰더", age=17, hobby='게임', address="서울") >>> sunnydale.pop("age")17>>> sunnydale{'name': '잰더', 'hobby': '게임', 'address': '서울'}>>> sunnydale.popitem()('address', '서울')>>> sunnydale{'name': '잰더', 'hobby': '게임'}

clear()

>>> sunnydale.clear()>>> sunnydale        {}

3.2.2 딕셔너리 성능 측정

딕셔너리를 벤치마킹 테스트하여 성능을 측정해보자, 다음 코드는 리스트와 딕셔너리의 멤버십 연산을 테스트한다. 다음 코드는 리스트와 딕셔너리의 멤버십 연산을 테스트한다. 멤버십 연산에 대한 시간복잡도는 O(n)인 반면, 딕셔너리는 O(1)이다.

import timeitimport randomfor i in range(10000, 1000001, 20000):    t = timeit.Timer("random.randrange(%d) in x" % i, "from __main__ import random, x")    x = list(range(i)) #리스트    lst_time = t.timeit(number=1000)    x = { j: None for j in range(i)} #딕셔너리    d_time = t.timeit(number=1000)    print("%d,%10.3f,%10.3f" % (i, lst_time, d_time))    

성능측정결과 i(10000부터 1000001까지 20000씩 더하면서 체크), lst_time(리스트 측정시간), d_time(딕셔너리 측정시간)

10000,     0.085,     0.00130000,     0.284,     0.00150000,     0.561,     0.00370000,     0.837,     0.00290000,     0.926,     0.002110000,     1.174,     0.002130000,     1.452,     0.002150000,     1.613,     0.002170000,     1.877,     0.002190000,     1.979,     0.002210000,     2.160,     0.002230000,     2.843,     0.002250000,     2.735,     0.002270000,     2.942,     0.002290000,     3.239,     0.002310000,     4.085,     0.002330000,     4.984,     0.002350000,     4.616,     0.002370000,     3.971,     0.002390000,     4.256,     0.002410000,     4.316,     0.002430000,     4.892,     0.002450000,     5.564,     0.003470000,     6.352,     0.003

딕셔너리 메서드의 시간복잡도는 다음과 같다.

연산시간복잡도

복사 O(n)
항목 조회 O(1)
항목 할당 O(1)
항목 삭제 O(1)
멤버십 테스트 in O(1)
반복 O(n)

3.2.3 딕셔너리 순회

반복문에서 딕셔너리를 순회할 때는 기본적으로 키를 사용한다. 딕셔너리의 키는 임의의 순서대로 나타나지만, sorted()함수를 사용하면 정렬된 상태로 순회할 수 있다. sorted()함수는 딕셔너리의 keys(), values(),items()에 대해 사용할 수 있다.

>>> d = dict(c="!", b="world", a="hello")>>> for key in sorted(d.keys()):...     print(key, d[key])... a hellob worldc !

3.2.4 딕셔너리 분기

다음 두 함수를 조건에 따라 실행해야 한다고 가정해보자.

def hello():	print("hello")	def world():	print("world")	

이럴 때 우리는 보통 if문을 사용하여 다음과 같이 분기문을 작성한다.

action = "h"if action == "h":	hello()elif action == "w":	world()

하지만 딕셔너리를 사용하면 다음과 같이 더 효율적으로 분기할 수 있다.

functions = dict(h=hello, w=world)functions[action]()

3.3 파이썬 컬렉션 데이터 타입

파이썬의 collections 모듈은 다양한 딕셔너리 타입을 제공한다. 범용의 내장기능보다 더 강력한 성능을 보인다.

3.3.1 기본 딕셔너리

기본 딕셔너리(default dictionary)는 collections.defaultdict 모듈에서 제공하는 추가 딕셔너리 타입이다.

defaultdict는 내장 딕셔너리의 모든 연산자와 메서드를 사용할 수 있고, 추가로 다음 코드와 같이 누락된 키도 처리할 수 있다.

from collections import defaultdictdef defaultdict_example():    pairs = {("a",1), ("b",2), ("c",3)}    # 일반 딕셔너리    d1 = {}    for key, value in pairs:        if key not in d1:            d1[key] = []        d1[key] = []        d1[key].append(value)    print(d1)       #defaultdict    d2 = defaultdict(list)    for key, value in pairs:        d2[key].append(value)    print(d2)if __name__ == "__main__":    defaultdict_example()
PS D:\Mastering-Python-Design-Patterns-Second-Edition\algo\collection_data_Structure> python .\5_defaultdict_example.py{'c': [3], 'b': [2], 'a': [1]}defaultdict(<class 'list'>, {'c': [3], 'b': [2], 'a': [1]})

3.2.2 정렬된 딕셔너리

정렬된 딕셔너리(ordered dictionary)는 collections.OrderedDict 모듈에서 제공하는 정렬된 딕셔너리 타입이다. OrderedDict 역시 내장 딕셔너리의 모든 메서드와 속성을 가지고 있고, 추가로 삽입 순서대로 항목을 저장한다.

>>> from collections import OrderedDict>>> tasks = OrderedDict()>>> tasks[8031] = "백업">>> tasks[4027] = "이메일 스캔">>> tasks[5733] = "시스템 빌드">>> tasksOrderedDict([(8031, '백업'), (4027, '이메일 스캔'), (5733, '시스템 빌드')])

다음은 표준 딕셔너리와 OrderedDict 결과를 출력하는 예제다.(사용하는 파이썬 버전에따라 출력하는 결과가 다를 수 있다.)

6_orderedDict_example.py

from collections import OrderedDictdef orderedDict_example():    pairs = [("c",1),("b",2),("a",3)]    #일반 딕셔너리    d1 = {}    for key, value in pairs:        if key not in d1:            d1[key] = []        d1[key].append(value)    for key in d1:        print(key, d1[key])    #OrderedDict    d2 = OrderedDict(pairs)    for key in d2:        print(key, d2[key])if __name__ == "__main__":    orderedDict_example()

결과

c [1]b [2]a [3]c 1b 2a 3

키 값을 변경해도 순서는 변경되지 않는다. 항목을 맨 끝으로 저장하려면, 해당 항목을 삭제한 후 다시 삽입해야한다. 은 popitem() 메서드를 호출하여 딕셔너리의 마지막 키-값 항목을 제거한 후 반환할 수도 있다.

일반적으로, 정렬된 딕셔너리를 사용하는 것은 딕셔너리를 여러 번 순회할 것으로 예상되는 경우와 항목의 삽입을 거의 수행하지 않을 것으로 예상되는 경우에만 효율적이다.

3.3.3 카운터 딕셔너리

카운터(counter) 타입은 해시 가능한(hashable) 객체를 카운팅하는 특화된 서브클래스다. collections.Counter 모듈에서 제공한다.

from collections import Counterdef counter_example():    """항목의 발생 횟수를 매핑하는 딕셔너리를 생성한다."""    seq1=[1,2,3,5,1,2,5,5,2,5,1,4]    seq_counts=Counter(seq1)    print("seq_counts : Counter(seq1) ",seq_counts)    """항목의 발생 횟수를 수동적으로 갱신하거나, update()메서드를 사용할 수 있다."""    seq2 = [1,2,3]    seq_counts.update(seq2)    print("seq_counts.update(seq2)",seq_counts)    seq3 = [1,4,3]    for key in seq3:        seq_counts[key]+=1    print("seq_counts[key]+=1",seq_counts)    """ a+b, a-b 같은 연산을 사용할 수 있다."""    seq_counts_2= Counter(seq3)    print("seq_counts_2", seq_counts_2)    print("seq_counts + seq_counts_2",seq_counts + seq_counts_2)    print("seq_counts - seq_counts_2",seq_counts - seq_counts_2)if __name__ == "__main__":    counter_example()
PS D:\Mastering-Python-Design-Patterns-Second-Edition\algo\collection_data_Structure> python .\7_counterDict_example.pyseq_counts : Counter(seq1)  Counter({5: 4, 1: 3, 2: 3, 3: 1, 4: 1})seq_counts.update(seq2) Counter({1: 4, 2: 4, 5: 4, 3: 2, 4: 1})seq_counts[key]+=1 Counter({1: 5, 2: 4, 5: 4, 3: 3, 4: 2})seq_counts_2 Counter({1: 1, 4: 1, 3: 1})seq_counts + seq_counts_2 Counter({1: 6, 2: 4, 3: 4, 5: 4, 4: 3})seq_counts - seq_counts_2 Counter({1: 4, 2: 4, 5: 4, 3: 2, 4: 1})

3.4 연습문제

3.4.1 단어횟수 세기

collections.Counters의 most_common() 메서드를 사용하면 문자열에서 가장 많이 나오는 단어와 그 횟수를 쉽게 찾을 수 있다.

from collections import Counterdef find_top_N_recurring_words(seq,N):    dcounter = Counter()    print("dcounter",dcounter)    print("seq:",seq.split())    for word in seq.split():        print("word",word)        dcounter[word] += 1        print("dcounter[word]",dcounter[word])    return dcounter.most_common(N)def test_find_top_N_recurring_words():    seq = "버피 에인절 몬스터 잰더 윌로 버피 몬스터 슈퍼 버피 에인절"    N=3    assert(find_top_N_recurring_words(seq,N) == [("버피",3), ("에인절",2), ("몬스터",2)])    print("테스트 통과")if __name__ == "__main__":    test_find_top_N_recurring_words()

출력 결과

seq: ['버피', '에인절', '몬스터', '잰더', '윌로', '버피', '몬스터', '슈퍼', '버피', '에인절']word 버피dcounter[word] 1word 에인절dcounter[word] 1word 몬스터dcounter[word] 1word 잰더dcounter[word] 1word 윌로dcounter[word] 1word 버피dcounter[word] 2word 몬스터dcounter[word] 2word 슈퍼dcounter[word] 1word 버피dcounter[word] 3word 에인절dcounter[word] 2테스트 통과

assert에 대하여

#assert는 뒤의 조건이 True가 아니면 AssertError를 발생한다.'''왜 assert가 필요한 것일까?어떤 함수는 성능을 높이기 위해 반드시 정수만을 입력받아 처리하도록 만들 수 있다. 이런 함수를 만들기 위해서는 반드시 함수에 정수만 들어오는지 확인할 필요가 있다. 이를 위해 if문을 사용할 수도 있고 '예외 처리'를 사용할 수도 있지만 '가정 설정문'을 사용하는 방법도 있다.'''assert 조건, '메시지''메시지'는 생략할 수 있다.assert는 개발자가 프로그램을 만드는 과정에 관여한다. 원하는 조건의 변수 값을 보증받을 때까지 assert로 테스트 할 수 있다.이는 단순히 에러를 찾는것이 아니라 값을 보증하기 위해 사용된다.예를 들어 함수의 입력 값이 어떤 조건의 참임을 보증하기 위해 사용할 수 있고 함수의 반환 값이 어떤 조건에 만족하도록 만들 수 있다. 혹은 변수 값이 변하는 과정에서 특정 부분은 반드시 어떤 영역에 속하는 것을 보증하기 위해 가정 설정문을 통해 확인 할 수도 있다.이처럼 실수를 가정해 값을 보증하는 방식으로 코딩 하기 때문에 이를 '방어적 프로그래밍'이라 부른다.

counter에 대하여 추가자료

dictionary를 이용한 카운팅아래 코드는 어떤 단어가 주어졌을 때 단어에 포함된 각 알파멧의 글자 수를 세어주는 간단한 함수입니다.def countLetters(word):    counter = {}    for letter in word:        if letter not in counter:            counter[letter] = 0        counter[letter] += 1    return countercountLetters('hello world'))# {'h': 1, 'e': 1, 'l': 3, 'o': 2, ' ': 1, 'w': 1, 'r': 1, 'd': 1}이처럼 데이터의 개수를 셀 때 파이썬의 내장 자료구조인 사전(dictionary)이 많이 사용되는데요.*******************************************************************************************************************************collections.Counter를 이용한 카운팅파이썬에서 제공하는 collections 모듈의 Counter 클래스를 사용하면 위 코드를 단 한 줄로 줄일 수가 있습니다.from collections import CounterCounter('hello world') # Counter({'l': 3, 'o': 2, 'h': 1, 'e': 1, ' ': 1, 'w': 1, 'r': 1, 'd': 1})collections.Counter 기본 사용법collections 모듈의 Counter 클래스는 별도 패키지 설치 없이 파이썬만 설치되어 있다면 다음과 같이 임포트해서 바로 사용할 수 있습니다.from collections import Countercollections 모듈의 Counter 클래스는 파이썬의 기본 자료구조인 사전(dictionary)를 확장하고 있기 때문에, 사전에서 제공하는 API를 그대로 다 시용할 수가 있습니다.예를 들어, 주어진 단어에서 가장 많이 등장하는 알페벳과 그 알파벳의 개수를 구하는 함수는 다음과 같이 마치 사전을 이용하듯이 작성할 수 있습니다.from collections import Counterdef find_max(word):    counter = Counter(word)    print("counter",counter)    max_count = -1    for letter in counter:        print("letter",letter)        if counter[letter] > max_count:            max_count = counter[letter]            print("max_count",max_count)            max_letter = letter            print("max_letter",max_letter)    return max_letter, max_countfind_max('hello world') # ('l', 3)

결과

counter Counter({'l': 3, 'o': 2, 'h': 1, 'e': 1, ' ': 1, 'w': 1, 'r': 1, 'd': 1})letter hmax_count 1max_letter hletter eletter lmax_count 3max_letter lletter oletterletter wletter rletter d

3.4.2 애너그램

애너그램( anagram)은 문장 또는 단어의 철자 순서를 바꾸는 놀이를 말한다. 두 문자열이 서로 애너그램인지 확인하고 싶다고 하자. 셋은 항목의 발생횟수를 계산하지 않고, 리스트의 항목을 정렬하는 시간복잡도는 최소 O(n log n)이다. 따라서 애너그램을 확인하는 데 딕셔너리를 사용하는 것이 가장 좋은 해결책이 될 수 있다.

다음 코드는 두 문자열이 애너그램인지 확인한다. 먼저 첫 번째 문자열의 문자 발생횟수를 더해서 딕셔너리에 저장한다. 두 번째 문자열의 문자 발생횟수를 빼어 딕셔너리에 저장한다. 마지막으로 딕셔너리의 모든 항목 값이 0이면 두 문자열은 애너그램이다.

from collections import Counterdef is_anagram(s1,s2):    counter = Counter()    print("counter",counter)    for c in s1:        counter[c] += 1        print("+=c : ",c)        print("counter[c]1:",counter[c])    for c in s2:        counter[c] -= 1        print("-=c : ", c)        print("counter[c]2:",counter[c])    for i in counter.values():        if i:            print("for i in counter.values",i)            print("counter.values",counter.values())            return False    return Truedef test_is_anagram():    s1 = "marina"    s2 = "aniram"    assert(is_anagram(s1,s2) is True)    s1 = "google"    s2 = "gouglo"    assert(is_anagram(s1,s2) is False)    print("테스트 통과")if __name__ == "__main__":    test_is_anagram()

출력결과

PS D:\Mastering-Python-Design-Patterns-Second-Edition\algo\collection_data_Structure> python .\9_is_anagram.pycounter Counter()+=c :  mcounter[c]1: 1   +=c :  acounter[c]1: 1   +=c :  rcounter[c]1: 1   +=c :  icounter[c]1: 1   +=c :  ncounter[c]1: 1   +=c :  acounter[c]1: 2   -=c :  acounter[c]2: 1   -=c :  ncounter[c]2: 0   -=c :  icounter[c]2: 0   -=c :  rcounter[c]2: 0   -=c :  acounter[c]2: 0   -=c :  mcounter[c]2: 0   counter Counter()+=c :  gcounter[c]1: 1+=c :  ocounter[c]1: 1+=c :  ocounter[c]1: 2+=c :  gcounter[c]1: 2+=c :  lcounter[c]1: 1+=c :  ecounter[c]1: 1-=c :  gcounter[c]2: 1-=c :  ocounter[c]2: 1-=c :  ucounter[c]2: -1-=c :  gcounter[c]2: 0-=c :  lcounter[c]2: 0-=c :  ocounter[c]2: 0for i in counter.values 1counter.values dict_values([0, 0, 0, 1, -1])테스트 통과

두 문자열이 애너그램인지 확인하는 또 다른 방법은 해시 함수의 속성을 이용하는 것이다.</br> ord()함수는 인수가 유니코드 객체일 때, 문자의 유니코드를 나타내는 정수를 반환한다. </br> 문자열에서 모든 문자의 ord() 함수 결과를 더했을 때 그 결과가 같으면 , 두 문자열은 애너그램이다.

import stringdef hash_func(astring):    s = 0    for one in astring:        if one in string.whitespace:                continue        print("astring",astring)        print("one",one)        s = s + ord(one)        print("s=s+ord",s)    return s    def find_anagram_hash_function(word1,word2):    return hash_func(word1) == hash_func(word2)def test_find_anagram_hash_function():    word1 = "buffy"    word2 = "bffyu"    word3 = "bffya"    assert(find_anagram_hash_function(word1,word2) is True)    assert(find_anagram_hash_function(word1,word3) is False)    print("테스트 통과")if __name__ == "__main__":    test_find_anagram_hash_function()

출력결과

PS D:\Mastering-Python-Design-Patterns-Second-Edition\algo\collection_data_Structure> python .\10_is_anagram_using_ord.pyastring buffyone bs=s+ord 98astring buffyone us=s+ord 215astring buffyone fs=s+ord 317astring buffyone fs=s+ord 419astring buffyone ys=s+ord 540astring bffyuone bs=s+ord 98astring bffyuone fs=s+ord 200astring bffyuone fs=s+ord 302astring bffyuone ys=s+ord 423astring bffyuone us=s+ord 540astring buffyone bs=s+ord 98astring buffyone us=s+ord 215astring buffyone fs=s+ord 317astring buffyone fs=s+ord 419astring buffyone ys=s+ord 540astring bffyaone bs=s+ord 98astring bffyaone fs=s+ord 200astring bffyaone fs=s+ord 302astring bffyaone ys=s+ord 423astring bffyaone as=s+ord 520테스트 통과

3.4.3 주사위 합계 경로

주사위를 두 번 던져서 합계가 특정 수가 나오는 경우의 수와 경로를 구해보자.</br> 예를 들어 5가 나올 수 있는 모든 경로는 [1,4] , [2,3], [3,2], [4,1]이다. </br> 다음 코드에서는 두 개의 딕셔너리를 사용하여 주사위의 합계 경로를 구한다.

#주사위 합계경로from collections import Counter, defaultdictdef find_dice_probabilities(S,n_faces=6):    if S > 2 * n_faces or S < 2:        return None    cdict = Counter()    ddict = defaultdict(list)    #두 주사위의 합을 모두 더해서 딕셔너리에 넣는다.    for dice1 in range(1, n_faces+1):        for dice2 in range(1,n_faces+1):            t = [dice1, dice2 ]            cdict[dice1+dice2] += 1            ddict[dice1+dice2].append(t)    return [cdict[S], ddict[S]]def test_find_dice_probabilities():    n_faces=6    S=5    results = find_dice_probabilities(S,n_faces)    print(results)    assert(results[0] == len(results[1]))    print("테스트 통과!")if __name__ == "__main__":    test_find_dice_probabilities()
PS D:\Mastering-Python-Design-Patterns-Second-Edition\algo\collection_data_Structure> python .\11_find_dice_probabilities.py[4, [[1, 4], [2, 3], [3, 2], [4, 1]]]테스트 통과!

3.4.4 단어의 중복 문자 제거

다음은 딕셔너리를 사용하여 단어에서 중복되는 문자를 모두 찾아서 제거하는 코드다.

 

 
import string
def delete_unique_word(str1):
    table_c = {key:0 for key in string.ascii_lowercase}
    print("string.ascii_lowercase",string.ascii_lowercase)
    
    for i in str1:
        table_c[i] += 1
        print("i",i)
        print("table_c",table_c[i])
    for key, value in table_c.items():
        #print("table_c.items()",table_c.items())
        if value >1:
            str1 = str1.replace(key, "")
        print("str1",str1)
    return str1
def test_delete_unique_word():
    str1 = "google"
    assert(delete_unique_word(str1) == "le")
    print("테스트 통과!")
if __name__ == "__main__":
    test_delete_unique_word()
 

 
PS D:\Mastering-Python-Design-Patterns-Second-Edition\algo\collection_data_Structure> python .\12_delete_duplicate_char_str.py
string.ascii_lowercase abcdefghijklmnopqrstuvwxyz
i g        
table_c 1  
i o        
table_c 1  
i o        
table_c 2  
i g        
table_c 2  
i l        
table_c 1  
i e        
table_c 1  
str1 google
str1 google
str1 google
str1 google
str1 google
str1 google
str1 oole  
str1 oole  
str1 oole  
str1 oole  
str1 oole  
str1 oole  
str1 oole  
str1 oole
str1 le
str1 le
str1 le
str1 le
str1 le
str1 le
str1 le
str1 le
str1 le
str1 le
str1 le
str1 le
테스트 통과!

 

+ Recent posts