슬라이싱(slicing)
시퀀스(리스트, 튜플, 문자열 등)의 일부분을 잘라내어 새로운 시퀀스를 만드는 방법이다. 슬라이싱을 사용하면 코드를 더 간결하고 읽기 쉽게 만들 수 있으며, 데이터의 부분 집합을 효율적으로 처리할 수 있다.
슬라이싱 기본 문법
슬라이싱은 대괄호([]) 안에 시작 인덱스:끝 인덱스:간격 형태로 표현된다.
- 시작 인덱스는 포함되고,
- 끝 인덱스는 포함되지 않습니다.
- 간격은 선택적이며, 각 요소 사이의 간격을 정의합니다.
numbers = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
# 인덱스 2부터 5까지 요소를 슬라이싱 (5는 포함되지 않음)
print(numbers[2:5]) # [2, 3, 4]
# 리스트의 처음부터 인덱스 5까지 요소를 슬라이싱 (5는 포함되지 않음)
print(numbers[:5]) # [0, 1, 2, 3, 4]
# 인덱스 5부터 리스트 끝까지 요소를 슬라이싱
print(numbers[5:]) # [5, 6, 7, 8, 9]
# 전체 리스트를 슬라이싱 (복사)
print(numbers[:]) # [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
# 간격(스텝)을 2로 설정하여 인덱스 0부터 2씩 증가하며 슬라이싱
print(numbers[::2]) # [0, 2, 4, 6, 8]
# 리스트를 역순으로 슬라이싱
print(numbers[::-1]) # [9, 8, 7, 6, 5, 4, 3, 2, 1, 0]
# 문자열 슬라이싱
greeting = "Hello, World!"
print(greeting[7:12]) # World
# 튜플 슬라이싱
tuple_data = (0, 1, 2, 3, 4, 5)
print(tuple_data[2:4]) # (2, 3)
슬라이스 객체 생성
파이썬에서 슬라이스 객체를 사용하면 슬라이스 연산을 하나의 객체로 정의하고, 이를 여러 번 재사용할 수 있다. 이 방식은 특정 슬라이스 연산에 "이름"을 붙이는 것과 비슷하며, 코드의 가독성과 유지 보수성을 향상시킬 수 있다.
my_slice = slice(1, 5, 2) # 인덱스 1부터 5까지, 간격 2로 요소를 선택
numbers = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
result = numbers[my_slice]
print(result) # [1, 3]
# 동일한 슬라이스를 다른 리스트에 적용
more_numbers = [10, 11, 12, 13, 14, 15, 16, 17, 18, 19]
another_result = more_numbers[my_slice]
print(another_result) # [11, 13]
# 문자열에 적용
text = "Hello, World!"
text_slice = text[my_slice]
print(text_slice) # "el"
다차원 슬라이싱
다차원 슬라이싱과 생략 기호는 주로 다차원 배열을 다루는 라이브러리인 NumPy에서 사용되는 기법이다. 파이썬의 기본 자료구조인 리스트에서는 다차원 슬라이싱을 직접 지원하지 않지만, NumPy와 같은 라이브러리를 통해 이러한 기능을 활용할 수 있다.
import numpy as np
# 4x4 배열 생성
arr = np.array([[ 1, 2, 3, 4],
[ 5, 6, 7, 8],
[ 9, 10, 11, 12],
[13, 14, 15, 16]])
# 배열의 첫 번째 및 두 번째 행과, 두 번째 및 세 번째 열을 슬라이싱
sub_array = arr[0:2, 1:3]
print(sub_array)
# [[2 3]
# [6 7]]
생략 기호 (Ellipsis)
생략 기호는 다차원 배열에서 한 개 이상의 차원을 슬라이싱하지 않고 전체를 선택할 때 사용된다. Python에서 생략 기호는 **...**로 표현되며, 특히 다차원에서 중간 차원을 건너뛸 때 유용다.
# 3x4x5 배열 생성
arr = np.random.randint(1, 100, (3, 4, 5))
# 첫 번째 차원의 첫 번째 요소, 중간 차원 전체, 마지막 차원의 첫 번째 요소 슬라이싱
# 3차원 배열의 첫 번째 "페이지"에서 각 행의 첫 번째 열을 선택한다. ...는 배열의 모든 행을 포함하도록 한다.
slice_result = arr[0, ..., 0]
print(slice_result)
슬라이스 할당
슬라이스에 값을 할당하는 것은 파이썬에서 리스트 같은 가변 시퀀스를 수정하는 강력한 방법이다. 이 기능을 사용하면 리스트의 일부분을 한 번에 교체하거나 업데이트할 수 있으며, 코드의 복잡성을 줄이고, 실행 시간을 단축할 수 있다.
슬라이스에 할당하기 위해서는 슬라이스된 부분에 새로운 시퀀스를 할당한다. 이 때 주의할 점은 할당하는 값이 반드시 반복 가능한(iterable) 객체여야 한다는 것이다.
numbers = [0, 1, 2, 3, 4, 5]
numbers[2:4] = [9, 9] # 인덱스 2부터 3까지의 요소를 [9, 9]로 변경
print(numbers) # [0, 1, 9, 9, 4, 5] 출력
numbers = [0, 1, 2, 3, 4, 5]
numbers[2:4] = [] # 인덱스 2부터 3까지의 요소를 삭제
print(numbers) # [0, 1, 4, 5] 출력
numbers = [0, 1, 2, 3, 4, 5]
numbers[2:2] = [8, 9] # 인덱스 2의 위치에 [8, 9] 삽입
print(numbers) # [0, 1, 8, 9, 2, 3, 4, 5] 출력
numbers = [0, 1, 2, 3, 4, 5]
del numbers[1:4] # 인덱스 1부터 3까지의 요소를 삭제
print(numbers) # [0, 4, 5] 출력
슬라이스 할당의 유의 사항
- 할당되는 값은 반드시 반복 가능한(iterable) 객체여야 한다.
- 할당하는 시퀀스의 길이와 슬라이스된 부분의 길이가 같을 필요는 없다. 할당하는 데이터의 길이에 따라 리스트의 길이가 자동으로 조절된다.
시퀀스에 덧셈, 곱셈 연산자 사용하기
리스트에 대한 덧셈(+)과 곱셈(*) 연산자를 사용할 때 주의해야 할 몇 가지 위험성이 있다. 이들 연산자는 편리하게 사용될 수 있지만, 특히 리스트의 리스트를 생성할 때는 예상치 못한 결과를 초래할 수 있다. 리스트에 곱셈 연산자를 사용하면 리스트의 요소들을 반복하여 새로운 리스트를 만든다. 하지만, 이 때 객체의 참조가 반복될 뿐, 객체 자체가 복사되지는 않는다. 이는 리스트 안에 리스트와 같은 변경 가능한(mutable) 객체가 포함된 경우 문제를 일으킬 수 있다.
list1 = [1, 2, 3]
list2 = [4, 5, 6]
combined_list = list1 + list2 # [1, 2, 3, 4, 5, 6]
rows = 3
cols = 2
wrong_matrix = [[0] * cols] * rows # 이 방법은 문제를 일으킬 수 있음
wrong_matrix[0][0] = 1
print(wrong_matrix) # 출력: [[1, 0], [1, 0], [1, 0]]
# 올바른 방법
correct_matrix = [[0] * cols for _ in range(rows)]
correct_matrix[0][0] = 1
print(correct_matrix) # 출력: [[1, 0], [0, 0], [0, 0]]
파이썬에서 리스트를 다룰 때, 특히 리스트의 리스트를 만들 때는 객체 참조에 대한 이해가 필요하다. 곱셈 연산자(*)를 사용하여 리스트의 리스트를 생성할 때는 각 요소가 서로 독립적이지 않다는 점을 기억해야 한다. 올바른 방법은 컴프리헨션을 사용하는 것으로, 이를 통해 각 요소에 대한 새로운 리스트를 생성할 수 있다. 이는 의도치 않은 부작용을 방지하고 코드의 정확성을 보장한다.
시퀀스 복합 할당
파이썬에서 복합 할당 연산자는 시퀀스와 다른 타입의 데이터를 함께 사용할 때 효율적이고 읽기 쉬운 코드를 작성할 수 있도록 돕는다. 복합 할당 연산자는 할당(=)과 다른 연산자를 결합한 것으로, 연산과 할당을 한 줄에서 처리할 수 있다. 시퀀스에 자주 사용되는 복합 할당 연산자에는 +=와 *=가 있다.
*= 연산자는 시퀀스를 반복 확장할 때 사용된다. 주어진 시퀀스를 지정된 횟수만큼 반복하여 확장한다. 주의할 점은, 이 연산자를 사용할 때 내부의 객체들은 복사되지 않고 참조만 반복된다는 것이다. 따라서, 리스트의 리스트 같은 구조에서는 예기치 않은 문제를 일으킬 수 있다.
numbers = [1, 2, 3]
numbers += [4, 5] # 리스트에 [4, 5] 추가
print(numbers) # 출력: [1, 2, 3, 4, 5]
numbers = [1, 2, 3]
numbers *= 2 # 리스트를 2번 반복
print(numbers) # 출력: [1, 2, 3, 1, 2, 3]
*= 연산자를 사용할 때는, 복합 객체에 포함된 모든 객체들이 같은 참조를 공유하게 된다. 따라서 리스트 안의 리스트를 반복할 경우, 모든 내부 리스트가 동일한 참조를 가리키게 되므로 한 리스트를 변경하면 다른 모든 리스트에도 같은 변경이 적용된다.
sort() vs sorted()
파이썬에서 리스트를 정렬하는 데 사용할 수 있는 두 가지 주요 방법이 있다. list.sort() 메소드와 sorted() 내장 함수이다. 두 방법 모두 유사하게 작동하지만, 중요한 차이점이 있어 사용 목적에 따라 적합한 방법을 선택할 수 있다.
list.sort() 메소드
list.sort()는 리스트 자체를 정렬한다. 이 메소드는 원본 리스트를 정렬하며, 반환 값은 None이다. 즉, list.sort()를 호출하면 원본 리스트가 변경되며, 별도의 정렬된 리스트를 반환하지 않는다.
numbers = [3, 1, 4, 1, 5, 9, 2, 6]
numbers.sort()
print(numbers) # [1, 1, 2, 3, 4, 5, 6, 9]
sorted() 내장 함수
sorted() 함수는 어떤 이터러블(리스트, 딕셔너리, 튜플 등)도 받아서 정렬된 새 리스트를 반환한다. 원본 데이터는 변경되지 않는다. 이 함수는 더 유연하며, 함수형 프로그래밍 스타일에 적합하다.
numbers = [3, 1, 4, 1, 5, 9, 2, 6]
sorted_numbers = sorted(numbers)
print(sorted_numbers) # [1, 1, 2, 3, 4, 5, 6, 9]
print(numbers) # [3, 1, 4, 1, 5, 9, 2, 6] 원본은 변경되지 않음
매개변수 key와 reverse
두 메소드 모두 key와 reverse 매개변수를 사용할 수 있다. key 매개변수는 각 요소에 대해 호출할 함수를 지정하며, 이 함수의 반환 값에 따라 요소들이 정렬된다. reverse는 불리언 값을 받으며, True로 설정하면 요소들이 내림차순으로 정렬된다.
words = ["banana", "apple", "cherry"]
sorted_words = sorted(words, key=len) # 길이에 따라 정렬
print(sorted_words) # ['apple', 'banana', 'cherry']
bisect 모듈
bisect 모듈은 파이썬에서 이진 검색 알고리즘(binary search algorithm)을 제공하여, 정렬된 시퀀스에서 요소의 삽입 위치를 효율적으로 찾을 수 있도록 한다. 이진 검색은 정렬된 데이터에 대해 매우 효율적인 검색 방법으로, 대상 시퀀스를 반복적으로 반으로 나누어 탐색 범위를 좁혀가는 방식으로 작동한다. 이 과정을 통해, 데이터의 크기가 커져도 검색 시간이 로그 시간 복잡도(O(logn))를 유지할 수 있어, 매우 빠른 성능을 제공다.
bisect 모듈의 주요 함수
- bisect.bisect_left(list, item):
- 이 함수는 리스트에 들어갈 item의 삽입 위치를 찾는다. 리스트 내에 item과 같은 값이 이미 있을 경우, 그 값의 왼쪽 위치를 반환한다. 이 위치는 item을 삽입한 후 리스트가 정렬된 상태를 유지하게 해준다.
- bisect.bisect_right(list, item) 또는 bisect.bisect(list, item):
- bisect_right는 bisect_left와 유사하지만, item과 동일한 값이 리스트에 있을 경우 해당 값들의 오른쪽 끝 위치를 반환한다. bisect 함수는 bisect_right와 동일하게 동작한다.
- bisect.insort_left(list, item):
- 이 함수는 item을 적절한 위치에 삽입한다. 만약 item과 같은 값이 리스트에 이미 있다면, 그 값들의 왼쪽에 item을 삽입한다.
- bisect.insort_right(list, item) 또는 bisect.insort(list, item):
- insort_right는 item을 리스트에 삽입하되, item과 동일한 값이 있다면 그 오른쪽에 삽입한다. insort 함수는 insort_right와 동일하게 동작한다.
import bisect
numbers = [10, 20, 30, 40, 50]
# 적절한 위치에 25 삽입
bisect.insort(numbers, 25)
print(numbers) # 출력: [10, 20, 25, 30, 40, 50]
# 30의 삽입 위치를 찾기
position = bisect.bisect_left(numbers, 30)
print(position) # 출력: 3
# 45를 삽입하되, 동일 값의 오른쪽에 삽입
bisect.insort_right(numbers, 40)
print(numbers) # 출력: [10, 20, 25, 30, 40, 40, 50]
bisect 모듈은 정렬된 리스트에 대한 빠른 삽입과 검색 작업을 제공하여, 데이터를 효율적으로 관리할 수 있도록 돕는다. 특히 정렬된 데이터 구조를 유지하면서 동적으로 데이터를 추가할 때 매우 유용하다. 이 모듈을 사용하면 정렬 연산을 수행하지 않고도 요소를 적절한 위치에 빠르게 삽입할 수 있어, 성능을 크게 향상시킬 수 있다.
파이썬의 배열 사용
파이썬에서 배열을 사용하려면 array 모듈을 불러와서 배열 객체를 생성해야 한다. 이 모듈은 고정된 타입의 배열을 지원하여, 리스트에 비해 더 적은 메모리를 사용하고 기본적인 배열 연산을 더 빠르게 수행할 수 있다. array.array 클래스는 배열을 생성할 때 타입 코드를 지정해야 하며, 이 코드는 배열이 저장할 데이터의 타입을 결정한다.
import array
# 실수(float) 타입의 배열 생성
float_array = array.array('f', [1.0, 1.5, 2.0, 2.5])
# 배열 요소 출력
for element in float_array:
print(element)
# 배열 요소에 접근
print(float_array[1]) # 출력: 1.5
# 배열 요소 변경
float_array[1] = 1.6
print(float_array[1]) # 출력: 1.6
배열 타입 코드
array.array에서 사용할 수 있는 몇 가지 타입 코드는 다음과 같다:
- 'b' : signed char
- 'B' : unsigned char
- 'u' : Unicode character
- 'h' : signed short
- 'H' : unsigned short
- 'i' : signed int
- 'I' : unsigned int
- 'l' : signed long
- 'L' : unsigned long
- 'f' : float
- 'd' : double
배열과 리스트의 차이점
- 타입 안정성: 배열은 모든 요소가 동일한 타입을 갖도록 강제한다. 이는 배열을 더 효율적으로 만들고, 데이터 처리 속도를 빠르게 한다.
- 성능: 배열은 저장 공간을 더 효율적으로 사용하고, 특히 큰 데이터 세트와 수치 계산에서 성능 이점을 제공한다.
- 용도: 배열은 주로 수치 데이터 처리에 사용된다. 리스트는 다양한 타입의 데이터를 하나의 컬렉션에 저장할 때 더 유연하게 사용할 수 있다.
메모리 뷰
메모리 뷰(memory view)는 파이썬의 내장 데이터 구조로, 객체의 메모리 버퍼(buffer)를 "보기(view)"로서 다룰 수 있게 해준다. 이는 버퍼 프로토콜을 지원하는 어떠한 객체의 메모리를 저수준에서 접근할 수 있도록 도와준다. 메모리 뷰는 데이터를 복사하지 않고도 버퍼의 내용을 다룰 수 있게 해서, 큰 데이터 집합을 효율적으로 처리할 수 있도록 한다.
메모리 뷰의 목적과 사용
메모리 뷰는 특히 대용량 바이너리 데이터를 다룰 때 유용하다. 예를 들어, 이미지나 오디오 데이터 스트림과 같은 바이너리 데이터를 수정하거나 조작할 때, 데이터의 복사본을 만들지 않고 직접 접근할 수 있어 처리 효율성이 크게 향상된다. 메모리 뷰를 사용함으로써 메모리 사용을 최적화하고, 성능을 개선할 수 있다.
메모리 뷰의 주요 기능
- 슬라이싱과 인덱싱: 메모리 뷰는 배열과 유사하게 슬라이싱과 인덱싱을 지원하여, 원본 데이터를 변경하지 않고 특정 부분에 대한 접근을 제공한다.
- 변경 가능성: 메모리 뷰는 생성된 객체가 변경 가능(mutable)한지, 변경 불가능(immutable)한지에 따라 해당 데이터를 변경할 수도 있다. 예를 들어, bytes 객체는 변경할 수 없지만, bytearray 객체는 변경할 수 있다.
- 다차원 슬라이싱: NumPy와 같은 라이브러리에서 메모리 뷰를 사용하여 다차원 배열을 효율적으로 다룰 수 있다.
data = bytearray(b"hello world")
mv = memoryview(data)
# 메모리 뷰를 사용하여 데이터 변경
mv[6:11] = b"WORLD"
print(data) # bytearray(b'hello WORLD')
# mv를 통해 데이터의 일부를 "WORLD"로 변경하면, data의 내용도 업데이트되어 바뀐 내용을 반영
Numpy와 Scipy
NumPy와 SciPy는 파이썬에서 과학적 계산을 위해 널리 사용되는 두 가지 주요 라이브러리이다. 이 라이브러리들은 수치 데이터를 효율적으로 다루고, 과학적 분석을 위한 다양한 기능을 제공한다. 각각의 라이브러리는 특정 목적에 최적화되어 있으며, 함께 사용될 때 강력한 데이터 분석과 과학적 계산 기능을 제공한다.
NumPy (Numerical Python)
주요 특징과 기능:
- 다차원 배열 (ndarray): NumPy는 강력한 N차원 배열 객체를 제공한다.
- 방대한 수학 함수 라이브러리: 수학적 연산을 위한 다양한 함수를 내장하고 있다.
- 배열 기반의 연산: 모든 연산은 배열 기반으로, 반복문 없이 데이터를 처리할 수 있다.
- 브로드캐스팅: 서로 다른 크기의 배열 간 연산을 지원한다.
- 효율적인 메모리 사용: 연속적인 메모리 할당과 사용으로 높은 성능을 제공한다.
- C/C++ 및 Fortran 코드 통합: 기존 코드를 파이썬에 통합하여 사용할 수 있다.
SciPy (Scientific Python)
SciPy는 NumPy를 기반으로 구축된 라이브러리로, 과학, 수학, 엔지니어링 분야의 더 전문화된 작업을 위한 함수와 모듈을 제공한다. SciPy는 최적화, 선형 대수, 통계, 신호 처리 등 다양한 과학적 계산 모듈을 포함하고 있다.
주요 특징과 기능:
- 최적화: 비선형 방정식의 최소화, 곡선 피팅 등을 위한 루틴을 제공한다.
- 선형 대수: 행렬 분해, 행렬 연산, 선형 시스템 해결기 등을 포함한다.
- 통계: 확률 분포, 통계적 테스트, 기술적 통계를 계산할 수 있는 모듈을 제공한다.
- 신호 처리: 필터 설계, 주파수 분석 등의 기능을 제공한다.
- 이미지 처리: 이미지 파일 작업을 위한 기본 함수를 제공한다.
NumPy와 SciPy의 관계
NumPy와 SciPy는 서로 보완적인 관계에 있다. NumPy는 배열과 배열 연산에 초점을 맞추어 기본적인 기능을 제공하는 반면, SciPy는 NumPy의 배열을 사용하여 보다 복잡한 수치적, 과학적 계산을 수행하는 데 필요한 추가 기능을 제공한다. 일반적으로 NumPy는 데이터 조작과 간단한 연산에 사용되며, SciPy는 NumPy의 기능을 확장하여 보다 고급 수학적 및 과학적 알고리즘을 실행하는 데 사용된다.
덱 (Deque) & 기타 큐 (Queue)
파이썬에서는 collections 모듈을 통해 덱(Deque, Double-Ended Queue)과 같은 고성능 컨테이너 데이터 타입을 제공한다. 덱은 양쪽 끝에서 빠르게 아이템을 추가하거나 제거할 수 있는 최적화된 리스트의 일종으로, 큐(Queue)와 스택(Stack)의 기능을 모두 합친 형태이다.
덱 (Deque)
주요 특징
- 양방향: 덱은 양쪽 끝에서 요소의 추가와 삭제가 가능하다. 이를 통해 스택과 큐의 연산을 모두 효율적으로 지원한다.
- 스레드-세이프: 덱의 메서드는 멀티스레딩 환경에서 사용하기에 적합하도록 스레드-세이프하게 설계되어 있다.
- 메모리 효율적: 리스트에 비해 더 효율적인 메모리 구조를 가지고 있으며, 크기 조정 시 중간 데이터 복사가 적게 발생한다.
주요 메서드
- append(x): 덱의 오른쪽 끝에 **x**를 추가한다.
- appendleft(x): 덱의 왼쪽 끝에 **x**를 추가한다.
- pop(): 덱의 오른쪽 끝에서 아이템을 하나 제거하고 그 값을 반환한다.
- popleft(): 덱의 왼쪽 끝에서 아이템을 하나 제거하고 그 값을 반환한다.
- extend(iterable): 주어진 이터러블의 모든 요소를 덱의 오른쪽 끝에 추가한다.
- extendleft(iterable): 주어진 이터러블의 모든 요소를 덱의 왼쪽 끝에 추가한다(주의: 추가하는 순서가 반대로 된다).
- rotate(n): 덱을 n만큼 회전한다(양수면 오른쪽, 음수면 왼쪽).
from collections import deque
dq = deque([1, 2, 3, 4, 5])
dq.append(6) # [1, 2, 3, 4, 5, 6]
dq.appendleft(0) # [0, 1, 2, 3, 4, 5, 6]
dq.pop() # [0, 1, 2, 3, 4, 5]
dq.popleft() # [1, 2, 3, 4, 5]
기타 큐 (Queue)
파이썬의 queue 모듈은 멀티스레딩 환경에서 사용할 수 있는 여러 종류의 큐를 제공한다. 주요 큐 유형은 다음과 같다:
- Queue: 일반적인 FIFO(First In First Out) 큐이다.
- LifoQueue: LIFO(Last In First Out) 큐, 즉 스택이다.
- PriorityQueue: 우선순위 큐, 각 요소는 우선순위와 함께 저장되며, 가장 낮은 우선순위를 가진 요소가 먼저 나온다.
import queue
# 일반적인 Queue
q = queue.Queue()
q.put(1) # 큐에 1 추가
q.put(2) # 큐에 2 추가
print(q.get()) # 1 출력
print(q.get()) # 2 출력
# LifoQueue 생성
lifo_queue = queue.LifoQueue()
# 요소 추가
lifo_queue.put('a')
lifo_queue.put('b')
lifo_queue.put('c')
# 요소 제거 및 출력
print(lifo_queue.get()) # 출력: c
print(lifo_queue.get()) # 출력: b
print(lifo_queue.get()) # 출력: a
# PriorityQueue 생성
priority_queue = queue.PriorityQueue()
# 요소 추가 (우선순위, 값)
priority_queue.put((2, 'level two'))
priority_queue.put((1, 'level one'))
priority_queue.put((3, 'level three'))
# 요소 제거 및 출력
print(priority_queue.get()[1]) # 출력: level one
print(priority_queue.get()[1]) # 출력: level two
print(priority_queue.get()[1]) # 출력: level three
모든 코드는 github에 저장되어 있습니다.
'Python study' 카테고리의 다른 글
텍스트(text)와 바이트(byte) (0) | 2024.05.04 |
---|---|
딕셔너리(dictionary)와 집합(set) (0) | 2024.04.29 |
시퀀스(sequence) - 지능형 리스트(List Comprehension), 튜플(tuple) (0) | 2024.03.28 |
Python Special method (magic method) (0) | 2024.03.18 |
Variables and memory (0) | 2023.08.01 |