CODICT

9. Python으로 시스템 다루기 본문

Programming/Python

9. Python으로 시스템 다루기

Foxism 2020. 3. 16. 06:18

컴퓨터를 사용하면, 폴더 또는 디렉터리의 컨텐츠를 나열하고, 파일을 생성하거나 지우고, 이들을 정리하는 일을 매일 수행하게 됩니다. 이러한 작업을 파이썬으로 해결할 수 있습니다.

 

1. 파일

 

파이선은 많은 다른 언어처럼 유닉스의 파일 연산 패턴을 갖고 있습니다. chown(), chmod() 함수 등은 똑같은 이름을 사용합니다. 그리고 몇 가지 새로운 함수가 존재합니다.

 

1.1. 생성하기: open()

 

이전 게시글의 파일 입출력과 관련된 부분에서 open() 함수를 소개하면서 파일을 여는 방법과 파일이 존재하지 않는 경우 새로운 파일을 생성하는 방법을 배웠습니다. 먼저 oops.txt 텍스트 파일을 생성해봅시다.

>>> fout = open('oops.txt', 'wt')
>>> print('Oops, I created a file.', file=fout)
>>> fout.close()

이제 몇 가지 테스트를 수행해 봅시다.

 

1.2. 존재여부 확인하기: exists()

 

파일 혹은 디렉터리가 실제로 존재하는지 확인하기 위해 exitsts() 함수를 사용합니다. 다음 코드와 같이 상대 경로와 절대 경로를 사용할 수 있습니다.

>>> import os
>>> os.path.exists('oops.txt')
#True
>>> os.path.exists('./oops.txt')
#True
>>> os.path.exists('waffles')
#False
>>> os.path.exists('.')
#True
>>> os.path.exists('..')
#True

 

1.3. 타입 확인하기: isfile()

 

이 절에 등장하는 세 함수(isfile, isdir, isabs)는 이름이 파일인지, 디렉터리인지, 또는 절대 경로인지 확인합니다.

먼저 isfile() 함수를 사용하여 평범한 파일인지 간단한 질문을 던져봅니다.

>>> name = 'oops.txt'
>>> os.path.isfile(name)
#True

디렉터리는 다음과 같이 질문할 수 있습니다.

>>> os.path.isdir(name)
#False

하나의 점(.)은 현재 디렉터리를 나타내고, 두 개의 점(..)은 부모(상위) 디렉터리를 나타냅니다. 이들은 항상 존재하기 때문에 True를 반환합니다.

>>> os.path.isdir('.')
#True

os 모듈은 절대 경로와 상대 경로를 처리하는 많은 함수를 제공합니다. isabs() 함수는 인자가 절대 경로인지 확인합니다. 실제로 존재하는 파일 이름을 인자에 넣지 않아도 됩니다.

>>> os.path.isabs('name')
#False
>>> os.path.isabs('/big/fake/name')
#True
>>> os.path.isabs('big/fake/name/without/a/leading/slash')
#False

 

1.4. 복사하기: copy()

 

copy() 함수는 shutil이라는 다른 모듈에 들어 있습니다. 다음 예제는 oops.txt를 ohno.txt로 복사합ㄴ디ㅏ.

>>> import shutil
>>> shutil.copy('oops.txt', 'ohno.txt')
#'ohno.txt'

shutil.move() 함수는 파일을 복사한 후 원본 파일을 삭제합니다.

 

1.5. 이름 바꾸기: rename()

 

rename()은 말 그대로 파일 이름을 변경합니디. 다음 예제는 ohno.txt를 ohwell.txt로 이름을 바꿉니다.

>>> import os
>>> os.rename('ohno.txt', 'ohwell.txt')

 

1.6. 연결하기: link(), symlink()

 

유닉스에서 파일은 한 곳에 있지만, 링크(Link)라 불리는 여러 이름을 가질 수 있습니다. 저수준의 하드 링크(Hard Link)에서 주어진 파일을 모두 찾는 것은 쉬운 일이 아닙니다. 심벌릭 링크(Symbolic Link)는 원본 파일을 새 이름으로 연결합니다. 원본 파일과 새 이름의 파일을 한 번에 찾을 수 있도록 해줍니다. link() 함수는 하드 링크를 생성하고, symlink() 함수는 심벌릭 링클르 생성합니다. islink() 함수는 파일이 심벌릭 링크인지 확인합니다.

oops.txt 파일의 하드 링크인 새 yikes.txt 파일을 만들어봅시다.

>>> os.link('oops.txt', 'yikes.txt')
>>> os.path.isfile('yikes.txt')
#True

oops.txt 파일의 심벌릭 링크인 새 jeepers.txt 파일을 만들어봅시다.

>>> os.path.islink('yikes.txt')
#False
>>> os.symlink('oops.txt', 'jeepers.txt')
>>> os.path.islink('jeepers.txt')
#True

 

1.7. 퍼미션 바꾸기: chmod()

 

유닉스 시스템에서 chmod()는 파일의 퍼미션(Permission)을 변경합니다. 사용자에 대한 읽기, 쓰기, 실행 퍼미션이 있습니다. 그리고 사용작 속한 그룹과 나머지에 대한 퍼미션이 각각 존재합니다. 이 명령은 사용자, 그룹, 나머지 퍼미션을 묶어서 압축된 8진수의 값을 취합니다. oops.txt를 이 파일의 소유자(파일을 생성한 사용자)만 읽을 수 있도록 만들어봅시다.

>>> os.chmod('oops.txt', 0o400)

이러한 수수께끼 같은 8진수 값을 사용하기보다는 (약간) 잘 알려지지 않은 아리송한 심벌을 사용하고 싶다면 stat 모듈을 임포트하여 다음과 같이 쓸 수 있습니다.

>>> import stat
>>> os.chmod('oops.txt', stat.S_IRUSR)

 

1.8. 오너십 바꾸기: chown()

 

이 함수는 유닉스/리눅스/맥에서 사용됩니다. 숫자로 된 사용자 아이디(uid)그룹 아이디(gid)를 지정하여 파일의 소유자와 그룹에 대한 오너십(Ownership)을 바꿀 수 있습니다.

>>> uid = 5
>>> gid = 22
>>> os.chown('oops', uid, gid)

 

1.9. 절대 경로 얻기: abspath()

 

이 함수는 상대 경로를 절대 경로로 만들어줍니다. 만약 현재 디렉터리가 /Users/D3C1이고, oops.txt 파일이 거기에 있다면, 다음과 같이 입력할 수 있습니다.

>>> os.path.abspath('oops.txt')
#'C:\\Users\\D3C1\\oops.txt'

 

1.10. 심벌릭 링크 경로 얻기: realpath()

 

이전 절에서 oops.txt 파일의 심벌릭 링크인 jeepers.txt 파일을 만들었습니다. 다음과 같이 relapath() 함수를 사용하여 jeepers.txt 파일로부터 원본 파일인 oops.txt 파일의 이름을 얻을 수 있습니다.

>>> os.path.realpath('jeepers.txt')
#'C:\\Users\\D3C1\\jeepers.txt'

 

1.11. 삭제하기: remove()

 

remove() 함수를 사용하여 짧은 순간이었지만, 즐거웠던 oops.txt 파일과 작별인사를 나눕시다.

>>> os.remove('oops.txt')
>>> os.path.exists('oops.txt')
#False

 

2. 디렉터리

 

대부분의 운영체제에서 파일은 디렉터리(Directory)의 계층구조 안에 존재합니다(최근에는 폴더(Folder)라고 부릅니다). 이러한 모든 파일과 디렉터리의 컨테이너는 파일시스템입니다(볼륨(Volume)이라고도 부릅니다). 표준 os 모듈은 이러한 운영체제의 특성을 처리하고, 조작할 수 있는 함수를 제공합니다.

 

2.1. 생성하기: mkdir()

 

시를 저장할 poems 디렉터리를 생성합니다.

>>> os.mkdir('poems')
>>> os.path.exists('poems')
#True

 

2.2. 삭제하기: rmdir()

 

앞에서 생성한 poems  디렉터리를 삭제합니다.

>>> os.rmdir('poems')
>>> os.path.exists('poems')
#False

 

2.3. 콘텐츠 나열하기: listdir()

 

다시 poems 디렉터리를 생성합니다.

>>> os.mkdir('poems')

그리고 이 디렉터리의 콘텐츠를 나열합니다(지금까진 아무것도 없습니다).

>>> os.listdir('poems')
#[]

이제 하위 디렉터리를 생성합니다.

>>> os.mkdir('poems/mcintyre')
>>> os.listdir('poems')
#['mcintyre']

하위 디렉터리에 파일을 생성합니다(시를 매우 좋아하는게 아니면 적당히 씁시다).

>>> fout = open('poems/mcintyre/Ode_On_The_Mammoth_Cheese', 'wt')
>>> fout.write('''We have seen the Queen of cheese,
Laying quietly at your ease,
Gently fanned by evening breeze --
Thy fair form no flies dare seize.

All gaily dressed soon you'll go
To the great Provincial Show,
To be admired by many a beau
In the city of Toronto.

Cows numerous as a swarm of bees --
Or as the leaves upon the trees --
It did require to make thee please,
And stand unrivalled Queen of Cheese.

May you not receive a scar as
We have heard that Mr. Harris
Intends to send you off as far as
The great World's show at Paris.

Of the youth -- beware of these --
For some of them might rudely squeeze
And bite your cheek; then songs or glees
We could not sing o' Queen of Cheese.

We'rt thou suspended from baloon,
You'd cast a shade, even at noon;
Folks would think it was the moon
About to fall and crush them soon.''')
#814
>>> fout.close()

드디어 파일이 생겼습니다. 디렉터리의 콘텐츠를 나열해봅시다.

>>> os.listdir('poems/mcintyre')
#['Ode_On_The_Mammoth_Cheese']

 

2.4. 현재 디렉터리 바꾸기: chdir()

 

이 함수를 이용하여 현재 디렉터리에서 다른 디렉터리로 이동할 수 있습니다. 즉, 현재 디렉터리를 바꿀 수 있습니다. 현재 디렉터리를 떠나서 poems 디렉터리로 이동해봅시다.

>>> import os
>>> os.chdir('poems')
>>> os.listdir('.')
#['mcintyre']

 

2.5. 일치하는 파일 나열하기: glob()

 

glob() 함수는 복잡한 정규표현식이 아닌, 유닉스 쉘 규칙을 사용하여 일치하는 파일이나 디렉터리의 이름을 검색합니다. 규칙은 다음과 같습니다.

  • 모든 것에 일치: *(re 모듈에서의 .*와 같습니다).
  • 한 문자에 일치: ?
  • a, b, 혹은 c 문자에 일치: [abc]
  • a, b, 혹은 c를 제외한 문자에 일치: [!abc]

m으로 시작하는 모든 파일이나 디렉터리를 찾습니다.

>>> import glob
>>> glob.glob('m*')
#['mcintyre']

두 글자로 된 파일이나 디렉터리를 찾습니다.

>>> glob.glob('??')
#[]

m으로 시작하고 e로 끝나는 여덟 글자의 단어를 찾습니다.

>>> glob.glob('m??????e')
#['mcintyre']

k, l, 혹은 m으로 시작하고, e로 끝나는 단어를 찾습니다.

>>> glob.glob('[klm]*e')
#['mcintyre']

 

3. 프로그램과 프로세스

 

하나의 프로그램을 실행할 때, 운영체제는 한 프로세스를 생성합니다. 프로세스는 운영체제의 커널(Kernel; 파일과 네트워크를 연결, 사용량 통계 등 핵심 역할 수행)에서 시스템 리소스(CPU, 메모리, 디스크 공간) 및 자료구조를 사용합니다. 한 프로세스는 다른 프로세스로부터 독립된 존재입니다. 프로세스는 다른 프로세스가 무엇을 하는지 참조거하나 방해할 수 있습니다.

운영체제는 실행 중인 모든 프로세스를 추적합니다. 각 프로세스에 시간을 조금씩 할애하여 한 프로세스에서 다른 프로세스로 전환합니다. 운영체제는 두 가지 목표가 있는데, 프로세스를 공정하게 실행하여 되도록 많은 프로세스가 실행되게 하고, 사용자의 명령을 반응적으로 처리하는 것입니다. 맥의 활성 상태 보기(Activity Monitor)와 우니도우의 작업 관리자(Task Manager) 같은 그래픽 인터페이스 환경에서 프로세스의 상태를 볼 수 있습니다.

또한 여러분 자신의 프로그램에서 프로세스 데이터에 접근할 수 있습니다. 표준 라이브러리의 os 모듈에서 시스템 정보를 접근하는 몇 가지 함수를 제공합니다. 다음 코드는 실행 중인 파이썬 인터프리터에 대한 프로세스 ID현재 작업 디렉터리의 위치를 가져옵니다.

>>> import os
>>> os.getpid()
#8084
>>> os.getcwd()
#'C:\\Users\\D3C1'

os.getuid() 함수와 os.getgid() 함수를 이용하여 사용자 ID그룹 ID도 출력할 수 있습니다(Unix Only).

 

3.1. 프로세스 생성하기(1): subprocess

 

지금까지 본 모든 코드는 개별 프로세스에서 실행되었습니다. 파이선 표준 라이브러리의 subproceess 모듈로 존재하는 다른 프로그램을 시작하거나 멈출 수 있습니다. 쉘에서 프로그램을 실행하여 생성된 결과(표준 출력 및 에러 출력)를 얻고 싶다면 getoutput() 함수를 사용하면 됩니다. 다음 코드는 유닉스 date 프로그램의 결과를 얻어옵니다.

>>> import subprocess
>>> ret = subprocess.getoutput('date')
>>> ret
#'Sun Mar 15 18:47:33 UTC 2020'

프로세스가 끝날 때까지 아무런 결과를 받지 않습니다. 시간이 오래 걸리는 뭔가를 호출해야 할 경우 추후에 작성될 게시글에서 병행성을 참조하십시오. getoutput() 함수의 인자는 완전한 쉘 명령의 문자열이므로 인자, 파이프, I/O 리다이렉션(<, >) 등을 포함할 수 있습니다.

>>> ret = subprocess.getoutput('date -u')
>>> ret
#'Sun Mar 15 18:48:35 UTC 2020'

date -u 명령에서 파이프로 wc 명령을 연결하면 1줄, 6단어, 29글자를 셉니다.

>>> import subprocess
>>> ret = subprocess.getoutput('date -u | wc')
>>> ret
#'      1       6      29'

check_output()이라는 변형 메소드(Variant Method)는 명령과 인자의 리스트를 취합니다.

>>> import subprocess
>>> ret = subprocess.check_output(['date', '-u'])
>>> ret
#b'Sun Mar 15 18:54:39 UTC 2020\n'

프로그램의 종료 상태를 표시하려면 getstatusoutput()을 사용합니다. 프로그램 상태 코드와 결과를 튜플로 반환합니다.

>>> ret = subprocess.getstatusoutput('date')
>>> ret
#(0, 'Sun Mar 15 18:58:33 UTC 2020')

결과가 아닌 상태 코드만 저장하고 싶다면 call()을 사용합니다.

>>> ret = subprocess.call('date')
#Sun Mar 15 18:59:02 UTC 2020
>>> ret
#0

유닉스 계열에서 상태 코드 0은 성공적으로 종료되었다는 것을 의미합니다.

화면에는 날자와 시간이 출력되었지만, ret 변수에는 상태 코드를 저장합니다.

인자를 사용하여 두 가지 방법으로 프로그램을 실행할 수 있습니다. 첫 번째 방법은 인자를 한 문자열에 지정하는 것입니다. 다음 코드에서는 date -u를 사용하는데, 이 명령은 현재 날짜와 시간을 UTC(Universal Time Coordinated)로 출력합니다(UTC는 잠시 후에서 살펴봅니다).

>>> ret = subprocess.call('date -u', shell=True)
#Sun Mar 15 19:01:40 UTC 2020

그리고 date -u 명령을 인식할 shell=True가 필요합니다. 명령을 별도의 문자열로 분할하고, *와 같은 와일드 카드 문자를 사용할 수 있습니다(이 코드에서는 사용하지 않음).

두 번째 방법은 인자의 리스트를 사용하는 것입니다. 그러므로 쉘을 호출할 필요가 없습니다. 

>>> ret = subprocess.call(['date', '-u'])
#Sun Mar 15 19:02:13 UTC 2020

 

3.2. 프로세스 생성하기(2): multiprocessing

 

multiprocessing 모듈을 사용하면 파이썬 함수를 별도의 프로세스로 실행하거나 한 프로그램에서 독립적인 여러 프로세스를 실행할 수 있습니다. 다음의 간단한 코드를 살펴봅시다. 다음 코드를 mp.py로 저장하고, python mp.py를 입력하여 실행합니다.

import multiprocessing
import os

def do_this(what):
    whoami(what)
    
def whoami(what):
    print("Process %s says: %s" % (os.getpid(), what))
    
if __name__ == "__main__":
    whoami("I'm the main program")
    for n in range(4):
        p = multiprocessing.Process(target=do_this, args=("I'm function %s" % n,))
        p.start()

제 컴퓨터에서 실행한 결과는 다음과 같습니다.

#Process 542 says: I'm the main program
#Process 544 says: I'm function 1
#Process 545 says: I'm function 2
#Process 546 says: I'm function 3
#Process 543 says: I'm function 0

Process() 함수는 새 프로세스를 생성하여 그곳에서 do_this() 함수를 실행합니다. for 문에서 루프를 4번 돌았기 때문에 do_this() 함수를 실행한 후 종료하는 4개의 새로운 프로세스가 생성되었습니다.

multiprocessing 모듈은 생각보다 더 많은 기능을 갖고 있습니다. 그리고 프로그램의 전반적인 시간을 줄이기 위해 하나의 작업을 여러 프로세스에 할당할 수 있습니다. 예를 들어 내려받은 웹 페이지를 스크래핑하고, 이미지 크기를 조정하는 것 등에 대한 작업을 여러 프로세스로 수행할 수 있습니다. multiprocessing 모듈은 프로세스 간의 상호 통신과 모든 프로세스가 끝날 때까지 기다리는 큐(Queue) 작업을 포함합니다. 자세한 사항은 추후에 작성될 게시글에서 병행성을 참조하세요.

 

3.3. 프로세스 죽이기: terminate()

 

하나 이상의 프로세스를 생성했고, 어떠한 이유(프로세스가 루프에 빠져서 무한정 기다리거나 심한 과부하를 일으킬 때)로 하나의 프로세스를 종료하고자 하면 terminate()를 사용합니다. 다음 코드는 100만 개의 프로세스를 생성하는데, 각 스텝마다 1초 동안 아무런 일도 하지 않으며(Sleep) 짜증나는 메시지를 출력합니다. 하지만 메인 프로그램의 인내심 부족으로 코드를 5초 동안만 실행합니다.

import multiprocessing
import time
import os

def whoami(name):
    print("I'm %s, in process %s" % (name, os.getpid()))
    
def loop(name):
    whoami(name)
    start = 1
    stop = 1000000
    for num in range(start, stop):
        print("\tNumber %s of %s. Argh!!!" % (num, stop))
        time.sleep(1)
        
if __name__ == "__main__":
    whoami("main")
    p = multiprocessing.Process(target=loop, args=("loop", ))
    p.start()
    time.sleep(5)
    p.terminate()

제가 이 프로그램을 실행한 결과는 다음과 같습니다.

#I'm main, in process 4954
#I'm loop, in process 4955
#	Number 1 of 1000000. Argh!!!
#	Number 2 of 1000000. Argh!!!
#	Number 3 of 1000000. Argh!!!
#	Number 4 of 1000000. Argh!!!
#	Number 5 of 1000000. Argh!!!

 

4. 달력과 시간

 

프로그래머는 날짜와 시간에 대해 많은 노력을 기울입니다. 이번 절에서는 프로그래머들이 부딪히는 몇 가지 문제에 대해 살펴보고, 이를 좀 더 단순하게 만드는 방법과 모범 사례를 살펴봅니다.

날짜는 다양한 형식으로 표현할 수 있습니다. 표현 방식이 실제로 너무 많습니다. 영어로 된 로마 달력에서도 다음과 같이 많은 변형된 날짜 방식을 볼 수 있습니다.

  • February 29 2000
  • 20 Feb 2000
  • 29/2/2000
  • 2/29/2000

서로 다른 프로그램 간에 날자가 모호하게 표현될 수 있습니다. 위 코드에서 2은 월, 29는 일로 쉽게 알아볼 수 있습니다. 29월은 없기 때문입니다. 그렇다면 1/2/2000은 어떤가요? 1월 2일인지, 아니면 2월 1일인지 쉽게 알아볼 수 없습니다.

월 이름은 로마 달력의 언어에 따라 다릅니다. 심지어 년과 월은 문화에 따라 정의가 다를 수도 있습니다. 윤년(Leap Year)은 프로그래머가 부딪히는 또 다른 문제입니다. 윤년이 그리고 하계올림픽과 미국 대통령 선거가 4년마다 오는 것을 알고 있을 것입니다. 그렇다면 매 100년마다 오는 해는 윤년이 아니지만, 400년 마다 오는 해는 윤년이라는 사실을 알고 계신가요? 여러 년도를 테스트하는 코드는 다음과 같습니다.

>>> import calendar
>>> calendar.isleap(1900)
#False
>>> calendar.isleap(1996)
#True
>>> calendar.isleap(1999)
#False
>>> calendar.isleap(2000)
#True
>>> calendar.isleap(2002)
#False
>>> calendar.isleap(2004)
#True

시간 또한 다루기 힘든 문제입니다. 특히 타임존(Time Zone; 표준 시간대)과 일광절약시간제(Daylight Savings Time; 섬머 타임(Summer Time)이라고도 함) 때문에 더 힘듭니다. 표준시간대의 지도를 보면 경도 15(360도 / 24)를 따르기보다는 국가의 경계를 따릅니다. 그리고 국가에 따라서 연중 일광절약시간의 시작과 끝이 다릅니다. 북반구 국가가 겨울이면, 남반구 국가는 시간을 앞당깁니다. 반대로 남반구 국가가 겨울이면, 북반구 국가는 시간을 앞당깁니다(조금만 생각해보면 왜 그런지 알 수 있습니다).

파이썬 표준 라이브러리는 datetime, time, calendar, dateutil 등 시간과 날짜에 대한 여러가지 모듈이 있습니다. 일부 중복되는 기능이 있어서 조금 혼란스럽습니다.

 

4.1. datetime 모듈

 

먼저 표준 datetime 모듈을 살펴봅시다. 이는 여러 메소드를 가진 4개의 주요 객체를 정의합니다.

  • date: 년, 월, 일
  • time: 시, 분, 초, 마이크로초
  • datetime: 날짜와 시간
  • timedelta: 날짜 와/또는 시간 간격

년, 월, 일을 지정하여 date 객체를 만들 수 있습니다. 이 값은 속성으로 접근할 수 있습니다.

>>> from datetime import date
>>> halloween = date(2020, 10, 31)
>>> halloween
#datetime.date(2020, 10, 31)
>>> halloween.day
#31
>>> halloween.month
#10
>>> halloween.year
#2020

isoformat() 메소드로 날짜를 출력할 수 있습니다.

>>> halloween.isoformat()
#'2020-10-31'

iso는 국제표준화기구(ISO)에서 제정한 날짜와 시간 표현에 대한 국제표준규격인 ISO 8601을 참고합니다. 이것은 범위가 큰 년(Year)에서 범위가 작은 일(Day) 순으로 표현합니다. 즉, 년, 월, 일 순으로 표현합니다. 저는 보통 이 날짜 표현 방식으로 프로그램 이름이나 파일이름(날짜로 파일을 저장할 때)으로 사용합니다. 다음 절에서는 좀 더 복잡한 strptime()과 strftime() 메소드를 사용하여 날짜를 파싱하고 포매팅합니다.

다음 코드는 today() 메소드를 사용하여 오늘 날짜를 출력합니다.

>>> from datetime import date
>>> now = date.today()
>>> now
#datetime.date(2020, 3, 15)

timedelta 객체를 사용하여 날자에 시간 간격을 더해봅시다.

>>> from datetime import timedelta
>>> one_day = timedelta(days = 1)
>>> tomorrow = now + one_day
>>> tomorrow
#datetime.date(2020, 3, 16)
>>> now + 17 * one_day
#datetime.date(2020, 4, 1)
>>> yesterday = now - one_day
>>> yesterday
#datetime.date(2020, 3, 14)

날짜의 범위는 date.min(year=1, month=1, day=1)부터 date.max(year=9999, month=12, day=31)까지입니다. 결과적으로 역사적 혹은 천문학적인 날짜는 계산할 수 없습니다.

datetime 모듈의 time 객체는 하루의 시간을 나타내는 데 사용됩니다.

>>> from datetime import time
>>> noon = time(12, 0, 0)
>>> noon
#datetime.time(12, 0)
>>> noon.hour
#12
>>> noon.minute
#0
>>> noon.second
#0
>>> noon.microsecond
#0

인자는 가장 큰 시간 단위(시(Hour))부터 가장 작은 시간 단위(마이크로초(Microsecond))순으로 입력합니다. 인자를 입력하지 않으면 time 객체의 초기 인자는 0으로 간주됩니다. 그리고 컴퓨터는 마이크로초를 정확하게 계산할 수 없습니다. 마이크로초 측정의 정확성은 하드웨어와 운영체제의 많은 요소에 따라 달라집니다.

datetime 객체는 날짜와 시간 모두를 포함합니다. January, 2, 2020, at 3:04 A.M, 5초, 6마이크로초와 같이 한 번에 생성됩니다.

>>> from datetime import datetime
>>> some_day = datetime(2020, 1, 2, 3, 4, 5, 6)
>>> some_day
#datetime.datetime(2020, 1, 2, 3, 4, 5, 6)

datetime 객체에도 isoformat() 메소드가 있습니다.

>>> some_day.isoformat()
#'2020-01-02T03:04:05.000006'

중간의 T는 날짜와 시간을 구분합니다.

datetime 객체에서 now() 메소드로 현재 날짜와 시간을 얻을 수 있습니다.

>>> from datetime import datetime
>>> now = datetime.now()
>>> now
#datetime.datetime(2020, 3, 15, 19, 43, 55, 820274)
>>> now.year
#2020
>>> now.month
#3
>>> now.day
#15
>>> now.hour
#19
>>> now.minute
#43
>>> now.second
#55
>>> now.microsecond
#820274

combine()으로 date 객체와 time 객체를 datetime 객체로 병합할 수 있습니다.

>>> from datetime import datetime, time, date
>>> noon = time(12)
>>> this_day = date.today()
>>> noon_today = datetime.combine(this_day, noon)
>>> noon_today
#datetime.datetime(2020, 3, 15, 12, 0)

datetime 객체에서 date()와 time() 메소드를 사용하여 날짜와 시간을 얻을 수 있습니다.

>>> noon_today.date()
#datetime.date(2020, 3, 15)
>>> noon_today.time()
#datetime.time(12, 0)

 

4.2. time 모듈

 

파이썬에서 datetime 모듈의 time 객체와 별도의 time 모듈이 혼란스럽습니다. 더군다나 time 모듈에는 time()이라는 함수가 있습니다.

절대 시간을 나타내는 한 가지 방법은 어떤 시작점 이후 시간의 초를 세는 것입니다. 유닉스 시간은 1970년 1월 1일 자정(이 때가 대충 유닉스가 탄생한 시점입니다.) 이후 시간의 초를 사용합니다. 이 값을 에폭(Epoch)이라고 부르며, 에폭은 시스템 간에 날짜와시간을 교환하는 아주 간단한 방식입니다.

time 모듈의 time() 함수는 현재 시간을 에폭값으로 반환합니다.

>>> import time
>>> now = time.time()
>>> now
#1584302019.8319478

숫자를 보면 1970년 새해부터 지금까지 15억 초가 넘습니다. 시간은 어디로 흘러갔을까요?

ctime() 함수를 사용하여 에폭값을 문자열로 변환할 수 있습니다.

>>> time.ctime(now)
#'Mon Mar 16 04:53:39 2020'

다음 절에서는 날짜와 시간을 마음에 드는 포맷으로 얻을 수 있습니다.

에폭값은 자바스크립트와 같은 다른 시스템에서 날짜와 시간을 교환하기 위한 유용한 최소 공통분모입니다. 그리고 각각의 날짜와 시간 요소를 얻기 위해 time 모듈의 struct_time 객체를 사용할 수 있습니다. localtime() 메소드는 시간을 시스템의 표준시간대로, gmtime() 메소드는 시간을 UTC로 제공합니다.

>>> time.localtime(now)
#time.struct_time(tm_year=2020, tm_mon=3, tm_mday=16, tm_hour=4, tm_min=53, tm_sec=39, tm_wday=0, tm_yday=76, tm_isdst=0)
>>> time.gmtime(now)
#time.struct_time(tm_year=2020, tm_mon=3, tm_mday=15, tm_hour=19, tm_min=53, tm_sec=39, tm_wday=6, tm_yday=75, tm_isdst=0)

 한국의 현재 시간 04:53는 그 전날의 UTC(이전에는 그리니치 시간(Greenwich Time) 또는 줄루 시간(Zulu Time)이라고 했습니다)로 19:53입니다. localtime() 혹은 gmtime()에서 인자를 생략하면 현재 시간으로 가정합니다.

이와 반대로 mktime() 메소드는 struct_time 객체를 에폭 초로 변환합니다.

>>> tm = time.localtime(now)
>>> time.mktime(tm)
#1584302019.0

이 값은 조금 전에서 본 now()의 에폭값과 정확하게 일치하지 않습니다. struct_time 객체는 시간을 초까지만 유지하기 때문입니다.

몇 가지 조언을 하자면, 가능하면 표준시간대 대신 UTC를 사용하십시오. UTC는 표준시간대와 독립적인 절대 시간입니다. 서버를 운영하고 있다면 현지 시간이 아닌 UTC로 설정하십시오.

그리고 피할 수 있다면 일광절약시간은 사용하지 않는 것을 추천드립니다. 이것을 사용하면, 연중 한 시간이 한 번에 사라지고(봄이 앞당겨집니다), 이 시간이 다른 시간에 두 번 발생합니다(가을이 늦게 옵니다). 어떤 이유에서인지 많은 조직의 컴퓨터 시스템에서 일광절약시간을 사용합니다. 그러나 매년 데이터 중복과 손실에 시달려 결국 눈믈을 흘리고 맙니다.

참고로, 시간에 대해서는 UTC, 문자열에 대해서는 UTF-8을 가까이 하는 것이 좋습니다.

 

4.3. 날짜와 시간 읽고 쓰기

 

isoformat()이 날짜와 시간을 쓰기 위한 유일한 방법은 아닙니다. time 모듈에서 본 ctime() 함수로 쓸 수도 있습니다. 이 함수는 에폭 시간을 문자열로 변환합니다.

>>> import time
>>> now = time.time()
>>> time.ctime(now)
#'Mon Mar 16 04:59:35 2020'

또한 strftime()을 사용하여 날짜와 시간을 문자열로 변환할 수 있습니다. 이는 datetime, date, time 객체에서 메소드로 제공되고, time 모듈에서 함수로 제공됩니다. strftime()은 다음의 표처럼 문자열의 출력 포맷을 지정할 수 있습니다.

문자열 포맷 날짜 / 시간 단위 범위
%Y 1900 ~ ...
%m 01 ~ 12
%B 월 이름 January, ...
%b 월 축약 이름 Jan, ...
%d 월의 일자 01 ~ 31
%A 요일 이름 Sunday, ...
%a 요일 축약 이름 Sun, ...
%H 24시간 00 ~ 23
%I 12시간 01 ~ 12
%p 오전 / 오후 AM, PM
%M 00 ~ 59
%S 00 ~ 59

숫자는 자릿수에 마줘 왼쪽에 0이 채워집니다.

다음은 tiem 모듈에서 제공하는 strftime() 함수입니다. 이것은 struct_time 객체를 문자열로 변환합니다. 먼저 포맷 문자열 fmt를 정의하고, 이것을 다시 사용합시다.

>>> import time
>>> fmt = "It's %A, %B, %d, %Y, local time %I:%M:%S%p"
>>> t = time.localtime()
>>> t
#time.struct_time(tm_year=2020, tm_mon=3, tm_mday=16, tm_hour=5, tm_min=57, tm_sec=45, tm_wday=0, tm_yday=76, tm_isdst=0)
>>> time.strftime(fmt, t)
#"It's Monday, March, 16, 2020, local time 05:57:45AM"

이것을 다음과 같이 date 객체에 사용하면 날짜 부분만 작동합니다. 그리고 시간은 기본값으로 지정됩니다.

>>> from datetime import date
>>> some_day = date(2020, 2, 29)
>>> fmt = "It's %B, %d, %Y, local time %I:%M:%S%p"
>>> some_day.strftime(fmt)
#"It's February, 29, 2020, local time 12:00:00AM"

time 객체는 시간 부분만 변환됩니다.

>>> from datetime import time
>>> some_time = time(9, 22)
>>> some_time.strftime(fmt)
#"It's January, 01, 1900, local time 09:22:00AM"

time 객체에서 날짜를 사용하는 것은 의미가 없습니다.

문자열을 날짜나 시간으로 변환하기 위해 같은 포맷 문자열로 strptime()을 사용합니다. 정규표현식 패턴 매칭은 없습니다. 문자열의 비형식 부분(% 제외)이 정확히 일치해야 합니다. 2020-03-15과 같이 년-월-일이 일치하는 포맷을 지정해봅시다. 날짜 문자열에서 대시(-) 대신 공백을 사용하면 무슨 일이 일어날까요?

>>> import time
>>> fmt = "%Y-%m-%d"
>>> time.strptime("2020 03 16", fmt)
#Traceback (most recent call last):
#  File "<pyshell#27>", line 1, in <module>
#    time.strptime("2020 03 16", fmt)
#  File "C:\Program Files (x86)\Microsoft Visual Studio\Shared\Python37_64\lib\_strptime.py", line 571, in _strptime_time
#    tt = _strptime(data_string, format)[0]
#  File "C:\Program Files (x86)\Microsoft Visual Studio\Shared\Python37_64\lib\_strptime.py", line 359, in _strptime
#    (data_string, format))
#ValueError: time data '2020 03 16' does not match format '%Y-%m-%d'

대시(-)를 붙이면 어떻게 될까요?

>>> time.strptime("2020-03-16", fmt)
#time.struct_time(tm_year=2020, tm_mon=3, tm_mday=16, tm_hour=0, tm_min=0, tm_sec=0, tm_wday=0, tm_yday=76, tm_isdst=-1)

잘 됩니다.

문자열 포맷이 맞는 것처럼 보이지만, 값이 범위를 벗어나면 예외가 발생합니다.

>>> time.strptime("2020-13-16", fmt)
#Traceback (most recent call last):
#  File "<pyshell#29>", line 1, in <module>
#    time.strptime("2020-13-16", fmt)
#  File "C:\Program Files (x86)\Microsoft Visual Studio\Shared\Python37_64\lib\_strptime.py", line 571, in _strptime_time
#    tt = _strptime(data_string, format)[0]
#  File "C:\Program Files (x86)\Microsoft Visual Studio\Shared\Python37_64\lib\_strptime.py", line 359, in _strptime
#    (data_string, format))
#ValueError: time data '2020-13-16' does not match format '%Y-%m-%d'

이름은 운영체제의 국제화 설정인 로케일(Locale)에 따라 다릅니다. 다른 월, 일의 이름을 출력하려면 setlocale()을 사용하여 로케일을 바꿔야 합니다. setlocale()의 첫 번째 인자는 날짜와 시간을 위한 locale.LC_TIME이고, 두 번째는 언어와 국가 약어가 결합된 문자열입니다. 외국인 친구를 할로윈 파티에 초대해봅시다. 월, 일, 요일을 한국어, 영어, 프랑스어, 독일어, 스페인어, 아이슬란드어로 출력해야 할 것입니다.

>>> import locale
>>> from datetime import date
>>> halloween = date(2020, 10, 31)
>>> for lang_country in ['ko_kr', 'en_us', 'fr_fr', 'de_de', 'es_es', 'is_is',]:
	locale.setlocale(locale.LC_TIME, lang_country)
	halloween.strftime('%A, %B %d')

	
#'ko_kr'
#'토요일, 10월 31'
#'en_us'
#'Saturday, October 31'
#'fr_fr'
#'samedi, octobre 31'
#'de_de'
#'Samstag, Oktober 31'
#'es_es'
#'s\udce1bado, octubre 31'
#'is_is'
#'laugardagur, okt\udcf3ber 31'

lang_country에 대한 값은 어디에서 찾을 수 있을까요? 다음 코드를 실행하여 (몇 백 개의) 값을 모두 찾을 수 있습니다.

>>> import locale
>>> names = locale.locale_alias.keys()

이전 코드 setlocale()에서 사용한 두 글자의 언어 코드, 언더스코어, 두 글자의 국가 코드처럼 names로부터 로케일 이름을 얻어옵니다.

>>> good_names = [name for name in names if \
	      len(name) == 5 and name[2] == '_']

처음 5개만 볼까요?

>>> good_names[:5]
#['a3_az', 'aa_dj', 'aa_er', 'aa_et', 'af_za']

모든 독일어 로케일을 원한다면 다음과 같이 실행합니다.

>>> de = [name for name in good_names if name.startswith('de')]
>>> de
#['de_at', 'de_be', 'de_ch', 'de_de', 'de_it', 'de_lu']

 

4.4. 대체 모듈

 

표준 라이브러리 모듈이 헷갈리거나 특정 포맷 변환이 부족하다고 생각되는 경우, 외부 모듈을 사용할 수 있습니다. 그 중 몇 가지를 소개하겠습니다.

  • arrow

    • 많은 날짜와 시간 함수를 결합하여 간단한 API를 제공합니다.

  • dateutil

    • 이 모듈은 대부분의 날짜 포맷을 파싱하고, 상대적인(Relative) 날짜와 시간에 대해서도 처리합니다.

  • iso8601

    • ISO8601 포맷에 대한 표준 라이브러리의 부족한 부분을 보충합니다.

  • fleming

    • 이 모듈은 표준시간대 함수를 제공합니다.