nuitka와 가상 환경

nuitka가 python version 3.대와 맞지 않는다는 ChatGPT의 말에 따라 venv를 3.12.6으로 낮추고 nuitka –standalone –onefile df_base.py를 실행하니 python 3.13에서 nuitka 2.7.13이 실행된다는 메시지가 나옵니다. 그래서 python -m nuitka –standalone –onefile df_base.py를 하니 python 버전 3.12에서 실행됩니다.

1. python code

import pandas as pd
import numpy as np
import openpyxl # nuitka때문에 해야 하나?

index = pd.date_range('1/1/2000', periods=8)
print(index)

df = pd.DataFrame(np.random.rand(8,3),index = index, columns=list('ABC'))
df['D'] = df['A'] / df['B']
df['E'] = np.sum(df, axis=1)
df = df.sub(df['A'], axis=0)
df = df.div(df['C'], axis=0)
df.to_csv('test.csv')
print(df.head())
# print(df)
df2 = df[df['B']>0.4].T
print(df2.head())
df.to_excel('날짜랜덤.xlsx', engine="openpyxl")

import pandas as pd
import numpy as np
=> pandas와 numpy 라이브러리를 호출해서 각각 pd와 np라는 alias로 선언합니다.

index = pd.date_range(‘1/1/2000’, periods=8)
=> 2000/1/1부터 8개를 생성해서 index로 삼습니다.

2000-01-01부터 8일간을 인덱스 변수에 대입함

dtype(데이터 형식)은 datetime64인데, ns는 nanosecond의 약자로 nanosecond까지 저장한다는 의미이고,

freq는 frequency의 약자로 D(날짜), 다시 말해 1일 단위라는 것입니다.

print(index)
=> index를 화면에 출력합니다.

df = pd.DataFrame(np.random.rand(8,3),index = index, columns=list(‘ABC’))
=> random 함수로 0과 1사이의 숫자를 8행 3열로 생성하고, index는 위에서 생성한 index 변수로 지정하고, column은 A,B,C로 함.

8행 3열로 난수를 생성 후 데이터프레임 df 생성

df[‘D’] = df[‘A’] / df[‘B’]
=> A열을 B열로 나눠 D열을 생성해서 값을 넣고

A를 B로 나눈 후 D열을 만들어 저장함

df[‘E’] = np.sum(df, axis=1)
=> df에 E열을 생성해서 행별 합계를 넣습니다.

행별로 합산 후 출력 화면
구분장점주의점
np.sum(df, axis=1)빠름, NumPy 배열과 함께 사용 시 효율적DataFrame에 숫자가 아닌 값이 있으면 오류
df.sum(axis=1)pandas 데이터에 최적화, numeric_only 옵션으로 안전속도는 NumPy보다 조금 느림

df = df.sub(df[‘A’], axis=0)
=> 모든 열에서 A열의 값을 빼서 새로운 df를 생성합니다.
아래는 5개만 출력해서 5일까지만 보이는 것입니다.

모든 열에서 A열 값을 뺀 후 출력 화면

위 표에서 2000-01-01의 B값은 당초 0.933993에서 A값인 0.632852을 빼서 0.301141이 된 것입니다. 이런 식으로 모든 열에서 A열의 값을 빼서 기록한 것입니다.

df = df.div(df[‘C’], axis=0)
=> 모든 열의 값을 C열 값으로 나눠서 새로운 df를 생성합니다.

C열 기준으로 나눈 후 출력 화면

위 표에서 2000-01-01의 B값은 당초 0.301141을 C값인 0.338077으로 나눠서 0.890747이 된 것입니다. 이런 식으로 모든 열을 C열의 값으로 나눠서 기록한 것입니다.

df.to_csv(‘test.csv’)
=> df를 test.csv 파일로 저장합니다.

csv 파일을 열어보면 아래와 같이 ,(쉼표)로 열이 구분되어 있고, 소숫점이하 자릿수가 화면에 표시되는 것보다 훨씬 많아 15~17자리로 표시됩니다.

데이터프레임을 csv 파일로 저장한 화면

print(df.head())
=> df의 값을 처음부터 5개 화면에 출력합니다.

C열 기준으로 나눈 후 결과 출력 화면

df2 = df[df[‘B’]>0.4].T
=> B열이 0.4보다 큰 것만을 구해서 df에 넣고,
Transpose, 다시 말해 행과 열을 바꾼 후 df2라는 새로운 데이터프레임에 넣습니다.

print(df2.head())
=> df2 데이터프레임 중 첫 5개를 화면에 출력합니다.

B열을 기준으로 0.4보다 큰 행만 고르기때문에 1/1, 1/5, 1/6, 1/7만 추출됐고, 행/열 전환이 되다보니 아래와 같이 표시됩니다.

데이터프레임을 B열 기준으로 조건을 지정한 후 행/열 전환한 경우

df.to_excel(‘날짜랜덤.xlsx’, engine=”openpyxl”)
=> df를 날짜랜덤.xlsx라는 엑셀 파일로 저장하는데, openpyxl을 사용합니다.

날짜가 시,분,초까지 표시되고, 숫자는 저장하지를 않아서 다르지만, 참고로 보기 바랍니다.

데이터프레임을 엑셀로 저장한 경우

2. 실행 파일 만들기

그동안 pyinstaller를 사용했는데, nuitka(뉴트카)가 실행속도가 빠르다고 해서 해보는데, 다른 실행파일 생성 도구와 비교할 때 장,단점은 아래와 같습니다.

가. Python 실행파일 생성 도구 정리

도구특징장점단점
PyInstaller스크립트를.exe로 묶음사용 쉽고 자료 많음, GUI/CLI 모두 지원일부 대형 라이브러리(TensorFlow 등) 복잡, 초기 실행 느림
cx_Freeze비슷하게 스크립트 묶음안정적, 초기 실행 빠름설정 조금 까다로움
Nuitka파이썬을 C로 변환 후 컴파일실행 속도 빠르고 네이티브, 소스 보호빌드 느림, 대형 프로젝트는 빌드 복잡
py2exe윈도우 전용단순 CLI/GUI에 가벼움윈도우 전용, 유지보수 제한적
UV (UltraViolet / uv-py?)최근 언급되는 Python 컴파일러비교적 빠름, 단일 실행파일 생성 가능자료가 적고 안정성 검증 필요

PC에 파이썬 버전이 여러 개 있을 경우 실행 방법이 다르다는 것을 이제야 알았습니다.

나. nuitka로 바로 실행하기

nuitka –standalone –onefile df_base.py
로 바로 실행하니 venv는 3.12.6인데

가상환경의 파이썬 버전이 3.12.6임

Nuitka: Version ‘2.7.13’ on Python 3.13 … 이라고 파이썬 버전 3.13에서 실행됩니다.

nuitka로 바로 실행하니 python 3.13에서 실행됨

그래서 계속 3.13에서 실행 파일 만드면 openpyxl 모듈이 없다는 에러 메시지가 떠서

python 3.13에서 nuitka로 실행 파일을 만들고 실행하니 openpyxl 에러 발생함

Ctrl + C키를 눌러 실행을 중단시켰습니다.

다. python -m nuitka로 실행하기

그런데 python -m 다음 nuitka 명령을 넣어 실행하니

python -m nuitka로 실행하니 venv의 파이썬 버전과 일치되게 실행됨

python 버전이 3.13이 아니라 3.12로 바뀝니다.

라. 실행 파일 실행

그리고, 실행하니 아무런 에러 메시지 없이 잘 됩니다.

nuitka로 에러 없이 실행결과를 보여주는 화면

마. 코드 수정

python 3.13 버전에서 컴파일시 openpyxl 라이브러리를 import하려면 import openpyxl이 있어서 한다고 해서 넣었었는데, python 3.12에서는 필요 없어서 뺐고,

df.to_excel(‘날짜랜덤.xlsx’,  engine=”openpyxl”)에서도
openpyxl을 강제로 import하도록 넣어야 한다고 해서 넣고 컴파일 했었는데, 3.13에서는 결국은 안되고, 3.12에서는 필요가 없어서 뺐습니다.

바. pyinstaller는 .\.venv\pyinstaller로 실행

아무 생각없이 pyinstaller를 pyinstaller -F -w df_base.py로 실행하니 아래와 같은 에러가 발생해서

pyinstaller를 가상환경은 3.12이고, 3.13에서 실행 후 오류 화면

컴파일 과정을 살펴보니 마찬가지로 python 버전이 3.13으로 실행됐네요.

python 3.12가상 환경에서 pyinstaller 바로 실행시 python 3.13에서 실행됨

그런데 nuitka와 마찬가지로 python -m pyinstaller -F -w df_base.py라고 하니
pyinstaler가 있는데, pyinstaller 모듈이 없다고 나옵니다.

pyinstaller를 python -m으로 실행시 오류 화면

그래서 .venv\Scripts\pyinstaller.exe -F -w df_base.py
로 해야 합니다.

pyinstaller를 가상환경에서 실행하기

경우에 따라 많이 다르네요.

그리고, 또 하나의 차이점은 pyinstaller의 경우는 .\dist 폴더에 실행파일이 생기는데,

nuitka의 경우는 .py 폴더에 생깁니다.

세로로 중복된 값 다루기

1. 문제

아래와 같이 중복된 숫자가 있으면 중복을 제거해야 할 경우가 생깁니다. 이와 관련해서 다양한 경우를 다루고자 합니다.

2. 중복된 숫자의 개수 세기

=COUNTIF(A2:A9,A2:A9)라고 하면

A2셀에서 A9셀까지의 범위에서 같은 숫자의 개수를 구해줍니다.

Microsoft 365라 아래와 같이 보이는 것이지 낮은 버전이라면 B2셀에서 B9셀까지 선택한 후 수식을 입력하고 Ctrl + Shift + Enter 키를 눌러야 할 겁니다.

A10셀에 2를 추가하고, 수식을 A10셀까지 수정하면 2라는 숫자의 개수가 모두 3으로 변경됩니다.

엑셀이 구 버전이라면 수식을 모두 지우고 새로 수식을 입력해야 할 수도 있습니다. 그렇다면 수식을 먼저 복사해서 다른데 붙여넣고 작업하는 것이 안전합니다.

2. 순수한 숫자의 개수 세기

위의 경우 1, 2, 4, 5, 7, 8과 같이 중복된 수라도 한번만 세려면

B열의 수식을 1/로 수정해서

=1/COUNTIF(A2:A10,A2:A10)이라고 하면 1/중복 개수가 되므로 1이면 1이지만, 2라면 1/2=0.5가 되고, 1/3=0.333333이 됩니다.

따라서, 위 수식을 sum 하면 0.5+0.5 = 1, 0.333333*3 = 1과 같이 고유한 숫자의 개수가 구해집니다.

=SUM(1/COUNTIF(A2:A10,A2:A10))을 하니 개별적으로 표시되던 것이 합계값 하나로 6이라고 표시됩니다. 1,2,4,5,7,8이므로 6 맞습니다.

3. 순수한 숫자만 표시하기

순수한 숫자 1, 2, 4, 5, 7, 8을 구해보겠습니다.

가. 중복 숫자는 한 번만 표시하고, 두번째부터는 공란으로 표시하기

D2셀에 =IF(COUNTIF($A$2:A2,A2)=1,A2,””) 이라고 입력합니다.

아래로 내려가면서 해당 셀의 개수를 세서 1보다 크면 빈 셀로 만들려고 하는 것입니다. 첫 셀인 A2셀을 절대 참조로 하는 것이 중요합니다. 왜냐하면 항상 A2셀부터 현재 셀(상대 참조)까지의 현재 셀의 개수를 세려고 하는 것이기 때문입니다.

D2셀의 채우기 핸들(D2셀 오른쪽 아래 네모)을 더블 클릭하면 D10셀까지 수식이 채워지는데(복사),

1, 2, “”, 4, 5, “”, 7, 8, “”라고 숫자가 중복되면 빈 셀로 표시되므로 이 숫자 들을 결합하면 됩니다.

나. 숫자 결합해서 표시하기

(1) TextJoin 함수

TextJoin함수는 Delimiter를 지정해서 텍스트를 결합할 수 있기 때문에 Concat함수보다 훨씬 편리합니다.

=TEXTJOIN(“, “,,D2:D10)라고,

구분자로 ,를 지정하고, 빈셀을 무시하도록 두번째 인수는 입력하지 않고 통과하고, 세번째 인수로 결합할 텍스트의 범위를 지정하면 원하는대로 1, 2, 4, 5, 7, 8이 구해집니다.

(2) Concat 함수

Concat 함수는 구분자를 지정할 수 없고, 결합할 텍스트만 지정할 수 있어서

=CONCAT(D2:D10)라고 하면 124578이라고 숫자가 단순히 결합된 결과만을 반환합니다.

(대체 해법)

어렵지만 할 수 없는 것은 아닙니다.

먼저 숫자가 있을 경우는 숫자 뒤에 공백을 한 칸 추가한 다음

Concat으로 연결한 다음 Substitute 함수를 이용해 공백 한 칸을 쉼표 + 공백 한칸으로 대체하면 됩니다.

=SUBSTITUTE(CONCAT(IF(D2:D10<>””,D2:D10&” “,””)),” “,”, “)라고 입력하면

결괏값을 보니 필요없이 마지막에도 ,가 들어가 있습니다.

따라서, concat한 후 trim을 해서 빈 공백을 제거해줘야 합니다.

수정된 수식은

=SUBSTITUTE(TRIM(CONCAT(IF(D2:D10<>””,D2:D10&” “,””))),” “,”, “) 입니다.

원하는대로 마지막에 쉼표 없이 1, 2, 4, 5, 7, 8이 구해졌습니다.

Rust의 이터레이터(Iterator)

Rust에서 이터레이터(iterator)는 값을 순회(iterate)할 수 있도록 해주는 강력하고 유연한 추상화입니다. 이터레이터는 반복 가능한 값을 하나씩 꺼내면서 작업을 수행할 때 사용되며, 지연 평가(lazy evaluation)를 통해 성능도 뛰어납니다.

Rust에서 지연 평가(lazy evaluation)는 계산이 필요한 시점까지 연산을 연기하는 전략입니다. 즉, 값을 즉시 계산하는 대신, 해당 값이 필요할 때까지 계산을 미루는 방식입니다. 이를 통해 불필요한 계산을 방지하고 성능을 향상시킬 수 있습니다.

1. 기본 개념

Rust에서 이터레이터는 Iterator 트레잇을 구현한 타입입니다. 이 트레잇은 next() 메서드를 정의합니다.

pub trait Iterator {
    type Item;

    fn next(&mut self) -> Option<Self::Item>;
}
  • next()는 Option Enum 형식으로 반환하므로, Some(value)를 반환하다가, 더 이상 값이 없으면 None을 반환합니다.
  • for 루프는 내부적으로 이 next()를 호출하여 동작합니다.

가. 예제 1: 기본 사용

fn main() {
    let v = vec![10, 20, 30];
    let mut iter = v.iter(); // 불변 참조로 이터레이터 생성

    while let Some(x) = iter.next() {
        println!("값: {x}");
    }
}
let v = vec![10, 20, 30]
  • v는 정수형 벡터입니다.
  • 즉, Vec 타입이고 [10, 20, 30]이라는 세 개의 요소를 가지고 있습니다.
let mut iter = v.iter()
  • v.iter()는 벡터 v의 각 요소에 대한 불변 참조 (&i32)를 반환하는 이터레이터를 생성합니다.
  • 즉, iter는 &10, &20, &30을 순서대로 반환할 준비가 된 상태입니다.
  • iter는 가변 변수로 선언되었습니다(mut) → .next()를 호출할 때 이터레이터 내부 상태를 바꾸기 때문입니다.

while let Some(x) = iter.next() { … }
  • .next()는 이터레이터에서 다음 값을 하나씩 꺼냅니다.
  • 반환값은 Option<&i32>입니다.
  • 값이 있으면 Some(&값)
  • 끝나면 None
  • while let Some(x)는 Some일 때 루프를 돌고, None이면 종료됩니다.
println!(“값: {x}”);
  • x는 &i32 타입이므로 10, 20, 30이 참조 형태로 출력됩니다.
  • println!은 참조를 자동으로 역참조해서 출력해주기 때문에 따로 *x를 쓰지 않아도 됩니다.

나. 예제 2: for 루프 사용

fn main() {
    let v = vec![1, 2, 3];

    for num in v.iter() {
        println!("num = {num}");
    }
}

while문과 아래가 다릅니다.

for num in v.iter()
  • v.iter()는 불변 참조 이터레이터를 생성합니다.
    • 즉, &1, &2, &3을 순서대로반환합니다.
  • for 루프는 이터레이터의 .next()를 자동으로 반복 호출하여 값을 하나씩 꺼냅니다.
  • 변수 num의 타입은 &i32입니다 (참조).
  • v.iter()는 벡터를 소유하지 않고 참만 하므로, v는 이루프 이후에도 여전히 사용 가능합니다.
  • println!(“num = {num}”);에 따라 1,2,3이 출력됩니다.

2. 소비(consuming) 어댑터

이터레이터를 사용해 데이터를 소모하는 메서드입니다.

  • .sum(): 합계 반환
  • .count(): 요소 개수
  • .collect(): 컬렉션으로 변환
fn main() {
    let v = vec![1, 2, 3, 4, 5];
    let sum: i32 = v.iter().sum();
    println!("합계: {}", sum);
}

여기서 특이한 점은 sum 다음에 i32라고 타입이 명시되어 있다는 점입니다.

: i32를 빼고 실행하면 “type annotations needed”라고 에러가 발생하는데,

iter().sum()의 반환 형식을 지정하지 않으면 에러 발생

sum이 &i32를 받아서 더할 수는 있지만, 반환값의 형식을 추론할 수 없기 때문에 안정성과 명확성을 추구하는 Rust가 에러를 발생시키는 것입니다.

3. 변형(transforming) 어댑터

이터레이터에서 새로운 이터레이터를 생성하지만 실제 순회는 for, collect 등으로 실행되기 전까지 지연 평가됩니다.

  • .map(): 각 요소를 변형
  • .filter(): 조건에 맞는 요소만 남김

가. map() 예제

fn main() {
    let v = vec![1, 2, 3, 4];

    let doubled: Vec<i32> = v.iter()
        .map(|x| x * 2)
        .collect();

    println!("{doubled:?}"); // [2, 4, 6, 8]
}
v.iter()
  • 벡터 v에 대해 불변 참조 이터레이터를 생성합니다.
  • 반환 타입은 impl Iterator<Item = &i32> → 각 요소는 &1, &2, &3, &4.
.map(|x| x * 2)
  • map은 이터레이터의 각 항목에 closure를 적용해 새로운 이터레이터를 만듭니다.
  • 여기서 x는 &i32이므로, x * 2는 실제로는 *x * 2와 같은 의미입니다.
  • 즉, 값은 다음과 같이 변합니다:
  • &1 → 1 * 2 → 2
  • &2 → 2 * 2 → 4
  • x는 &i32이기 때문에 직접 곱하려면 *x * 2라고 해야 하지만, Rust는 x * 2를 보면 자동으로 역참조(*x) 해주기 때문에 생략 가능합니다.

.collect()
  • 이터레이터 결과를 컨테이너 타입(여기서는 Vec)으로 수집합니다.
  • 이 부분에서 타입 추론이 불가능할 수 있기 때문에, doubled: Vec<i32>로 타입을 명시했습니다.

println!(“{doubled:?}”);
  • {:?}는 벡터를 디버그 형식으로 출력해줍니다.
  • 출력 결과는 [2, 4, 6, 8]입니다.

나. filter() 예제

fn main() {
    let v = vec![1, 2, 3, 4, 5];

    let even: Vec<_> = v.into_iter()
        .filter(|x| x % 2 == 0)
        .collect();

    println!("{:?}", even); // [2, 4]
}

다른 것은 같고 filter 부분만 다른데, x를 2로 나눴을 때 나머지가 0인 것을 % 연산자(나머지 연산자)로 구해서 해당되는 것만 collect(수집)하는 것입니다.

4. 소유권과 이터레이터

이터레이터는 다음 세 가지 방식으로 만들 수 있습니다.

메서드설명
.iter()불변 참조 이터레이터
.iter.mut()가변 참조 이터레이터
.into_iter()소유권을 이동하는 이터레이터
fn main() {
    let mut v = vec![1, 2, 3];

    for x in v.iter_mut() {
        *x *= 10;
    }

    println!("{:?}", v); // [10, 20, 30]
}

vector 변수 v를 가변 참조로 선언한 다음,
값을 하나씩 꺼내서 10을 곱한 다음 x에 저장하므로 v 변수가 변경됩니다.
이후 벡터 변수 v를 디버그 포맷으로 출력합니다.

5. 사용자 정의 이터레이터

직접 구조체에 Iterator 트레잇을 구현하여 사용자 정의 이터레이터를 만들 수도 있습니다.

struct Counter {
    count: usize,
}

impl Counter {
    fn new() -> Self {
        Counter { count: 0 }
    }
}

impl Iterator for Counter {
    type Item = usize;

    fn next(&mut self) -> Option<Self::Item> {
        self.count += 1;
        if self.count <= 5 {
            Some(self.count)
        } else {
            None
        }
    }
}

fn main() {
    let c = Counter::new();
    for i in c {
        println!("{}", i);
    }
}
가. 구조체 정의
struct Counter {
    count: usize,
}

usize 타입의 count 필드를 가진 Counter 구조체를 정의합니다.

나. Counter의 new 메소드 구현
impl Counter {
    fn new() -> Self {
        Counter { count: 0 }
    }
}

Counter 구조체의 fn new() 메소드를 정의하는데, count 필드의 값을 0으로 초기화합니다.

다. Counter를 위한 Iterator 트레잇 구현
impl Iterator for Counter {
    type Item = usize;

    fn next(&mut self) -> Option<Self::Item> {
        self.count += 1;
        if self.count <= 5 {
            Some(self.count)
        } else {
            None
        }
    }
}

Counter 구조체를 위해 Iterator 트레잇을 구현하는데
fn new() 메소드에서 반환값은 Item인데 usize 형식이고,
매번 next()가 호출될 때 count를 1씩 증가시키고, 5보다 작거나 같으면 Some(count)을 반환하고, 그렇지 않으면 None을 반환하여 반복을 종료합니다.

라. 메인 함수
fn main() {
    let c = Counter::new();
    for i in c {
        println!("{}", i);
    }
}

Count 구조체의 count 필드를 0으로 초기화한 후 c에 넣고
c를 1씩 증가시키면서 실행하는데 5까지만 출력하고 종료합니다.
Counter::new()로 만든 c는 Iterator를 구현하고 있기 때문에 for 루프에서 사용 가능합니다.

6. 정리

  • Iterator는 next()를 통해 요소를 하나씩 반환합니다.
  • .map, .filter 등은 지연 평가(lazy evaluation) 방식으로 동작합니다.
  • .collect()나 for 루프 등을 통해 실제로 실행됩니다.
  • 반복 가능한 자료형은 대부분 이터레이터를 제공합니다 (Vec, HashMap, Range 등)
  • Rust의 함수형 프로그래밍 스타일을 구성하는 핵심 개념입니다.