본문 바로가기

비단뱀과 알/튜토리얼

[Python] 기본적인 Sequence (1) : String(문자열)과 List(리스트)

프로그래밍에서 중요한 여러가지 도구들에 대해 간단히 정리를 해보자.

 

시퀀스(Sequence)는 값들의 정렬이다. 대충 벡터 비슷한 걸 생각하면 된다.

아래의 스트링, 리스트, 튜플 등이 시퀀스의 예시다.

 

1. String, 문자열

ex='GreatHandsomeTerrapin'

type(ex)
#> str

훌륭한 스트링이다.

 

인덱스. 첫번째 글자부터 0, 1, ...이다.

ex[0]
#> 'G'

 

글자의 꼬리쪽에 붙어있는 순서는 -1, -2, ... 로 센다.

ex[-1]
#> 'n'

 

인덱스는 정수여야만 한다.

ex[5.5]
#> TypeError: string indices must be integers

 

범위 지정도 당연히 된다. '문자열 슬라이스' 라고 하는 교재도 있는 것으로 안다.

ex[0:10]
#> 'GreatHands'

ex[3:-5]
#> 'atHandsomeTer'

 

스트링을 가지고 프로그래밍을 하다 보면 이 스트링의 글자 수 자체를 얻어야 하는 경우가 많이 생긴다.

내장 함수 len을 사용하면 된다.

lyrics="Cause we are always starting over every life we are living, yes, we are always just awake every step we take"
n=len(lyrics)

print(n)
#> 108

 

브라이언 요키, 톰 킷 콤비의 뮤지컬 <IF / THEN> 중 'Always starting over' 이라는 넘버의 가사다.

2022년 12월 기준 한국 초연중이니 뮤지컬이 궁금하다면 꼭 보도록 하자. 이거 보고 내 인생 뮤지컬 갱신했다.
(TMI : 자라는 연 평균 30-40회 정도 연극/뮤지컬을 관극한다.)

 

len은 아래와 같이 써먹을 수 있다.

import math
lyrics[math.floor(n/2)]
#> 'v'

대충 한가운데의 글자를 꺼낸 느낌이다.

 

For 루프에서는 각 글자 별로 루핑한다.

for letter in ex:
    print(letter+'짜잔')
#> G짜잔
#> r짜잔
#> e짜잔
#> a짜잔
#> t짜잔
#> H짜잔
#> a짜잔
#> n짜잔
#> d짜잔
#> s짜잔
#> o짜잔
#> m짜잔
#> e짜잔
#> T짜잔
#> e짜잔
#> r짜잔
#> r짜잔
#> a짜잔
#> p짜잔
#> i짜잔
#> n짜잔

 

가장 중요한 특징으로, 한 번 만든 스트링은 수정이 안된다.

내용을 바꾸고 싶다면 새로운 스트링을 만들자.

ex[1]="T"
#> TypeError: 'str' object does not support item assignment

ex[1]=="T"
#> False

참고로 아래는 boolean expression이다. 위랑 아래는 완전히 다른 코드다.

 

ex="Terrapin"
ex
#> 'Terrapin'

내용을 수정하고 싶다면 대놓고 처음부터 새로 만들어야 한다.

즉, for 루프든 뭐든 시퀀스에서 글자 단위로 수정하는 것은 안된다는 것이다. 튜플이나 리스트를 쓰도록 하자.

 

ex가 너무 길어서 줄인 것 맞다.

 

연습삼아 인덱스( [ ] )의 역과정 함수를 만들어 보자.

 

letter(시퀀스, 알파벳)을 넣으면, 시퀀스[인덱스]==알파벳 이 되는 인덱스를 return.

 

 

def letter(sequence, letter) :
    index=0
    while index < len(sequence):
        if sequence[index]==letter:
            return index
        else:
            index += 1
    print("니가 찾는 거 없다 여기")
    
    letter(ex, 'z')
    #> 니가 찾는 거 없다 여기
    
    letter(ex, 'p')
    #> 5

그러나 실제로는 이런 코드를 우리가 짤 필요가 없다. 이미 누군가 이런 거 다 만들어 두었다.

 

1-1. 유용한 것들

dot notation으로 작동시키는 것들을 '메서드(method)' 라고 한다. 바로 아래는 find 메서드이다.

lyrics.find('always')
#> 13

i=lyrics.find('always')
lyrics[i:i+len('always')]
#> 'always'

 

find 메서드의 argument는 다음과 같다 : find('찾을 글자', '찾기를 시작할 위치', '찾기를 중단할 위치')

lyrics.find('always', 15)
#> 72

lyrics.find('always', 15, 70)
#> -1

-1은 없다는 뜻이다.

 

in은 꽤 직관적이다.

'ra' in ex
#> True

'raa' in ex
#> False

 

문자열끼리 비교할 때, 부등호는 알파벳 순서 기준이다.

ex < lyrics
#> False

ex는 'Terrapin' 이었고, lyrics는 'Cause ~' 로 시작하는 이프덴 넘버 가사였다. 

 

그 외에 .strip, .replace, .count, .islower 등의 메서드가 있다.

ex.replace('p', '  HANDSOME  ')
#> 'Terra  HANDSOME  in'

strip_sample="   handsome terrapin   "

strip_sample.strip()
#> 'handsome terrapin'

strip_sample.rstrip()
#> '   handsome terrapin'

strip_sample.lstrip()
#> 'handsome terrapin   '

strip은 스트링 양쪽 끝에 있는 공백을 한방에 없앤다.

그러나 기억하자, 시퀀스는 불변이다. 메서드로 바꾼 시퀀스는 일회용이다. 이 결과를 저장하고 싶으면 새로운 오브젝트로 지정해야 한다.

 

 

2. List, 리스트

리스트도 시퀀스다.

스트링은 문자열이었다면 리스트는 어떤 것이든 될 수 있다.

ex=['terrapin', 183, [72, 76], 0.86]
type(ex)
#> list

보는 것처럼 리스트 안에는 많은 것들이 들어올 수 있다.

 

type(ex[0])
#> str

type(ex[1])
#> int

type(ex[2])
#> list

type(ex[3])
#> float

ex 리스트는 그 원소(element, 아이템 item 이라고도 한다)에 다른 리스트를 들고 있을 수 있다. 이런 형태를 'nested list', 중첩된 리스트 라고 한다.

 

빈 리스트는 그냥 square brackets( [ ] )을 할당해주면 된다.

 

리스트는 스트링과 달리 원소 변경이 가능하다.

ex[0]="The great handsome terrapin"
ex
#> ['The great handsome terrapin', 183, [72, 76], 0.86]

ex[2:]=[72, 76, 0.86]
ex
#> ['The great handsome terrapin', 183, 72, 76, 0.86]

 

in 연산자도 역시 사용할 수 있다.

'The' in ex
#> False

183 in ex
#> True

 

리스트의 원소 변경이 가능하다는 건, 자료를 저장하고 사용하는 데 제약이 덜 하다는 것이다.

생긴 게 벡터처럼 생겨서 연산이 벡터 같겠지만 그렇지는 않다.

ex2=[0]
ex2*4
#> [0, 0, 0, 0]

ex + ex2*4
#> ['The great handsome terrapin', 183, 72, 76, 0.86, 0, 0, 0, 0]

더하기는 리스트를 이어붙인 것이다.

 

리스트도 범위 지정이 가능하다.

ex3 = ex + ex2*4

ex3[1]
#> 183

ex3[1:2]
#> [183]

ex3[-5:]
#> [0.86, 0, 0, 0, 0]

스트링도, 리스트도, 시퀀스의 인덱스는 각 element 사이를 지정한다.

즉, [1:2] 인덱스를 넣어 준 두번째 예시에서 1은 183의 앞을, 2는 183의 뒤를 지정한다.

 

그래서 [183] 이라는 리스트가 출력된 것이다.

type(ex3[1:2])
#> list

 

2-1. 쓸만한 것들

위의 더하기 기호('+')는 단순히 이어붙인 리스트를 출력해서 보여줄 뿐이지만, 기존 리스트를 수정하여 저장하는 extend 메서드가 존재한다. 거기에 오름차순으로 리스트를 정렬하는 sort 메서드까지 보자.

sample1=['c', 'a', 'b']
sample2=[2, 1]
sample1.extend(sample2)

sample1
#> ['c', 'a', 'b', 2, 1]

sample2.sort()
sample2
#> [1, 2]

sample2.sort(reverse=True)
sample2
#> [2, 1]

.extend나 .sort 등의 메서드는 기존의 리스트를 수정하는 기능을 한다. 즉, 결과를 보기 위해서는 리스트를 다시 불러야 한다.

또한 R과 달리 파이썬에서는 True, False 대신 T, F 만 적으면 못 알아먹으니 참고하자.

 

sort는 문자와 숫자가 섞이면 에러가 뜬다.

sample1.sort()
#> TypeError: '<' not supported between instances of 'int' and 'str'

당연하게도 리스트에 스트링만 있다면 알파벳 순으로 정렬된다.

 

리스트의 원소들을 뭔가 하나의 값으로 연산하는 과정을 리듀스(reduce) 라고 한다.

가장 대표적인 것으로 sum이 있다.

sum(sample2)
#> 3

 

연습삼아 한 번 sum과 같이 작동하는 코드를 손으로 짜보자.

 

 

def summation(list):
    i=0
    for element in list:
        i += element
    return i
  
 summation(sample 2)
 #> 3

 

 

원소를 추가하는 것 만큼 중요한 게 원소를 삭제하는 것이다.

sample1.pop(-1)
#> 1

sample1.pop(1)
#> 'b'

sample1
#> ['a', 'c', 2]

보다시피 이 메서드는 원본 리스트에 그대로 수정을 하기 때문에, 원본 리스트를 손상시키지 않으려면 삭제 리스트를 새로운 object로 지정해주는 게 좋다.

 

.pop 메서드를 썼을 때 출력되는 결과는 삭제된 원소다.

sample1=['c', 'a', 'b', 2, 1]
sample3=sample1.pop(1)

sample3
#> 'a'

sample1
#> ['c', 'b', 2, 1]

이 .pop 메서드는 내가 삭제한 정보를 다른 리스트에 백업할 수 있다는 장점이 있다.

그렇지만 한번에 하나씩 처리해야 하며, 삭제할 원소의 인덱스를 알아야 한다.

 

백업은 못하지만, 한번에 여러개의 원소를 삭제할 수 있는 방법으로 del 연산자가 있다. 얘는 메서드 아니므로 주의

sample1=['c', 'a', 'b', 2, 1]
del sample1[0:3]

sample1
#> [2, 1]

안타깝게도 del은 연산자라서

sample2=del sample1[0:3]
#> SyntaxError: invalid syntax

이렇다. 다른 오브젝트로 지정하고 싶으면 다른 방법을 쓰자.

 

위 둘은 인덱스를 알아야 한다. 간단한 리스트에서 인덱스를 찾는 게 많이 어려운 일은 아니지만, 그래도 원소를 특정해서 삭제하는 메서드도 있다.

sample1=['c', 'a', 'b', 2, 1]
sample1.remove('a')

sample1
#> ['c', 'b', 2, 1]

 

 

3. 스트링과 리스트

스트링과 리스트를 서로 왔다 갔다 할 수 있으면 좋은 일이 많이 생길 것 같다.

일단 먼저, 가장 간단한 방법으로 내장함수 "list()" 를 이용하는 방법이 있다.

handsome="terrapin"
t=list(handsome)
t
#> ['t', 'e', 'r', 'r', 'a', 'p', 'i', 'n']

 

여러가지 구분자를 쓰고 싶다면 .split(구분자) 메서드를 쓰면 좋다.

Default 구분자는 공백(' ')으로 설정되어 있다.

위에서 지정한 이프덴 넘버 가사의 스트링, lyrics를 가져와서 보자.

l=lyrics.split()
l
#> ['Cause', 'we', 'are', 'always', 'starting', 'over', 'every', 'life', 'we', 'are', 'living,', 'yes,', 'we', 'are', 'always', 'just', 'awake', 'every', 'step', 'we', 'take']

 

다른 구분자는 그냥 지정해주면 된다.

ME="The-Great-Handsome-Gorgeous-Terrapin"
m=ME.split('-')
m
#> ['The', 'Great', 'Handsome', 'Gorgeous', 'Terrapin']

그러니까 그냥 데이터 형식 보고 잘 넣어주면 된다.

 

나눴으면 붙이는 것도 할 줄 알아야 인지상정이겠다.

delimiter='-'
me=delimiter.join(m)
me
#> 'The-Great-Handsome-Gorgeous-Terrapin'

자, delimiter(i. e. 구분자)를 string으로 만들고 .join 메서드를 사용한 뒤, argument로 리스트를 넣어주는 서순,

즉 join 메서드로 이어 붙이는 구분자는 그냥 글자 그 자체여도 된다는 뜻이다.

 

ifthen=" ".join(l)
ifthen
#> 'Cause we are always starting over every life we are living, yes, we are always just awake every step we take'

꼭 구분자를 객체 지정해줘야 하는 건 아니다.

 

 

4. 객체 참조

마지막으로 객체 참조의 레벨을 살펴보자.

무슨 소리냐면,

 

a="terrapin"
b="terrapin"

이 a와 b는 똑같은 놈이다.

a is b
#> True

즉 얘네 둘은 같은 객체(object)를 참조했다는 건데, string의 경우는 몇 개의 변수를 지정하든 같은 객체를 참조한다.

 

앞서 적어놨듯 스트링은 불변이니까, "세상에 서로 다른, 같은 단어가 있다" 라는 문장은 조금 이상하지 않은가. 그냥 그 뜻이다.

 

반면에, 수정이 가능한 리스트는 각각 다른 객체를 참조한다.

a=list("terrapin")
b=list("terrapin")

a is b
#> False

여기서의 a와 b는 똑같이 생긴 다른 놈이다.

즉, 지금 상태에서는 a와 b가 똑같이 생겼지만, 여기서 a를 수정한다고 해도 b의 모양에는 변함이 없다.

 

a
#> ['t', 'e', 'r', 'r', 'a', 'p', 'i', 'n']
b
#> ['t', 'e', 'r', 'r', 'a', 'p', 'i', 'n']

del a[0:4]

a
#> ['a', 'p', 'i', 'n']
b
#> ['t', 'e', 'r', 'r', 'a', 'p', 'i', 'n']

 

이해를 위해, 같은 객체를 참조하는 두 리스트를 가져와 보자.

c=[0, 1, 2, 3, 4, 5]
d=c
d
#> [0, 1, 2, 3, 4, 5]

c is d
#> True

위의 a is b는 F였는데 얘는 T다.

d를 생성한 방식이 c를 복제해서 그렇다.

아무튼 c와 d는 같은 객체를 참조한다.

 

del c[0:2]
c
#> [4, 5]

d
#> [4, 5]

d에는 아무것도 안했는데, 뿌리가 같은 c를 수정하니 d의 값도 바뀌었다.

뭐 이 때 이 객체, 리스트 그 자체가 두가지 이름을 가지니, 이 오브젝트가 별칭이 있다(aliased) 라고 하기도 한다고.

 

근데 뭐 단어 자체가 중요한 건 아니고 이 객체 구조를 이해하는 게 중요한 것 같다.

리스트를 만질 때는 항상 원래 있던 오브젝트를 참조하는 것인지, 새로운 오브젝트를 만드는 것인지 뇌정지하지 말고 정신을 똑바로 차리는 걸로.

 

적당한 예시로는 append 메서드와 + 기호의 차이가 있다.

l1=[0,1,2]
l2=l1.append(3)

l1
#> [0, 1, 2, 3]
l2
#>

.append 메서드는 원래 있던 리스트는 수정하지만 빈 함수이다. return 값이 없어서 l2에서 아무것도 안나온다.

즉, append 메서드는 기존의 오브젝트를 참조하는 메서드라고 할 수 있겠다.

 

반면,

l1=[0,1,2]
l2=l1 + [3]

l1
#> [0, 1, 2]
l2
#> [0, 1, 2, 3]

여기서는 기존의 l1이 변경되지 않고, 새로 지정한 l2에 새로운 리스트가 추가되었다.

즉 이 둘은 서로 다른 오브젝트를 참조한다. 이제 l2를 수정해도 l1에는 변화가 없을 것이다.

 

l2.remove(0)

l2
#> [1, 2, 3]
l1
#> [0, 1, 2]

 

아 피곤해.