6.파이썬 고급주제

멀티프로세스와 멀티 스레드

좋은 습관

단위테스트

6.1.1 subprocess 모듈

import subprocess


subprocess.run(["echo","이것은 subprocess입니다."])
subprocess.run(["sleep","10"])
root@DESKTOP-JPGB0S5:/mnt/d/Mastering-Python-Design-Patterns-Second-Edition/algo/advanced_subject# python3.6 0_sub_pro.py
이것은 subprocess입니다.

6.1.2 threading 모듈

스레드가 여러 개로 분리되면, 스레드 간 데이터 공유의 복잡성이 증가한다. 또한 락(lock)과 데드락(deadlock)을 회피하는 데 주의를 기울여야 한다. 파이썬 프로그램에는 단 하나의 메인 스레드만 존재한다. 멀티 스레드를 사용하려면 threading 모듈을 사용한다.

내부적으로 락을 관리하려면 queue 모듈을 사용한다. 큐에 의존하면 자원의 접근을 직렬화할 수 있고, 이는 곧 한 번에 하나의 스레드만 데이터에 접근할 수 있게 한다는 뜻이다(FIFO first in, first out 방식으로). 실행 중인 스레드가 있는 동안에는 프로그램은 종료되지 않는다.

워커 스레드(worker thread)가 작업을 완료했는데도, 프로그램이 종료되지 않고 계속 실행되는 경우 문제가 될 수 있다. 스레드를 데몬(daemon)으로 변환하면 데몬 스레드가 실행되지 않는 즉시 프로그램이 종료된다. queue.join() 메서드는 큐가 빌 때까지 (큐의 모든 항목이 처리될 때까지) 기다린다. queue 모듈의 공식 문서 예쩨를 조금 수정한 다음 코드를 살펴보자.

import queue
import threading

q = queue.Queue()

def worker(num):
    while True:
        item = q.get()
        if item is None:
            break
            # 작업을 처리한다. 
        print("스레드 {0} : 처리 완료 {1}".format(num+1, item))
        q.task_done()
if __name__ == "__main__":
    num_worker_threads = 5
    threads  = []
    for i in range(num_worker_threads):
        t = threading.Thread(target=worker, args=(i,))
        t.start()
        threads.append(t)
    for item in range(20):
        q.put(item)
    
    #모든 작업이 끝날 떄까지 기다린다.(block).
    q.join()

결과

PS D:\Mastering-Python-Design-Patterns-Second-Edition\algo\advanced_subject> python .\1_threading_with_queue.py
스레드 1 : 처리 완료 0
스레드 2 : 처리 완료 1
스레드 5 : 처리 완료 3
스레드 1 : 처리 완료 5
스레드 1 : 처리 완료 8
스레드 5 : 처리 완료 7
스레드 2 : 처리 완료 6
스레드 1 : 처리 완료 9
스레드 5 : 처리 완료 10
스레드 5 : 처리 완료 13
스레드 1 : 처리 완료 12
스레드 2 : 처리 완료 11
스레드 5 : 처리 완료 14
스레드 1 : 처리 완료 15
스레드 1 : 처리 완료 18
스레드 1 : 처리 완료 19
스레드 3 : 처리 완료 4
스레드 5 : 처리 완료 17
스레드 2 : 처리 완료 16
스레드 4 : 처리 완료 2

6.1.3 뮤텍스와 세마포어

**뮤텍스**는 락과 같다. 뮤텍스는 공유 리소스에 한 번에 하나의 스레드만 접근 할 수 있도록 하는 상호 배제(mutual exclusion) 동시성 제어정책을 강제하기 위해 설계되었다. 예를 들어 한 스레드가 배열을 수정하고 있다고 가정해보자. 배열 작업을 절반 이상 수행했을 떄, 프로세서가 다른 스레드로 전환했다고 하자, 여기에서 뮤텍스를 사용하지 않는다면, 두 스레드가 동시에 배열을 수정하는 일이 벌어질 것이다. 개념적으로. 뮤텍스는 1부터 시작하는 정수다. 스레드는 배열을 변경해야 할 때마다 뮤텍스를 '잠근다.' 즉, 스레드는 뮤텍스가 양수가 될 때까지 대기한 다음 숫자를 1 감소시킨다.(이것이 곧 락이다.) 배열 수정을 마치면 뮤텍스가 잠금 해제되어 숫자가 1 증가한다.(언락). 배열을 수정하기 전에 뮤텍스를 잠근 후 수정작업이 끝나고 잠금을 해제하면, 두 스레드가 배열을 동시에 수정하는 일은 일어나지 않는다. 다음 뮤텍스 예제를 살펴보자. 예제파일을 다운로드 했다면 thread_safe 변수가 True로 작성되어 있을 텐데, 비교를 위해 다음과 같이 False로 지정하여 실행해보자.

from threading import Thread, Lock
import threading

def worker(mutex, data, thread_safe):
    if thread_safe:
        mutex.acquire()
    try:
        print("스레드 {0}: {1}\n".format(threading.get_ident(),data))
    finally:
        if thread_safe:
            mutex.release()

if __name__ == "__main__":
    threads = []
    thread_safe = False
    mutex =Lock()
    for i in range(20):
        t = Thread(target=worker, args=(mutex, i,thread_safe))
        t.start()
        threads.append(t)
    for i in threads:
        t.join()
        

실행할 때마다 결과가 다르게 나올 것이다. 이제 뮤텍스를 사용하기 위해 thread_safe 변수를 True로 설정한 후 다시 코드를 실행해보자.

한편, **세마포어**는 뮤텍스보다 더 일반적으로 사용되는 개념이다. 세마포어는 1보다 큰 수로 시작할 수 있다. 세마포어 값은 곧 한 번에 자원에 접근할 수 있는 스레드의 수다. 세마포어는 뮤텍스의 락 및 언락 작업과 유사한 대기(wait) 및 신호(signal) 작업을 지원한다. 파이썬의 뮤텍스(락)와 세마포어에 관한 내용은 threading 모듈의 공식 문서를 참조한다. 다음 세마포어 예제를 살펴보자.

import threading
import time

class ThreadPool(object):
    def __init__(self):
        self.active = []
        self.lock = threading.Lock()

    def acquire(self,name):
        with self.lock:
            self.active.append(name)
            print("획득: {0} | 스레드 풀: {1}".format(name,self.active))
    
    def release(self,name):
        with self.lock:
            self.active.remove(name)
            print("반환: {0} | 스레드 풀: {1}".format(name,self.active))
    
def worker(semaphore,pool):
    with semaphore:
        name = threading.currentThread().getName()
        pool.acquire(name)
        time.sleep(1)
        pool.release(name)

if __name__ == "__main__":
    threads=[]
    pool = ThreadPool()
    semaphore = threading.Semaphore(3)
    for i in range(10):
        t = threading.Thread(target=worker, name="스레드 "+ str(i), args=(semaphore,pool))
        t.start()
        threads.append(t)
    for t in threads:
        t.join()

결과

PS D:\Mastering-Python-Design-Patterns-Second-Edition\algo\advanced_subject> python .\3_threading_semaphore.py
획득: 스레드 0 | 스레드 풀: ['스레드 0']
획득: 스레드 1 | 스레드 풀: ['스레드 0', '스레드 1']
획득: 스레드 2 | 스레드 풀: ['스레드 0', '스레드 1', '스레드 2']
반환: 스레드 2 | 스레드 풀: ['스레드 0', '스레드 1']
반환: 스레드 1 | 스레드 풀: ['스레드 0']
반환: 스레드 0 | 스레드 풀: []
획득: 스레드 3 | 스레드 풀: ['스레드 3']
획득: 스레드 4 | 스레드 풀: ['스레드 3', '스레드 4']
획득: 스레드 5 | 스레드 풀: ['스레드 3', '스레드 4', '스레드 5']
반환: 스레드 5 | 스레드 풀: ['스레드 3', '스레드 4']
반환: 스레드 4 | 스레드 풀: ['스레드 3']
반환: 스레드 3 | 스레드 풀: []
획득: 스레드 6 | 스레드 풀: ['스레드 6']
획득: 스레드 7 | 스레드 풀: ['스레드 6', '스레드 7']
획득: 스레드 8 | 스레드 풀: ['스레드 6', '스레드 7', '스레드 8']
반환: 스레드 8 | 스레드 풀: ['스레드 6', '스레드 7']
반환: 스레드 6 | 스레드 풀: ['스레드 7']
반환: 스레드 7 | 스레드 풀: []
획득: 스레드 9 | 스레드 풀: ['스레드 9']
반환: 스레드 9 | 스레드 풀: []

6.1.4 데드락과 스핀락

**데드락**(교착상태)은 두 개 이상의 프로세스나 스레드가 서로 상대방의 작업이 끝나기만을 기다리고 있기 때문에 결과적으로 아무것도 완료되지 못하는 상태다. 프로그램에서 락을 할당하고, 락을 순서대로 획득한다면, 교착 상태를 막을 수 있다.(이는 일반적인 접근법일뿐 정교한 것은 아니다.)

다음 네 가지 조건을 모두 충족하면 데드락이 발생한다. 네 가지 조건 중 하나라도 막을 수 있다면, 데드락을 해결할 수 있다.

  • **상호배제**(mutual exclusion): 자원은 한 번에 한 프로세스(혹은 스레드)만 사용할 수 있다.
  • **점유와 대기**(hold and wait) : 한 프로세스가 자원을 가지고 있는 상태에서, 다른 프로세스가 쓰는 자원의 반납을 기다린다.
  • **비선점**(no preemtion) : 다른 프로세스가 이미 점유한 자원을 강제로 뻇어오지 못한다.
  • **순환대기**(circular wait) : 프로세스 A,B,C가 있다고 가정할 떄 A는 B가 점유한 자원을, B는 C가 점유한 자원을, C는 A가 점유한 자원을 대기하는 상태다.

**스핀락**은 (전체 시스템이 단일 애플리케이션 전용이고, 코어당 하나의 스레드만 사용하는 ) 고성능 컴퓨팅 상황에 유용한 바쁜 대기(busy wating )의 한 형태다. 스핀락은 임계 구역에 진입이 불가능할 때, 진입이 가능할 때까지 반복문을 돌면서 재시도하는 방식으로 구현된 락이다.

6.1.5 스레딩에 대한 구글 파이썬 스타일 가이드

내장 타입의 원자성(atomicity)에 의존하지 않는다. 딕셔너리 같은 파이썬 기본 데이터 타입은 원자적 연산을 수행하는 반면, 내장 타입이 원자적이지 않은 경우가 있어서 __hash__() 또는 __eq__() 메서드가 구현된 경우), 내장 타입의 원자성에 의존해선 안 된다. 또한 원자적 변수 할당에 의존하지 않아야한다. (이것은 결국 딕셔너리에 의존하기 때문이다.)

queue 모듈의 Queue 데이터 타입을 스레드 간 데이터를 전달하는 기본 방식으로 사용한다. 그렇지 않으면, threading 모듈의 락을 사용한다. 저수준의 락 대신, threading.Condition을 사용할 수 있도록 조건 변수를 적절하게 사용하는 방법을 숙지한다. 생산자-소비자 모델의 간단한 예제를 살펴보자.

import threading

def consumer(cond):
    name = threading.currentThread().getName()
    print("{0} 시작".format(name))
    with cond:
        print("{0} 대기".format(name))
        cond.wait()
        print("{0} 자원 소비".format(name))

def producer(cond):
    name = threading.currentThread().getName()
    print("{0} 시작".format(name))
    with cond:
        print("{0} 자원 생산 후 모든 소비자에게 알림".format(name))
        cond.notifyAll

if __name__ == "__main__":
    condition = threading.Condition()
    consumer1 = threading.Thread(
        name="소비자1", target=consumer,args=(condition,))
    consumer2 = threading.Thread(
        name="소비자2", target=consumer, args=(condition,))
    producer = threading.Thread(name="생산자", target=producer, args=(condition,))

    consumer1.start()
    consumer2.start()
    producer.start()
    

결과

소비자1 시작
소비자1 대기
소비자2 시작
생산자 시작
소비자2 대기
생산자 자원 생산 후 모든 소비자에게 알림
소비자2 자원 소비
소비자1 자원 소비

-기타

_ = "_"

6.2. 좋은 습관


6.2.1 가상 환경

프로젝트 경험이 많아질수록, 다양한 버전의 파이썬이나 라이브러리로 작업하는 일이 생길 것이다. 별도의 파이썬 가상환경을 만들기 위한 라이브러리는 많다. 이 책에서는 virtualenv와 virtualenvwrapper로 파이썬 가상환경을 간단하게 만들어보겠다.

**virtualenv**는 파이썬 프로젝트에 필요한 패키지를 사용하기 위해 필요한 모든 실행파일을 포함하는 폴더를 생성한다. 공식문서(https://docs.python-guide.org/dev/virtualenvs/#lower-level-virtualenv)에서 예제를 볼 수 있다.

#virtualenv를 설치한다.
$pip install virtualenv


#설치된 버전을 확인한다.
$virtualenv --version

#가상 환경 프로젝트를 생성한다.
$cd my_project_folder
$virtualenv my_project

#가상 환경 프로젝트를 활성화한다.
$source my_project/bin/activate

# 파이썬 외부 패키지 모듈을 설치한다. (다음 예제에서는 request 라이브러리를 설치한다.).
$(my_project)pip install requests

#가상 환경에서 설치된 외부 패키지 목록을 확인한다.
$(my_project) pip freeze

# 가상 환경 프로젝트를 비활성화한다.
$(my_project) deactivate

이렇게 설정한 로컬 가상환경을 삭제하려면, 생성한 폴더(my_project)를 삭제하면 된다.

**virtualenvwrapper**는 virtualenv를 사용하여 모든 가상 환경을 한곳에 배치한다.(https://docs.python-guide.org/dev/virtualenvs/#virtualenvwrapper). 윈도우 사용자를 위한 문서도 있으니 참조한다.

#virtualenvwrapper를 설치한다.
$pip install virtualenvwrapper

#가상 환경 폴더를 생성한다.
$export WORKON_HOME=~/Envs
$mkdir -p $WORKON_HOME
$source /usr/local/bin/virtualenvwrapper.sh

#가상 환경 프로젝트를 생성한다.
$mkvirtualenv env1

#requests 라이브러리를 설치한다.
(env1)$pip install requests

#설치된 패키지를 확인한다.
(env1)$ pip freeze

#가상 환경 프로젝트를 활성화한다.
$workon env1

$가상 환경 프로젝트를 비활성화한다.
(env1)$ deactivate

# 가상 환경 프로젝트를 삭제한다.
(env1)$ rmvirtualenv env1

6.2.2 디버깅


파이썬 디버거 **pdb**를 이용하면 디버깅을 할 수 있다.(http://pymotw.com/3/pdb) 자세한 사용법은 파이썬 공식 문서를 참조한다.

파이썬 스크립트 파일을 대화식 인터프리터를 사용해 살펴보고 싶다면 -i 뒤에 파일명을 적거나 -m pdb 뒤에 파일명을 적어서 실행하면 된다. 스크립트에 있는 변수와 함수 등을 사용할 수 있다. 먼저 -i 실행 예이다.

root@DESKTOP-JPGB0S5:/mnt/d/Mastering-Python-Design-Patterns-Second-Edition/algo/advanced_subject# python3.6 -i 1_threading_with_queue.py
스레드 1 : 처리 완료 0
스레드 1 : 처리 완료 5
스레드 4 : 처리 완료 3
스레드 2 : 처리 완료 1
스레드 1 : 처리 완료 6
스레드 4 : 처리 완료 7
스레드 2 : 처리 완료 8
스레드 1 : 처리 완료 9
스레드 4 : 처리 완료 10
스레드 2 : 처리 완료 11
스레드 1 : 처리 완료 12
스레드 4 : 처리 완료 13
스레드 2 : 처리 완료 14
스레드 1 : 처리 완료 15
스레드 4 : 처리 완료 16
스레드 2 : 처리 완료 17
스레드 1 : 처리 완료 18
스레드 4 : 처리 완료 19
스레드 5 : 처리 완료 4
스레드 3 : 처리 완료 2
>>> q
<queue.Queue object at 0x7f7c52c5cda0>
>>> 5

다음은 -m pdb 실행 예이다.

root@DESKTOP-JPGB0S5:/mnt/d/Mastering-Python-Design-Patterns-Second-Edition/algo/advanced_subject# python3.6 -m pdb 1_threading_with_queue.py
> /mnt/d/Mastering-Python-Design-Patterns-Second-Edition/algo/advanced_subject/1_threading_with_queue.py(1)<module>()
-> import queue
(Pdb) help

Documented commands (type help <topic>):
========================================
EOF    c          d        h         list      q        rv       undisplay
a      cl         debug    help      ll        quit     s        unt
alias  clear      disable  ignore    longlist  r        source   until
args   commands   display  interact  n         restart  step     up
b      condition  down     j         next      return   tbreak   w
break  cont       enable   jump      p         retval   u        whatis
bt     continue   exit     l         pp        run      unalias  where

Miscellaneous help topics:
==========================
exec  pdb

(Pdb) help n
n(ext)
        Continue execution until the next line in the current function
        is reached or it returns.
(Pdb) n
> /mnt/d/Mastering-Python-Design-Patterns-Second-Edition/algo/advanced_subject/1_threading_with_queue.py(2)<module>()
-> import threading
(Pdb) n
> /mnt/d/Mastering-Python-Design-Patterns-Second-Edition/algo/advanced_subject/1_threading_with_queue.py(4)<module>()
-> q = queue.Queue()
(Pdb) n
> /mnt/d/Mastering-Python-Design-Patterns-Second-Edition/algo/advanced_subject/1_threading_with_queue.py(6)<module>()
-> def worker(num):
(Pdb) n
> /mnt/d/Mastering-Python-Design-Patterns-Second-Edition/algo/advanced_subject/1_threading_with_queue.py(14)<module>()
-> if __name__ == "__main__":

pdb의 명령어를 몇 가지 살펴보겠다. c(continue)를 입력하면 프로그램을 끝까지 실행하고, s(step)은 코드 다음 줄로 넘어간다.(한 단계 씩 코드 실행step into). n(next)도 코드 다음줄로 넘어가되 프로시저 단위 실행(step over)으로서, s와 다른 점은 어떤 함수를 만날 경우 함수 전체를 실행한 뒤 다음 줄로 넘어간다는 점이다. p(point)는 표현식의 값을 출력한다. l(line)은 다음 실행할 코드를 몇 줄 보여준다. h(help)는 도움말이다.

스크립트에서 디버깅하고 싶은 위치에 pdb.set_trace() 함수를 삽입하는 방법도 있다.

import pdbpdb.set_trace()

6.2.3 프로파일링

프로그램이 매우 느리게 실행되거나 예상보다 많은 메모리가 소비된다면, 자료구조나 알고리즘을 잘못 선택했거나 비효율적으로 구현했기 때문인 경우가 많다. 이 경우 다음과 같이 성능 항목을 검토한다.

  • 읽기 전용 데이터는 리스트 대신 튜플을 사용한다.
  • 반복문에서 항목이 많은 리스트나 튜플 대신 **제너레이터**를 사용하여 순회한다.
  • 문자열을 연결할 떄 + 연산자로 문자열을 연결(concatenate)하는 대신, 리스트에 문자열을 추가(append)한 후, 마지막에 리스트의 항목을 모두 하나로 연결(join)한다. 다음 구글 파이썬 스타일 가이드의 예제 코드를 살펴보자.
#좋은 예items=['<table>']for last_name, first_name in employee_list:    items.append('<tr><td>%s,%s</td></tr>' %(last_name,first_name))    items.append('</table>')    employee_table=''.join(items)# 나쁜 예employee_table = '<table>'for last_name, first_name in employee_list:    employee_table += '<tr><td>%s, %s</td></tr>' % (last_name, first_name)    employee_table += '</table>'

cProfile 모듈

cProfile 모듈은 호출 시간에 대한 세부 분석을 제공하며,병목 현상(bottleneck)을 찾는 데 사용된다. 흔히 다음과 같은 형태로 사용한다.

import cProfilecProfile.run('main()')

조금 더 실제적인 예는 다음과 같다.

import cProfileimport timedef sleep():    time.sleep(5)def hello_world():    print("Hello World!")def main():    sleep()    hello_world()cProfile.run('main()')

실행 결과

PS D:\Mastering-Python-Design-Patterns-Second-Edition\algo\advanced_subject> python .\test.pyHello World!         8 function calls in 5.014 seconds   Ordered by: standard name   ncalls  tottime  percall  cumtime  percall filename:lineno(function)        1    0.000    0.000    5.014    5.014 <string>:1(<module>)        1    0.000    0.000    5.013    5.013 test.py:17(sleep)        1    0.000    0.000    0.001    0.001 test.py:20(hello_world)        1    0.000    0.000    5.014    5.014 test.py:23(main)        1    0.000    0.000    5.014    5.014 {built-in method builtins.exec}        1    0.001    0.001    0.001    0.001 {built-in method builtins.print}        1    5.013    5.013    5.013    5.013 {built-in method time.sleep}        1    0.000    0.000    0.000    0.000 {method 'disable' of '_lsprof.Profiler' objects}

혹은 다음과 같이 스크립트 파일에 대해 실행할 수도 있다.

python -m cProfile -o profile.dat 1_threading_with_queue.pyPS D:\Mastering-Python-Design-Patterns-Second-Edition\algo\advanced_subject> python -m cProfile -o profile.dat .\1_threading_with_queue.py스레드 2 : 처리 완료 0스레드 1 : 처리 완료 1스레드 2 : 처리 완료 3스레드 5 : 처리 완료 5스레드 5 : 처리 완료 8스레드 2 : 처리 완료 7스레드 2 : 처리 완료 10스레드 4 : 처리 완료 2스레드 4 : 처리 완료 12스레드 4 : 처리 완료 13스레드 4 : 처리 완료 14스레드 2 : 처리 완료 11스레드 1 : 처리 완료 6스레드 5 : 처리 완료 9스레드 2 : 처리 완료 16스레드 2 : 처리 완료 19스레드 5 : 처리 완료 18스레드 4 : 처리 완료 15스레드 3 : 처리 완료 4스레드 1 : 처리 완료 17$python -m pstats profile.dat

이 밖에 프로파일링에 대한 자세한 내용은 파이썬 공식 문서를 참조한다.

timeit 모듈

코드 일부분의 실행 시간을 확인하는 데 사용한다. 다음예제를 살펴보자.

import timeitprint(timeit.timeit("x = 2+2"))print(timeit.timeit("x = sum(range(10))"))

결과

PS D:\Mastering-Python-Design-Patterns-Second-Edition\algo\advanced_subject> python .\test.py0.0251481999999999960.9078216

다음과 같이 스크립트로 실행할수도 있다.

PS D:\Mastering-Python-Design-Patterns-Second-Edition\algo\advanced_subject> python -m timeit "d={}"5000000 loops, best of 5: 49.3 nsec per loopPS D:\Mastering-Python-Design-Patterns-Second-Edition\algo\advanced_subject> python -m timeit "import collections" "d = collections.OrderedDict()"1000000 loops, best of 5: 371 nsec per loop

time 모듈의 time()함수를 사용한 아주 간단한 예제를 살펴보자.

import time


def sumOfN2(n):
    start = time.time()
    theSum = 0
    for i in range(1,n+1):
        theSum = theSum + i
    end = time.time()
    return theSum, end-start

if __name__ == "__main__":
    n = 5
    print("총 합계: %d\t 시간: %10.7f초" % sumOfN2(n))
    n = 200
    print("총 합계: %d\t 시간: %10.7f초" % sumOfN2(n))
PS D:\Mastering-Python-Design-Patterns-Second-Edition> cd .\algo\advanced_subject\
PS D:\Mastering-Python-Design-Patterns-Second-Edition\algo\advanced_subject> python .\5_using_time_module.py
총 합계: 15      시간:  0.0000000초
총 합계: 20100   시간:  0.0000000초

6.3. 단위 테스트

개별 함수 및 클래스의 메서드에 대한 테스트 코드를 작성하여, 예상한 값이 맞게 나오는지 확인하는 것이 좋은 습관이다. 파이썬 표준 라이브러리는 이러한 단위 테스트(unit test)를 위해 doctest와 unittest 모듈을 제공한다. 또한 외부 라이브러리인 pytest 모듈도 있다.

6.3.1 용어

  • 테스트 픽스처(test fixture) : 테스트 설정을 위한 코드( 예: 테스트용 입력 파일을 만들었다 삭제하는 코드)
  • 테스트 케이스(test case):테스트의 기본 단위
  • 테스트 스위트(test suitte): unittest.TestCase의 하위 클래스에 의해 생성된 테스트 케이스 집합. 각 테스트 케이스의 메서드 이름은 test로 시작한다.
  • 테스트 러너(test runner): 하나 이상의 테스트 스위트를 실행하는 객체

6.3.2 doctest

먼저 **doctest** 모듈은 모듈과 함수의 독스트링(docstring) 안에 테스트 코드를 작성할 때 사용한다. 테스트를 작성한 후 , 다음 코드 세 줄만 추가하면된다.

if __name__ == "__main__"   import doctest   doctest.testmod()

doctest 모듈이 포함된 프로그램은 두 가지 방법으로 실행할 수 있다. 먼저 -v 옵션으로 파이썬을 실행하는 방법이다. 파이썬 공식문서의 예제를 살펴보자.

"""This is the "example" module.the example module supplies one function, factorial(). For example>>> factorial(5)120"""def factorial(n):    """Return the factorial of n, an exact integer >= 0        >>> [factorial(n) for n in range(6)]      [1,1,2,6,24,120]    >>> factorial(30)    >>> factorial(-1)     >>> factorial(30.0)     >>> factorial(1e100)    """    import math    if not n >= 0:        raise ValueError("n must be >=0")    if math.floor(n) != n:        raise ValueError("n must be exact integer")    if n+1 == n: #catch a value like 1e300        raise OverflowError("n too large")    result =1    factor =2    while factor <= n:        result *= factor        factor += 1    return result    if __name__ == "__main__":        import doctest        doctest.testmod()        

다음과 같이 unittest 모듈과 함께 실행할 수도 있다.

>>> import doctest>>> import unittest>>> import doctest_factorial>>>>>> suite unittest.TestSuite()>>> suite.addTest(doctest.DocTestSuite(doctest_factorial))>>> runner = unittest.TextTestRunner()>>> print(runner.run(suite))

6.3.3 pytest

외부 라이브러리인 **pytest**는 사용법이 매우 쉽다. test로 시작하는 파일에서 test로 시작하는 함수를 작성하면 된다. 간단한 예를 살펴보겠다.

먼저 pytest 라이브러리를 설치한다.

$ pip install pytest

다음 코드를 간단하게 테스트해보자.

def func(x):
    return x + 1

def test_answer():
    assert func(3) == 51

    

터미널의 현재 위치에서 다음 명령을 실행하면, 파일명이 test로 시작하는 파이썬 스크립트가 실행된다.

결과

================================================================================= test session starts ===================================================================================
platform win32 -- Python 3.7.4, pytest-6.2.4, py-1.10.0, pluggy-0.13.1
rootdir: D:\Mastering-Python-Design-Patterns-Second-Edition\algo\advanced_subject
collected 1 item / 1 error

========================================================================================= ERRORS =========================================================================================
_____________________________________________________________________________ ERROR collecting study_test.py _____________________________________________________________________________
study_test.py:4: in <module>
    for last_name, first_name in employee_list:
E   NameError: name 'employee_list' is not defined
================================================================================ short test summary info =================================================================================
ERROR study_test.py - NameError: name 'employee_list' is not defined
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! Interrupted: 1 error during collection !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!

다음과 같이 파일을 지정하여 실행할 수 있다.

$ py.test test_pytest.py

파이썬 디버거 pdb와 같이 실행할 수 있다.

$ py.test --pdb

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
테스트 통과!

 

2 내장 시퀸스 타입

깊은 복사와 슬라이싱 연산

이번 장에서는 파이썬 내장 시퀸스(sequence) 데이터 타입을 살펴본다. 시퀸스 타입은 다음과 같은 속성을 가진다.

join

A.join(B)는 리스트 B에 있는 모든 문자열을 하나의 단일 문자열 A로 결합한다. 문자열을 연결하는 데에는 +기호를 사용할 수도 있지만, 리스트에 많은 양의 문자열이 있으면 비효율적이다.

slayers = ["버피","앤","아스틴"]
b= " ".join(slayers)
print(b)



PS D:\Mastering-Python-Design-Patterns-Second-Edition\algo> python .\test.py
버피 앤 아스틴

split()메서드

A.split(t,n)는 문자열A에서 문자열t를 기준으로 정수n번만큼 분리한 문자열 리스트를 반환한다. n을 지정하지 않으면 대상 문자열을 t로 최대한 분리한다. t도 지정하지 않으면 공백문자로 구분한 문자열 리스트를 반환한다.

slayers="버피*크리스-메리*16"
fields = slayers.split("*")
job=fields[1].split("-")

print(fields)
print(job)

결과
['버피', '크리스-메리', '16']
['크리스', '메리']

split()메서드를 사용하여 문자열에서 모든 스페이스를 제거하는 함수를 하나 만들어보면 다음과 같다.

def erase_space_from_String(string):
    s1 = string.split(" ")
    s2 = "".join(s1)
    return s2
c = erase_space_from_String("asdf asdfs bbbb dddd")
print(c)

결과
asdfasdfsbbbbdddd

다음 코드는 strip() 메서드를 사용하여, 한 파일에서 사용된 모든 단어를 알파벳순으로 출력하며 각 단어가 등장한 횟수도 함께 출력한다.

import string
import sys

    #입력한 파일에서 사용한 단어를 추출하는 스크립트
    #string.whitespace 공백으로 간주하는 모든 ASCII 문자를 포함하는 문자열. # 여기에는 스페이스, 탭, 줄 바꿈, 캐리지 리턴, 세로 탭 및 폼 피드 문자가 포함됩니다.
    #string.puctuation C 로케일에서 구두점 문자로 간주하는 ASCII 문자의 문자열: !"#$%&'()*+,-./:;<=>?@[\]^_`{|}~.
    #string.digits 문자열 '0123456789'.

def count_unique_word():
    words = {}
    print('words',words)
    strip = string.whitespace + string.punctuation + string.digits + "\"'"

    print('strip',strip)
    for filename in sys.argv[1:]:
        with open(filename,  encoding='UTF8') as file:
            for line in file:
                print('line',line)
                for word in line.lower().split():
                    word = word.strip(strip)
                    print('word',word)
                    if len(word) > 2:
                        words[word] = words.get(word,0) + 1
                        #사용한 워드 빈도수 세기 
                        print('words.get : ',  words[word] )

    for word in sorted(words):
        print("{0}: {1}번".format(word, words[word]))
    #sorted() 오름차순 정렬
if __name__ == "__main__":
   count_unique_word()





결과
PS D:\Mastering-Python-Design-Patterns-Second-Edition\algo>
python .\ch2_1_count_unique_words.py .\1_testing_floats.py

a.denominator: 1번
a.numerator: 1번
assert(float_to_fractions(number: 1번
assert(get_denominator(number: 1번
assert(get_numerator(number: 1번
assert(rounding_floats(number: 1번
assert(rounding_floats(number1,number: 1번
def: 5번
float_to_fractions(number: 1번
fraction: 1번
fraction(*number.as_integer_ratio: 1번
fraction(number: 2번
fractions: 1번
from: 1번
get_denominator(number: 1번
get_numerator(number1,number: 1번
import: 1번
main: 1번
name: 1번
number: 14번
places: 2번
print("테스트: 1번
return: 4번
round(number: 1번
rounding_floats(number: 1번
test_testing_floats: 2번
반환한다: 2번
분모를: 1번
분자를: 1번
  • index(), find() 메서드

문자열 안에서 또 다른 문자열의 인덱스 위치를 찾는 메서드가 있다. A.index(sub, start, end)는 문자열A에서 부분 문자열 sub의 인덱스 위치를 반환하며, 실패하면 ValueError 예외를 발생시킨다. A.find(sub,start,end)는 문자열 A에서 부분 문자열 sub의 인덱스 위치를 반환하며, 실패하면 -1을 반환한다. 인덱스 start와 end는 문자열 범위이며, 생략할 경우 전체 문자열에서 부분 문자열 sub를 찾는다.

slayers = "Buffy and Faith"
slayers.find("y")
>>> 4

slayers.find("k")
>>> -1

slayers.index("y")
>>> 4
  • f-strings

f스트링은 파이썬3.6부터 사용가능하다. 다음 예제와 같이 문자열 앞에 접두사 f를 붙이면 사용할 수 있다. 기존의%나 .format 방식에 비해 간결하고 직관적이며 속도도 빠르다.

name = "프레드"
testa = f"그의 이름은{name!r}입니다."
print(testa)

testb = f"그의 이름은{repr(name)}입니다." #repr()은 !r과 같다.
print(testb)

import decimal
width=10
precision = 4
value = decimal.Decimal("12.34567")
testc= f"결과: {value:{width}.{precision}}"
print(testc)
  • 결과
  • 그의 이름은'프레드'입니다.
    그의 이름은'프레드'입니다.
    결과: 12.35

2.3 튜플

튜플은 쉽표(,)로 구분된 값으로 이루어지는 불변 시퀸스 타입이다.

>>> t1=1234,"안녕!">>> t1[0]1234>>> t1(1234, '안녕!')>>> t2=t1,(1,2,3,4,5)>>> t2((1234, '안녕!'), (1, 2, 3, 4, 5))

2.3.1 튜플 메서드

A.count(x)는 튜플 A에 담긴 항목 X의 개수를 반환한다.

>>> t=1,5,7,8,9,4,1,4>>> t.count(4)2>>> t.count(1)2

2.3.2 튜플 언패킹

파이썬에서 모든 반복 가능한 (iterable)객체는 시퀸스 언패킹 연산자(sequence unpacking operator) *를 사용하여 언패킹할 수 있다. 변수를 할당하는 문장에서 왼쪾에 두 개이상의 변수를 사용하고 한 변수 앞에 * 연산자가 붙으면 , 오른쪽 값들 중 할당되고 남은 값들이 * 연산자가 붙은 변수에 할당된다.

>>> x, *y = (1,2,3,4)>>> x1>>> y[2, 3, 4]

네임드 튜플

파이썬 표준 모듈 collections에는 네임드튜플(named tuple)이라는 시퀸스 데이터 타입이 있다. 네임드 튜플은 일반 튜플과 비슷한 성능과 특성을 갖지만 튜플 항목을 인덱스 위치뿐만 아니라 이름으로도 참조할 수 있다.

collections.namedtuple()메서드의 첫 번째 인수는 만들고자 하는 사용자 정의 튜플 데이터 타입의 이름이다.(보통 왼쪽에 할당하는 변수의 이름과 똑같이 사용한다.) 두 번째 인수는 사용자 정의 튜플 각 항목을 지정하는 '공백으로 구분된 문자열'이다(리스트 또는 튜플로 지정해도 된다. ) 다음 예제를 살펴보자(주석으로 처리한 라인도 결과는 모두 같다. )

>>> import collections>>> Person = collections.namedtuple('Person', 'name age gender')>>> p = Person('아스틴', 30, '남자')>>> p[0]'아스틴'>>> p.name'아스틴'>>> p.age=20Traceback (most recent call last):  File "<stdin>", line 1, in <module>AttributeError: can't set attribute

2.4 리스트

일반적으로 컴퓨터 과학에서 배열(array)는 여러원소(element)들이 연속된 메모리에 순차적으로 저장되는 매우 간단한 구조다. 연결리스트(linked list)는 여러 분리된 노드가 서로 연결되어 있는 구조다. 자료구조의 내용을 순회(iterating)하는 데에는 둘 모두 똑같이 효율적이지만, 어떤 요소(또는 노드)에 직접 접근할 때 배열의 시간복잡도(time complexity)는 O(1)이고, 연결 리스트는 O(n)이다.(연결 리스트는 어떤 노드에 접근하려면 처음부터 순회를 시작해야 한다.) 또한 연결 리스트에서 어떤 노드를 삽입할 때, 그 위치를 안다면 연결 리스트 노드 수에 상관없이 시간복잡도는 O(1)이다. 배열에서 어떤 위치에 항목을 삽입 하려면, 그 위치에서부터 모든 항목을 오른쪽으로 옮겨야 하므로 시간복잡도는 O(n)이다.

파이썬에서 배열과 유사한 객체는 리스트(list)다. 리스트는 크기를 동적으로 조정할 수 있는 배열이다. 연결 리스트와는 아무런 관련도 없다. 연결 리스트는 매우 중요한 추상 데이터 타입(ADT)('7.5연결리스트 참조') , 배열(또는 파이썬 리스트)와 연결 리스트의 차이점을 아는 것은 매우 중요하다.

리스트는 항목을 쉼표로 구분하고, 대괄호 [ ]로 감싼다. 리스트의 항목은 모두 다른 데이터 타입이어도 된다. 불변 타입인 문자열과는 달리, 리스트는 가변 타입이다.

>>> q=[2,3]>>> p=[1,q,4]>>> p[1].append("버피")>>> p[1, [2, 3, '버피'], 4]>>> q[2, 3, '버피']

리스트 끝에서 항목을 추가하거나 제거할 때는 각각 append()와 pop() 메서드를 사용하며, 시간복잡도는 O(1)이다. 리스트 항목을 검색해야 하는 remove(), index()메서드, 멤버십 테스트 in등의 시간복잡도는 O(n)이다. insert() 메서드 또한 지정한 인덱스에 항목을 삽입한 후, 그 이후의 인덱스 항목들을 한 칸씩 뒤로 밀어야 하므로 시간복잡도는 O(n)이다.

검색이나 멤버십 테스트 시 빠른 속도가 필요하다면 셋(set)이나 딕셔너리(dictionary)같은 컬렉션 타입을 선택하는 것이 더 적합할 수 있다.('3장 컬렉션 자료구조' 참조).

또는 리스트에서 항목을 순서대로 정렬하여 보관하면 빠른 검색을 제공할 수 있다(10장에서 시간복잡도가 O(log n)인 이진 검색 알고리즘에 대해서 살펴본다 )

2.4.1 리스트 메서드

append()

A.append(x)는 리스트 A 끝에 항목 x를 추가한다. 다음 코드에서 볼 수 있듯 A[len(A):] = [x]와 동일한 의미다.

>>> people = ["버피", "페이스"]>>> people.append("자일스")>>> people['버피', '페이스', '자일스']>>> people[len(people):] = ["잰더"]>>> people['버피', '페이스', '자일스', '잰더']

extend()

A.extend(c)는 반복가능한 모든 항목c를 리스트 A에 추가한다. A[len(A):]=c 또는 A+=c와 동일하다.

>>> people=["버피","페이스"]>>> people.extend("자일스")>>> people['버피', '페이스', '자', '일', '스']>>> people += "윌로">>> people['버피', '페이스', '자', '일', '스', '윌', '로']>>> people +=["잰더"]>>> people['버피', '페이스', '자', '일', '스', '윌', '로', '잰더']>>> people[len(people):] = "아스틴">>> people['버피', '페이스', '자', '일', '스', '윌', '로', '잰더', '아', '스', '틴']>>> 

insert()

A.insert(i,x)는 리스트A의 인덱스 위치 i에 항목x를 삽입한다.

>>> people=["버피", "페이스"]>>> people.insert(1,"잰더")>>> people['버피', '잰더', '페이스']

remove

A.remove(x)는 리스트 A의 항목x를 제거한다. x가 존재하지 않으면 ValueError예외를 발생시킨다.

>>> people=["버피", "페이스"]>>> people.remove("버피")>>> people['페이스']>>>>>> people.remove("버피")Traceback (most recent call last):  File "<stdin>", line 1, in <module>ValueError: list.remove(x): x not in list

pop()

A.pop(x)는 리스트 A에서 인덱스 x에 있는 항목을 제거하고 그 항목을 반환한다. X를 지정하지 않으면, 리스트 맨 끝 항목을 제거하고 그 항목을 반환한다.

>>> people=["버피", "페이스", "아스틴"]>>> people.pop(1)'페이스'>>> people['버피', '아스틴']

del문

del문은 리스트 인덱스를 지정하여 특정한 항목을 삭제한다. 그뿐만 아니라, 슬라이스를 사용하여 특정 범위 항목들을 삭제할 수도 있다.

>>> del a[0]>>> a[4, 5, 7, 10]>>> del a[2:3]>>> a[4, 5, 10]>>> del a #변수 a자체를 삭제한다.>>> aTraceback (most recent call last):  File "<stdin>", line 1, in <module>NameError: name 'a' is not defined

index()

A.index(x)는 리스트 A에서 항목x의 인덱스를 반환한다.

>>> people=["버피", "페이스"]>>> people.index("버피")0

count()

A.count(x)는 리스트 A에 항목x가 몇 개 들어 있는지 개수를 반환한다.

>>> people=["버피","페이스","버피"]>>> people.count("버피")2

sort()

리스트A의 항목을 정렬하여 그 변수 자체에 (in place)적용한다. A.sort(key,re-verse)에 아무런 인수가 없으면 오름차순으로 정렬하며, 인수를 지정할 때는 키워드인수(keyword argument)를 사용해야한다. 예를 들어 리스트의 항목을 내림차순으로 정렬하려면 sort(Reverse=True) 형식으로 지정해야 하고, 인수key를 지정하려면 항수를 넣어야한다. 다음 예제를 살펴보자

>>> people=["잰더","페이스","버피"]>>> people.sort()>>> people['버피', '잰더', '페이스']>>> people.sort(reverse=True)>>> people['페이스', '잰더', '버피']

reverse()

A.reverse()메서드는 리스트A의 항목들을 반전시켜서 그 변수에 적용한다. list[::-1]을 사용하는 것과 같다.

>>> people=["잰더", "페이스", "버피"]>>> people.reverse()>>> people['버피', '페이스', '잰더']>>> people[::-1]['잰더', '페이스', '버피']

2.4.2 리스트 언패킹

리스트 언패킹은 튜플 언패킹과 비슷하다.

>>> first, *rest=[1,2,3,4,5]>>> first1>>> rest[2, 3, 4, 5]

함수의 전달 인수로 리스트에 별(*)인수 starred argument를 사용할수도 있다.

def example_args(a,b,c):    return a * b * c #여기에서 *연산자는 곱셈이다.L=[2,3,4]a=example_args(*L) #ㄹ;ㅅ,ㅌ, 안펰;ㅇprint("a", a)b=example_args(2,*L[1:])print("b",b)

결과

a 24b 24

2.4.3 리스트 컴프리헨션

리스트 컴프리헨션(list comprehension)은 반복문의 표현식이다. (조건문을 포함할 수도 있다.) 형식은 다음과 같이 대괄호 []로 묶는다.

  • [항목 for 항목 in 반복 가능한 객체]
  • [표현식 for 항목 in 반복 가능한 객체]
  • [표현식 for 항목 in 반복 가능한 객체 if 조건문]

몇 가지 예제를 통해 리스트 컴프리헨션을 익혀보자.

  • 소스코드
  • a = [ y for y in range(1900,1940) if y%4 == 0]print("a",a)b = [2 ** i for i in range(13)]print("b",b)c = [x for x in a if x%2==0]print("c",c)d = [str(round(355/113.0,i) for i in range(1,6))]print("d",d)words = 'Buffy is awesome and vampire slayer'.split()e = [[w.upper(), w.lower(), len(w) ] for w in words] for i in e: print(i)
  • 결과
  • a [1900, 1904, 1908, 1912, 1916, 1920, 1924, 1928, 1932, 1936]b [1, 2, 4, 8, 16, 32, 64, 128, 256, 512, 1024, 2048, 4096]c [1900, 1904, 1908, 1912, 1916, 1920, 1924, 1928, 1932, 1936]d ['<generator object at 0x01700EF0>']['BUFFY', 'buffy', 5]['IS', 'is', 2]['AWESOME', 'awesome', 7]['AND', 'and', 3]['VAMPIRE', 'vampire', 7]['SLAYER', 'slayer', 6]

리스트 컴프리헨션은 단순한 경우에만 사용하는 것이 좋다. 코드 내용의 가독성을 위해서는 여러 줄의 표현식과 조건문으로 표현하는 것이 리스트 컴프리헨션 한 줄로 표현하는 것보다 나을수도 있다. 구글 파이썬 스타일 가이드에서 소개하는 리스트 컴프리헨션의 좋은 예와 나쁜 예를 참조한다. 먼저 좋은 예이다.

def test():    result = []    for x in range(10):        for y in range(5):            if x * y > 10:                result.append((x,y))                print("result",result)    for x in range(5):        for y in range(5):            if x != y:                for z in range(5):                    if y != z:                        yield (x, y, z)                        # print("x,y,z",x,y,z)    return ((x, complicated_transform(x))            for x in long_generator_function(parameter)            if x is not None)                squares = [ x * x for x in range(10)]    eat(jelly_bean for jelly_bean in jelly_beans        if jelly_bean.color == 'black')if __name__ == "__main__":    test()

다음은 나쁜 예이다.

def test2():    result = [(x,y) for x in range(10) for y in range(5) if x * y > 10]    print("result",result)    return ((x,y,z)            for x in xrange(5)            for y in xrange(5)            if x != y            for z in xrange(5)            if y != z)test2()

2.4.4 리스트 메서드 성능 측정

리스트의 메서드를 벤치마킹 테스트하여 성능을 측정해보자. 아래 테스트에서는 timeit모듈의 Timer 객체를 생성해 사용한다. Timer 객체의 첫 번째 매개 변수는 우리가 측정하고자 하는 코드이며, 두 번째 매개변수는 테스트를 위한 설정 문이다, timeit 모듈은 명령문을 정해진 횟수만큼 실행하는 데 걸리는 시간을 측정한다(기본값은 number = 1000000이다.) 테스트가 완료되면, 문장이 수행된 시간(밀리초)을 부동소수점 값으로 반환한다.

def test1():    l = []    for i in range(1000):        l = l + [i]        #print("l",l)def test2():    l = []    for i in range(1000):        l.append(i)def test3():    l = [i for i in range(1000)]def test4():    l = list(range(1000))if __name__ == "__main__":    import timeit    t1 = timeit.Timer("test1()", "from __main__ import test1")    print("concat",t1.timeit(number=1000),"milliseconds")    t2 = timeit.Timer("test2()","from __main__ import test2")    print("append", t2.timeit(number=1000),"milliseconds")    t3 = timeit.Timer("test3()", "from __main__ import test3")    print("comprehension ", t3.timeit(number=1000), "milliseconds")    t4 = timeit.Timer("test4()", "from __main__ import test4")    print("list range", t4.timeit(number=1000), "milliseconds")

결과값

concat 2.3762083 millisecondsappend 0.2712005999999998 millisecondscomprehension  0.16088440000000004 millisecondslist range 0.05822790000000033 milliseconds



역자주_ timeit()메서드로 성능을 측정할 때는 임시로 가비지 컬렉션 기능이 중지된다. 가비지 컬렉션 수행 시간도 성능 측정에 같이 포함하고 싶다면, 다음과 같이 gc.enabled()를 추가해야 한다. import gctimeit.Timer('for i in range(10): oct(i)', 'gc.enable()').timeit()

종합적으로 리스트 메서드의 시간복잡도는 다음과 같다.

n은 리스트의 총 항목 수이고, k는 연산(조회 및 추가) 항목 수다.

연산 시간복잡도
인덱스 []접근 O(1)
인덱스 할당 O(1)
append() O(1)
pop() O(1)
pop(i) O(n)
insert(i,항목) O(n)
del 연산자 O(n)
삽입 O(n)
멤버십 테스트 in O(n)
슬라이스 [x:y] 조회 O(k)
슬라이스 삭제 O(n)
슬라이스 할당 O(n+k)
reverse() O(n)
연결(concatenate) O(k)
sort() O(n log n)
곱하기 O(nk)

2.5 바이트와 바이트 배열

파이썬은 원시 바이트(raw byte)를 처리하는 데 사용할 수 있는 데이터 타입으로 불변 타입의 바이트(byte)와 가변타입의 바이트배열(bytearray) 을 제공한다. 두 타입 모두 0~255 범위의 부호 없는 8비트 정수 시퀸스로 이루어진다. 바이트 타입은 문자열 타입과 유사하며, 바이트 배열 타입은 리스트 타입과 유사하다.

blist = [1,2,3,255]the_bytes = bytes(blist)print(the_bytes)the_byte_array = bytearray(blist)print(the_byte_array)the_bytes[1]=127 #불변print(the_bytes)the_Byte_array[1]=127 #가변(mutable)print(the_byte_array)

결과

b'\x01\x02\x03\xff'bytearray(b'\x01\x02\x03\xff')Traceback (most recent call last):  File ".\test.py", line 200, in <module>    the_bytes[1]=127 #불변TypeError: 'bytes' object does not support item assignmentbytearray(b'\x01\x7f\x03\xff')

2.5.1 비트와 비트 연산자

비트 연산자는 비트로 표현된 숫자를 조작하는 데 유용하다. 예를 들어 곱셈 연산자를 사용하는 대신 비트 연산자로 곱셈을 할 수 있다. 1 << x는 숫자 1을 x번 만큼 왼쪽으로 이동(left shift) 한다는 의미로 2^x을 신속하게 계산 할 수 있다. 또한 x & (x-1)이 ()인지 확인하면, x가 2의 제곱인지 아닌지 신속하게 확인할 수 있다. 예를 들어 x가 4(=2^2)인 경우, 4는 100(2)이고 x-1인 3은 011(2)이다. 두 값에 비트 AND(&) 연산을 적용하면 0이된다. 즉, 2^x를 비트로 표현하면 100...0이 되고 2^x-1을 비트로 표현하면 011...1이 되므로, 이 두 값에 비트 AND 연산을 적용하면 0이 된다는 뜻이다.(x는 0보다 큰 정수여야 한다. )

x1 = 41 << x1print("x1",x1)x2 = 8a = x2 & (x2-1)print("a",a)x3 = 6b = x3 & (x3-1)print("b",b)

결과

x1 4a 0b 4

2.6 연습문제

2.6.1 문자열 전체 반전하기

이후 연습문제들을 다루기 위해 단순한 예제를 먼저 살펴보자. 파이썬은 다음과 같이 문자열을 쉽게 반전시킬 수 있다.

def revert(s):    if s:        s = s[-1] + revert(s[:-1])    return sdef revert2(string):    return string[::-1]if __name__ == "__main__":    str1 = "안녕 세상!"    str2 = revert(str1)    str3 = rever2(str1)    print(str2)    print(str3)    



!상세 녕안!상세 녕안

2.6.2 문자열 단어 단위로 반전하기

이번에는 문자열 내의 단어 단위로 반전해보자. 이 문제에는 해결 방법이 여러 가지 있다. 파이썬 문자열은 불변 타입이기 때문에, 리스트를 사용하여 문제를 해결하는 것을 고려해야 할 것이다. 리스트 및 문자열 메서드를 사용할 수도 있지만 포인터를 사용할 수도 있다. 포인터를 사용할 경우, 코드는 두 개의 반복문으로 구성된다. 첫 번째 반복문은 두 포인터를 사용하여 전체 문장을 반전한다. 두 번째 반복문에서는 공백을 만났을 때, 각 단어를 다시 반전한다.(즉 첫 번째 반복문에서 반전했던 단어를 원래대로 돌려놓는다). 공백은 " "또는 유니코드(u0020)로 나타낼 수 있다는 것과 마지막 단어의 공백 처리에 주의하자.

def reverser(string1, p1=0, p2=None):    if len(string1) < 2:        return string1    p2 = p2 or len(string1)-1 #p2값이 없으면 len(String1) -1 값을 넣음    print("len(string1)",len(string1))    print("p2 = p2 or len(string1)-1 ",p2)    while p1 < p2:        string1[p1], string1[p2] =  string1[p2], string1[p1]        print("string1[p1]",string1[p1])        print("string1[p2]",string1[p2])        p1 += 1        p2 -= 1    print("p1",p1)    print("p2",p2)    print("p1 = p2 > end reverser")def reversing_words_setence_logic(string1):    #먼저, 문장 전체를 반전한다.    reverser(string1)    print("reverser",string1)    p = 0    start = 0    while p < len(string1):        print("string1[p]",string1[p])        if string1[p] == u"\u0020": #u0020은 공백을 나타내는 유니코드            # 단어를 다시 반전한다.(단어를 원위치로 돌려놓는다.)            print("p",p)            print("start",start)            print("string1",string1)            reverser(string1, start, p-1)                        start = p+1        p+=1        print("start is  ",start)        print("p is  ",p)    #마지막 단어를 반전한다.(단어를 원위치로 돌려놓는다.)    reverser(string1,start,p-1)    #print(string1)    return "".join(string1)if __name__ == "__main__":    str1 = "파이썬 알고리즘 정말 재미있다."    str2 = reversing_words_setence_logic(list(str1))    #print("str2: ", str2)

결과

len(string1) 17p2 = p2 or len(string1)-1  16string1[p1] .string1[p2] 파string1[p1] 다string1[p2] 이string1[p1] 있string1[p2] 썬string1[p1] 미string1[p2]string1[p1] 재string1[p2] 알string1[p1]string1[p2] 고string1[p1] 말string1[p2] 리string1[p1] 정string1[p2] 즘p1 8p2 8p1 = p2 > end reverserreverser ['.', '다', '있', '미', '재', ' ', '말', '정', ' ', '즘', '리', '고', '알', ' ', '썬', '이', '파']string1[p] .start is   0p is   1string1[p] 다start is   0p is   2string1[p] 있start is   0p is   3string1[p] 미start is   0p is   4string1[p] 재start is   0p is   5string1[p]p 5start 0string1 ['.', '다', '있', '미', '재', ' ', '말', '정', ' ', '즘', '리', '고', '알', ' ', '썬', '이', '파']len(string1) 17p2 = p2 or len(string1)-1  4string1[p1] 재string1[p2] .string1[p1] 미string1[p2] 다p1 2p2 2p1 = p2 > end reverserstart is   6p is   6string1[p] 말start is   6p is   7string1[p] 정start is   6p is   8string1[p]p 8start 6string1 ['재', '미', '있', '다', '.', ' ', '말', '정', ' ', '즘', '리', '고', '알', ' ', '썬', '이', '파']len(string1) 17p2 = p2 or len(string1)-1  7string1[p1] 정string1[p2] 말p1 7p2 6p1 = p2 > end reverserstart is   9p is   9string1[p] 즘start is   9p is   10string1[p] 리start is   9p is   11string1[p] 고start is   9p is   12string1[p] 알start is   9p is   13string1[p]  p 13start 9string1 ['재', '미', '있', '다', '.', ' ', '정', '말', ' ', '즘', '리', '고', '알', ' ', '썬', '이', '파']len(string1) 17p2 = p2 or len(string1)-1  12string1[p1] 알string1[p2] 즘string1[p1] 고string1[p2] 리p1 11p2 10p1 = p2 > end reverserstart is   14p is   14string1[p] 썬start is   14p is   15string1[p] 이start is   14p is   16string1[p] 파start is   14p is   17len(string1) 17p2 = p2 or len(string1)-1  16string1[p1] 파string1[p2] 썬p1 15p2 15p1 = p2 > end reverser



재미있다. 정말 알고리즘 파이썬

다음은 하나의 반복문만 사용하는 방법이다. 단어 단위로 나눠 리스트에 추가한 후, 리스트를 반전한다.

def reverse_words_brute(string):    word, sentence = [], []    for character in string:        if character != " ":            word.append(character)            print("word",word)        else:            # 조건문에서 빈 리스트는 False다. 여러 공백이 있는 경우, 조건문을 건너뛴다.            if word:                sentence.append("".join(word))                            word = []            print("sentence",sentence)    #마지막 단어가 있다면, 문장에 추가한다.    if word !="":        sentence.append("".join(word))    sentence.reverse()    print("sentence.reverse",sentence)    return " ".join(sentence)if __name__ == "__main__":    str1 = "파이썬 알고리즘 정말 재미있다."    str2 = reverse_words_brute(str1)    print(str2)        

결과

word ['파']word ['파', '이']word ['파', '이', '썬']sentence ['파이썬']word ['알']word ['알', '고']word ['알', '고', '리']word ['알', '고', '리', '즘']sentence ['파이썬', '알고리즘']word ['정']word ['정', '말']sentence ['파이썬', '알고리즘', '정말']word ['재']word ['재', '미']word ['재', '미', '있']word ['재', '미', '있', '다']word ['재', '미', '있', '다', '.']sentence.reverse ['재미있다.', '정말', '알고리즘', '파이썬']재미있다. 정말 알고리즘 파이썬

문자열을 공백으로 구분해서 리스트를 생성한 다음, 슬라이스를 사용할 수도 있다.

def reversing_words_slice(word):    new_word = []    words = word.split(" ")    print("words[::-1]",words[::-1])    for word in words[::-1]:               print("word",word)        new_word.append(word)    print("new_word",new_word)    b = "".join(new_word)    print("b",b)    return " ".join(new_word)    if __name__ == "__main__":    str1 = "파이썬 알고리즘 정말 재미있다."    str2 = reversing_words_slice(str1)    print(str2)



words[::-1] ['재미있다.', '정말', '알고리즘', '파이썬']word 재미있다.word 정말word 알고리즘word 파이썬new_word ['재미있다.', '정말', '알고리즘', '파이썬']b 재미있다.정말알고리즘파이썬재미있다. 정말 알고리즘 파이썬

아예 반복문 없이 리스트와 문자열 메서드만으로 해결할 수도 있다.

def reversing_words(str1):    words = str1.split(" ")    print("words",words)    rev_set = " ".join(reversed(words))    return rev_setdef reversing_words2(str1):    words = str1.split(" ")    print("words",words)    words.reverse()    return " ".join(words)if __name__ == "__main__":    str1 = "파이썬 알고리즘 정말 재미있다"    str2 = reversing_words(str1)    str3 = reversing_words2(str1)    print(str2)    print(str3)

결과

words ['파이썬', '알고리즘', '정말', '재미있다']words ['파이썬', '알고리즘', '정말', '재미있다']재미있다 정말 알고리즘 파이썬재미있다 정말 알고리즘 파이썬

이 문제를 조금 더 확장하면 ! ? ; - . 등의 기호를 구분자로 사용하는 코드도 만들 수 있을 것이다.

2.6.3 단순 문자열 압축

다음은 문자열 aabcccccaaa를 a2b1c5a3 같은 형식으로 압축하는 예제다.

def str_compression(s):    count, last = 1, ""    list_aux = []    for i, c in enumerate(s):        #반복문 사용 시 몇 번째 반복문인지 확인이 필요할 수 있습니다. 이때 사용합니다.        #인덱스 번호와 컬렉션의 원소를 tuple형태로 반환합니다.        print("i",i)        print("c",c)        print("last",last)        print("count",count)        if last == c:            count += 1        else:            if i != 0:                list_aux.append(str(count))            list_aux.append(c)            print("list_aux",list_aux)            count = 1            last = c    list_aux.append(str(count))    print("list_aux",list_aux)    return "".join(list_aux)if __name__ == "__main__":    result = str_compression("aabcccccaaa")    print(result)

결과

i 0c alastcount 1list_aux ['a']i 1c alast acount 1i 2c blast acount 2list_aux ['a', '2', 'b']i 3c clast bcount 1list_aux ['a', '2', 'b', '1', 'c']i 4c clast ccount 1i 5c clast ccount 2i 6c clast ccount 3i 7c clast ccount 4i 8c alast ccount 5list_aux ['a', '2', 'b', '1', 'c', '5', 'a']i 9c alast acount 1i 10c alast acount 2list_aux ['a', '2', 'b', '1', 'c', '5', 'a', '3']a2b1c5a3

2.6.4 문자열 순열

순열(permutation) 은 서로 다른 n개 중 r개를 골라 순서를 고려해 나열한 경우의 수이다. 입력으로 들어오는 길이 n의 문자열에서 n개 문자를 모두 선택하는 경우의 문자열을 나열해보자. perm2()함수에서는 itertools모듈을 사용했다.

import itertools def perm(s):    if len(s) < 2:        return s    res = []    for i , c in enumerate(s):        print("i",i)        print("c",c)        for cc in perm(s[:i] + s[i+1:]):            print("cc",cc)            print("perm(s[:i])",perm(s[:i]))            print("perm(s[i+1:])",perm(s[i+1:]))            res.append(c + cc)     return resdef perm2(s):    res = itertools.permutations(s)    return ["".join(i) for i in res]if __name__ == "__main__":    val = "012"    print(perm(val))    print(perm2(val))

결과

i 0c 0i 0c 1cc 2perm(s[:i]) perm(s[i+1:]) 2i 1c 2cc 1perm(s[:i]) 1perm(s[i+1:])cc 12perm(s[:i])i 0c 1cc 2perm(s[:i]) perm(s[i+1:]) 2i 1c 2cc 1perm(s[:i]) 1perm(s[i+1:])perm(s[i+1:]) ['12', '21']cc 21perm(s[:i]) i 0c 1cc 2perm(s[:i]) perm(s[i+1:]) 2i 1c 2cc 1perm(s[:i]) 1perm(s[i+1:])perm(s[i+1:]) ['12', '21']i 1c 1i 0c 0cc 2perm(s[:i]) perm(s[i+1:]) 2i 1c 2cc 0perm(s[:i]) 0perm(s[i+1:])cc 02perm(s[:i]) 0perm(s[i+1:]) 2cc 20perm(s[:i]) 0perm(s[i+1:]) 2i 2c 2i 0c 0cc 1perm(s[:i]) perm(s[i+1:]) 1i 1c 1cc 0perm(s[:i]) 0perm(s[i+1:])cc 01i 0c 0cc 1perm(s[:i])perm(s[i+1:]) 1i 1c 1cc 0perm(s[:i]) 0perm(s[i+1:])perm(s[:i]) ['01', '10']perm(s[i+1:])cc 10i 0c 0cc 1perm(s[:i])perm(s[i+1:]) 1i 1c 1cc 0perm(s[:i]) 0perm(s[i+1:])perm(s[:i]) ['01', '10']perm(s[i+1:])['012', '021', '102', '120', '201', '210']['012', '021', '102', '120', '201', '210']

이해를 쉽게 하기 위해서 인덱스와 입력값을 표로 살펴보자. 입력값이 01인 경우 perm()함수의 경과를 분석해보면 다음과 같다.

s[i] = c s[:i] + s[i+1:]=cc c + cc
s[0] = 0 s[:0] + s[1:] = 1 01
s[1] = 1 s[:1] + s[2:] = 0 10

s[i] = c s[:i] + s[i+1:]=cc c + cc
s[0] = 0 s[:0] + s[1:] = 12
c | cc | c+cc
s[0]=1 | 2 | 12
s[1]=2 | 1 | 21 012
021
s[1] = 1 s[:1] + s[2:] = 02
c |cc |c+cc
s[0]=0 |2 |02
s[1]=2 |0 |20 102
120
s[2] = 2 s[:2] + s[3:] = 01
c |cc |c+cc
s[0]=0 |1 |01
s[1]=1 |0 |10 201
210

012의 3개 문자에서 3개를 나열하는 순열의 경우의 수는 3P3 = 3 X 2 X 1 = 6이다. nPn = n!이므로 시간복잡도 역시 O(n!)이다.

위 코드를 응용하면 다음과 같이 입력 길이 n일 때 n이하의 수에 대해서도 모든 순열의 경우를 나열할 수 있다.

def combinations(s):    if len(s) < 2:         return s    res = []    for i ,c in enumerate(s):        res.append(c) #추가된 부분        print("res",res)        for j in combinations(s[:i] + s[i+1:]):            res.append(c + j)            print("combination res",res)    return resif __name__ == "__main__":    result = combinations("abc")    print('result',result)

결과

res ['a']res ['b']combination res ['b', 'bc']res ['b', 'bc', 'c']combination res ['b', 'bc', 'c', 'cb']combination res ['a', 'ab']combination res ['a', 'ab', 'abc']combination res ['a', 'ab', 'abc', 'ac']combination res ['a', 'ab', 'abc', 'ac', 'acb']res ['a', 'ab', 'abc', 'ac', 'acb', 'b']res ['a']combination res ['a', 'ac']res ['a', 'ac', 'c']combination res ['a', 'ac', 'c', 'ca']combination res ['a', 'ab', 'abc', 'ac', 'acb', 'b', 'ba']combination res ['a', 'ab', 'abc', 'ac', 'acb', 'b', 'ba', 'bac']combination res ['a', 'ab', 'abc', 'ac', 'acb', 'b', 'ba', 'bac', 'bc']combination res ['a', 'ab', 'abc', 'ac', 'acb', 'b', 'ba', 'bac', 'bc', 'bca']res ['a', 'ab', 'abc', 'ac', 'acb', 'b', 'ba', 'bac', 'bc', 'bca', 'c']res ['a']combination res ['a', 'ab']res ['a', 'ab', 'b']combination res ['a', 'ab', 'b', 'ba']combination res ['a', 'ab', 'abc', 'ac', 'acb', 'b', 'ba', 'bac', 'bc', 'bca', 'c', 'ca']combination res ['a', 'ab', 'abc', 'ac', 'acb', 'b', 'ba', 'bac', 'bc', 'bca', 'c', 'ca', 'cab']combination res ['a', 'ab', 'abc', 'ac', 'acb', 'b', 'ba', 'bac', 'bc', 'bca', 'c', 'ca', 'cab', 'cb']combination res ['a', 'ab', 'abc', 'ac', 'acb', 'b', 'ba', 'bac', 'bc', 'bca', 'c', 'ca', 'cab', 'cb', 'cba']result ['a', 'ab', 'abc', 'ac', 'acb', 'b', 'ba', 'bac', 'bc', 'bca', 'c', 'ca', 'cab', 'cb', 'cba']

참고로 순열은 순서를 고려하여 나열하는 반면, 조합(combination)은 순열에서 순서를 고려하지 않는다. 앞에서 본 파이썬 itertools 모듈은 순열뿐만 아니라 조합을 계산하는 함수도 제공한다. 대략 어떻게 구현되어 있는지는 문서를 참조한다.

순열: https://docs.python.org/3/library/itertools.html#iter-tools.permutations

조합: https://docs.python.org/3/library/itertools.html#iter-tools.combinations

2.6.5 회문

회문(palindrome)이란 앞에서부터 읽으나 뒤에서부터 읽으나 동일한 단어나 구를 뜻한다. 어떤 문자열이 회문인지 확인하는 코드를 작성해보자. 공백은 무시하며, 세가지 버전으로 구현했다.

def is_palindrome(s):
    l = s.split(" ")
    s2 = "".join(l)
    return s2 == s2[::-1]

def is_palindrome2(s):
    l = len(s)
    f, b = 0, l-1
    while f < l //2:
        while s[f] == " ":
            f+=1
            while s[b]== " ":
                b-=1
            if s[f] != s[b]:
                return False
            f+=1
            b-=1
        return True

def is_palindrome3(s):
    s = s.strip()
    if len(s) < 2:
        return True
    if s[0] == s[-1]:
        return is_palindrome(s[1:-1])
    else:
        return False

if __name__ == "__main__":
    str1 = "다시 합창합시다"
    str2 = ""
    str3 = "hello"
    print(is_palindrome(str1))
    print(is_palindrome(str2))
    print(is_palindrome(str3))
    print()

    print(is_palindrome2(str1))
    print(is_palindrome2(str2))
    print(is_palindrome2(str3))
    print()


    print(is_palindrome3(str1))
    print(is_palindrome3(str2))
    print(is_palindrome3(str3))
    print()

회문의 예제 문장은 위키백과에서 참조할수 있다.

역자주_https://ko.wikipedia.org/wiki/회문

1.파이썬 자료구조와 알고리즘

**미아 스타인 지음**


CHAPTER 01 숫자

목차

숫자

부동소수점

복소수

fraction 모듈

decimal 모듈

2진수 , 8진수, 16진수

연습문제

넘파이 패키지

1.1 정수


파이썬에서 정수는 int로 나타내며 불변(immutable)형이다. 불변형 객체는 변수와 객체 참조간에 차이가 없다. 파이썬 정수 크기는 컴퓨터 메모리에 의해 제한된다(C또는 자바 내장컴파일러에 따라 다르다.) 파이썬 정수크기는 적어도 32비트(4바이트)이다. 정수를 나타내는 데 필요한 바이트 수를 확인하려면 파이썬 3.1 이상에서 (정수).bit_length() 메서드를 사용할 수 있다.

(999).bit_length()

= 10

어떤 문자열을 정수로 변환(casting)하거나, 다른 진법의 문자열을 정수(10진법)로 변환하려면 int(문자열, 밑) 메서드를 사용한다.

>>> s = '11'
>>> d = int(s)
>>> print(d)
11

>>> b = int(s,2)
>>> print(b)
3

int 메서드의 밑은 2에서 36사이의 선택적 인수(optional argument)다. 문자열 s에서 밑범위의 숫자를 벗어나는 값을 입력한다면, int 메서드에서 ValueError 예외가 발생한다. 예를 들어 s='12'에 대해 실행하면 예외가 발생한다.

1.2 부동 소수점


IEEE 754는 전기 전자 기술자 협회(IEEE)에서 개발한, 컴퓨터에서 부동소수점을 표현하는 가장 널리 쓰이는 표준이다. 파이썬에서 부동소수점은 float로 나타내며 불변형이다. 단정도(single precision)방식에서 32비트 부동소수점을 나타낼 때 1비트는 부호(sign, 1:양수, 0:음수), 23비트는 유효숫자 자릿수(significant digits ) 혹은 가수(mantissa) , 8비트는 지수(exponent)다.

예를 들어 십진수 -118.625(10)을 32비트 단정도로 표현해보자. 먼저 숫자의 절댓값을 이진수로 변환한다. 부호는 음수이므로 1이다.

118 / 2 = 59 > 0
59  / 2 = 29 > 1
29  / 2 = 14 > 1
14  / 2 = 7  > 1
7   / 2 = 3  > 1
3   / 2 = 1  > 1
0.625 * 2 = 1.250
0.250 * 2 = 0.500
0.500 * 2 = 1.000

즉 1110110.101(2)이다.

그 다음 변환된 이진수를 다음과 같이 정규화한다.(소수점을 왼쪽으로 이동시켜 왼쪽 부분에 1만 남게 한다.)

1110110.101(2) = 1.110110101(2) * 2^6

위의 숫자를 가수부(23비트)에 넣는다. 부족한 자릿수는 0으로 채운다.

11011010100000000000000

지수는 6이므로 바이어스(bias)를 더한다. 즉, 지수6에 127(0111 1111(2))을 더한다. 6(10) + 127(10) = 133(10) = 10000101(2)

이상의 결과를 종합하여 -118.625(10)을 32비트 단정도로 표현한 결과는 다음과 같다.

부호| 지수 | 가수 |

1 | 10000101 | 11011010100000000000000|

배정도(double precision)방식에서는 64비트 부동소수점을 나타낼 때 1비트는 부호, 52비트는 가수, 11비트는 지수로 나타낸다.(단정도 방식일 때 127 (2^7 -1)을 더하고, 배정도 방식일 떄 1023(2^10 -1)을 더한다.)

1.2.1 부동소수점끼리 비교하기


부동소수점은 이진수 분수(binary fraction)로 표현되기 때문에 함부로 비교하거나 빼면 안된다. 2진수는 대개 10진법으로 정확하게 표현할 수 있지만, 2진법으로 표현하기 어려운 숫자도 있다.

(0.1(10) = 0.00110011001100---(2)). 다음과 같은 예를 보자.

Type "help", "copyright", "credits" or "license" for more information.
>>> 0.2 * 3 == 0.6
False
>>> 1.2 - 0.2 == 1.0
True
>>> 1.2 - 0.1 == 1.1
False
>>> 0.1 * 0.1 == 0.01
False

위의 방식을 해결하기 위해서는 그럼 어떤 방법이 있을까??

`decimal.Decimal`, `math.fsum()`, `round()`, `float.as_integer_ratio()`, `math.is_close()` 함수 혹은 다른 방법을 통해서 실수를 방지할 수 있다. 이 중 가장 추천하는 방법은 `decimal` 표준 라이브러리를 사용한 방법이고 그 외에도 존재하는 관련 함수들을 소개한다.

>>> import decimal

>>> decimal.Decimal('0.2') * 3 == decimal.Decimal('0.6')
True

>>> decimal.Decimal('1.2') - decimal.Decimal('0.1')  == decimal.Decimal('1.1')
True

>>> decimal.Decimal('0.1') * decimal.Decimal('0.1')  == decimal.Decimal('0.01')
True

위와같이 decimal을 활용해서 해결할 수 있다.

그 대신 동등성 테스트(equality test는 사전에 정의된 정밀도 범위 내에서 수행되어야 한다. 예를 들어 unittest 모듈의 assertAlmostEqual() 메서드 같은 접근법을 사용하는 방법이 있다.

>>> def(x,y,places=7):
    return round(abs(x-y), places) == 0
    

또한 부동소수점의 숫자는 메모리에서 비트 패턴으로 비교할 수 있다. 먼저 부호비교를 별도로 처리한다. 두 숫자가 음수이면, 부호를 뒤집고, 숫자를 반전하여 비교한다. 지수 패턴이 같으면 가수를 비교한다.

1.2.2 정수와 부동소수점 메서드


파이썬에서 나누기 연산자 / 는 항상 부동소수점을 반환한다. // 연산자(floor 또는 truncation)를 사용하면

정수를 반환할 수 있다. %연산자(module or remainder)는 나머지를 구한다. divmod(x,y) 메서드는 x를 y로 나눌 때, 몫과 나머지를 반환한다.

divmod(45,6)
45를 6으로 나누면?

>> (7,3)  [몫 : 7 | 나머지 : 3]

round(x,n) 메서드는 n이 음수인 경우, x를 n만큼 반올림한 값을 반환한다. n이 양수인 경우, x를 소수점 이하 n자리로 반올림한 값을 반환한다.

>>> round(100.96,-2)
100.0
>>> round(100.96, 2)
100.96

as_integer_ratio() 메서드는 부동소수점을 분수로 표현한다.

>>> 2.75.as_integer_ratio()
(11, 4)

1.4 fraction 모듈


파이썬에서 분수를 다루려면 fraction 모듈을 사용한다. 다음 코드 예제를 살펴보자.

from fractions import Fraction

def rounding_floats(number1, places):
    return round(number1, places)

def float_to_fractions(number):
    return Fraction(*number.as_integer_ratio())

def get_denominator(number1, number2):
    """ 분모를 반환한다."""
    a = Fraction(number1, number2)
    return a.denominator

def get_numerator(number1,number2):
    """ 분자를 반환한다."""
    a = Fraction(number1, number2)
    return a.numerator

def test_testing_floats():
    number1 = 1.25
    number2 = 1
    number3 = -1
    number4 = 5/4
    number6 = 6
    assert(rounding_floats(number1,number2) == 1.2)
    assert(rounding_floats(number1*10, number3) == 10)
    assert(float_to_fractions(number1) == number4)
    assert(get_denominator(number2, number6) == number6)
    assert(get_numerator(number2, number6) == number2)
    print("테스트 통과!")


if __name__ == "__main__":
    test_testing_floats()
테스트 통과 !

1.5 decimal 모듈


정확한 10진법의 부동소수점 숫자가 필요한 경우, 부동소수점 불변 타입인 decimal.Decimal을 사용할 수 있다. Decimal()메서드는 정수 또는 문자열을 인수로 취한다(파이썬3.1부터 사용할 수 있으며, decimal.Decimal.from_float()메서드로 부동소수점을 decimal.Decimal 타입으로 나타낼 수도 있다.) 이 모듈은 부동소수점의 반올림, 비교, 뺼셈등에서 나타나는 문제를 효율적으로 처리할 수 있다.

>>> sum(0.1 for i in range(10)) == 1.0
False
>>> from decimal import Decimal
>>> sum(Decimal("0.1") for i in range(10)) == Decimal("1.0")
True

decimal 모듈에는 Decimal.exp(x) 같은 내장 함수가 있어 대부분의 경우에 사용할 수 있다.

여기서 x는 decimal.Decimal 객체 타입이다. math와 cmath 모듈에도 exp() 함수가 있지만, 정확도가 필요하다면 decimal 모듈을 사용해야한다.

1.6 2진수, 8진수, 16진수


bin(i) 메서드는 정수i의 2진수 문자열을 반환한다.

>>> bin(999)
'0b1111100111'

oct(i) 메서드는 정수i의 8진수 문자열을 반환한다.

>>> oct(999)
'0o1747'

hex(i) 메서드는 정수i의 16진수 문자열을 반환한다.

>>> hex(999)
'0x3e7'

1.7 연습문제


이 책에서는 연습문제와 해답 코드를 바로 제시하지만, 실력 향상을 위해서는 해답 코드를 바로 읽기 전에 직접 작성해보는 것을 권장한다.

1.7.1 진법 변환

진법을 변환하는 함수를 직접 만들어보자. 다음 코드는 다른 진법의 숫자를 10진수로 변환한다.(2<= base <= 10)

1장 숫자/ 2_convert_to_decimal.py

def convert_to_decimal(number, base):
    multiplier, result = 1, 0
    #multiplier = 1 , result=0
    while number > 0:
        result += number % 10 * multiplier # % = 나머지 
        #result = 1 one
        #result = 1 two
        #result = 1 three
        #result = 9 four
        multiplier *= base
        #base = 2
        #multiplier = 2 one
        #multiplier = 4  two
        #multiplier = 8 three
        #multiplier = 16 four
        number = number // 10 #  // = 몫
        #number = 100
        #number = 10
        #number = 1
        #number = 0

        
    print('multiplier',multiplier)
    print("result",result)
    return result

def test_convert_to_decimal():
    number , base = 1001, 2
    assert(convert_to_decimal(number,base) == 9)
    print("테스트 통과!")

if __name__ == "__main__":
    test_convert_to_decimal()

위 코드의 핵심 부분은 % 연산자다. 반복문을 돌면서 number % 10(base)로 일의 자리 숫자를 하나씩 가져와서 계산한다. 그다음 반복문의 계산을 위해서 number = number // 10을 사용한다. 이와 같은 방법으로 10진수를 다른 진법의 숫자로 변환하는 함수도 만들어보자.

1장_숫자/ 3_convert_from_Decimal.py

def convert_to_decimal(number, base):
    multiplier , result = 1,0

    while number > 0:
        result += number % 2 * multiplier
        #result = result + namerge 
        #result = 10^number + namerge
        # result = 1 
        # result = 1 + 0
        # result = 1 + 0
        # result = 1 +  1 * multi = 1001
        multiplier *= 10
        #multiplier = 10
        #multiplier = 100
        #multiplier = 1000
        #multiplier = 10000
        number = number // 2
        #number = 4
        #number = 2
        #number = 1
        #number = 0

    return result



def test_convert_to_decimal():
    number , base = 9 ,2
    assert(convert_to_decimal(number,base) == 1001)
    print("test 통과!!")




if __name__ == "__main__":
   test_convert_to_decimal()
    

--참고--

산술 연산자 (Arithmetic Operators):

a = 10, b = 20, c = 3 이라 가정한다.

OperatorDescriptionExample

+ 더하기 a + b = 30
- 빼기 a - b = -10
* 곱하기 a * b = 200
/ 나누기 b / a = 2.0
% 나머지 b % a = 0
** 제곱 a ** c = 1000
// a // c = 3

1장_숫자/4_convert_from_Decimal_larger_bases.py


def convert_to_decimal_larger_bases(number,base):
    strings = "0123456789ABCDEFGHIJ"
    result = ""
    while number > 0:
        digit = number % base
        print("digit",digit)
        result = strings[digit] + result
        print("result",result)
        number = number // base
        print("number",number)
    return number

def test_convert_from_decimal_larger_bases():
    number, base = 31, 16
    assert(convert_to_decimal_larger_bases(number, base) == "1F")
    print("테스트 통과!")

if __name__ == "__main__":
        test_convert_from_decimal_larger_bases()
출력값digit 15result Fnumber 1...digit 1result 1Fnumber 0
테스트 통과!

마지막으로 다음 코드는 재귀 함수(recursive function를 사용한 진법 변환이다. 재귀 알고리즘에 대한 내용은 8.2 재귀 알고리즘을 참조한다.)

def convert_dec_to_any_base_rec(number, base):    convertString = "0123456789ABCDEF"    print("number",number)    print("base",base)       if number < base:        print("if")        return convertString[number]    else:        print("else")        return convert_dec_to_any_base_rec(number // base, base)\            + convertString[number % base]       def test_convert_dec_to_any_base_rec():    number = 9     base = 2    assert(convert_dec_to_any_base_rec(number,base) == "1001")    print("테스트 통과!")if __name__ == "__main__":    test_convert_dec_to_any_base_rec()
출력결과number 9base 2elsenumber 4base 2elsenumber 2base 2elsenumber 1base 2if테스트 통과!
풀이과정return convert_dec_to_any_base_rec(4,2) + '1'return convert_dec_to_any_base_rec(2,2) + '0' + '1'return convert_dec_to_any_base_rec(1,2) + '0' + '0' + '1'return constring[1] + '0' + '0' + '1'

1.7.2 최대공약수

다음 코드는 두 정수의 최대공약수 greatest common divisor(GCD)를 계산한다.

1장_숫자/6_finding_gcd.py

def finding_gcd(a, b):    while (b != 0):        result = b        a, b = b, a % b    return resultdef test_finding_gcd():    number1 = 21    number2 = 12    assert(finding_gcd(number1, number2) == 3)    print("테스트 통과")if __name__ == "__main__":    test_finding_gcd()
출력 결과a 12b 9result 12a 9b 3result 9a 3b 0result 3테스트 통과

1.7.3 random 모듈

난수를 생성하는 random모듈의 예제 코드를 살펴보자. 난수이므로 출력 결과는 실행 때마다 다르다.

1장_숫자/7_testing_random.py

import randomdef testing_random():    """ random 모듈 테스트"""    values = [1,2,3,4]    print(random.choice(values))    print(random.choice(values))    print(random.choice(values))    print(random.sample(values,2))    print(random.sample(values,3))    """ values 리스트를 섞는다."""    random.shuffle(values)    print(values)    """0~10의 임의의 정수를 생성한다. """    print(random.randint(0,10))    print(random.randint(0,10))if __name__ == "__main__":    testing_random()

출력결과

root@DESKTOP-JPGB0S5:/mnt/d/Mastering-Python-Design-Patterns-Second-Edition/algo# python3 7_testing_random.py434[4, 3][2, 4, 1][2, 4, 3, 1]04

1.7.4 피보나치 수열

피보나치 수열은 첫째 및 둘째 항이 1이며, 그 이후의 모든 항은 바로 앞 두 항의 합인 수열이다.

1 1 2 3 5 8 13 21

다음 코드는 피보나치 수열에서 세 가지 다른 방법으로 n번째 숫자를 찾는다. 재귀 호출을 사용하는 find_fibonacci_seq_rec()함수의 시간복잡도는 O(2n)이고, 반복문을 사용하는 find_fibonacci_seq_iter()함수의 시간복잡도는 O(n)이다. 수식을 사용하는 find_fibonacci_seq_from함수의 시간복잡도는 O(1)이다. (단 70번째 이상 결과에서 정확하지 않다.)

1장_숫자/8_find_fibonacci_seq.py

import mathdef find_fibonacci_seq_iter(n):    if n < 2:return n     a, b = 0, 1    for i in range(n):        a, b = b, a+b    return adef find_fibonacci_seq_rec(n):    if n < 2: return n    return find_fibonacci_seq_rec(n - 1) + find_fibonacci_seq_rec(n - 2)def find_fibonacci_seq_form(n):    sq5 = math.sqrt(5)    phi = ( 1 + sq5) / 2    return int(math.floor(phi ** n / sq5))    #sqrt(x) x의 제곱근을 반환합니다.    #floor 함수는 실수를 입력하면내림하여 정수를 반환하는 함수이다.     #floor은 바닥을 의미한다. 실수의 바닥의 수인 바로 아래 정수로 내림def test_find_fib():    n = 10    assert(find_fibonacci_seq_rec(n) == 55)    assert(find_fibonacci_seq_iter(n) == 55)    assert(find_fibonacci_seq_form(n) == 55)    print("테스트 통과!!")if __name__ == "__main__":    test_find_fib()
테스트 통과!

또한 다음과 같이 제너레이터(generator)를 사용하여 피보나치 수열을 구할 수도 있다. 제너레이터는 파이썬의 시퀸스를 생성하는 객체다. 제너레이터는 파이썬의 시퀸스를 생성하는 객체다. 제너레이터를 이용하며, 전체 시퀸스를 한 번에 메모리에 생성하고 정렬할 필요 없이, 잠재적으로 아주 큰 시퀸스를 순회할 수 있다. 제너레이터를 순회할 때마다 마지막으로 호출된 요소를 기억하고 다음 값을 반환한다. 제너레이터 함수는 yield문을 사용한다. yield문에 대한 내용은 4.2.4 return 대 yield를 참조한다.

1장_숫자/9_find_fibonacci_by_generator.py

def fib_generator():    a, b = 0, 1    while True:        yield b        a, b = b, a+bif __name__ == "__main__":    fg = fib_generator()    for _ in range(10):        print(next(fg), end=" ")
1 1 2 3 5 8 13 21 34 55
NOTE_ 시간 복잡도에 대한 내용은 8장에서 간단히 살펴본다. 여기서 간단히 위키백과 설명을 일부 옮기자면 다음과 같다.(http://ko.wikipedia.org/wiki/시간_복잡도). 이하 이 책의 모든 박스는 옮긴이가 추가한 것이다. 컴퓨터과학에서 알고리즘의 시간복잡도는  입력을 나타내는 문자열 길이의 함수로서 작동하는 알고리즘을 취해 시간을 정량화하는 것이다. 알고리즘의 시간복잡도는 주로 빅-오 표기법을 사용하여 나타내며, 이 빅-오 표기법은 개수와 낮은 차수의 항을 제외시키는 방법이다. 이런 방식으로 표현할 때, (예를 들어 입력크기를 무한대로 입력하여) 시간복잡도를 점근적으로 묘사한다고 말한다. 예시로서, 만약 크기 n의 모든 입력에 대한 알고리즘에 필요한 시간이 최대(어떤 n0보다 크지 않은 모든n에 대하여) 5n^3 + 3n의 식을 가진다면, 이 알고리즘의 점근적 시간복잡도는 O(n^3)이라고 할 수 있다.

1.7.5 소수

다음 예제에서는 세 가지 다른 방법으로 한 숫자가 소수(prime number)인지 판단한다. 소수란 자신보다 작은 두 개의 자연수를 곱하여 만들 수 없는 1보다 큰 자연수다. 즉, 약수로 1과 자기 자신만을 가지는 수다. 첫 번째 함수는 브루트 포스(brute force, 즉 무차별 대입 방법을 사용한다.)

두 번째 함수는 제곱근을 이용한다. m = 루트 n, m*n = n이라고 가정한다. n이 소수가 아니면, n = a * b이므로 m * m = n이라고 가정한다. n이 소수가 아니면, n = a* b이므로 m*m = a*b이다. m은 실수, n,a,b 는 자연수다. 그러면 다음과 같은 세 가지 경우가 있을 수 있따.

1) a > m 이면, b < m2) a = m 이면 b=m3) a<m 이면, b>m

세 가지 경우 모두 min(a,b) <= m이다. 따라서 m까지의 수를 검색한다면 적어도 하나의 n과 나누어 떨어지는 수를 발견할 것이고, 이것은 n이 소수가 아님을 보여주기에 충분하다.


1장_숫자/10_finding_prime.py -이해필요

import mathimport randomdef finding_prime(number):    num = abs(number)    if num < 4 : return True    for x in range(2,num):        if num % x == 0:            return False    return Truedef finding_prime_sqrt(number):    num = abs(number)    if num < 4 : return True     for x in range(2, int(math.sqrt(num)) + 1):        if number % x == 0:            return False    return Truedef finding_prime_fermat(number):    if number <= 102:        for a in range(2, number):            if pow(a, number-1, number) != 1:                return False            return True     else:        for i in range(100):            a = random.randint(2,number - 1)            if pow(a, number -1, number) != 1:                return False        return Truedef test_finding_prime():    number1 = 17    number2 = 20    assert(finding_prime(number1) is True)    assert(finding_prime(number2) is False)    assert(finding_prime_sqrt(number1) is True)    assert(finding_prime_sqrt(number2) is False)    assert(finding_prime_fermat(number1) is True)    assert(finding_prime_fermat(number2) is False)    print("테스트 통과!")if __name__ == "__main__":    test_finding_prime()    

페르마의 소정리

위키백과, 우리 모두의 백과사전.

[수론](https://ko.wikipedia.org/wiki/수론)에서, **페르마의 소정리**(Fermat小定理, [영어](https://ko.wikipedia.org/wiki/영어): Fermat’s little theorem)는 어떤 수가 [소수](https://ko.wikipedia.org/wiki/소수_(수론))일 간단한 [필요 조건](https://ko.wikipedia.org/wiki/필요_조건)에 대한 정리이다. 추상적으로, [소수](https://ko.wikipedia.org/wiki/소수_(수론)) 크기의 [유한체](https://ko.wikipedia.org/wiki/유한체) 위의 [프로베니우스 사상](https://ko.wikipedia.org/wiki/프로베니우스_사상)[항등 함수](https://ko.wikipedia.org/wiki/항등_함수)임을 의미한다.

p가 소수이고 a가 정수라고 하자. 페르마의 소정리에 따르면 , 법p에서 a^p와  a는 서로 합동이다.a^p = a (mod p)위 식은 p | a일 때 자명하게 성립한다. 만약 p !| a일 경우 양변을 약분하여 다음과 같이쓸 수 있다.a^(p-1) = 1 (mod b)를 만족하면서 소수가 아닌 b를, a를 밑수로 하는 카마이클 수라고 부른다.

다음 코드는 random 모듈을 사용하여 n비트 소수를 생성한다. 3을 입력하면, 5(101(2)) 또는 7(111(2))의 결과가 나온다.

1장_숫자/11_generate_prime.py


import mathimport randomimport sysdef finding_prime_sqrt(number):    num = abs(number)    if num < 4:        return True    for x in range(2, int(math.sqrt(num) + 1)):        if number % x == 0:            return False    return True def generate_prime(number=3):    while 1:        p = random.randint(pow(2, number -2), pow(2,number-1)-1)        p = 2 * p + 1        if finding_prime_sqrt(p):            return pif __name__ == "__main__":    if len(sys.argv) < 2:        print("usage: generate_prime.py number")        sys.exit()    else:        number = int(sys.argv[1])        print(generate_prime(number))
root@DESKTOP-JPGB0S5:/mnt/d/Mastering-Python-Design-Patterns-Second-Edition/algo# python3.6 11_generate_prime.py  35 or 7

1.8 넘파이 패키지

넘파이는 파이썬 프로그래밍 언어의 확장 패키지이며, 대규모의 다차원 배열 및 행렬을 지원한다.

또한 배열 연산에 쓰이는 수학 함수 라이브러리를 제공한다.

넘파이 배열은 임의의 차원(dimension)을 가진다. 넘파이 모듈의 array 메서드를 사용하여

'시퀸스의 시퀸스(리스트 또는 튜플)'를 2차원 넘파이 배열로 생성할 수 있다.

np.array( ((11,12,13), (21,22,23), (31,32,33)) )
import numpy as np


def testing_numpy():
    """ test many features of numpy"""

ax = np.numpy([1,2,3])
ay = np.array([3,4,5])
print(ax)
print(ax * 2)
print(ax + 10)
print(np.sqrt(ax))
print(np.cos(ax))
print(ax-ay)
print(np.where(ax<2, ax, 10))


m = np.matrix([ax,ay,ax])
print(m)
print(m.T)

grid1 = np.zeros(shape=(10,10), dtype=float)
grid2 = np.ones(shape=(10,10), dtype=float)
print(grid1)
print(grid2)
print(grid1[1]+10)
print(grid2[:,2]*2)

if __name__ == "__main__":
    testing_numpy()

넘파이 배열은 파이썬 리스트보다 훨씬 더 효율적이다. 다음 테스트 결과를 살펴보자(ㄲ)

import numpy
import time

def trad_version():
    t1 = time.time()
    X = range(10000000)
    Y = range(10000000)
    Z = []
    for i in range(len(X)):
        Z.append(X[i] + Y[i])
    return time.time() - t1

def numpy_version():
    t1 = time.time()
    X = numpy.arange(10000000)
    Y = numpy.arange(10000000)
    Z = X + Y
    return time.time() - t1

if __name__ == "__main__":
    print(trad_version())
    print(numpy_version())
    
21.062912225723267 <리스트>
0.11400818824768066 <넘파이>

이 튜토리얼에서는 Go 언어 내에서 사용할 수있는 모든 기본 데이터 유형을 살펴볼 것입니다.이 튜토리얼이 끝나면 언어 내에서 사용할 수있는 다양한 유형에 익숙해져야 하며 Go 프로그램 내에서 이러한 유형을 사용할 수 있는 방법에 대해 어느 정도 이해해야 합니다.

이런 종류의 자료는 배우기에 상당히 건조하고 지루할 수 있으므로 필요한 기본 사항을 다루면서 내용을 약간 재미있게 만들고 약간 흥미롭게 만들 것입니다.

데이터 유형

따라서 시작하려면 Go 프로그래밍 언어 내에 4 가지 유형의 개별 범주가 있음을 아는 것이 중요합니다.

  • 기본 유형-이 튜토리얼에서 다룰 내용
  • 집계 유형-배열 및 구조체입니다.
  • 참조 유형-포인터와 슬라이스입니다.
  • 인터페이스 유형-표준 인터페이스입니다.

정수

우리가 다룰 첫 번째 기본 유형은Integer유형입니다.

프로그램 내에서 부호 있는 정수 나 부호 없는 정수를 사용할 수 있으며 필요한 정수의 크기를 지정할 수 있습니다. 프로그램의 메모리 사용률을 최적화하려고 한다고 상상해보십시오.특정 숫자가 특정 값을 초과하지 않을 것이라는 것을 알고 있다면 해당 값에 적합한 크기를 선택할 수 있습니다.

uint 또는int끝에 int 크기를 추가하여새 정수 변수를 만들 수 있습니다.

// all numeric types default to 0

// unsigned int with 8 bits
// Can store: 0 to 255
var myint uint8

// signed int with 8 bits
// Can store: -127 to 127
var myint int8

// unsigned int with 16 bits
var myint uint16

// signed int with 16 bits
var myint int16

// unsigned int with 32 bits
var myint uint32

// signed int with 32 bits
var myint int32

// unsigned int with 64 bits
var myint uint64

// signed int with 64 bits
var myint int64

처리 할 수있는 것보다 더 큰 값을 int에 할당하려고하면 다음과 같은 점에 유의해야합니다.

var myint int8
myint = 2500

Go 컴파일러는 프로그램 실행 또는 빌드에 실패하고 2500이 오버플로된다는 사실을 출력합니다 int8. 그러나 런타임에 정수를 오버플로하면 이상한 결과가 나타날 수 있습니다. 예를 들어,이 프로그램을 실행하고 출력을 검토하십시오.

package main

import (
"fmt"
)

func main() {
fmt.Println("Hello World")

var myint int8
for i := 0; i < 129; i++ {
    myint += 1
}
fmt.Println(myint) // prints out -127

}

유형 변환

var men uint8
men = 5
var women int16
women = 6

var people int
// this throws a compile error
people = men + women
// this handles converting to a standard format
// and is legal within your go programs
people = int(men) + int(women)

부동 소수점 숫자
다음으로 부동 소수점 수에 대해 설명합니다. 이는 float32또는 두 가지 크기로 제공되며 float64표준 int64데이터 유형에 맞지 않는 예외적으로 큰 숫자로 작업 할 수 있습니다 .

var f1 float32
var f2 float64

var maxFloat32 float32
maxFloat32 = 16777216
fmt.Println(maxFloat32 == maxFloat32+10) // you would typically expect this to return false
// it returns true
fmt.Println(maxFloat32+10) // 16777216
fmt.Println(maxFloat32+2000000) // 16777216

// converting from int to float
var myint int
myfloat := float64(myint)

// converting from float to int
var myfloat2 float64
myint2 := int(myfloat2)

부울

이제 모든 기본 숫자 데이터 유형을 다루었으므로 Go에서 사용할 수있는 다른 기본 데이터 유형으로 넘어갈 수 있습니다. 첫 번째는 bool 또는 boolean 데이터 유형입니다.

부울은 true 또는 false을 나타냅니다. Go 프로그램 중 하나에서이 기능을 어떻게 사용할 수 있는지 살펴 보겠습니다.

var amazing bool
amazing = true
if amazing {
subscribeToChannel()
}

훌륭하고 간단하지만 프로그램 내에서 약간의 부울 논리를 수행하려면 어떻게 될까요? ||그리고 &&연산자를 사용하면 가능합니다!

var isTrue bool = true
var isFalse bool = false
// AND
if isTrue && isFalse {
fmt.Println("Both Conditions need to be True")
}
// OR - not exclusive
if isTrue || isFalse {
fmt.Println("Only one condition needs to be True")
}

문자열
Go 언어 내의 문자열은 우리가 문자 조각이라고 부르는 것입니다. 다음을 사용하여 새 문자열 변수를 선언 할 수 있습니다 string.

var myName string
myName = "Elliot Forbes"

상수
상수는 Go 언어 내의 최종 기본 데이터 유형입니다. 이를 통해 프로그램 실행 과정에서 변경되지 않는 불변 값을 지정할 수 있습니다.

const meaningOfLife = 42

# python 이진수 함수 이용하여 정수를 이진수로 변경 하는 함수만들가

# 파이썬은 정수를 이진수로 바꿔주는 개꿀함수가 있습니다

 

# 바로  bin함수인데요

 

#-------------파이썬 공식문서-------

#https://docs.python.org/ko/3/library/functions.html

# bin(x)

# 정수를 《0b》 가 앞에 붙은 이진 문자열로 변환합니다. 결과는 올바른 파이썬 표현식입니다. x 가 파이썬 int 객체가 아니라면, 정수를 돌려주는 __index__() 메서드를 정의해야 합니다.

#  몇 가지 예를 들면:

 

>>>

bin(3)

'0b11'

bin(-10)

'-0b1010'



파이참에서 한번 실행해보시거나 visualstudiocode에서 test.py를 만드시고

python test.py 를 실행하시면 됩니다.

visualstudio에서는 python .\test.py 형식으로 실행됩니다.

 

a1=bin(45)

a2=bin(25)

a3=bin(23)

 

print(a1)

print(a2)

print(a3)

결과는 파이썬 실행후에 3초후에 나옵니다.

 

# 그런데 이렇게 계속 변수를 넣어서 실행하는 것은 귀찮죠?

# 입력하는 숫자를 이진수로 바꿔주는 함수를 만들어볼까요?

 

def like_bin_func():  #def로 함수정의 

    b = int(input())  #b 라는 변수에 int()함수로 감싼 input함수를 넣어줍니다 int()는 ()안에 숫자로 바꾸는 함수구요 input()은 입력값을 받는 함수입니다. 공부를 위해서는 int()와 input()함수를 구글링!

    c = bin(b) #c라는 변수에 bin()함수를 대입하고 b라는 값을 넣습니다

    print(c) #c를 출력하는 print()함수실행

 

#결과는?

like_bin_func()  #입력하는 숫자를 이진수로 변환해주는 함수실행




# 대신 결과값이 <0b>가 붙은 이진문자열로 나옵니다. 이결 없애기 위해서는??

 

# 파이썬에 format 함수가 있었네요

 

---------------------------------------------------------------------------------------

 

def like_bin_func2():

    b = int(input())

    c = bin(b)

    print(c)                 ###여기까진 위의 함수와같습니다. ob가 표현되지만 그다음부터는?

    d = format(bin(b)[2:])   ###format이라는 함수안에 bin(b)를 넣고 [2:] 파이썬에 슬라이스라는 기능을 이용해보면??

    print(d)                   

 

like_bin_func2()

 

---------------------------------------------------------------------------------------

사용된 파이썬 내장함수

python 프롬프트창을열어보세요 윈도우 창을열고 python 검색후 커맨드창이 뜨면 밑에 명령이를 테스트 해보세요

---------------------------------------------------------------------------------------

int() 

 

>>> a='324'

>>> b=int(a)

>>> b

324

>>> type(b)

<class 'int'>

 

---------------------------------------------------------------------------------------

 

input()

>>> c=input()

234

>>> c

'234'

>>> type(c)

<class 'str'>

---------------------------------------------------------------------------------------

 

format()

>>> d=234234234

>>> e=format(d[2:])

Traceback (most recent call last):

  File "<stdin>", line 1in <module>

TypeError'int' object is not subscriptable

>>> e = format(d[2:])

Traceback (most recent call last):

  File "<stdin>", line 1in <module>

TypeError'int' object is not subscriptable

>>> f=str(d)

>>> e = format(f[2:])

>>> e

'4234234'



---------------------------------------------------------------------------------------

파이썬의 개꿀함수 slice함수

list_like_bin이름은 임의로 생성한 리스트

 

list_like_bin = [1,3,4,5,6,7,8,]

list_like_bin[0:2]

list_like_bin[0]부터 list_like_bin[1] 까지 잘라 줍니다

list_like_bin[2:]

list_like_bin[0]부터 list_like_bin[1] 이후 숫자까지 출력해줍니다



0부터 4까지 출력시 파이썬 while문

 count=0       #count변수 0으로 대입
while count < 5:  # count변수가 5보다 작을때까지 반복하라
     print(count)   # count변수출력
     count += 1   # count= count + 1실행

프린트결과값 0 1 2 3 4

 

--------------------------------------

0부터 4까지 출력하되 2는 건너띌때

while,if , countinue문을 이용

count = 0   #count = 0 대입
while True:   # while문 true일때 무한반복
     if count >=5:   #count 변수가 5보다 크거나같을때 종료
          break

     if count == 2:           #2일때 1을 추가하고 continue를 만나서 count찍고 반복해서 위로 올라감
          count += 1         #count= count + 1
          continue             #continue가실행 되서 다시 반복

     print(count)              #count변수출력
     count += 1              #count=count + 1

 

프린트결과값: 0,1,3,4

+ Recent posts