Python의 배열: 숫자 데이터를 효율적으로 사용하기

2024. 1. 4. 16:43python/intermediate

  • Python에서 숫자의 동종 배열 만들기
  • 다른 시퀀스와 마찬가지로 숫자 배열 수정
  • 배열과 다른 데이터 유형 간 변환
  • Python 배열에 적합한 유형 코드 선택
  • 배열의 비표준 유형 에뮬레이트
  • Python 배열의 포인터를 C 함수에 전달

프로그래밍의 배열 이해

컴퓨터 과학의 배열

  • 추상 데이터 유형
  • 데이터 구조
  • 데이터 유형

컴퓨터 과학은 요소 삽입이나 삭제와 같은 특정 작업을 지원하는 추상 데이터 유형(ADT)으로 데이터 컬렉션을 모델링합니다. 이러한 작업은 추상 데이터 유형의 고유한 동작을 설명하는 추가 제약 조건을 충족해야 합니다.

이 맥락에서 단어 추상은 이러한 데이터 유형이 구현 세부 사항을 사용자에게 맡기고 예상 의미 체계 또는 집합만 정의한다는 의미입니다. 결과적으로 데이터 구성에 대한 동일한 개념적 접근 방식을 구체적으로 구현한 몇 가지 대체 데이터 구조를 사용하여 하나의 추상 데이터 유형을 나타낼 수 있는 경우가 많습니다.

프로그래밍 언어는 일반적으로 편의를 위해 내장된 데이터 유형 형태로 몇 가지 데이터 구조를 제공합니다. 이는 매번 처음부터 시작하는 대신 보다 추상적인 문제를 해결하는 데 집중할 수 있음을 의미합니다.

추상 데이터 유형의 가장 일반적인 예는 다음과 같습니다.

  • Dictionaries
  • Graphs
  • Lists
  • Queues
  • Sets
  • Stacks
  • Trees

배열이 목록 추상 데이터 유형을 나타내는 특정 데이터 구조이기 때문입니다. ADT 목록은 어레이가 지원해야 하는 작업과 표시해야 하는 동작을 나타냅니다.

ADT(추상 데이터 유형) 나열파이썬의list
인덱스로 요소 가져오기 fruits[0]
특정 인덱스에 요소 설정 fruits[0] = "banana"
주어진 인덱스에 요소 삽입 fruits.insert(0, "banana")
인덱스로 요소 삭제 fruits.pop(0),del fruits[0]
값으로 요소 삭제 fruits.remove("banana")
모든 요소 삭제 fruits.clear()
주어진 요소의 인덱스 찾기 fruits.index("banana")
오른쪽 끝에 요소 추가 fruits.append("banana")
다른 목록과 병합 fruits.extend(veggies),fruits + veggies
요소 정렬 fruits.sort()
요소 수를 가져옵니다. len(fruits)
요소 반복 iter(fruits)
요소가 있는지 확인 "banana" in fruits

데이터 구조로서의 배열

배열은 다음과 같아야 합니다.

  • 동질성: 배열의 모든 요소는 공통 데이터 유형을 공유하므로 균일한 크기를 가질 수 있습니다. 이는 혼합 유형의 요소를 포함할 수 있는 Python 목록과 다릅니다.
  • 연속: 배열 요소는 컴퓨터 메모리에서 서로 인접하여 연속 블록을 차지합니다. 메모리 공간의. 배열의 시작과 끝 사이에는 다른 데이터가 허용되지 않습니다. 이러한 레이아웃은 컴퓨터 메모리가 논리적으로 구성되는 방식에 해당합니다.
  • 고정 크기: 배열이 차지하는 메모리 블록 바로 뒤에 다른 데이터가 올 수 있으므로 블록을 확장하면 해당 데이터를 덮어쓰게 되어 잠재적으로 심각한 오류나 오작동이 발생할 수 있습니다. 따라서 배열에 메모리를 할당할 때 나중에 변경할 수 없도록 크기를 미리 알아야 합니다.

배열과 연결 목록

예를 들어 배열 또는 연결된 목록을 사용하여 목록 추상 데이터 유형을 구현할 수 있습니다. 둘 다 요소의 시퀀스이지만 자주 조회하는 시나리오에는 배열이 더 적합하고, 삽입 또는 삭제 횟수가 더 많을 것으로 예상되면 연결 목록이 더 잘 작동합니다.

배열은 항상 임의 요소에 대한 즉각적인 액세스를 제공하지만 새 요소를 추가하려면 추가 시간이 필요합니다. 반대로 연결된 목록을 사용하면 빠르게 삽입할 수 있지만 인덱스로 요소를 찾는 데 시간이 더 오래 걸릴 수 있습니다. 이러한 절충안은 두 데이터 구조가 메모리에서 데이터를 구성하는 방식의 결과입니다.

Python의 배열

Python은 내부적으로 배열 데이터 구조에 의존하지만 이를 사용자에게 직접 노출하지는 않습니다. Python에서 배열에 가장 가까운 방법은 표준 라이브러리의 array 또는 NumPy ndarray 데이터 유형을 사용하는 것입니다. 그러나 둘 중 어느 것도 실제 배열을 제공하지 않습니다. 숫자 값만 보유할 수 있기 때문입니다.

Python의 array 모듈 알아보기

참조 문서에 따르면 Python의 array 모듈은 요소가 소수의 숫자 유형으로 제한되는 효율적인 배열 데이터 구조를 정의합니다. 따라서 이 모듈을 사용하여 문자열이나 사용자 정의 개체와 같은 임의 데이터 유형의 일반 배열을 생성할 수 없습니다. 하지만 이를 통해 수행할 수 있는 작업은 고성능 숫자 처리를 위한 대규모 데이터 세트를 간결하게 표현하는 것입니다.

Python에서 배열을 생성하는 기본 구문은 다음과 같습니다.

array(typecode[, initializer])

array 클래스를 array 모듈에서 가져온 후 최소한 유형 코드를 지정해야 합니다.

In [ ]:
from array import array
array("i", [1, 2, 3, 4])

배열 요소의 유형을 선택하세요

모듈에서 array 클래스의 인스턴스를 생성하기 전 첫 번째 단계는 해당 요소에 대한 공통 숫자 유형을 결정하는 것입니다. 모든 배열 요소는 동일한 고정 숫자 유형을 가져야 합니다. 잘 알려진 몇 가지 숫자 유형 중에서 한 번 선택하면 마음이 바뀔 수 없으므로 현명하게 선택하십시오. 숫자 유형에 따라 가능한 값의 범위가 결정되므로 해당 범위를 벗어나는 숫자는 배열에 저장할 수 없습니다.모듈에서 array 클래스의 인스턴스를 생성하기 전 첫 번째 단계는 해당 요소에 대한 공통 숫자 유형을 결정하는 것입니다. 모든 배열 요소는 동일한 고정 숫자 유형을 가져야 합니다. 잘 알려진 몇 가지 숫자 유형 중에서 한 번 선택하면 마음이 바뀔 수 없으므로 현명하게 선택하십시오. 숫자 유형에 따라 가능한 값의 범위가 결정되므로 해당 범위를 벗어나는 숫자는 배열에 저장할 수 없습니다.

Python은 구문을 통해 비교적 적은 수의 숫자 유형을 제공합니다.

  • 정수
  • 부동 소수점 숫자
  • 복소수

array 모듈에는 13개의 숫자 유형이 있으며, 각 유형은 단일 문자 유형 코드로 표시됩니다.

In [ ]:
import array
array.typecodes

대문자는 부호가 없는 숫자에 해당하고, 소문자는 부호가 있는 숫자를 나타냅니다.

유형 코드C타입최소 크기(바이트)
b signed char 1
B unsigned char 1
h signed short 2
H unsigned short 2
i signed int 2
I unsigned int 2
l signed long 4
L unsigned long 4
q signed long long 8
Q unsigned long long 8
f float 4
d double 8
u wchar_t 2
In [ ]:
def get_range(num_bytes, signed=True):
    num_bits = num_bytes * 8
    if signed:
        return -2 ** (num_bits - 1), 2 ** (num_bits - 1) - 1
    else:
        return 0, (2 ** num_bits) - 1

get_range(num_bytes=2, signed=False)
In [ ]:
get_range(num_bytes=2, signed=True)
In [ ]:
get_range(num_bytes=4, signed=False)
In [ ]:
get_range(num_bytes=4, signed=True)
In [ ]:
get_range(num_bytes=8, signed=False)
In [ ]:
get_range(num_bytes=8, signed=True)

나중에 채울 빈 배열 만들기

배열의 생성자에는 원하는 요소 유형을 지정해야 하는 필수 인수가 하나만 필요합니다. 선택적 두 번째 인수를 생략하면 숫자의 빈 배열이 정의됩니다. 예를 들어, 다음 코드는 부호 없는 2바이트 정수인 부호 없는 shorts의 빈 배열을 만듭니다.

In [ ]:
from array import array
empty = array("H")
len(empty)

길이가 고정된 기존 배열 데이터 구조와 달리 Python의 array 데이터 유형은 동적 크기입니다. 따라서 나중에 빈 배열에 요소를 추가하면 그에 따라 자동으로 크기가 조정됩니다.

다른 Iterable을 사용하여 새 배열 초기화

빈 배열을 만드는 대신 두 번째 인수로 전달된 초기화 값의 요소로 새 배열을 채울 수도 있습니다.

  • Integers
  • Floats
  • Characters

다음은 Python 배열을 사용하여 동일한 바이트 시퀀스를 다양한 방식으로 해석하는 방법을 보여주는 몇 가지 예입니다. 예를 들어, 이를 통해 파일에서 이진 데이터를 읽는 방법을 제어할 수 있습니다. 이를 테스트하려면 snake emoji 을 UTF-8로 인코딩하여 샘플 바이트를 생성하세요.

In [ ]:
snake_emoji = "\N{snake}".encode("utf-8")

snake_emoji
In [ ]:
len(snake_emoji)
In [ ]:
for byte in snake_emoji:
    print(format(byte, "b"))

이 바이트 시퀀스를 초기화 값으로 새 배열에 전달할 수 있습니다.

In [ ]:
array("b", snake_emoji)
In [ ]:
array("B", snake_emoji)
In [ ]:
array("h", snake_emoji)
In [ ]:
array("H", snake_emoji)
In [ ]:
array("i", snake_emoji)
In [ ]:
array("I", snake_emoji)
In [ ]:
array("f", snake_emoji)

정수, 부동 소수점 또는 문자로 구성된 또 다른 반복 가능한 컨테이너가 이미 있는 경우 이를 배열 생성자에 전달하여 해당 값으로 배열을 초기화할 수 있습니다.

In [ ]:
array("i", [2 ** i for i in range(8)])
In [ ]:
array("d", (1.4, 2.8, 4, 5.6))
In [ ]:
array("u", "café")
In [ ]:
array("u", ["c", "a", "f", "é"])

첫 번째 배열에는 8개의 정수가 포함되어 있으며 이는 목록 이해에서 계산하는 2의 거듭제곱입니다. 두 번째 배열에는 숫자의 튜플에서 파생된 4개의 배정밀도 부동 소수점 숫자가 있습니다. 마지막 두 배열은 각각 문자열과 개별 단일 문자 문자열 목록에서 얻은 동일한 문자를 나타냅니다.

내부적으로 각 Python 배열은 숫자의 선형 모음입니다. 비어 있지 않은 배열의 텍스트 표현은 대신 Python 문자열로 렌더링되는 유니코드 문자 시퀀스를 제외하고 해당 숫자를 나타냅니다. 그럼에도 불구하고 문자는 즉시 표시되지 않더라도 내부적으로 숫자 코드 포인트로 인코딩됩니다.

기존 어레이를 프로토타입으로 사용

특별한 경우로, 다른 Python 배열을 초기화 값으로 제공하여 해당 요소를 복사할 수 있습니다. 결국 배열은 생성자에 전달할 수 있는 숫자의 반복 가능한 것일 뿐입니다.

In [ ]:
original = array("i", [1, 2, 3])
cloned = array("i", original)
cloned == original     # 동일한 값
In [ ]:
cloned is original     # 별도의 엔티티(같은 개체가 아님)

일반적으로 숫자가 사용 가능한 값 범위에 편안하게 들어갈 수 있는 경우 배열을 초기화할 수 있습니다.

In [ ]:
array("B", array("Q", range(256)))

여기서는 요소가 숫자당 최소 8배 더 많은 메모리를 사용하는 다른 배열을 사용하여 부호 없는 바이트 배열을 초기화합니다!

정수 배열을 새로운 부동 소수점 숫자 배열에 입력하면 흥미로운 일이 발생합니다.

In [ ]:
array("f", array("i", [1, 2, 3]))

단정밀도 및 배정밀도 부동 소수점은 입력 유형의 상위 집합이므로 Python은 자동으로 숫자를 더 넓은 유형으로 승격합니다.

In [ ]:
array("d", (1, 2.8, 4, 5.6))

입력 튜플은 Python 정수와 부동 소수점으로 구성되며, 모두 결국 배정밀도 부동 소수점 숫자로 변환됩니다.

그러나 이 유형 변환은 부동 소수점 숫자의 소수 부분에 대한 잠재적인 정보 손실을 초래할 수 있으므로 반대 방향으로는 작동하지 않습니다.

In [ ]:
array("i", array("f", [3.14, 2.72]))

배열 생성 시 일반적인 함정 방지

In [ ]:
array("d", [3 + 2j, 3 - 2j])

첫 번째 경우에는 배정밀도 부동 소수점 숫자의 배열을 선언했지만 배열이 지원하는 숫자 유형에 속하지 않는 복소수로 구성된 목록을 전달했습니다.

In [ ]:
array("u", ["café", "tea"])

다중 문자 Python 문자열 목록을 사용하여 유니코드 문자 배열을 초기화하려고 했습니다. 그런데 "u" 유형 코드를 사용할 때는 전체 문자열이나 개별 문자 목록과 같은 반복 가능한 문자만 제공할 수 있습니다.
다른 모든 유형 코드의 경우 Python은 다시 TypeError 를 발생시킵니다:

In [ ]:
array("i", "café")
In [ ]:
array("i", ["c", "a", "f", "é"])

아마도 가장 일반적인 오류는 정수 오버플로입니다. 이는 숫자 중 하나 이상이 지원되는 값 범위를 벗어날 때 발생합니다.

In [ ]:
array("B", [-273, 0, 100])

배열 생성자에 대한 첫 번째 호출에서는 부호 없는 바이트가 음수가 될 수 없기 때문에 언더플로가 발생합니다.

In [ ]:
array("b", [55, 89, 144])

두 번째 호출에서는 부호 있는 바이트의 최대값이 127이므로 오버플로가 발생합니다.

배열의 데이터 유형에서 표현할 수 있는 최대값을 초과하는 숫자를 우연히 발견하면 관련 문제가 발생할 수 있습니다.

In [ ]:
array("i", [10 ** 309])
In [ ]:
array("Q", [10 ** 309])
In [ ]:
array("d", [10 ** 309])
In [ ]:
array("d", [1e309])

첫 번째 경우 Python의 정수 10309은 C long 데이터 유형으로 표현 가능한 최대값을 초과합니다. 두 번째 경우에는 이렇게 많은 수를 수용할 수 있는 해당 C 데이터 유형이 존재하지 않습니다. 다음으로, 큰 정수가 포함된 Python의 list을 사용하여 부동 소수점 숫자 배열을 초기화해 봅니다. 지수 연산자(**)를 사용하여 정수 값을 계산하는 것과 해당 부동 소수점 리터럴 1e309을 지정하는 것의 차이점을 확인하세요. 부동 소수점 숫자는 제한되어 있기 때문에 Python은 자동으로 리터럴을 무한대를 나타내는 특수 값으로 반올림합니다.

단정밀도 부동 소수점 배열을 정의할 때 발생할 수 있는 정밀도 손실에 주의하세요.

In [ ]:
array("f", (1.4, 2.8, 4, 5.6))

사용할 수 있는 비트 수가 적으면 이러한 값 중 일부를 정확하게 표현할 수 없습니다.

또한 멀티바이트 숫자로 이동할 경우 초기화 값으로 전달된 바이트 시퀀스가 ​​지정된 요소 크기의 배수인지 확인해야 합니다.

In [ ]:
array("H", b"\xff\x00\x80")

이 경우, 각각 2바이트 길이의 부호 없는 short 배열을 3바이트 시퀀스로 초기화하려고 했습니다. 초기화 프로그램에서 1바이트가 누락되었기 때문에 Python은 적절한 메시지와 함께 ValueError을 발생시킵니다.

Python 및 그 이상에서 배열 사용

배열을 가변 시퀀스로 조작

array 클래스는 상당히 낮은 수준이고 Python에서 자주 사용되지 않지만, 자주 사용되는 list 및 tuple 데이터 유형과 함께 완전한 기능을 갖춘 시퀀스 유형입니다.

In [ ]:
from array import array
fibonacci_numbers = array("I", [1, 1, 2, 3, 5, 8, 13, 21, 34, 55])
len(fibonacci_numbers)
In [ ]:
sum(fibonacci_numbers)

당연히 유한 시퀀스이므로 배열을 반복할 수 있습니다. 따라서 원하는 경우 요소를 반복하거나 수동 순회를 위해 반복자 객체를 반환하도록 배열에 요청할 수 있습니다.

In [ ]:
for i, number in enumerate(fibonacci_numbers):
    print(f"[{i}] = {number:>2}")
In [ ]:
it = iter(fibonacci_numbers)
next(it)
In [ ]:
next(it)
In [ ]:
next(it)

Python 배열의 인터페이스는 대체로 list와 유사하므로 편안함을 느낄 수 있습니다.

In [ ]:
fibonacci_numbers[-1]
In [ ]:
fibonacci_numbers[2:9:3]
In [ ]:
fibonacci_numbers + array("I", [89, 144])
In [ ]:
3 * array("I", [89, 144])
In [ ]:
42 in fibonacci_numbers
In [ ]:
fibonacci_numbers.count(1)
In [ ]:
fibonacci_numbers.index(13)

불변 튜플 및 문자열과 달리 Python의 배열은 해시 불가능 및 변경 가능 시퀀스 즉, 내용을 마음대로 수정할 수 있습니다.

In [ ]:
fibonacci_numbers.append(89)
fibonacci_numbers.extend([233, 377])
fibonacci_numbers.insert(-2, 144)
fibonacci_numbers.reverse()
fibonacci_numbers.remove(144)
fibonacci_numbers
In [ ]:
fibonacci_numbers.pop(0)
In [ ]:
fibonacci_numbers.pop()
In [ ]:
fibonacci_numbers[0] = 144
fibonacci_numbers
In [ ]:
del fibonacci_numbers[-1]
fibonacci_numbers

동일한 유형 코드로 선언된 배열 슬라이스만 할당할 수 있습니다.

In [ ]:
fibonacci_numbers[2:9:3] = array("I", [3, 13, 55, 233])

배열과 다른 유형 간 변환

배열로배열에서
.frombytes() .tobytes()
.fromfile() .tofile()
.fromlist() .tolist()
.fromunicode() .tounicode()

왼쪽 열에 나열된 메서드는 기존 값을 덮어쓰지 않고 배열에 요소를 추가합니다. 따라서 동일한 인스턴스에서 여러 번 호출하여 배열에 더 많은 숫자를 입력할 수 있습니다. 실제로 목록이나 문자열을 초기화 매개변수로 배열 생성자에 전달하면 커튼 뒤의 일부 메서드에 실행이 위임됩니다.

반대로, 오른쪽에 있는 메서드를 사용하면 배열의 내용을 해당 유형으로 표현할 수 있습니다. 이는 순수 Python에서 데이터를 처리하거나 파일로 내보내려는 경우 유용할 수 있습니다.

In [ ]:
from array import array
from struct import pack, unpack

def save(filename, numbers):
    with open(filename, mode="wb") as file:
        file.write(numbers.typecode.encode("ascii"))
        file.write(pack("<I", len(numbers)))
        numbers.tofile(file)


def load(filename):
    with open(filename, mode="rb") as file:
        typecode = file.read(1).decode("ascii")
        (length,) = unpack("<I", file.read(4))
        numbers = array(typecode)
        numbers.fromfile(file, length)
        return numbers


save("binary.data", array("H", [12, 42, 7, 15, 42, 38, 21]))
load("binary.data")

운영 체제 또는 컴퓨터 아키텍처 간에 바이너리 파일을 공유하는 경우 엔디안 또는 바이트 순서의 잠재적인 차이를 고려해야 합니다. 엔디안에는 두 가지 주요 유형이 있습니다.

  1. 빅 엔디안: 최상위 바이트가 먼저 옵니다.
  2. 리틀 엔디안: 최하위 바이트가 먼저 옵니다.
In [ ]:
unpack("<I", pack("<I", 42))  # Consistent byte order
In [ ]:
unpack(">I", pack("<I", 42))  # Inconsistent byte order

배열을 바이트형 객체로 처리

마지막으로 Python 배열은 버퍼 프로토콜을 지원하여 바이트- CPython의 하위 수준 메모리 영역에 직접 액세스해야 하는 컨텍스트에서 사용할 수 있는 객체와 같습니다. 이를 통해 불필요한 데이터 복사를 피하면서 C 및 C++ 라이브러리와 원활하게 통합할 수 있습니다.

기본 버퍼에 대한 세부정보를 얻으려면 배열의 .buffer_info() 메소드를 호출하세요.

In [ ]:
from array import array
data_points = array("L", [72057594037927936, 144115188075855872])

data_points.buffer_info()
In [ ]:
id(data_points)

버퍼의 메모리 주소와 현재 보유하고 있는 총 요소 수로 구성된 튜플을 반환합니다. 버퍼의 메모리 주소는 이를 래핑하는 Python 배열 객체의 주소와 약간 다릅니다.

다음 코드를 사용하여 배열의 버퍼에 할당된 총 바이트 수를 계산할 수 있습니다.

In [ ]:
data_points.itemsize * data_points.buffer_info()[1]

data_points 배열에는 두 개의 요소가 있고 각 요소의 길이는 8바이트이므로 전체 크기는 16바이트입니다. C 확장 모듈에서 버퍼에 액세스할 때 이 정보가 필요할 수 있습니다.

Python 배열의 성능 측정

목록은 다양한 데이터 유형을 처리할 수 있는 범용 개체 컨테이너인 반면, 배열은 숫자로 제한된 고정 유형을 갖습니다. 게다가 목록은 구문에 내장되어 있어 많은 편리함을 제공합니다. 반면에 배열은 사용하기가 더 까다롭지만 메모리 오버헤드가 거의 없으며 효율적인 하위 수준 메커니즘을 활용할 수 있습니다.

In [ ]:
from array import array
from timeit import timeit

large_list = list(range(10**6))
large_array = array("I", large_list)

timeit(lambda: sum(large_list), number=100)
In [ ]:
timeit(lambda: sum(large_array), number=100)

먼저 백만 개의 Python 정수 목록과 동일한 C 부호 없는 정수 배열을 만듭니다. 그런 다음 두 시퀀스에 대해 timeit()을 호출하여 해당 요소를 합산하는 데 걸리는 시간을 측정합니다.

순수 Python에서 배열을 처리하면 실행 속도 측면에서 다른 시퀀스 유형에 뒤처질 가능성이 높습니다. 컴파일된 C 라이브러리의 배열에 직접 액세스하여 박싱 오버헤드를 피할 수 있는 경우에만 기회가 있습니다. Python 배열이 약속하는 메모리 절약은 어떻습니까?

In [ ]:
from sys import getsizeof
from pympler.asizeof import asizeof

getsizeof(large_array)
In [ ]:
asizeof(large_array)모든 크기의 부호 있는 정수 해석
In [ ]:
getsizeof(large_list)
In [ ]:
asizeof(large_list)

Python 목록은 실제 요소 외에 포인터 배열을 유지한다는 점을 기억하세요. 따라서 이 경우 백만 개의 정수와 또 다른 백만 개의 정수 포인터, 그리고 56바이트의 메타데이터가 필요합니다.

array 모듈을 사용하면 여러 하위 수준 C 유형 중 하나를 사용하여 숫자 배열을 선언할 수 있지만 Python은 기본적으로 세 가지 유형의 숫자만 지원합니다.

모든 크기의 부호 있는 정수 해석

위 예의 오디오 샘플은 3바이트 또는 24비트로 구성됩니다. int.from_bytes() 클래스 메소드를 호출한 후 원시 바이트를 스피커에 들어오는 음파의 상대적 진폭을 나타내는 Python 정수로 변환합니다.

출처 :https://realpython.com/python-array/
 

 

'python > intermediate' 카테고리의 다른 글

python-for-data-analysis-II  (0) 2024.01.21
python-for-data-analysis  (0) 2024.01.20
Python에서 JSON 데이터 작업  (1) 2024.01.07
Python으로 데이터 직렬화2  (1) 2024.01.06
Python으로 데이터 직렬화1  (2) 2024.01.05