반응형

https://www.acmicpc.net/problem/4949

입력으로 주어지는 각 문자열들에 대해, 소괄호 또는 대괄호의 열고 닫는 쌍이 갖추어져 있는지(="균형잡혀 있는지") 묻는 문제이다.

문제는 stack을 해결하면 풀린다.

문자열의 각 문자를 순회하면서,

  • 여는 괄호일 때는 push하고
  • 닫는 괄호일 때는 stack 내부에서 top의 위치에 매칭되는 여는 괄호가 이미 있는지 확인 -> 없으면 균형잡혀 있지 않은 것이다.

순회가 끝난 뒤, stack에 여는 괄호가 남아 있다면 이것도 균형잡혀 있지 않은 것이다.

관건은 구현이다.

 

먼저 아래 방법을 썼다.

p라는 문자열은 닫는 괄호에 대해 매칭되는 여는 괄호를 찾기 위한 저장소다. 이를 위해 여는 괄호 2개와 닫는 괄호 2개를 이어붙였다. 그래서 p 문자열 상에서 index() 계산을 사용해서 매칭되는 괄호를 찾는다.

정답 코드 1

import sys


def is_balanced(s):
    p = "([)]"
    stack = []
    for c in s:
        if c in p[0:2]:
            stack.append(c)
        elif c in p[2:]:
            if not stack or (stack[-1] != p[p.index(c) - 2]):
                return False
            stack.pop()
    return not stack


sys.stdout.write(
    "\n".join(
        ("yes" if is_balanced(s) else "no") for s in sys.stdin if s.rstrip() != "."
    )
)

문자열 p의 길이가 4밖에 안되기 때문에, p.index()가 linear search긴 해도 사실상 O(1)으로 동작할 것으로 생각했다. 

그렇지만, 실제로는 O(4)라고 볼 수 있다.

만약 입력 문장의 갯수가 매우 많아지면, 이 미세한 차이가 누적되서 성능에 영향을 끼칠 수 있다.

 

매칭되는 여는 괄호를 한번에 찾을 수 있는 방법을 사용하면 성능을 좀 더 최적화할 수 있다.

이를 위해, 해싱 기반으로 동작하는 dict를 사용했다.

정답 코드 2 - dict를 사용한 해싱 탐색

import sys


def is_balanced(s):
    stack = []
    pairs = {")": "(", "]": "["}
    for c in s:
        if c in "([":
            stack.append(c)
        elif c in ")]":
            if not stack or stack[-1] != pairs[c]:
                return False
            stack.pop()
    return not stack


sys.stdout.write(
    "\n".join(
        ("yes" if is_balanced(s) else "no") for s in sys.stdin if s.rstrip() != "."
    )
)

성능이 좀 더 개선된 것을 확인할 수 있다.

반응형
반응형

Django를 사용시 as_view() 메서드를 자주 마주치게 된다. URL 설정에서 클래스 기반 뷰(Class-based View)를 연결할 때 항상 이 메서드를 호출한다. 이 메서드가 어떤 일을 하는지, 그리고 왜 필요한지 알아본다.

 

as_view()의 필요성

Django의 URL 라우팅 시스템은 기본적으로 함수를 기대한다. 하지만 코드를 더 체계적으로 구조화하기 위해,   클래스 기반 뷰를 사용한다. 바로 여기서 as_view()가 등장한다. as_view()는 클래스 기반 뷰를 URL 라우터가 이해할 수 있는 함수로 변환해주는 다리 역할을 한다.

내부 동작 원리

View 클레스의 as_view()의 내부 동작을 간략화된 형태로 살펴보자. 

class View:

	# ...생략...
    
    @classonlymethod
    def as_view(cls, **initkwargs):
        # ...생략...
        def view(request, *args, **kwargs):
            self = cls(**initkwargs)
            self.setup(request, *args, **kwargs)
            return self.dispatch(request, *args, **kwargs)
        # ...생략...
        return view
        
    # ...생략...

이 코드가 하는 일을 단계별로 살펴보면:

  1. 뷰 클래스의 새 인스턴스를 생성한다.
  2. (request 같은) 속성들을 초기화한다. 이는 뷰의 모든 메서드에 공유되는 속성이다.
  3. HTTP 메서드에 따라 적절한 핸들러로 요청을 전달한다.
    • dispatch 함수를 보면, request에서 HTTP 메서드를 알아낸 다음, 뷰가 해당 HTTP 메서드에 대해 구현하는 핸들러 함수(get()이나 post() 등)를 호출하는 것을 확인 가능하다.
    def dispatch(self, request, *args, **kwargs):
        # Try to dispatch to the right method; if a method doesn't exist,
        # defer to the error handler. Also defer to the error handler if the
        # request method isn't on the approved list.
        if request.method.lower() in self.http_method_names:
            handler = getattr(
                self, request.method.lower(), self.http_method_not_allowed
            )
        else:
            handler = self.http_method_not_allowed
        return handler(request, *args, **kwargs)

 

실제 사용 예시

# views.py
from django.views import View
from django.http import HttpResponse

class BlogPostView(View):
    def get(self, request, *args, **kwargs):
        return HttpResponse("블로그 포스트를 보여줍니다")
    
    def post(self, request, *args, **kwargs):
        return HttpResponse("새 포스트를 생성했습니다")

# urls.py
from django.urls import path
from .views import BlogPostView

urlpatterns = [
    path('post/', BlogPostView.as_view(), name='blog-post'),
]

고급 사용법 - 초기화 배개면수 전달

as_view()는 뷰 클래스에 초기 설정을 전달할 수 있는 기능도 제공한다:

path('post/', BlogPostView.as_view(template_name='custom_template.html'), name='blog-post')

이렇게 전달된 매개변수는 뷰 클래스의 인스턴스가 생성될 때 적용된다.

 

요약

as_view() 메서드는 Django의 클래스 기반 뷰 시스템을 가능하게 하는 핵심 요소이다. 이 메서드를 통해, 객체 지향적인 뷰를 작성하면서도 Django의 URL 시스템과 원활하게 통합할 수 있게 된다.

반응형

'Django' 카테고리의 다른 글

[Django] Messages Framework  (0) 2025.01.23
[Django] 사용자 모델 구현 방법 비교  (0) 2025.01.22
[Django] 정적 파일(Static Files) 관리  (0) 2025.01.17
[Django] urlpatterns의 작동 원리  (0) 2024.12.29
[Django] reverse 함수  (0) 2024.12.28
반응형

파이썬은 코드를 체계적으로 관리 및 재사용하기 위한 두 가지 핵심 개념인 모듈(module)과 패키지(package)를 제공한다.

이 두 개념의 차이점과 실제 개발에서 어떻게 활용되는지를 살펴본다.

 

Module과 Package의 기본 개념

모듈은 파이썬 코드를 담고 있는 단일 파이썬 파일로서, 관련된 함수, 클래스, 변수들을 하나의 파일에 모아놓은 것이다.

예를 들어, 수학 연산과 관련된 기능들을 math_operations.py라는 모듈에 다음과 같이 구현할 수 있다:

# math_operations.py
def add(a, b):
    return a + b

def subtract(a, b):
    return a - b

PI = 3.14159

반면 패키지는 여러 모듈을 체계적으로 관리하기 위한 디렉토리 구조이다.

패키지는 반드시 __init__.py 파일을 포함해야 하며, 여러 모듈과 하위 패키지들을 포함할 수 있다.

 

실제 개발에서의 중요성

1. 프로젝트 규모에 따른 선택

작은 규모의 프로젝트에서는 몇 개의 모듈만으로도 충분할 수 있다. 하지만 프로젝트가 커질수록 패키지 구조의 필요성이 증가한다. Django와 같은 대형 웹 프레임워크가 패키지 구조를 채택하고 있는 것도 이런 이유에서이다.

2. 코드 배포와 관리의 용이성

패키지를 사용하면 pip를 통한 설치가 가능하며, setup.py를 통해 의존성과 버전을 체계적으로 관리할 수 있다. 이는 특히 여러 개발자가 협업하는 환경에서 큰 장점이 된다.

  • 참고로, setup.py는 파이썬 패키지를 배포하기 위한 설정 파일이다. 여기선 자세히 다루지 않는다.

3. 이름 충돌 방지

패키지 구조를 사용하면 명확한 네임스페이스를 통해 이름 충돌을 방지할 수 있다. 예를 들어:

from mycompany.utils.database import helper

이런 방식으로 코드를 구조화하면, 다른 라이브러리의 동일한 이름을 가진 모듈과의 충돌을 예방할 수 있다.

4. 확장성과 유지보수

패키지 구조는 새로운 기능을 추가할 때 특히 유용하다.관련 기능들을 새로운 하위 패키지나 모듈로 추가할 수 있어, 코드의 구조를 깔끔하게 유지할 수 있기 때문이다.

또한 테스트 코드도 패키지 구조에 맞춰 체계적으로 구성할 수 있어, 품질 관리가 용이해진다.

 

결론

모듈과 패키지는 단순한 코드 구조화 도구 이상의 의미를 가지는, 프로젝트의 확장성, 유지보수성, 그리고 협업 효율성을 결정짓는 중요한 요소이다.

처음에는 단순한 모듈로 시작하더라도, 프로젝트의 규모가 커지고 복잡해질수록 패키지 구조로의 전환을 고려하는 중요한 아키텍처 상 결정을 해야 한다.

반응형

'Python > 문법' 카테고리의 다른 글

[Python] Enum 정리  (0) 2025.04.12
[Python] dataclass  (0) 2025.01.27
[Python] import 문 사용 팁  (0) 2025.01.13
[Python] Module import로 이해하는 Namespace와 Scope  (1) 2025.01.09
[Python] Unpacking의 다양한 예시  (2) 2024.12.27
반응형

Django 프로젝트를 개발시 CSS, JavaScript, 이미지와 같은 정적 파일들을 관리해야 할 필요가 있다. Django는 이러한 정적 파일들을 효율적으로 관리할 수 있는 시스템을 제공한다. Django의 정적 파일 관리 방법에 대해 정리해봤다.

자세한 내용은 공식문서를 확인하자 : https://docs.djangoproject.com/en/5.1/ref/settings/#std-setting-STATIC_URL

 

Settings | Django documentation

The web framework for perfectionists with deadlines.

docs.djangoproject.com

 

 

기본 설정하기

먼저 프로젝트 설정(config) 디렉토리의 settings.py에서 정적 파일 관련 설정을 해야 한다:

STATIC_URL = "static/"
STATICFILES_DIRS = [BASE_DIR / "static"]
  • STATIC_URL: 웹 URL에서 정적 파일을 참조할 때 사용할 기본 URL prefix이다.
    • STATIC_URL의 목적은 템플릿에서 "정적 파일 URL의 접두어 문자열 설정", 즉 정적파일의 URL 생성이다. 
    • 예를 들어 http://example.com/static/css/style.css와 같이 "static/"이 URL의 일부가 된다.
    • 배포 환경에서는 웹서버(nginx, apache 등)이 /static/에 대한 요청이 올 때 어떤 디렉터리에서 파일을 서빙할지 웹서버의 설정파일에 개발자가 명시해야 한다.
  • STATICFILES_DIRS: Django가 정적 파일을 찾아볼 추가 디렉토리 목록을 지정한다.
    • 위 설정은 "프로젝트 루트의 static 폴더에서도 정적 파일을 찾아보라"는 의미이다.
    • STATICFILES_DIRS는 주로 프로젝트 단위 공통 static 파일을 관리하려고 지정한다.
    • 배포시에 collectstatic이 STATICFILES_DIRS까지 참고해서 정적 파일을 모으나, 실제 서비스에서는 웹서버(Nginx 등)들이 STATICFILES_DIRS가 아닌 STATIC_ROOT만 바라본다.

 

템플릿에서 정적 파일 사용하기

템플릿에서 정적 파일을 사용하려면 두 가지 단계가 필요하다:

1. 템플릿 최상단에 static 템플릿 태그를 로드한다:

{% load static %}

{% load static %} 탬플릿 태그는,  static 템플릿 태그를 사용 가능하게 함으로써 CSS, JavaScript, 이미지 등의 정적 파일을 템플릿에서 사용할 수 있게 해준다.

2. static 템플릿 태그를 사용하여 정적 파일을 참조한다:

<!DOCTYPE html>
<html>
<head>
    <title>My Site</title>
    <link rel="stylesheet" href="{% static 'css/main.css' %}">
</head>
<body>
    <img src="{% static 'images/logo.jpg' %}" alt="Logo">
    <script src="{% static 'js/app.js' %}"></script>
</body>
</html>
{% static ... %} 는 정적 파일의 실제 URL을 (STATIC_URL 설정에 따라) 자동으로 생성한다.
현재 STATIC_URL'static/'으로 설정되있으므로, 아래와 같이 변환된다.
  • {% static 'css/main.css' %}  '/static/css/main.css'
  • {% static 'images/logo.jpg' %}  '/static/images/logo.jpg'
  • {% static 'js/app.js' %}  '/static/js/app.js'

참고로, STATIC_URL이 상대경로일 때, SCRIPT_NAME이라는 값이 설정되있지 않다면, '/'가 prefix된다. 그래서 STATIC_URL'static/'이어도 '/static/...'으로 변환되는 것이다.

# 일반적인 경우
STATIC_URL = 'static/'
결과: '/static/css/style.css'  # SCRIPT_NAME 없을 때 '/'가 붙음

# 서브 경로에서 실행되는 경우
SCRIPT_NAME = '/myapp'
STATIC_URL = 'static/'
결과: '/myapp/static/css/style.css'

 

{% static ... %} 태그가 해석되는 시점은, Django가 템플릿을 렌더링할 때이다. 구체적인 과정을 살펴보면, 

1. 브라우저가 페이지 요청
2. Django 뷰 함수 실행
3. 템플릿 렌더링 시작
4. {% static 'css/main.css' %} 발견
5. STATIC_URL + 파일경로로 변환 
   예: 'static/' + 'css/main.css' -> 'static/css/main.css'
6. 변환된 URL이 포함된 HTML을 브라우저에 전송

 

프로젝트 구조

일반적인 Django 프로젝트의 정적 파일 구조는 다음과 같다:

my_project/
    ├── manage.py
    ├── my_project/
    │   ├── settings.py
    │   └── urls.py
    ├── static/           # 프로젝트 레벨 정적 파일
    │   ├── css/
    │   ├── js/
    │   └── images/
    └── my_app/
        └── static/      # 앱 레벨 정적 파일

 

정적 파일 탐색

{% static 'css/style.css' %}

위와 같은 탬플릿 파일의  {% static ... %} 태그는 URL을 생성하는 역할을 수행한다.

브라우저는 해당 URL이 적힌 html 파일을 수신한다. 그리고 이 URL로 요청을 보낸다.

요청을 받을 때, Django 서버가 현재 개발 환경인지, 아니면 운영 환경인지에 따라 대응 로직이 다르다:

 

1. 개발 환경(DEBUG=True) 일 때

파일 탐색 시점

  • 브라우저가 정적 파일 URL을 요청할 때마다 실시간으로 탐색.
  • Django 개발 서버가 요청을 받은 시점에 탐색 시작.

탐색 프로세스

  1. 브라우저가 정적파일의 URL로 정적 파일을 요청.
  2. Django 개발 서버가 요청 수신.
  3. staticfiles 앱이 다음 순서로 실시간 탐색:
    • STATICFILES_DIRS에 지정된 경로들 확인.
    • INSTALLED_APPS의 각 앱의 static 폴더 확인.
  4. 파일을 찾으면 해당 파일 제공, 못 찾으면 404 반환.

(URL 경로의 파일이 양쪽 위치에 모두 있다면, STATICFILES_DIRS에 있는 파일이 우선적으로 사용된다.)

위에 언급했듯, 정적 파일을 탐색하는 주체는 staticfiles 앱(django.contrib.staticfiles)이다. 

정리하자면, Django의 개발 서버는 직접 정적 파일 요청이 들어올 때 때마다 실시간으로 파일을 찾아서 제공한다.

 

2. 운영 환경(DEBUG=False) 일 때

파일 탐색 시점

  • 배포 전 python manage.py collectstatic 실행 시 한 번만 수행.
  • 실제 서비스 중에는 탐색 과정 없음.
python manage.py collectstatic
  • 배포를 위해 모든 정적 파일을 한 곳으로 수집할 때 실행하는 명령이다.
  • STATIC_ROOT 디렉토리로 파일들을 복사한다.
    • STATIC_ROOT는 Django가 실행 중인 서버의 로컬 디렉터리이다.
    • 즉 운영 환경(배포)에서는 STATIC_ROOT를 settings.py에 미리 설정해둬야 한다.
STATIC_ROOT = BASE_DIR / 'staticfiles'

탐색 프로세스

  1. collectstatic 실행 시:
    • STATICFILES_DIRS 경로들 탐색.
    • INSTALLED_APPS의 각 앱의 static 폴더 탐색.
    • 발견된 모든 정적 파일을 STATIC_ROOT 디렉토리로 복사.
  2. 서비스 운영 중:
    • 브라우저가 정적 파일을 요청.
    • .웹 서버(Nginx, Apache 등)가 STATIC_ROOT 디렉토리에서 직접 파일 제공
      • 웹 서버가 /static/으로 시작하는  URL을 감지하고, 웹서버의 설정에 따라 특정 디렉토리(Django에서 STATIC_ROOT로 명시한 곳과 같은 디렉토리) 내부에서 파일을 찾고, 직접 클라이언트에 전송.
    • Django는 이 과정에 관여하지 않음
      • 따라서 Django 서버의 부하를 줄일 수 있음.
      • 웹서버는 정적 파일 서빙에 더 최적화 되어있으므로 서빙이 Django보다 더 빠름.
      • 필요하다면, 웹서버에서 정적파일을 캐싱할 수도 있음.

만약 정적 파일을 외부 서버에 두고 싶으면?

만약 정적 파일을 AWS S3, CloudFront, CDN, 혹은 다른 외부 서버에 올리고 싶다면:

  • STATIC_ROOT는 일단 로컬 경로로 지정해두고, 
  • collectstatic 명령어를 이용해서 로컬에 모은 다음,
  • 그걸 외부 서버로 업로드하는 추가 작업을 해야한다.
    • 예: django-storages 라이브러리를 사용해서 S3에 바로 업로드하도록 설정할 수 있다.

 

정적 파일 관리의 장점

  1. 개발/배포 환경 분리 (동일한 코드로 동작)
    • 개발 환경에서는 Django의 개발 서버가 정적 파일을 직접 제공
    • 배포 환경에서는 웹 서버나 CDN이 정적 파일을 제공
  2. 유연한 파일 경로 관리
    • STATIC_URL 설정만 변경하면 모든 정적 파일의 경로가 자동으로 업데이트
    • 파일 위치가 변경되어도 템플릿 코드를 수정할 필요가 없음
  3. 성능 최적화
    • 캐싱과 버전 관리를 위한 파일 핑거프린팅 지원
    • collectstatic 명령어를 통한 효율적인 정적 파일 관리

 

마무리

Django의 정적 파일 관리 시스템은 개발자가 효율적으로 정적 리소스를 관리할 수 있게 해준다.특히 {% static %} 템플릿 태그를 통해 유연하고 유지보수가 쉬운 방식으로 정적 파일을 참조할 수 있다. 이는 프로젝트가 커지고 복잡해져도 정적 파일 관리를 일관되게 유지할 수 있게 해주는 큰 장점이다.

반응형
반응형

파이썬에서 import 문을 쓸 때, 다양한 형태의 import 문을 마주치게 된다.

경험상, import 문의 동작 방식을 제대로 이해하지 못하면 예상치 못한 문제들을 마주할 수 있다.

 

먼저, import 대상이 점(.)으로 시작하는 경우와 그렇지 않은 경우가 있는데, 언제 점(.)을 쓰고 안 써야 하는지 정리해보려고 한다.

다음으로 import문의 충돌 문제에 대해서도 다뤄보겠다.

 

1. 절대 경로 import (점 없는 import)

점(.) 없는 import 문이 모듈을 찾는 경로는 Python의 sys.path에 등록된 모든 경로이다.

sys.path란 Python이 모듈을 검색할 때 참조하는 경로 목록으로 Python Path라고 부르며, 다음 경로들을 포함한다:

  1. 현재 실행중인 스크립트가 있는 디렉토리(sys.path[0])
  2. PYTHONPATH 환경변수에 등록된 디렉토리들
  3. Python이 설치될 때 기본으로 등록된 디렉토리들 (표준 라이브러리가 위치한 lib 디렉토리와 외부 라이브러리들이 설치되는 site-packages 디렉토리)

이런 방식의 import는 크게 세 가지 경우에서 볼 수 있다:

1.1 파이썬 기본 모듈

파이썬 설치 경로 밑의 (표준 라이브러리가 위치한 lib 디렉토리)에서 찾는다.

from datetime import datetime
import os
import sys

1.2 외부 라이브러리

lib 디렉토리 밑의 site-packages 폴더에서 찾는다.

import pandas
import numpy as np
from sklearn.model_selection import train_test_split

1.3 Python Path에 등록된 로컬 패키지

크게 3가지 경우이다:

  1. 현재 (스크립트의) 실행 위치(sys.path[0])
  2. PYTHONPATH 환경변수에 등록된 경로들
  3. 프레임워크나 도구가 자동으로 sys.path에 추가하는 경로들.

로컬 패키지가 Python Path에 등록되는 것은, Django 같은 웹 프레임워크에서 자주 볼 수 있는 케이스이다.

Django의 경우 프로젝트의 루트 디렉토리가 Python Path에 자동으로 추가되어, 앱 단위의 모듈을 절대 경로로 import할 수 있다.

# Django 프로젝트의 urls.py
from main.views import index  # main은 로컬 앱 이름
from blog.models import Post  # blog는 로컬 앱 이름

이것이 가능한 이유는, 파이썬의 기본동작과 manage.py 스크립트 실행 방식 때문에 프로젝트 루트 디렉토리가 Python Path에 추가되기 때문이다.

  • 파이썬은, 파이썬으로 스크립트를 직접 실행할 때(ex: python manage.py 실행) 스크립트가 위치한 디렉토리를 sys.path의 첫번째 항목(sys.path[0])에 자동 추가한다.
    • 참고로, 스크립트가 타 프로그램에 의해 간접 실행되는 경우, sys.path[0]에는 그 프로그램이 실행된 디렉토리(CWD)가 설정된다. 이는 아래의 "상대 경로 import(점 있는 import)" 절에서 더 다룬다.
  • manage.py는 Django 프로젝트의 루트 디렉토리에 위치하므로, python manage.py를 실행하면 루트 디렉토리가 sys.path[0]에 자동으로 추가된다.

아래처럼 manage.py를 수정해보면, manage.py 파일이 위치한 디렉토리(프로젝트 루트 디렉토리)가 출력되는 것을 확인 가능하다.

import sys
import os

# ... 코드 생략 ...

if __name__ == '__main__':
    print(sys.path[0])  # 현재 디렉토리인 프로젝트 최상위 디렉토리가 출력됨
    os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'myproject.settings')
    execute_from_command_line(sys.argv)

이렇게 해서 프로젝트 내의 모든 앱을 최상위 네임스페이스에서 접근 가능하게 된다(달리 말하면, sys.path에 프로젝트 루트 디렉토리가 포함되므로, 코드 작성시 프로젝트 내의 앱 디렉토리를 최상위 네임스페이스처럼 사용할 수 있게 된다).

 

1.1~1.3의 예시들을 정리하자면, 

  1. Python 기본 모듈 (예: datetime)은 Python 설치 경로의 기본 라이브러리 lib에서 찾는다.
  2. 외부 라이브러리 (예: pandas)는 Python 설치 경로의 site-packages에서 찾는다.
  3. Django의 앱 같은 로컬 패키지는 프로젝트 루트 디렉토리(Djangomanage.py 실행시 sys.path에 추가됨)에서 찾는다.

 

 

2. 상대 경로 import (점 있는 import)

현재 파일의 위치를 기준으로 다른 모듈의 위치를 지정하는 방식이다.

from .utils import helper          # 현재 디렉토리의 utils
from ..models import User          # 상위 디렉토리의 models
from ...core import settings       # 두 단계 상위 디렉토리의 core
 
프로젝트 실행 위치에 따라 import 문의 동작이 달라질 수 있다.
아래 구조의 FastAPI 프로젝트를 예로 들어보자:
my_fastapi_project/
    ├── src/
    │   ├── main.py
    │   ├── api/
    │   │   └── v1/
    │   │       └── endpoints.py
    │   └── utils/
    │       └── helpers.py

src 디렉토리에서 uvicorn으로 FastAPI 서버를 실행할 경우:

  1. 해당 명령어를 실행한 디렉토리 srcsys.path[0]에 추가된다.
  2. 이는  src에서 python main.py를 실행한 것과 동일한 효과를 가진다.
    • main.py가 위치한 src를 직접 sys.path[0]으로 넣는 효과를 의미한다.
# src/ 디렉토리에서
uvicorn main:app --reload
# src/main.py
from api.v1.endpoints import router  # ✅ 동작함
from utils.helpers import format_response  # ✅ 동작함

 

반면, 프로젝트 루트(my_fastapi_project)에서 uvicorn으로 FastAPI 서버를 실행할 경우:

  1. my_fastapi_projectsys.path[0]이 된다.
  2. uvicornmain.py를 직접 실행한 게 아니라, 자신의 CWD(현재 작업 디렉토리)에서 스크립트를 실행한 것이기 때문이다.
    • 스크립트를 간접 실행하는 프로그램의 경우, sys.path[0]에는 그 프로그램이 실행된 디렉토리(CWD)가 설정된다.
    • 이는 파이썬의 기본 동작이므로, uvicorn, pytest, 또는 다른 파이썬 기반 도구에서도 동일한 원칙이 적용된다.
  3. 따라서 위 예시의 import 문들은 동작하지 않는다.
# my_fastapi_project/ 디렉토리에서
uvicorn src.main:app --reload
# src/main.py
from api.v1.endpoints import router  # ❌ 실패
from src.api.v1.endpoints import router  # ✅ 동작함

# 또는 상대 경로 사용
from .api.v1.endpoints import router  # ✅ 동작함
 

 

3. 실제 프로젝트 예시

실제 프로젝트에서 import문을 어떻게 사용하는지 살펴보기 위해, 다음과 같은 구조의 프로젝트가 있다고 가정해보자:

myproject/
    └── src/
        ├── utils/
        │   ├── __init__.py
        │   └── helper.py
        └── models/
            ├── __init__.py
            ├── user.py
            └── services/
                └── auth.py

auth.py 파일에서 다른 모듈을 import할 때, 절대 경로 방식과 상대 경로 방식이 어떻게 사용되는지 비교해보자:

(myproject 폴더가 sys.path[0]에 등록된 상태임을 가정)

# 절대 경로 방식
from src.utils.helper import some_function
from src.models.user import User

# 상대 경로 방식
from ...utils.helper import some_function
from ..user import User

 

 

4. 언제 어떤 방식을 사용해야 할까?

상대 경로 import가 좋은 경우

  • 같은 프로젝트 내의 모듈을 import 할 때
  • 프로젝트 구조가 자주 변경될 수 있을 때
  • 코드의 재사용성을 높이고 싶을 때

절대 경로 import가 좋은 경우

  • 외부 라이브러리를 사용할 때
  • 프로젝트 구조가 안정적일 때
  • 코드의 명확성이 더 중요할 때

 

 

5. import 충돌 문제

동일한 import 문이 여러 모듈을 가리킬 수 있어 충돌이 발생할 수 있다.

주요 시나리오들을 살펴보자:

5.1 sys.path 충돌

# 프로젝트 구조:
my_project/
    utils/
        helper.py
    venv/
        lib/
            site-packages/
                utils/
                    helper.py  # pip로 설치된 외부 라이브러리

# my_project/main.py
from utils.helper import something  # 어떤 helper.py를 import 할까?

이 경우, 파이썬은 sys.path의 첫 번째로 매칭되는 모듈을 import한다.

5.2 이름 충돌

# 여러 utils.py가 있는 경우
from auth.utils import helper
from billing.utils import helper  # 이전 helper를 덮어씀

5.3 순환 import

# a.py
from .b import b_function

def a_function():
    print("This is a_function")
    b_function()

# b.py
from .a import a_function

def b_function():
    print("This is b_function")
    a_function()

이런 순환 import 문제는 특히 Django나 FastAPI 같은 웹 프레임워크에서 Model과 Serializer, 또는 Model과 View 사이에서 자주 발생한다. 가장 좋은 해결책은 애초에 순환 참조가 발생하지 않도록 코드를 구조화하는 것이지만, 불가피한 경우 위와 같이 지역 import를 사용하여 해결할 수 있다.

5.5 import 충돌 방지를 위한 Best Practices

1. 명확한 alias 사용하기

❌ alis 미사용:

# 여러 utils.py가 있는 경우
from auth.utils import helper
from billing.utils import helper  # 이전 helper를 덮어씀

✅ 명확한 alias 사용:

from auth.utils import helper as auth_helper
from billing.utils import helper as billing_helper

2. 패키지 이름을 구체적으로 짓기

❌ 안 좋은 예:

my_project/
    utils/
        date.py
        string.py    # Python 기본 모듈인 string과 이름이 같음
        json.py      # Python 기본 모듈인 json과 이름이 같음

✅ 좋은 예:

my_project/
    custom_utils/               # 더 구체적인 이름
        date_formatter.py      # 기능을 더 명확히 표현
        string_validator.py    # 기능을 더 명확히 표현
        json_handler.py       # 기능을 더 명확히 표현

3. 가능하면 상대경로보다 절대경로를 사용하기

❌ 복잡한 상대 경로:

# my_project/apps/users/views/auth/login.py
from ....utils.validators import validate_email  # 점이 너무 많아 혼란스러움
from ...models.user import User                 # 경로 파악이 어려움

✅ 명확한 절대 경로:

# my_project/apps/users/views/auth/login.py
from my_project.utils.validators import validate_email  # 명확한 경로
from my_project.apps.users.models import User          # 어디서 import하는지 바로 알 수 있음

4. 로컬 모듈과 외부 라이브러리 이름이 겹치지 않도록 주의하기

❌ 이름 충돌 가능성 있는 구조:

my_project/
    requests/            # requests는 유명한 HTTP 라이브러리 이름
        api.py
    json/               # json은 Python 기본 모듈 이름
        parser.py

✅ 충돌을 피한 구조:

my_project/
    http_client/        # requests 대신 더 구체적인 이름
        api.py
    json_processing/    # json 대신 더 구체적인 이름
        parser.py

이렇게 구조화하면, import문의 의미가 명확해진다.

# 혼란스러울 수 있는 import
import requests  # 외부 라이브러리? 로컬 모듈?
from json import loads  # Python 기본 모듈? 로컬 모듈?

# 명확한 import
import requests  # 확실히 외부 라이브러리
from http_client import api  # 확실히 로컬 모듈
from json_processing import parser  # 확실히 로컬 모듈

 

 

마무리

import 문은 단순해 보이나, 실제로는 복잡한 규칙과 주의사항들이 있다. 특히 프로젝트가 커질수록 import 충돌 문제는 디버깅이 어려운 버그의 원인이 될 수 있다. import에 대한 규칙들을 잘 이해하면, 더 견고한 파이썬 프로젝트를 만들 수 있다.

반응형

'Python > 문법' 카테고리의 다른 글

[Python] dataclass  (0) 2025.01.27
[Python] Module과 Package  (0) 2025.01.18
[Python] Module import로 이해하는 Namespace와 Scope  (1) 2025.01.09
[Python] Unpacking의 다양한 예시  (2) 2024.12.27
[Python] Iterable과 Sequence 자료형  (0) 2024.12.27
반응형

아래는 FastAPI에서 정적 파일을 제공하는 간단한 예시 코드이다.

# main.py

from fastapi import FastAPI
from fastapi.staticfiles import StaticFiles
from fastapi.responses import HTMLResponse

app = FastAPI()

# 정적 파일을 위한 디렉토리 마운트
app.mount("/static", StaticFiles(directory="static"), name="static")

@app.get("/", response_class=HTMLResponse)
async def read_root():
    return """
    <html>
        <head>
            <title>FastAPI Static Files Example</title>
        </head>
        <body>
            <h1>FastAPI Static Files 예시</h1>
            <img src="/static/example.jpg" alt="예시 이미지">
            <link rel="stylesheet" href="/static/styles.css">
        </body>
    </html>
    """

위 코드를 실행하려면 아래와  같은 디렉토리 구조가 필요하다.

your_project/
├── main.py (위 파일)
└── static/
    ├── example.jpg
    └── styles.css

 

정적 파일을 위한 디렉토리 마운트를 수행하는 코드를 각 파라미터 별로 살펴보자.

app.mount("/static", StaticFiles(directory="static"), name="static")

 

  1. "/static" (첫 번째 파라미터):
    • URL 경로를 지정한다.
    • 웹 애플리케이션에서 정적 파일에 접근할 때 사용할 기본 URL 경로이다.
      • ex: "/static/images/photo.jpg"처럼 접근
    • 다른 경로로도 변경 가능하다 (예: "/assets", "/public" 등)
  2. StaticFiles(directory="static") (두 번째 파라미터):
    • 실제 파일 시스템의 디렉토리를 지정한다
    • directory="static": 프로젝트 폴더 내의 'static' 폴더를 의미
    • 이 디렉토리 안에 있는 모든 파일들이 첫 번째 파라미터에서 지정한 URL 경로를 통해 접근 가능해진다.
    • 절대 경로도 사용 가능하다 (예: directory="/path/to/files")
  3. name="static" (세 번째 파라미터):
    • 마운트 지점의 이름을 지정한다.
    • FastAPI의 URL 생성 기능에서 이 이름을 참조할 때 사용 가능하다.
    • 템플릿에서 URL을 동적으로 생성할 때 이 이름을 사용할 수 있다(특히 Jinja2 템플릿에서 많이 사용됨).
    • 선택적 파라미터이다. (생략 가능)

 

정적 파일  마운팅의 여러 예시

from fastapi import FastAPI
from fastapi.staticfiles import StaticFiles

app = FastAPI()

# 기본적인 사용
app.mount("/static", StaticFiles(directory="static"), name="static")

# 다른 경로와 디렉토리 사용
app.mount("/assets", StaticFiles(directory="public_files"), name="assets")

# 절대 경로 사용
app.mount("/files", StaticFiles(directory="/var/www/files"), name="files")

 

 

실제 파일 시스템과 URL의 관계 예시

파일 시스템:        URL 접근:
static/             
├── images/         /static/images/
│   └── logo.png    /static/images/logo.png
├── css/            /static/css/
│   └── style.css   /static/css/style.css
└── js/             /static/js/
    └── script.js   /static/js/script.js

 

반응형
반응형

파이썬을 처음 배우다 보면 다음과 같은 의문이 생길 수 있다:

"서로 다른 파일에서 함수와 변수가 어떻게 서로를 찾아가는 걸까?"

 간단한 예제를 통해 이 개념을 명확하게 이해해보자.

 

아래와 같이 file_a.py file_b.py가 있다.

# file_a.py
some_variable = "Hello"

def some_function():
    print(some_variable)  # 이 some_variable은 어디서 오는가?
# file_b.py
from file_a import some_function

some_function()  # 실행하면 "Hello"가 출력됨

단순한 예제이지만, 파이썬의 중요한 동작원리가 숨어있다.

 

실행 순서 이해하기

python file_b.py 명령을 실행하면 다음과 같은 순서로 코드가 실행된다:

  1. file_b.py 실행 시작.
  2. from file_a import some_function 구문을 만남.
  3. 파이썬은 file_a.py를 찾아서 전체를 실행함.
    • some_variable = "Hello" 가 실행됨.
    • some_function() 함수가 정의됨.
  4. some_functionfile_b.py의 namespace로 가져옴.
    • file_a의 namespace에 있는 객체 중 오로지 some_function만 가져온 것.
    • 만약 모든 것을 가져오려면, from file_a import *를 사용해야 함.
  5. file_b.py에서 some_function() 호출.
    • "Hello"가 출력됨.

 

Namespace와 Scope

여기서 핵심적인 개념이 바로 namespacescope이다.

Namespace

정의: 코드가 실행되는 어떤 특정 시점에 사용 가능한 모든 이름과 그에 매핑된 객체들의 집합. 변수나 함수의 정의, import, 삭제 등의 작업이 발생할 때마다 namespace는 계속 변화한다.

# 시점 1: module이 처음 로드될 때
x = 1
# namespace: {'x': 1}

# 시점 2: 함수가 정의된 후
def add_number(y):
    return x + y
# namespace: {'x': 1, 'add_number': <function>}

# 시점 3: 새로운 변수가 추가된 후
z = 2
# namespace: {'x': 1, 'add_number': <function>, 'z': 2}

# 시점 4: 기존 변수가 삭제된 후
del x
# namespace: {'add_number': <function>, 'z': 2}
# 시점 1: 스크립트 시작
# namespace: {}

# 시점 2: math module의 import 후
import math
# namespace: {'math': <module>}

# 시점 3: math에서 특정 함수만 import 후
from math import sin
# namespace: {'math': <module>, 'sin': <function>}
  • 각 파이썬 module은 자신만의 namespace를 가진다.
    • file_a의 namespace는 some_variablesome_function의 집합으로 구성된다.
    • some_variablefile_a의 namespace에 존재하는 객체이다.
  • 한 module에서 다른 module의 객체를 직접 import해야, namespace에 해당 객체가 비로소 추가된다.
    • file_b에서 file_asome_variable을 import하므로, file_b의 namespace에 some_variable이 추가된다.

Scope

정의: "변수의 유효 범위" 혹은 "이름이 유효한 범위"를 의미한다. 즉, namespace의 유효 범위를 의미한다.

Scope는 코드 블록에서 변수가 접근 가능한 범위를 정의한다.

LEGB 규칙(Scope의 4단계)

  1. Local(L): 가장 안쪽의 함수나 클래스 메서드 내부
  2. Enclosing(E): 중첩 함수에서 바깥쪽 함수의 범위
  3. Global(G): module 수준의 전역 범위
  4. Built-in(B): 파이썬 내장 함수와 예약어가 있는 범위

Scope 검색 순서

파이썬에서 변수를 찾을 때는 다음 순서로 검색한다:

 

  1. local scope (함수 내부)
  2. enclosing scope (중첩 함수에서 바깥 함수 범위)
  3. global scope (Module 레벨)
  4. built-in scope (파이썬 내장 함수/변수)

따라서 some_functionsome_variable을 찾는 과정은 다음과 같다:

  1. local scope 검색: 함수 내부에 없음.
  2. enclosing scope 검색: 중첩 함수 구조가 아니므로 미해당.
  3. global scope 검색: file_a의 module 레벨에서 찾아냄.
  4. built-in scope 검색: global scope에서 찾았으므로 여기까지 오지 않음.

 

Module import의 특징

파이썬의 module 시스템에는 다음과 같은 중요한 특징이 있다:

  1. 한 번만 실행:
    • Module은 처음 import될 때만 실행된다.
    • 같은 module을 여러 번 import해도 한 번만 실행된다.
  2. 클로저(Closure):
    • 함수는 자신이 정의된 환경의 변수들을 기억한다.

 

(심화) 함수는 "자신이 정의된 환경"의 어디까지 기억하는가?

 

  • 함수는 자신의 enclosing scope의 변수들 중 자신이 사용하는 변수들만 기억한다.
  • global scope의 변수는 클로저에 저장되지 않고, 필요할 때 module에서 찾는다.
  • built-in scope의 함수/변수들도 저장되지 않고, 필요할 때 찾는다.
# global scope
global_var = "global"

def outer():
    outer_var = "outer"  # enclosing scope
    
    def inner():
        local_var = "local"  # local scope
        print(local_var)    # 1. 당연히 접근 가능
        print(outer_var)    # 2. outer 함수의 변수 기억
        print(global_var)   # 3. global 변수도 접근 가능
        print(len("test"))  # 4. built-in 함수도 사용 가능
    
    return inner  # inner 함수를 반환

# inner 함수를 할당
func = outer()

# 나중에 실행해도 outer_var를 기억
func()  # 모든 출력이 정상적으로 작동

 

 

1. 클로저의 범위

def outer():
    outer_var = "remembered"  # 이 변수는 클로저에 저장됨
    
    def inner():
        print(outer_var)
    
    return inner

func = outer()
print(func.__closure__)  # 클로저 정보 확인 가능

2. 실제로 기억하는 것

def outer():
    outer_var = "outer"
    other_var = "not used"  # inner에서 사용하지 않음
    
    def inner():
        print(outer_var)
    
    return inner

func = outer()
# outer_var만 클로저에 저장되고
# other_var는 저장되지 않음

3. 기억하지 않는 것

global_var = "global"

def simple_func():
    print(global_var)

# global_var는 클로저에 저장되지 않음
# 왜냐하면 module 레벨(global scope)의 변수이기 때문

 

따라서 앞선 예제의 some_functionsome_variable의 값을 기억하는게 아니라, some_variable이 global scope의 변수이므로 자신이 정의된 module인 file_a.py에서 찾아보는 것이다.

# file_a.py
some_variable = "Hello"

def some_function():
    print(some_variable)

# 클로저 확인
print(some_function.__closure__)  # None이 출력됨
# 클로저가 None이라는 것은 저장된 변수가 없다는 의미

 

 

결론

파이썬의 namespace와 scope는 변수와 함수의 가시성과 생명주기를 결정하는 중요한 개념이다. 이들을 제대로 이해하고 활용하면, 변수 이름 충돌을 방지하고, 코드의 module성을 높이는 데 유용하다.

반응형

'Python > 문법' 카테고리의 다른 글

[Python] Module과 Package  (0) 2025.01.18
[Python] import 문 사용 팁  (0) 2025.01.13
[Python] Unpacking의 다양한 예시  (2) 2024.12.27
[Python] Iterable과 Sequence 자료형  (0) 2024.12.27
[Python] 제너레이터(Generator)  (1) 2024.12.14
반응형

https://www.acmicpc.net/problem/10815

여러 숫자 카드들의 목록이 주어지고(최대 50만개) 또 숫자들의 목록이 주어졌을 때(역시 최대 50만개), 해당 숫자들을 숫자 카드로 가지고 있는지 체크하는 문제이다.

숫자들이 숫자 카드 목록 중에 있는지 확인하기 위해 tuple 목록에 대해 in 연산자를 사용하는 방식을 사용했더니, 시간 초과가 발생했다.

시간 초과 발생 코드

import sys

lines = sys.stdin.readlines()
cards = tuple(lines[1].split())
questions = lines[3].split()

sys.stdout.write(" ".join(("1" if q in cards else "0" for q in questions)))

in 연산자를 사용하는 것의 문제는, list 및 tuple의  in 연산자에 의한 list 탐색은 선형탐색이라 O(N)의 시간복잡도를 가지므로, 카드 수가 많을 경우 비효율적이게 된다.

 

set 자료형을 활용하면 검색 속도를 O(1)로 줄일 수 있다는 것을 활용해보자. 이는 set은 해시 테이블 기반으로 구현되어, 검색(in 연산)의 시간복잡도가 평균 O(1)이기 때문이다(해시 충돌이 많이 발생하면 O(N)이 될 수 있지만, 해시 함수 설계가 잘 되어 있다면 드문 경우이다).

정답 코드

import sys

lines = sys.stdin.readlines()
cards = set(lines[1].split())
questions = lines[3].split()

sys.stdout.write(" ".join(("1" if q in cards else "0" for q in questions)))

결과적으로, 위 시간 초과가 발생한 코드에서 달라진 점은 tuple이 아닌 set으로 숫자 카드를 저장한다는 것이다.

이렇게 자료구조만 바뀌었는데도, 같은 in 연산을 사용할 때 성능 차이가 발생한다는 것을 확인할 수 있는 좋은 문제라고 생각한다.

 

Set의 시간 복잡도 정리

연산 평균 시간 복잡도 최악 시간 복잡도
삽입 (add) O(1) O(N)
삭제 (remove, discard) O(1) O(N)
조회 (in) O(1) O(N)
순회 O(N) O(N)
반응형
반응형

https://www.acmicpc.net/problem/18870

최대 1,000,000개 숫자의 1차원 array가 주어졌을 때, 각 숫자에 대해 array 중 해당 숫자보다 작은 숫자들에 갯수를 출력하는 문제이다.

 

각 숫자가 unique하게 발생되는 목록을 sort한 것에서(unique_ordered), 숫자의 index를 찾으면 그 값이 곧 그 숫자보다 작은 숫자들의 갯수가 될 것이라고 생각해 풀이해봤으나 시간초과가 발생했다. 

시간 초과 발생 코드

import sys

x = tuple(map(int, sys.stdin.readlines()[1].rstrip().split()))
unique_ordered = sorted(set(x))
sys.stdout.write(" ".join(str(unique_ordered.index(i)) for i in x))

unique_ordered.index(i)는 list에서 특정 값의 index를 찾기 위해 list의 길이만큼 순차 검색을 수행한다. 이는 O(N) 시간이 걸리며, 이를 x의 모든 요소에 대해 반복하기 때문에 전체 복잡도가 O(N^2)가 된다. 1,000,000 ^ 2는 1조에 해당되므로 시간초과가 발생할 수밖에 없게 된다.

 

x의 각 숫자에 대해  unique_ordered를 매번 순회해서 index값을 찾는 대신에, unique_ordered를 단 1회 순회하면서 unique_ordered의 각 값과 index를 매핑하는 dict(아래의 index_map)를 만드는 방법이 있다.

정답 코드

import sys

x = tuple(map(int, sys.stdin.readlines()[1].rstrip().split()))
unique_ordered = sorted(set(x))
index_map = {v: i for i, v in enumerate(unique_ordered)}
sys.stdout.write(" ".join(str(index_map[i]) for i in x))

이렇게 코드를 수정하면, x의 모든 요소에 대해 index값 탐색이 dict 해싱 덕분에 O(1)로 해결된다. 즉 전체 x의 index 값 탐색은 O(N)에 해당된다.

 

정답 코드의 시간 복잡도

  • set과 sorted (unique_ordered 만들기) → O(N log N)
  • dict 생성 및 결과 조회 (index_map 만들기 및 전체 index값 조회 → O(N)

=> 전체 복잡도: O(N log N) 이 산출된다.

반응형
반응형

https://www.acmicpc.net/problem/10989

 

N개의 수가 주어졌을 때, 오름차순으로 정렬하는 프로그램을 작성해야 한다.

(조건: 1 ≤ N ≤ 10,000,000 이며,  각 숫자는 10,000보다 작거나 같은 자연수)

메모리 제한이 단 8MB에 불과해, 메모리 최적화가 중요한 문제이다.

 

단순히 list를 정렬하는 방식으로는 해결할 수 없다.

숫자의 갯수가 1천만개까지 가능하기 때문에, 이를 모두 담기 위한 list의 메모리 크기를 대략 계산해보자.

  • 10,000 이하의 자연수가 각각 int로 저장되는데, 파이썬의 int 형은 64bit 시스템에서 28 byte 정도가 소요된다고 한다.
  • List 자체는 각 자연수에 대한 포인터를 저장하는데, 그리고 각 포인터는 8 byte를 차지한다.

결론적으로, 단순히 모든 숫자를 list에 개별 저장하려면, 대략 (8 byte + 28 byte) * 10,000,000 = 360MB의 메모리가 필요하므로, 문제의 제한 사항을 아득히 뛰어넘게 된다.

 

대신 Counting Sort(계수 정렬)을 사용해야 한다. 

Counting Sort는 주어진 데이터에서 각 값의 빈도를 세어 해당 값들이 몇 번 나타나는지 기록한 후, 빈도 정보에 따라 데이터를 정렬하는 알고리즘이다. 이 알고리즘은 데이터의 값의 범위가 적고, 빈도가 많은 경우에 매우 효율적이다. 시간 복잡도는 O(N + K)로, N은 데이터의 개수, K는 데이터의 값 범위이다. 다만, 값의 범위가 너무 넓으면 메모리 사용량이 급격히 증가할 수 있다

 

Couting Sort를 사용해 간단히 구현해봤다. 작은 숫자부터 시작해서, 각 숫자의 발생 갯수만큼 출력하면된다.

그러나 아래처럼 메모리 초과가 발생하게 되었다.

메모리 초과 발생 코드

import sys

n = int(next(sys.stdin))
l = [0] * 10_001
for i in sys.stdin:
    l[int(i)] += 1

for i in range(1, 10_001):
    sys.stdout.write(f"{i}\n" * l[i]) # 메모리 초과 원인

메모리 초과가 발생한 이유는, 최종 결과 출력시 문자열 곱셈을 너무 naive하게 수행했기 때문이다. l[i]의 값이 최대 10,000,000이 될 수 있기 때문에, 수천만 byte짜리 string이 생길 수도 있는 코드 작성이다.

 

위 오류를 수정해서 통과하는 코드를 작성했다.

정답 코드 -  메모리 사용 

import sys

n = int(next(sys.stdin))
l = [0] * 10_001
for i in sys.stdin:
    l[int(i)] += 1

for i in range(1, 10_001):
    for _ in range(l[i]):
        sys.stdout.write(f"{i}\n")

개별 숫자에 대해 sys.stdout.write()를 일일이 호출하는 방식이다. 이 방식은 여러 문자를 합친 문자열을 출력하지 않으므로, 메모리 공간은 그만큼 사용하지 않을 수는 있으나, sys.stdout.write() 의 호출이 그만큼 증가하므로 성능에 페널티가 있을 수 있다.

 

이어서, 개별 숫자에 대해 일일이 sys.stdout.write() 를 호출하지 않고, 그룹화시킨 것에 대해 호출하는 최적화를 시도했다.

성능 개선된 정답 코드 -  write 함수 호출 횟수 줄이기(성능과 메모리 사용 간의 최적화)

import sys

n = int(next(sys.stdin))
l = [0] * 10_001
for i in sys.stdin:
    l[int(i)] += 1

BUFFER_SIZE = 1_000
for i in range(1, 10_001):
    for _ in range(l[i] // BUFFER_SIZE):
        sys.stdout.write(f"{i}\n" * BUFFER_SIZE)
    for _ in range(l[i] % BUFFER_SIZE):
        sys.stdout.write(f"{i}\n")

개별 숫자를 1,000개씩 buffer에 모아서 출력하도록 변경했다.

개별 숫자의 문자열("1"~"10000")의 크기는 5 byte이내이므로, 각 buffer의 메모리 크기는 5,000 byte에 해당된다.

이러면 8 MB의 메모리 제약 조건을 안전하게 지키면서, sys.stdout.write()의 호출 횟수를 크게 줄일 수 있다.

예를 들어, 숫자 1이 1,000번 등장했다고 가정하자.

이전 코드에서는 1에 대해  sys.stdout.write("1\n") 를 1,000번 호출했어야 했으나, 개선된 코드에서는 sys.stdout.write("1\n" * 1000) 를 한번만 호출하면 된다.

 

 

추가 개선 방법?

  • input, output 함수를 다른 것을 사용해서 입출력에서 성능 개선을 볼 수 있을 것 같다.
  • 그렇지만 현재 작성된 코드가 간결하기 때문에 불필요할 수도 있다.
반응형

+ Recent posts