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 <넘파이>

 

작가: 표도르 도스토예프스키

출판사 민음사
시리즈 세계문학전집 154,155,156

카라마조프가의 형제들 1권부터 3권까지 읽으면서..

처음 1권을 읽으면서는 " 어, 이건 뭐지? 왜 이런 복선들을 깔아놓지, 왜 종교 얘기가 이렇게 많이 나올까?" 하는 생각으로 읽었고 단어마다 한자어가 많아서 검색 하면서 읽느라 시간이 꽤 걸렸고 처음에는 재미도 많이 없었다. 그러나 점점 읽으면서 왜 등장인물들의 성격을 그렇게 자세하게 묘사했는지, "왜 인물마다 행동들을 저렇게 묘사함으로써 저 인물은 저럴것이다. "라는 인상을 남겼는지 이해를 하면서 읽었다. 물론 사람이란건 시간이 지나면서 변하기도 하고 안 변하고 그대로인 사람도 있지만 보통은 그 사람의 행동을 보면 어느정도 성격이 예상되기 때문에 작가는 그것을 생각해서 인물들을 묘사한것 같았다. 초반에 세 형제 각각의 인물의 성격과 행동들을 자세하게 설명했고, 또 그 아버지의 성격도 자세하게 설명했다. </br>

그 아버지란 사람은 엽기적이기도 하고 인간 본성을 많이 표현한것 같기도 하다는 생각도 했다. 1권에서 머리에 남은 부분은 그 아버지란 사람과 첫째아들이 같은 여자를 두고 연적이 된것, 그리고 조시마 장로란 사람이 수도원에서 가족끼리 모였을 때 첫째 형 앞에 절을 했던 것들이 충격적이라서 기억에 남는다.</br>

그 외에 조시마 장로란 사람이 기존 기독교나 종교에서 가지는 고정관념을 넘어서서 정말로 종교들이 지향해야할 이상들을, 고정관념이나 과거 규칙을 깨고 보여준다는 것이 인상 깊었다. 2권부터는 막장드라마 보는 느낌이 있었지만 그 안에서 종교와 심리학에 대한 얘기가 많이 나왔고 각 상황별로 "인간이라면 저럴 수 있겠다.", "저 사람이면 왜 저렇게 말했는지 이해가 된다" 같은 생각을 많이 하면서 읽었다. 특히 그 그루셴카라는 아버지와 첫째 아들이 좋아했던 그 여자도 처음에는 이해가 안 됐지만 읽다보니 저럴수도 있겠구나 했고, 둘째 아들의 성격도 점점 이해가 됐다. 그리고 3권은 정말 충격적인 장면도 많았고 특히 범인은 그 하인이 스메르쟈코프였다는 점이 충격적이었고, 그 하인의 성장배경, 성격, 그리고 여러 정황들까지도 이해가 됐다. </br>

그리고 둘째아들이 그 탐욕스런 아버지와 성격이 비슷하고, 똑같다고 했던 주장에 대해서도, 신을 믿지않는 그런 점이나, 자신의 욕망을 가장 우선적으로 생각하고 행동한다는 점까지 읽으면서는 점점 이해가 됐다. </br>

그리고 첫째 아들도 방탕하게 살아왔고, 그 하인인 노인을 때리고 도망친것도 잘못 했지만 그 외에 그 동안 그가 말했던 것들, 행동하던 것들, 그것이 한꺼번에 부메랑으로 돌아와서 감옥에 간 것도 인상깊었다. 등장인물 한명,한명 마다 실제 세상에 있는 사람들의 삶을 녹여냈고, 그들이 말하고 행동하는 것을 보여주면서 각자의 인생에 대해 , 독자들은 자신의 인생을 잘 살고 있는것인지, 정말 옳은 방향으로 살고있는것인지 이런 질문들을 작가가 던진것만 같았다. </br>

그리고 신에 대해서 단순히 어렸을 때 교회에서 성경을 읽고 깊게 생각하지 않았을때는 신은 존재하지도 않고 필요도 없다고 생각했었는데, 이 책을 읽으면서 세상이라는 곳에 신이라는 추상적 개념이 있어서 그나마 도덕이 지켜지는 부분도 있고 사람들이 옳고 나쁜것에 대한 기준들을 생각하고 그들의 삶에 영향을 크게 미치므로 설령 신이 정말 없다고 해도 그런 역활을 할 존재는 사람들 마음속에 있어야 할 것 같다고 생각했다. 카라마조프가의 형제들 각자의 인생이나 , 줄거리를 말하자면 끝도 없을것 같다. </br>

너무도 많은 인간 감정들이 녹아있고, 부모자식 관계, 형제 관계, 배 다른 형제 관계, 연인 관계, 질투,증오,분노,선량하고 긍정적인 관계, 멘토 관계 등 굉장히 많은 인간 관계도 녹아있다. </br>

그리고 맨 마지막에 기억에 남는 장면들 중 하나는 그 변호사와 검사가 논쟁을 하는 부분도 인상 깊었다. 검사는 기존 러시아 세력을 상징하는, 고지식하고, 고정관념 가득하고, 편견이 있는 그런 세력이었고, 변호사는 사실을 제대로 파악하려고 하며, 어떤 선입견도 없는 새로운 개혁세력 같은 느낌을 받았다. 그러면서 변호사가 아버지와 아들의 관계에 대해 말을 하는것도 꽤 인상깊었다. 그 첫째 아들의 어린시절 성격, 그리고 성장하면서 아버지와 겪은 갈등, 세상을 살면서 변하게된 그 성격과 아버지란 존재가 아들에게 얼마나 영향을 주는지, 그런 장면들을 말했을때는 나도 법원에 앉았던 사람들처럼 마음의 박수를 보냈다. </br>

그리고 두번째로 기억에 남은 장면은 그 일류세치카라는 아이의 장례식에서 알료샤와 그 친구들이 장례식을 해주는 장면에서는 "밀알 하나가 떨어지면 하나가 남고, 그 밀알이 죽으면 많은 열매가 맺힌다"는 그 책에 있는 글귀처럼 그 글귀를 실제 미래로 보여주었으며, 아이들과 알료샤가 함께 손을 잡고 있던 장면은 작가가 바라는 미래의 모습을 나타낸것 같았다. </br>

나중에야 알았지만 그 알료샤라는 아이의 이름은 실제 작가의 아들 이름이었고 그 아들은 어린 나이에 죽었다는 그 작가의 인생에 대해서도 읽었다. 정말 이 작가도 파란만장한 삶을 살았구나 하면서 이 책을 마무리했다. 나중에 다시 이 책을 읽고나서는 또 다른 시야와 관점이 보이고 다양한 내 인생경험과 어우러져서 다른 느낌을 받을수는 있겠지만 첫번째 독서에서 느꼈던 점은 이정도인것 같다.</br>

"끝"

'기타 > 독서' 카테고리의 다른 글

프랭클린 자서전 독후감  (0) 2022.01.08
제로투원을 읽고나서..  (0) 2022.01.08
사장을 위한 회계  (0) 2022.01.08
연세대 필독도서 추천  (0) 2021.03.17
정약용 그얼마나 좋을까 (1)  (0) 2021.03.17

+ Recent posts