언어적 특징

  • 인터프리터(Interpreter) 언어로 별도의 컴파일 없이 실행된다.

  • 별도로 타입 명시를 하지 않으며, 실행 중에 타입이 변경 가능(Dynamically typed)하다. 

  • 객체 지향적(Object-oriendted) 언어로, 접근 제한자를 가지지 않지만 네이밍(Naming)의 언더스코어(_)로 접근 제한을 구분한다.

    • 이름 맨 앞에 언더스코어 1개이면 protected, 2개이면 private 이며, 그 외는 public 으로 구분한다.

  • 함수(function)와 클래스(class)는 일급 객체(The first-class objects)로, 모든 변수(매개변수, 로컬변수 등)에 할당 가능하며 일반적으로 다른 객체에 적용 가능한 연산을 모두 지원한다.

  • PYTHONPATH인터프리터가 파이썬 프로그램이 실행될 때 import된 모듈들이 실제 디스크에 어디에 위치했는지 알기 위해 사용하는 환경변수. 따라서 프로그램이 내장 모듈에 대해 ImportError 를 일으키면 가장 먼저 살펴봐야하는 부분이다.

  • PEP는 Python Enhancement Proposal의 약자로, 가독성 높은 파이썬의 코딩 스타일 중 하나이며, PEP8이 대중적이다.

  • 주석은 # comment""" comment """ 가 있는데, 후자는 docstring 용도로 사용된다.

    • 참고: Docstrings are not actually comments, but, they are documentation strings. These docstrings are within triple quotes. They are not assigned to any variable and therefore, at times, serve the purpose of comments as well.

  • 파이썬 인터프리터에서는 help() 와 dir() 이라는 함수를 사용할 수 있는데, 라이브러리를 잘 모를 경우 굉장히 유용하다.

    • The help() function is used to display the documentation string and also facilitates you to see the help related to modules, keywords, attributes, etc.

    • The dir() function is used to display the defined symbols. (클래스의 어떤 메소드가 있는지 확인할 때 매우 편하다.)

  • 파이썬 패키지(package)는 여러 모듈을 포함하는 네임스페이스의 집합이다. 최상위 패키지 디렉토리 안에 패키지 디렉토리가 있을 수 있는데 이들 안에 __init__.py 를 작성하면 해당 디렉토리 패키지임을 명시하는 것이다. 따라서 다른 디렉토리에서 다음과 같이 해당 모듈을 사용할 수 있다.

"""
예시: from 패키지명 import 모듈명

~/mypkg $ tree
.
├── utils
│   └── __init__.py
│   └── mini_module.py
│   └── large_module.py
└── subpkg
    ├── __init__.py
    └── new_module.py

mypkg 를 상위 디렉터리로 하고 위와 같은 구조를 가질 때,
subpkg의 new_module.py에서는 아래의 형태로 utils 의 모듈을 사용할 수 있다.
"""

from utils import mini_moudle
from utils.mini_module import *
from utils.large_moudle import MyObject

 

메모리 관리

  • 파이썬은 내부적(C언어로 작성되었음)으로 PyObject 라는 객체를 생성해서 데이터를 관리하는데, 데이터들은 Python Private Heap Space 라는 곳에 저장된다. 일반적으로 지역 변수는 스택에 저장된다고 배웠으나, 곧 살펴볼 파이썬의 내장 타입들은 모두 객체이며 힙 영역에 저장된다.

>>> a = 1
>>> type(a)
<class 'int'>
  • Private Heap 은 파이썬이 스스로 관리하는 곳이기 때문에 파이썬 코드로는 프로그래머가 직접적으로 메모리를 할당하거나 해제할 수 없다. 직접 살펴보고 싶다면 파이썬 C 라이브러리를 사용해야 한다.

  • PyObject는 멤버로 참조 카운트(reference count)를 가지는데, 이는 객체가 참조될 때 1씩 증가하고 참조되지 않을 경우 1씩 감소한다. 그리고 reference count = 0 이 되면 메모리는 해제된다.
    • sys 모듈의 getrefcount() 함수를 이용해서 count를 직접 확인해볼 수 있다.

    • 아래의 첫 출력 결과가 2인 것은 sys.getrefcount() 함수의 인자로 참조되었기 때문에 1이 아니라 2로 출력되는 것이다.

>>> import sys
>>> a = []
>>> sys.getrefcount(a)
2
>>> b = a
>>> sys.getrefcount(a)
3
>>> del a
>>> sys.getrefcount(b)
2
  • 그러나, 아래와 같이 순환 참조(Circular References)가 발생할 경우 레퍼런스 카운팅 기법으로는 메모리를 관리할 수 없다.

    • 순환 참조란 서로 다른 객체가 서로를 참조하는 경우를 의미한다. 여기서 두 객체가 삭제될 경우 참조 횟수는 0보다 큰 상태이지만 객체를 이미 삭제해버려서 해당 객체들에 접근할 수 없다는 문제가 발생한다.

>>> class Obj:
...     def __init__(self):
...         print('Hello,', id(self))
...
...     def __del__(self):
...         print('Bye,', id(self))
...
>>> o1 = Obj()
Hello, 4470886656
>>> o2 = Obj()
Hello, 4470654432
>>> sys.getrefcount(o1)
2
>>> sys.getrefcount(o2)
2
>>> dir() # 선언한 클래스와 생성된 인스턴스가 네임스페이스에 존재한다.
['Obj', '__annotations__', '__builtins__', '__doc__', '__loader__', '__name__',
'__package__', '__spec__', 'o1', 'o2']
>>> o1.x = o2
>>> o2.x = o1
>>> sys.getrefcount(o1)
3
>>> sys.getrefcount(o2)
3
>>> del o1 # o2.x 로 참조되므로 o1의 reference count = 1
>>> del o2 # o1.x 로 참조되므로 o2의 reference count = 1
>>> dir() # 삭제되었기 때문에 네임 스페이스에는 없으나 실제로 __del__() 메소드는 호출되지 않았다.
['Obj', '__annotations__', '__builtins__', '__doc__', '__loader__', '__name__',
'__package__', '__spec__']
  • 순환 참조와 같은 이슈를 해결하기 위해 파이썬은 가비지 콜렉터(Garbage Collector) 기능을 제공한다. 가비지 콜렉터는 gc 모듈을 import 해서 수동으로 관리할 수 있지만 파이썬에서는 자동으로 관리할 것은 권장한다.

  • 가비지 콜렉팅은 다음과 같이 동작한다.

    • 0~2세대로 객체들을 구분(이를 Generational Garbage Collecting 이라 한다.)하는데, 처음 객체가 생성되면 0세대로 분류된다. 그리고 각 세대별로 저장된 객체가 임계값(threshold, 최대로 저장할 객체의 개수)에 이르면 쓰레기 수집(collect)이 진행된다.

    • 한 번 수집기가 실행될 때마다 해당 프로그램은 중단되므로 성능에 큰 영향을 미친다.

    • 수집기가 실행되고 각 세대별로 살아남은 객체는 다음으로 옮겨진다. 0세대에서 살아남은 객체는 1세대로, 1세대에서 살아남은 객체는 2세대로 옮겨진다.

    • 임계값은 세대마다 다르며, Generational Hypothesis(최근에 생성된 객체일수록 빨리 제거되며, 최근에 생성된 객체는 오래된 객체를 참조할 가능성이 적다는 가설)에 따라 0세대의 임계값이 가장 높다.

  • gc 모듈을 이용해서 임계값을 바꾸거나 쓰레기 수집 기능을 직접 실행할 수 있다.

    • 임계값 변경 함수: gc.set_threshold(threshold0[, threshold1[, threshold2]])

    • 쓰레기 수집 기능 함수: gc.collect()

>>> import gc
>>> gc.get_threshold()	# 0세대는 700, 1세대는 10, 2세대는 10개까지 보관한다.
(700, 10, 10)
>>> gc.get_count()	# 0세대는 71개의 객체가 있다.
(71, 0, 0)
>>> gc.collect()	# 순환 참조로 삭제되지 않았던 객체들이 삭제되었다.
Bye, 4470886656
Bye, 4470654432
4
>>> gc.get_count()
(22, 0, 0)
  • 그 외에 참조 카운트로 메모리를 관리하다보니 Global Interpreter Lock(이하 GIL) 과 관련된 이슈도 존재한다. GIL은 파이썬 인터프리터 전역에 락 변수(lock variable)를 설정해놓은 것으로, 멀티 쓰레딩 시 임계 영역(critical section)에서는 단일 쓰레드만이 실행되도록 한다.

    • 임계 영역이란 둘 이상의 쓰레드가 동시에 접근했을 때 프로그램의 실행에 critical 하게 영향을 줄 수 있는 부분을 뜻한다.

    • 한 프로세스에서 실행중인 쓰레드들은 스택(+레지스터)을 제외한 나머지 모든 영역을 공유한다.

  • GIL 을 도입한 이유는 파이썬의 모든 객체가 힙에 저장되다 보니 멀티쓰레딩 환경에서 reference count 변수에 경쟁 조건(race condition)이 발생하는 것을 막기 위해서이다. 만약 reference count 변수를 멤버로 가지는 파이썬 객체마다 락 변수를 걸게 되면 데드락(deadlock)이 발생할 수 있으며 성능에 좋지 않기 때문에 파이썬 인터프리터 전역적으로 막아버린 것이다. 즉, 파이썬에서 단일 쓰레드만이 특정 객체 접근할 수 있도록 했다. 따라서 threading 모듈을 이용해서 멀티쓰레딩을 하더라도 실제로는 1개의 CPU가 여러 쓰레드를 번갈아가면서 실행시키다보니 컨텍스트 스위칭(context switching)이 빈번하게 발생하여 단일 쓰레드보다 성능이 떨어질 수 있다. (물론 이와 관련된 해결책으로 비동기 프로그래밍, 멀티프로세싱 등이 있는데 다음에 후술하겠다.)

 

Built-in Types

  • Integers

  • Floating-point

  • Complext numbers

  • Strings

  • Boolean

  • Built-in functions: 파이썬 인터프리터에 내장된 함수들로 위에서 설명했던 help()와 dir()이 이에 해당된다. 별도의 모듈을 import하지 않고 사용가능하다.

    • type() vs isinstance(): 파이썬의 데이터들은 모두 객체로 저장되지만, 내장형 타입(표준 데이터 타입)을 검사하기 위해 type(변수명) 함수를 사용하고 커스텀 클래스로 만든 객체들은 isinstance(변수명, 클래스명)로 클래스 타입을 검사한다. 즉, 파이썬 환경에서 인스턴스(=객체)와 내장형 타입을 구분하기 위해 제공하는 함수라고 볼 수 있다. 물론 표준 데이터 타입에 대해서도 isinstance 함수를 사용할 수 있다. 예를 들어, x = 1; isinstance(x, int); 의 출력 결과는 True 이다.

  • 내장형 타입 관련 연산

  • 비교 연산자

    • 파이썬에서는 직접 변수의 값을 비교할 때는 == 연산자를 사용하지만, 메모리 주소를 비교할 때(=동일 객체인지 등)는 is 연산자를 사용한다. 변수에 저장된 메모리 주소를 확인하려면 id(변수명) 함수를 사용하면 된다.

>>> a = 1000
>>> b = 1000
>>> a == b
True
>>> a is b
False
>>> id(a), id(b)
(4398724144, 4398724176)

 

+ Recent posts