본문 바로가기
프로그래밍/Python

[Python] 시퀀스 (Sequences) - (1)

by 별준 2022. 3. 12.

References

  • Fluent Python

Contents

  • Built-In Sequences
  • List Comprehension
  • Generator Expression
  • Tuples
  • Slicing

Overview

파이썬 표준 라이브러리는 C로 구현된 다양한 시퀀스(Sequence)들을 제공합니다.

  • Container sequences : 셔로 다른 자료형의 원소들을 담을 수 있는 list, tuple, collection.deque
  • Flat sequences : 단 하나의 자료형만 담을 수 있는 str, bytes, bytearray, memoryview, array.array

컨테이너 시퀀스는 객체에 대한 참조(reference)를 담고 있으며, 객체는 어떠한 자료형이라도 될 수 있지만, 플랫 시퀀스는 객체에 대한 참조 대신 자신의 메모리 공간에 각 항목의 값을 직접 담습니다. 따라서 플랫 시퀀스가 메모리를 더 적게 사용하지만, 문자, 바이트, 숫자 등 기본적인 자료형만 저장할 수 있습니다.

 

시퀀스는 다음과 같이 가변성(mutability)에 따라 분류할 수도 있습니다.

  • 가변 시퀀스(mutable sequences) : list, bytearray, array.array, collections.deque, memoryview
  • 불변 시퀀스(immutable sequences) : tuple, str, bytes

아래 그림을 살펴보면 가변 시퀀스가 불변 시퀀스의 모든 메소드를 상속하고, 또 추가 메소드를 구현하고 있다는 것을 볼 수 있습니다.

내장되어 있는 구체적인 시퀀스 타입들이 그림에서 보는 것처럼 Sequence나 MutableSequence 추상 베이스 클래스(abstract base classes; ABCs)를 실제로 상속하는 것은 아니지만, 이들은 추상 베이스 클래스에 등록된 가상 서브클래스입니다. 아래에서 보듯이 tuple과 list는 다음의 테스트를 통과합니다.

 

가변성과 불변성, 그리고 컨테이너와 플랫의 공통적인 특성을 기억해두면 파이썬의 구체적인 시퀀스 타입의 기능을 추측하는데 도움이 됩니다.

 

가장 기본적인 시퀀스 타입인 list는 가변적이고 여러 타입들을 담을 수 있습니다. 리스트에 대해서는 이미 잘 알고 있으리라 생각을 하고, 바로 리스트 컴프리헨션(list comprehension)에 대해서 살펴보겠습니다. 문법이 조금 어색하지만, 이를 제대로 알고 있으면 제너레이터 표현식(generator expression)도 쉽게 이해할 수 있습니다. 

 


List Comprehensions and Generator Expressions

리스트 컴프리헨션이나 제너레이터 표현식을 사용하면 시퀀스를 간단히 생성할 수 있습니다. 이런 문법을 사용하고 있지 않는다면, 가독성이 좋고 때로는 실행 속도도 빠른 코드를 작성하는 기회를 놓치고 있는 것이라고 할 수 있습니다. 물론 '가독성이 좋다'라는 말에 의구심이 생길 수 있지만, 저자는 그렇다고 합니다..

파이썬 프로그래머들은 간단하게 리스트 컴프리헨션을 listcomp, 제너레이터 표현식을 genexp로 표현합니다.

 

List Comprehensions and Readability

우선 아래의 두 가지 코드 중에서 어느 코드가 읽기 쉬운지 생각해보겠습니다.

1.

symbols = '$¢£¥€¤'
codes = []
for symbol in symbols:
    codes.append(ord(symbol))
print(codes)

2.

symbols = '$¢£¥€¤'
codes = [ord(symbol) for symbol in symbols]
print(codes)

파이썬에 대해 조금이라도 아는 사람이라면 1번 코드를 읽을 수 있습니다. 하지만 리스트 컴프리헨션을 알고 나면, 2번이 더 읽기 좋다는 생각이 들 수 있을텐데, 이는 의도를 명확히 보여주기 때문입니다.

 

시퀀스를 읽어서 개수를 세거나 어떤 항목을 골라내거나 합계를 구하는 등의 for 루프는 아주 다양한 일에 사용할 수 있습니다. 1번 코드에서는 for 루프를 이용해서 리스트를 만듭니다. 이와 대조적으로 리스트 컴프리헨션은 오로지 새로운 리스트를 만드는 일만 수행합니다.

 

물론 리스트 컴프리헨션을 남용하면 정말 이해하기 어려운 코드를 만들 수도 있습니다. 만약 생성된 리스트를 사용하지 않을 거라면 리스트 컴프리헨션 구문을 사요하지 말아야 합니다. 그리고 코드를 짧게 만들어야 합니다. 리스트 컴프리헨션 구문이 두 줄 이상 넘어가는 경우에는 코드를 분할하거나 for문으로 작성하는 것이 더 좋습니다. 이는 경우에 맞게 상식선에서 판단하면 됩니다. 정답은 없습니다.

 

리스트 컴프리헨션은 항목을 필터링하거나 변환함으로써 시퀀스나 iterable한 타입으로부터 리스트를 만듭니다. 아래에서 살펴볼 내장 함수인 filter()와 map() 함수를 사용해서 동일한 작업을 수행할 수 있지만, 이는 가독성이 떨어집니다.

 

Listcomps vs. map/filter

map()과 filter() 함수를 이용해서 수행할 수 있는 작업은 람다(lambda)를 사용하지 않고 리스트 컴프리헨션을 사용하여 모두 구현할 수 있습니다. 다음 코드를 살펴보겠습니다.

 

아래 코드를 실행해보면,

import timeit

TIMES = 10000

SETUP = """
symbols = '$¢£¥€¤'
def non_ascii(c):
    return c > 127
"""

def clock(label, cmd):
    res = timeit.repeat(cmd, setup=SETUP, number=TIMES)
    print(label, *('{:.3f}'.format(x) for x in res))

clock('listcomp        :', '[ord(s) for s in symbols if ord(s) > 127]')
clock('listcomp + func :', '[ord(s) for s in symbols if non_ascii(ord(s))]')
clock('filter + lambda :', 'list(filter(lambda c: c > 127, map(ord, symbols)))')
clock('filter + func   :', 'list(filter(non_ascii, map(ord, symbols)))')

리스트 컴프리헨션이 map()/filter()를 조합한 방법보다 빠르다는 것을 볼 수 있습니다.

 

Cartesian Products

리스트 컴프리헨션을 사용하면 두 개 이상의 iterable한 타입의 데카르트 곱(cartesian products)을 나타내는 일련의 리스트를 만들 수 있습니다. 데카르트 곱 안에 들어 있는 각 항목은 입력으로 받은 iterable한 데이터의 각 요소에서 만들어진 튜플로 구성됩니다. 아래 그림처럼 생성된 리스트의 길이는 입력으로 받은 데이터의 길이의 곱과 동일합니다.

 

예를 들어, 두 가지 색상과 세 가지 크기의 티셔츠 리스트를 만드는 경우를 생각해보겠습니다. 리스트 컴프리헨션으로 이를 만드는 방법은 다음과 같습니다. 그 결과, 6개의 요소가 생성됩니다.

먼저 color 다음에 size를 배치해서 만든 튜플 리스트를 생성합니다. 그리고 바로 다음의 for문을 보면, color를 반복하는 루프 안에서 size를 반복하여 튜플 리스트를 출력합니다.

위 코드는 먼저 size를 반복하고 그 안에서 color로 반복합니다.

 

리스트 컴프리헨션 안에서 줄을 바꿔서 작성하면 생성될 리스트가 어떻게 정렬되는지 알아보기 쉽습니다.

 

다음 코드는 이전 포스팅에서 살펴봤던 FrenchDeck의 구현입니다.

import collections

Card = collections.namedtuple('Card', ['rank', 'suit'])

class FrenchDeck:
    ranks = [str(n) for n in range(2, 11)] + list('JQKA')
    suits = 'spades diamonds clubs hearts'.split()
    
    def __init__(self):
        self._cards = [Card(rank, suit) for suit in self.suits
                                        for rank in self.ranks]
    def __len__(self):
        return len(self._cards)

    def __getitem__(self, position):
        return self._cards[position]

__init__() 메소드 내에서 self._cards가 리스트 컴프리헨션으로 리스트를 생성하고 있습니다. 위의 경우에는 suit를 먼저 반복하고 그 안에서 rank를 반복하게 됩니다.

 

리스트 컴프리헨션은 단지 리스트를 만들 뿐입니다. 다른 종류의 시퀀스를 채우려면 제너레이터 표현식을 사용해야 합니다.

 

Generator Expressions

튜플, 배열 등의 시퀀스 타입을 초기화하려면 리스트 컴프리헨션을 사용할 수도 있지만, 제너레이터 표현식을 사용하면 메모리를 절약할 수 있습니다. 제너레이터 표현식은 다른 생성자에 전달할 리스트를 통째로 만들지 않고 반복자 프로토콜(iterator protocol)을 이용해서 항목을 하나씩 생성(yield)하기 때문입니다.

 

제너레이터 표현식은 리스트 컴프리헨션과 동일한 구문을 사용하는데, 대괄호 대신 소괄호를 사용합니다.

다음 코드는 튜플과 배열을 생성하는 기본적인 제너레이터 표현식입니다.

symbols = '$¢£¥€¤'
print(tuple(ord(symbol) for symbol in symbols))

import array
print(array.array('I', (ord(symbol) for symbol in symbols)))

튜플을 생성할 때는 함수에 보내는 인수가 하나여서, 괄호를 생략하였습니다. 배열의 생성자는 두 개의 인수를 받기 때문에 제너레이터 표현식의 앞뒤에 반드시 괄호를 넣어주어야 합니다. 배열 생성자의 첫 번째 인수는 배열에 들어 갈 숫자들을 저장할 자료형을 지정합니다.

 

다음 코드는 데카르트 곱에 제너레이터 표현식을 사용해서 두 가지 색상과 세 가지 크기의 티셔츠 목록을 출력합니다. 앞서 본 예제와는 달리 여기서는 티셔츠 리스트의 여섯 개 항목을 메모리 안에 생성하지 않습니다. 이는 제너레이터 표현식이 한 번에 하나의 항목만 생성할 수 있도록 for 루프에 데이터를 전달하기 때문입니다. 데카르트 곱을 만들기 위해 사용할 리스트에 각각 천 개의 항목이 들어 있는 경우 제너레이터 표현식을 사용하면 단지 for 루프에 전달하기 위해 항목이 백만 개나 들어있는 리스트를 생성하는 일을 피할 수 있습니다.

colors = ['black', 'white']
sizes = ['S', 'M', 'L']
for tshirt in (f"{c} {s}" for c in colors for s in sizes):
    print(tshirt)

제너레이터 표현식은 한 번에 하나의 항목만을 생성하며, 6개의 티셔츠 종류를 담고 있는 리스트는 만들지 않습니다.

 

제너레이터가 동작하는 방식은 나중에 다루어 볼 예정입니다. 여기서는 단순히 리스트 이외의 시퀀스를 초기화하거나, 메모리에 유지할 필요가 없는 데이터를 생성하기 위해 제너레이터 표현식을 사용하는 방법만 보여줍니다.

 


Tuples Are Not Just Immutable Lists

일반적으로 파이썬 입문서에는 튜플을 불변 리스트라고만 설명하기도 합니다. 하지만 이러한 설명만으로는 부족합니다. 튜플은 불변 리스트로 사용할 수도 있지만, 필드명이 없는 레코드로 사용할 수도 있습니다. 레코드로 사용하는 경우를 간과하는 경우가 있는데, 먼저 레코드로 사용하는 경우를 살펴보겠습니다.

 

Tuples as Records

튜플은 레코드를 담고 있습니다. 튜플의 각 항목은 레코드의 필드 하나를 의미하며 항목의 위치가 그 의미를 결정합니다. 튜플을 단지 불변 리스트로만 생각한다면 경우에 따라 항목의 크기와 순서가 중요할 수도 있고 그렇지 않을 수도 있습니다. 그러나 튜플을 필드의 집합으로 사용하는 경우에는 항목 수가 고정되어 있고 항목의 순서가 중요합니다.

 

아래 코드는 튜플을 레코드로 사용하는 경우를 보여줍니다. 튜플 안에서 항목의 위치가 항목의 의미를 나타내므로 튜플을 정렬하면 정보가 파괴된다는 점에 유의해야 합니다.

lax_coordinates = (33.9425, -118.408056)
city, year, pop, chg, area = ('Tokyo', 2003, 32_450, 0.66, 8014)
traveler_ids = [('USA', '31195855'), ('BRA', 'CE342567'), ('ESP', 'XDA205856')]

for passport in sorted(traveler_ids):
    print("%s/%s" % passport)
print()
for country, _ in traveler_ids:
    print(country)

첫 번째 for문에서 passport 변수는 각 튜플에 바인딩됩니다. 그리고 % 포맷 연산자는 튜플을 이해하고 각 항목을 하나의 필드로 처리합니다.

두 번째 for문은 튜플의 각 항목을 어떻게 가져와야 하는지 알고 있습니다. 이 과정을 언패킹(unpacking)이라고 합니다. 여기서 두 번째 항목은 관심없는 정보이기 때문에 더미 변수(dummy variable)인 '_'에 할당했습니다.

 

튜플은 언패킹 메커니즘 덕분에 레코드로 아주 잘 동작하는데, 언패킹은 바로 아래에서 살펴보도록 하겠습니다.

 

Unpacking

바로 위의 예제코드에서는 단 하나의 문장으로 city, year, pop, chg, area 변수에 ('Tokyo', 2003, 32_450, 0.66, 8014)를 할당했습니다. 그리고 % 연산자는 print() 함수의 인수로 전달한 포맷 문자열의 각 슬롯에 passport 튜플의 각 항목을 할당했습니다. 이 두 가지 예시가 바로 튜플 언패킹(tuple unpacking)을 보여줍니다.

튜플 언패킹은 iterable한 객체라면 어느 객체든 적용할 수 있습니다.

 

튜플 언패킹은 병렬 할당(parallel assignment)을 할 때 가장 눈에 띕니다. 병렬 할당은 다음 코드에서 보는 것처럼 iterable한 데이터를 변수로 구성된 튜플에 할당하는 것을 말합니다.

lax_coordinates = (33.9425, -118.408056)
latitude, longitude = lax_coordinates

 

튜플 언패킹을 사용하면 임시 변수를 사용하지 않고도 두 변수의 값을 서로 교환할 수 있습니다.

a, b = b, a

 

그리고 다음과 같이 함수를 호출할 때 인수 앞에 *를 붙여 튜플을 언패킹할 수 있습니다.

print(divmod(20, 8))

t = (20, 8)
print(divmod(*t))

quotient, remainder = divmod(*t)
print(quotient, remainder)

위 코드는 튜플 언패킹의 또 다른 사용방법을 보여주는데, 함수에서 호출자에 여러 값을 간단히 반환하는 기능을 보여줍니다. 예를 들어, 아래 코드처럼 os.path.split() 함수를 이용해서 파일시스템 경로에서 경로명과 파일명을 가져올 수 있습니다.

import os
_, filename = os.path.split('/home/luciano/.ssh/idrsa.pub')
print(filename)

 

언패킹할 때 *을 사용하여 일부 항목만 사용하는 다른 방법이 있는데, 아래에서 알아보도록 하겠습니다.

 

Using * To Grab Excess Items

함수 매개변수를 *args로 정의하여 임의의 개수의 인수를 가져오는 방법은 파이썬의 기본 기능입니다. 파이썬3에서는 이 개념을 확장하여 다음과 같이 병렬 할당에도 적용할 수 있습니다.

병렬 할당의 경우에 *는 단 하나의 변수에만 적용할 수 있습니다만, 어떤 위치에서도 사용할 수 있습니다.

마지막으로 튜플 언패킹은 중첩 구조체에서도 사용할 수 있습니다.

 

Nested Tuple Unpacking

언패킹할 튜플의 표현식이 (a, b, (c, d))와 같이 중첩된 표현식이 있을 때, 중첩 구조와 매칭되는 표현식이라면 파이썬은 올바르게 처리합니다. 다음 코드는 중첩된 튜플을 언패킹하는 것을 보여줍니다.

metro_areas = [
    ('Tokyo', 'JP', 36.933, (35.689722, 139.691667)),
    ('Delhi NCR', 'IN', 21.935, (28.613889, 77.208889)),
    ('Mexico City', 'MX', 20.142, (19.433333, -99.133333)),
    ('New York-Newark', 'US', 20.104, (40.808611, -74.020386)),
    ('Sao Paulo', 'BR', 19.649, (-23.547778, -46.635833)),
]

print('{:15} | {:^9} | {:^9}'.format('', 'lat.', 'long.'))
fmt = '{:15} | {:9.4f} | {:9.4f}'
for name, cc, pop, (latitude, longitude) in metro_areas:
    if longitude <= 0:
        print(fmt.format(name, latitude, longitude))

 

이처럼 튜플은 매우 편리합니다만, 레코드로 사용하기에 약간 부족한 점이 있긴 합니다. 때로는 필드에 이름을 붙일 필요가 있는데, 이를 위해서 namedtuple() 함수가 추가되었습니다.

 

Named Tuples

collections.namedtuple() 함수는 필드명과 클래스명을 추가한 튜플의 서브클래스를 생성하는 팩토리 함수로서, 디버깅에 유용합니다.

 

지난 포스팅과 위에서 Card 클래스를 namedtuple을 사용하여 정의했었습니다.

import collections

Card = collections.namedtuple('Card', ['rank', 'suit'])

 

다음 코드에서 named tuple을 정의해서 도시에 대한 정보를 담고 있는 객체를 만드는 방법을 보여줍니다.

from collections import namedtuple
City = namedtuple('City', 'name country population coordinates')
seoul = City('Seoul', 'KR', 982.809, (37.335887, 126.584063))
print(seoul)
print(seoul.population)
print(seoul.coordinates)
print(seoul[1])

여기서 namedtuple을 정의하려면 클래스명과 필드명을 담고 있는 리스트, 총 2개의 매개변수가 필요합니다. 클래스명은 보통 이를 저장하는 변수의 이름과 동일하게 해줍니다. 필드명을 담고 있는 리스트는 문자열 리스트나 공백으로 구분된 하나의 문자열을 이용해서 지정합니다.

 

namedt tuple은 튜플에서 상속받은 속성 외에 몇 가지 속성을 더 가지고 있는데, 다음 코드는 _fileds 클래스 속성, _make(iterable) 클래스 메소드, _asdict() 객체 메소드를 보여줍니다.

 

Tuples as Immutable Lists

파이썬 인터프리터와 표준 라이브러리는 튜플을 불변 리스트로 확장시켜줍니다. 튜플을 사용하면 두 가지 이점을 얻을 수 있습니다.

  • Clarity : 튜플을 보면, 이것이 길이가 절대로 변하지 않는다는 것을 알 수 있습니다.
  • Performance : 튜플은 같은 길이의 리스트보다 메모리를 덜 사용합니다. 따라서 이를 이용해 최적화를 수행할 수 있습니다.

그러나, 튜플의 불변성은 오직 튜플 내에 포함된 레퍼런스에만 적용된다는 사실에 주의해야합니다. 튜플 내에 포함되는 레퍼런스는 삭제되거나 바뀔 수 없습니다. 그러나 이러한 참조가 mutable 객체를 가리키고, 그 객체를 변경한다면, 튜플의 값은 변경됩니다. 아래 코드는 2개의 튜플 a, b를 생성하고 똑같이 초기화합니다. 그리고 b의 마지막 항목을 변경하는데, 이렇게 되면 a와 b는 더이상 동일하지 않게 됩니다.

이렇게 변경 가능한 항목이 있는 튜플은 버그의 원인이 될 수 있습니다. 만약 튜플이 변경되지 않도록 확실하게 하려면, hash를 사용하면 됩니다. 다음 포스팅에서 딕셔너리와 집합에 대해서 알아볼 때 자세하게 살펴볼텐데, 객체는 값을 절대로 변경할 수 없을 때만 hashable합니다. 따라서, 다음과 같이 튜플이 고정된 값을 가지는지 체크하는 방법으로 사용할 수 있습니다.

 

 

이러한 버그가 생길 수 있음에도 튜플은 불변 리스트로 많이 사용됩니다. 아래 stackoverflow에는 리스트 대신 튜플을 사용할 때 얻을 수 있는 성능상의 이점에 대해서 잘 설명해주고 있습니다.

performance - Are tuples more efficient than lists in Python? - Stack Overflow

 

Are tuples more efficient than lists in Python?

Is there any performance difference between tuples and lists when it comes to instantiation and retrieval of elements?

stackoverflow.com

 

 

Tuple vs. List Methods

튜플을 불변 리스트로 사용할 때, 튜플과 리스트의 API가 얼마나 비슷한지 알고 있으면 도움이 됩니다. 아래의 표는 항목을 추가하거나 삭제하는 기능과 __reversed__() 메소드를 제외하고는 리스트가 제공하는 메소드를 모두 지원합니다. 하지만, reversed(my_tuple) 메소드는 __reversed__()를 이용하지 않을 뿐, 잘 동작합니다.

  List Tuple Descriptions
s.__add__(s2) s + s2 - 리스트를 연결함(concatenation)
s.__iadd__(s2)   s += s2 - in place로 리스트를 연결
s.append(e)   제일 뒤에 요소를 하나 추가
s.clear()   모든 항목을 삭제
s.__contains__(e) e in s
s.copy()   얕은 복사(shallow copy)
s.count(e) e가 나타나는 횟수를 계산
s.__delitem__(p)   p 위치의 요소 삭제
s.extend(it)   iterable it에 있는 요소를 추가
s.__getitem__(p) s[p] - 해당 위치의 항목을 반환
s.__getnewargs__()   pickle을 이용하여 최적화된 직렬화(serialization) 지원
s.index(e) s 안에서 e가 처음 나타나는 위치를 찾음
s.insert(p, e)   p 위치에 있는 요소 앞에 e를 삽입
s.__iter__() iterator 반환
s.__len__() len(s) - 항목의 개수를 구함
s.__mul__(n) s * n - 리스트를 반복 (repeated concatenation)
s.__imul__(n)   s *= n - in-place로 리스트를 반복, 결과를 s에 저장함
s.__rmul__(n) n * s - 역순으로 repeated concatenation 수행
s.pop([p])   마지막 항목이나 p 위치의 항목을 제거하고 반환
s.remove(e)   처음으로 e 값이 나타나는 요소를 제거
s.reverse()   in place로 항목들을 역순으로 배치
s.__reversed__()   마지막에서 첫 번째 항목까지 반복하는 iterator를 반환
s.__setitem__(p, e)   s[p] = e - e를 p 위치에 저장하고 기존 항목을 덮어씀
s.sort([key], [reverse])   옵셔널인 key와 reverse에 따라 항목을 정렬하고 s에 저장(in place)

 


Slicing

파이썬에서 제공하는 list, tuple, str, 그리고 모든 시퀀스 타입은 슬라이싱(slicing) 연산을 지원합니다. 슬라이싱 연산은 일반적으로 알고있는 것보다 더 강력합니다. 여기서는 슬라이싱의 사용법에 대해서 알아보겠습니다.

 

Why Clices and Range Exclude the Last Item

슬라이스와 range에서 마지막 항목을 제외하는 파이썬 컨벤션은 파이썬, C 등에서 사용하는 0번째 인덱스(zero-based indexing)로 동작합니다. 이러한 특징 덕분에 다음과 같은 장점이 있습니다.

  • 3개의 항목을 생성하는 range(3)이나 my_list[:3]처럼 중단점(stop position)만 이용해서 슬라이스나 범위를 지정할 때 길이를 계산하기 쉽습니다.
  • 시작점과 중단점을 모두 지정할 때도 길이를 계산하기 쉽습니다. (중단점 - 시작점)
  • 아래 코드처럼, x 인덱스를 기준으로 겹치는 부분이 없이 시퀀스를 분할하기 쉽습니다. 단순히 my_list[:x]와 my_list[x:]로 지정하면 됩니다.

 

Slice Objects

이미 잘 알고 있겠지만, s[a:b:c]는 c 스텝(or stride)만큼씩 항목을 건너뛰어 반복하도록 해줍니다. 만약 이 값이 음수인 경우에는 거꾸로 거슬러 올라가면서 항목을 반복합니다.

 

a:b:c 표기법은 인덱스 연산을 수행하는 [ ] 안에서만 사용할 수 있으며, slice(a, b, c) 객체를 생성합니다. seq[start:stop:step] 표현식을 평가하기 위해서 파이썬은 seq.__getitem__(slice(start, stop, step))을 호출합니다. 시퀀스 타입을 직접 구현하지 않더라도 슬라이스 객체를 알아두면 꽤 도움이 됩니다. 스프레드시트에서 셀 범위에 이름을 붙이는 것처럼 슬라이스 객체는 슬라이스에 이름을 붙일 수 있도록 해주기 때문입니다.

 

예를 들어, 아래 코드에서 단순 텍스트 파일로 구성된 청구서를 파싱해야 하는 경우를 생각해볼 수 있습니다. 코드에서 슬라이스를 하드코딩하는 대신 각 슬라이스에 이름을 붙일 수 있습니다. 각 슬라이스에 이름을 붙여주면 for 루프에서의 가독성이 좋아집니다.

invoice = """
0.....6.................................40........52...55........
1909  Pimoroni PiBrella                 $17.50    3   $52.50
1489  6mm Tactile Switch x20            $4.95     2   $9.90
1510  Panavise Jr. - PV-201             $28.00    1   $28.00
1601  PiTFT Mini Kit 320x240            $34.95    1   $34.95
"""

SKU = slice(0, 6)
DESCRIPTION = slice(6, 40)
UNIT_PRICE = slice(40, 52)
QUANTITY = slice(52, 55)
ITEM_TOTAL = slice(55, None)

line_items = invoice.split('\n')[2:]
for item in line_items:
    print(item[UNIT_PRICE], item[DESCRIPTION])

 

Multidimensional Slicing and Ellipsis

[] 연산자는 콤마로 구분해서 여러 개의 인덱스나 슬라이스를 가질 수 있습니다. 이러한 방법은 예를 들면, Numpy에서 a[i, j] 구문으로 2차원 numpy.ndarray 배열의 항목이나 a[m:n, k:l] 구문으로 2차원 슬라이스를 가져올 때 사용합니다. 아래에서 이러한 표기법을 사용하는 예제 코드를 살펴볼 수 있습니다. [] 연산자를 처리하는 __getitem__()과 __setitem__() 스페셜 메소드는 a[i, j]에 들어 있는 인덱스들을 튜플로 받습니다. 즉, a[i, j]를 평가하기 위해서 파이썬은 a.__getitem__((i, j))를 호출합니다.

 

파이썬에 내장된 시퀀스 타입은 1차원이므로 단 하나의 인덱스나 슬라이스만 지원하고 튜플은 지원하지 않습니다.

 

3개의 마침표(...)로 표현된 생략기호(ellipsis)는 파이썬 parser에 의해 하나의 토큰으로 인식됩니다. 이 기호는 Ellipsis 객체의 앨리어스로서 하나의 ellipsis 클래스의 객체입니다. 이 객체는 f(a, ..., z)처럼 함수의 파라미터나, a[i:...]처럼 슬라이스의 한 부분으로 전달할 수 있습니다. Numpy는 다차원 배열을 슬라이싱할 때 생략기호(...)를 사용합니다. 예를 들어 4차원 배열이라면, x[i, ...]는 x[i, :, :, :,]와 동일합니다.

 

슬라이스는 시퀀스에서 정보를 추출할 뿐만 아니라 가변 시퀀스의 값을 변경할 때도 사용할 수 있습니다.

 

Assigning to Slices

할당문의 왼쪽에 슬라이스 표기법을 사용하거나 del문의 대상 객체로 지정함으로써 가변 시퀀스를 연결하거나, 잘라 내거나, 값을 변경할 수 있습니다. 아래 코드에서 보여주는 몇 가지 예제는 이러한 표기법의 강력함을 보여줍니다.

 


 

 

다음 포스팅에 이어서,

  • 시퀀스에서의 +, * 연산자 사용
  • +=, *= 연산자 사용
  • list.sort()와 sorted() build-in 함수
  • 졍렬된 시퀀스를 bitsect로 관리하는 방법
  • array.array, memoryview 등

에 대해서 살펴보도록 하겠습니다.

[Python] 시퀀스 (Sequences) - (2)

 

댓글