이 글은 문서를 참고했습니다.
처음 프로그래밍을 배울 때가 기억난다.
Visual Studio 를 이용해서 C/C++ 을 배울 때였는데,
내가 선언한 변수들이 알록달록 예쁜 색으로 변하고,
논리적인 문장, 문단을 실시간으로 구분하는 프로그램이 너무 신기했다.
파이썬이 C 기반으로 만들어진 언어라는 점을 인식하면서 공부하다보니,
이런 기억들이 새록새록 떠오르면서,
프로그램이 문자,문장을 구분하고 인식하는 방법에 대해 궁금해졌다.
레퍼런스에는 아래와 같이 기술되어 있다.
- BNF 문법 표기법??
: 배커스-나우르 표기법(Backus–Naur form) 이라고 불리며, 문맥 무관 문법을 나타내기 위해 만들어진 표기법이다. 컴퓨터가 글을 이해할 수 있도록 하는 기초적인 모델,, 같은 느낌이다. 검색해서 간단하게 봤다. 프로그래밍 언어들은 모두 BNF 규칙을 통해 만들어진다고 한다. 우리가 잘 아는 =operator 도 이 규칙을 통해 만들어졌다고 한다. 자세한 내용은 다른 블로그에 잘 정리되어 있다. 흥미로운 내용이다! 아직 인공지능 같은 분야를 공부해보지는 않았지만, 자연어 처리와 같은 기계학습에 등장할 것 같은 내용이다. 추후에 여유가 있다면 더 확실하게 공부해야겠다.
레퍼런스를 읽어본 바에 의하면 구조는 이런식인 듯하다.
1. 어휘분석기(Lexical Analysis)에 의한 "어휘분석"
: 어휘분석기는 특정 규칙에 따라 토큰들을 생성한다.
: NEWLINE / INDENT / DEDENT /식별자(identifier) / 키워드(keyword) / 리터럴(literal) / 연산자(operator) / 구분자(delimiter)
: 위의 토큰들의 스트림을 파서(parser) 에게 전달한다.
2. 파서(parser)에 의한 "문법정의"
: 입력받은 토큰 스트림을 기반으로 문법을 정의한다.
: 이 부분은 다음에 추가로 공부한 후에 글을 올리도록 하겠다.
이번 장에서는 어휘분석기(Lexical Analysis)의 역할에 대해 알아보자
어휘분석기(Lexical Analysis)는 특정 문자들을 토큰으로 만들고, 토큰의 스트림을 파서(parser)에게 전달한다.
레퍼런스에서 설명하는 순서는 아래와 같다.
1) 줄 구조 파악 원리 (NEWLINE 토큰 생성 원리)
2) 기타 다른 토큰 생성 원리
1) 줄 구조
- NEWLINE 토큰 -
① 물리적인 줄
- 우리가 흔히 "소스코드 몇번째 줄" 하고 말할 때와 같은 맥락의 뜻이다. 어휘분석기는 물리적인 줄을 구분한다.
- 소스코드 끝에 위치한 개행문자를 토큰으로 인식하는 듯하다.
- 개행문자를 제외한 여러 토큰(리터럴,연산자,구분자,키워드,식별자 등)들의 스트림이 한 줄에 이어지다가 개행문자를 만나면 물리적인 한 줄로 인식한다.
② 논리적인 줄
- 논리적인 줄은 하나 이상의 물리적인 줄로 이루어진다.
- 예를 들어, 물리적인 한 줄로 선언할 수 있지만, 코드의 가독성을 위해 여러줄로 나누어야 할 경우 등에 이용된다.
- c 언어의 경우에는 ' ; ' 토큰이 논리적인 줄의 끝부분에 위치하여 구분한다.
- 파이썬에서는 \NEWLINE 토큰 이라고 지칭한다.
- 뉴라인 토큰이 생성되는 경우 : 여러줄로 나누지 않는 경우에는 끝에 위치한 개행문자 접촉 시 생성, 여러줄로 나누는 경우에는 경우에 따라 다르다. (명시적인 방법과 묵시적인 방법 두 가지가 있다.) 또는 주석처리 토큰(#)을 만나면 뉴라인 토큰을 생성하고 논리적인 줄을 구분짓는다.
③ 명시적 줄 결합
- 보통 물리적인 줄은 주석처리나, 리터럴로 끝난 후 개행문자가 뒤따른다.
- 물리적인 줄 끝에 역 슬래쉬(\)를 입력함으로써 개행문자를 지우는 효과를 준다. 따라서 두 개 이상의 물리적인 줄을 하나의 논리적인 줄로 연결할 수 있다.
- 역 슬래쉬(\)는 문자열 리터럴을 제외한 다른 토큰들과는 결합할 수 없다. 따라서 명시적 줄 결합 사이에는 주석을 달 수 없다. (주석처리(#) 토큰 때문에)
- 역 슬래쉬의 역할이 개행문자를 제거하는 역할인데, 역슬래쉬와 개행문자 사이에 주석(#)토큰이 위치하면 개행문자가 제거되지 않는다. 이는 역 슬래쉬가 단일로 존재하는 하나의 논리적인 줄을 완성시키는 결과를 낳고, 이는 오류다! 문자열 리터럴 밖에 있는 역 슬래쉬가 물리적 줄 끝 외의 장소에 등장하는 것은 문법에 어긋나게 설정했기 때문이다.
if 1900 < year < 2100 and 1 <= month <= 12 \
and 1 <= day <= 31 and 0 <= hour < 24 \
and 0 <= minute < 60 and 0 <= second < 60: # Looks like a valid date
return 1
④ 묵시적 줄 결합
- 괄호(()), 대괄호([]), 중괄호({})가 사용되는 표현은 역 슬래시 없이도 여러 개의 물리적인 줄로 나눌 수 있다. 묵시적으로 이어지는 줄들은 주석을 포함할 수 있다. 이어지는 줄들의 들여쓰기는 중요하지 않다. 중간에 빈 줄이 들어가도 됩니다. 묵시적으로 줄 결합하는 줄 들 간에는 NEWLINE 토큰이 만들어지지 않기 때문이다.
- 괄호가 끝나야만 NEWLINE 토큰이 생성되게 설정했나보다.
month_names = ['Januari', 'Februari', 'Maart', # These are the
'April', 'Mei', 'Juni', # Dutch names
'Juli', 'Augustus', 'September', # for the months
'Oktober', 'November', 'December'] # of the year
⑤ 빈 줄
- 스페이스 / 탭 / 폼 피드와 주석만으로 구성된 논리적인 줄은 무시된다. (즉 NEWLINE 토큰이 만들어지지 않는다.)
- 표준 대화형 인터프리터에서는, 완전히 빈 줄(즉 공백이나 주석조차 없는 것)은 다중 행 문장을 종료시킨다.
⑥ 들여쓰기
- 개인적으로는 코딩하면서 가장 주의해야하는 부분이 아닌가 하는 생각이 들었다.
- 논리적인 줄의 제일 앞에 오는 공백(스페이스와 탭)은 줄의 들여쓰기 수준을 계산하는 데 사용되고, 이는 다시 문장들의 묶음을 결정하는 데 사용되게 된다.
- TAB 키는 스페이스 1~8개로 치환된다. 치환 후 스페이스 문자의 개수가 8의 배수가 되도록 한다. (유닉스 규칙에 맞추려는 것이다.. 라고 써있는데, 유닉스 규칙에 맞춤으로써 모든 OS 환경에서 동일하게 작동하게 되는 듯하다.)
- PEP8에서는 스페이스만 또는 탭으로만 공백을 구분할 것을 권장한다. 스페이스 4개 단위로 들여쓰기 할 것!
- 만약 탭과 스페이스를 섞어 쓰는 경우, 탭이 몇개의 스페이스에 해당하는지에 따라 다르게 해석될 수 있으면 오류(
TabError)를 일으킨다. 따라서 플랫폼 호환성을 위해 탭과 스페이스를 섞어쓰면 안된다.
- 어휘분석기(Lexical Analysis)가 들여쓰기 수준을 검사하는 알고리즘은 매우 간단하다. 올바른 괄호를 검사하는 알고리즘과 같다고 볼 수 있다. 스택을 이용한다. 처음에는 0을 하나 넣고 시작. 들여쓰기가 생기면 스페이스의 개수와 INDENT 토큰을 함께 스택에 저장한다. 들여쓰기가 사라지면 현재 들여쓰기 수준과 같을때까지 스택의 값을 꺼내고 그 횟수만큼 DEDENT 토큰을 만든다. 파일의 끝에서 INDENT 토큰과 DEDENT 토큰의 개수가 같아야한다. 스택에서 꺼내는 과정에서 들여쓰기 수준이 일치하는 INDENT 토큰이 없으면 오류! 다음은 레퍼런스에 써있는 내용이다.
파일의 첫 줄을 읽기 전에 0하나를 스택에 넣습니다(push); 이 값은 다시 꺼내는(pop) 일이 없습니다. 스택에 넣는 값은 항상 스택의 아래에서 위로 올라갈 때 단조 증가합니다. 각 논리적인 줄의 처음에서 줄의 들여쓰기 수준이 스택의 가장 위에 있는 값과 비교됩니다. 같다면 아무런 일도 일어나지 않습니다. 더 크다면 그 값을 스택에 넣고 하나의 INDENT 토큰을 만듭니다. 더 작다면 이 값은 스택에 있는 값 중 하나여야만 합니다. 이 값보다 큰 모든 스택의 값들을 꺼내고(pop), 꺼낸 횟수만큼의 DEDENT 토큰을 만듭니다. 파일의 끝에서, 스택에 남아있는 0보다 큰 값의 개수만큼 DEDENT 토큰을 만듭니다.
바른 예 :
def perm(l):
# Compute the list of all permutations of l
if len(l) <= 1:
return [l]
r = []
for i in range(len(l)):
s = l[:i] + l[i+1:]
p = perm(s)
for x in p:
r.append(l[i:i+1] + x)
return r
틀린 예 :
def perm(l): # error: first line indented
for i in range(len(l)): # error: not indented
s = l[:i] + l[i+1:]
p = perm(l[:i] + l[i+1:]) # error: unexpected indent
for x in p:
r.append(l[i:i+1] + x)
return r # error: inconsistent dedent
- 위에서는 4개의 오류가 발생하지만 어휘분석기(Lexical Analysis)에 의해서는 마지막 오류만 잡힌다.
- 첫번째 줄이라는 정보를 따로 이용해 잡아내야 한다. => 파서(parser)가 잡는 오류
- 두번째 줄 : 첫째 줄의 함수 정의에 의해 들여쓰기를 해야한다. def 라는 어휘를 인식해야하므로 어휘분석기가 def를 토큰으로 만들어낸 후 파서(parser)가 잡는다.
- 네번째 줄 : 들여쓰기를 해서는 안되는 곳이다. 어휘분석기가 만들어낸 토큰 기반으로 결정하는 것이므로 파서(parser)가 잡는다.
- 마지막 줄 : 어휘분석기의 스택 알고리즘으로 잡힌다.
2) 다른 토큰들
① 연산자(operator)
② 구분자(delimiter)
③ 식별자(identifier, name) / 키워드(keyword)
- 파이썬 3.0 이상부터는 ASCII 범위 밖의 문자들을 도입한다 (PEP 3131 참조). 이 문자들의 경우, unicodedata 모듈에 포함된 버전의 유니코드 문자 데이터베이스에 따라 분류된다.
- 모든 식별자는 파서에 의해 NFKC 정규화 형식으로 변환되고, 식별자의 비교는 NFKC 에 기반을 둔다.
(유니코드 등가성 문서 참조, 몰라서 찾아봤다.)
- 길이 제한은 없고, 케이스는 구분된다. 하지만 PEP8 에 의하면 길이제한은 72바이트로 두는게 현명하다.
- 예약된 식별자, 아래의 이름은 사용할 수 없다. 키워드라고 불린다.
④ 리터럴(literal)
- 내용이 가장 많은 관계로 많이 생략하도록 한다. 궁금하면 레퍼런스 직접 참고!
- 문자열 리터럴과 바이트 리터럴 정의
- 문자열 리터럴 이스케이프 시퀀스
- 포맷 문자열 리터럴 정의
- 포맷 문자열은 일반 문자열 리터럴과 같은 방식으로 디코딩되지만 Raw string 인 경우는 예외!
- 포맷 문자열 리터럴 예시 :
- 숫자 리터럴 : 정수, 실수, 허수. (음수는 존재하지 않는다. 숫자와 단항연산자(-)의 조합)
- 정수 리터럴 정의 : (예, 정의에 따라 100_000_000 도 가능, 출력 시 100000000 으로 출력)
- 실수 리터럴 정의 : (예, 정의에 따라 0.00_00_001 도 가능, 출력 시 0.0000001 으로 출력)
- 허수 리터럴 정의
⑤ 기타 토큰
- 다른 토큰들의 일부가 되는 것들
- 사용되지 않는 것들
'Python > Python 뜯어보기' 카테고리의 다른 글
[Python 뜯어보기] 5. 파이썬 가상머신(PVM) 과 컴파일방식 (0) | 2022.04.13 |
---|---|
[Python 뜯어보기] 4. WSGI 와 Python (0) | 2022.03.22 |
[Python 뜯어보기] 3. 파이썬 Thread 와 GIL (1) | 2022.03.22 |
[Python 뜯어보기] 2.극한의 '객체'충 파이썬 (0) | 2021.03.17 |
[Python 뜯어보기] 0. 파이썬을 공부하기로 결심한 이유 (0) | 2021.03.10 |