1. Prologue

"100-200*300-500+20"

프로그래머스 67257번 문제이자, 2020 카카오 인턴십 2번 문제다.

위와 같은 문자열 수식이 주어진다고 할 때, 저 수식을 활용하여 정답을 도출하는 문제였다.

문제는 저 입력이 '문자열'이라는 것이다.

어떻게 저 연산 기호와 숫자를 잘 분리하느냐가 핵심이었다.

 

string = "100-200*300-500+20"
nums = string.replace("*", "_").replace("+", "_").replace("-", "_").split("_")
for num in set(nums): string = string.replace(num, "_")
operator = string.split("_")[1:-1]

# ['100', '200', '300', '500', '20']
print(nums)

# ['-', '*', '-', '+']
print(operator)

해당 문제에서 숫자는 0에서 999까지의 숫자 범위라고 명시해놓았다.

하지만 숫자를 기준으로 나누기보다는, 3개뿐인 연산 기호를 기준으로 나누는 것이 빠르다고 생각했다.

그래서 우선 연산 기호 3종류를 전부 '_' 문자로 바꿔주었다.

 └ 익숙하면서도 무시하라는 의미가 담겨있도록 underscore를 사용하였다.

그런 다음 split() 내장 함수를 사용하여 분리하면 nums에 숫자들만 담긴다.

 

그런 다음 nums를 set 자료 구조로 바꾼다.

기존 string 문자열에서 해당 숫자 문자를 전부 '_'로 바꾸고, 다시 split()으로 분리하면, 연산 기호만 뽑을 수 있다.

이때 set을 사용하는 이유는, 숫자가 중복되는 경우가 있을 수 있기 때문이다.

예를 들어 nums에 ["10", "20", "20", "10"]이 담겼다고 해보자.

replace(bef, aft) 함수를 사용하면 해당 문자열 내에 있는 모든 bef를 aft로 바꿔준다.

즉 처음에 있는 "10"을 이미 바꿔주었기 때문에, 뒤에 있는 "10"은 바꿀 이유가 없다.

시간을 줄이기 위해 set을 사용하였다. 

 

import re

string = "100-200*300-500+20"
expression = re.split('([*+-])', string)

# ['100', '-', '200', '*', '300', '-', '500', '+', '20']
print(expression)

나름 깔끔하게 작성해서 괜찮다고 생각하던 찰나에, 정규 표현식을 접했다.

단 한 줄의 코드로 내가 위에서 작성한 과정을 보기 좋게 줄여버렸다.

이에 정규 표현식이라는 '문자열 처리'에 대한 필요성을 느꼈다.

 

과장을 조금 보태자면, 정규 표현식을 사용하면 어떤 문자열이든 단 한 줄 코딩으로 처리가 가능하다.

 

2. Regular Expression

    2.1. 개념

정규 표현식(Regular Expression)은 텍스트에서 문자열 패턴을 찾기 위한 형식이다.

특정 규칙을 가진 문자열을 표현하기 위해 사용하며, 검색ㆍ대체 ㆍ추출 같은 작업에 용이하다.

정규 표현식을 사용할 때 '메타 문자'라는 것을 사용한다.

 

    2.2. 메타 문자

메타 문자
종류 설명 예시
. 어떤 문자 하나와 일치하는지 확인 (줄바꿈 제외) a.b는 acb, aab, a3b와 일치, ab, a\nb와 불일치
^ 문자열의 시작과 일치하는지 확인 ^a는 abc, apk, a1a와 일치, ba, meta와 불일치
$ 문자열의 끝과 일치하는지 확인 a$는 aaa, sea와 일치, apple, naan과 불일치 
* 바로 앞의 문자가 0번 이상 반복하는지 확인 bo*는 b, bo, booo와 일치, bq, base와 불일치 
+ 바로 앞의 문자가 1번 이상 반복하는지 확인 b+c는 bc, bbc, bbbbc와 일치, bb, c, bac와 불일치
{ } 정확한 반복 횟수나 범위를 지정 a{2}는 aa와 일치, a{2, 3}은 aa, aaa와 일치
[ ] 대괄호 안에 문자 중 하나와 일치하는지 확인 [abc]는 a, b, c와 일치, ab, bc, ac와 불일치
\ 다음에 오는 메타 문자를 문자 그대로 취급 \^는 ^ 문자와 일치(메타 문자 취급을 하지 않음)
| 두 조건 중 하나와 일치하는지 확인 (OR) a|b는 a, b와 일치, ab, ba, aa, bb와 불일치
() 정규식을 그룹으로 묶어 여러 문자를 조합하여 사용 b(an)+a는 banana와 일치, ba, ban, bnaa과 불일치
? 바로 앞의 문자가 0번 혹은 1번 있는지 확인 ab?c는 ac와 일치, abc와 일치, abbc와 불일치

메타 문자란, 원래 문자가 가진 뜻이 아닌 다른 의미로 사용하는 문자들을 말한다.

예를 들어 마침표에 해당하는 온점(.)은 '문장의 맺음'을 표현하거나, 객체 메소드를 호출하는 용도(~의)로 사용한다.

하지만 메타 문자로서 온점은 '정확하게 이 문자와 대응하는 문자'라는 의미로서 사용한다. 

이런 메타 문자의 종류와 설명, 예시를 표로 정리하였다.

 

*, +, ?는 각각 {0, }, {1, }, {0, 1}로 표현하는 것이 가능하다.

하지만 가독성 때문에 메타문자로 적는 것을 권장한다고 한다.

적재적소에 필요에 따라 사용하면 된다.

 

    2.3. 자주 사용하는 표현 표기법

자주 사용하는 정규식은 별도의 표기법으로 표현할 수 있다
문자 클래스 설명
\d 숫자와 매치된다. [0-9]와 동일한 표현식이다.
\D 숫자가 아닌 것과 매치된다. [^0-9]와 동일한 표현식이다.
\s 공백(whitespace) 문자와 매치된다.
\S 공백 문자가 아닌 것과 매치된다.
\w 문자+숫자(alphanumeric)와 매치된다. [a-zA-Z0-9_]와 동일한 표현식이다.
\W 문자+숫자(alphanumeric)가 아닌 문자와 매치된다. [^a-zA-Z0-9_]와 동일한 표현식이다.

대괄호[ ]를 사용한 메타 문자는 다양하게 활용할 수 있다.

[a-z]는 영소문자, [A-Z]는 영대문자, [a-zA-Z]는 모든 영문자, [0-9]는 모든 숫자처럼 말이다.

그래서 이런 정규 표현식은 약어로 존재한다. 그것을 정리해 둔 표이다.

소문자와 대문자의 관계가 서로 상반됨을 알 수 있다.

 

    2.4. Regular Expression Method

Method 목적
match() 문자열의 맨 시작부터 정규식 패턴과 일치하는 내용을 찾는다.
예시 re.match('hi', 'hello hi') >>> None
re.match('hi', 'hi greeting') >>> <re.Match object; span=(0, 2), match='hi'>
search() 문자열 전체에서 정규식 패턴과 일치하는 첫 번째 내용을 찾는다.
예시 re.search('hi', 'hello hi') >>> <re.Match object; span=(6, 8), match='hi'>
re.match('hi', 'hi, hi?') >>> <re.Match object; span=(0, 2), match='hi'>
findall() 문자열 전체에서 정규식 패턴과 일치하는 모든 내용을 찾는다.
예시 re.findall('hi', 'hi hi hello') >>> ['hi', 'hi']
re.findall('a+b', 'aab, aba, aab, b') >>> ['aab', 'ab, 'aab']
finditer() 문자열 전체에서 정규식 패턴과 일치하는 모든 내용을 찾아 iterator로 반환한다.
예시 re.finditer('ab', 'abracadabra') >>> <callable_iterator object at 0x00001CB7AE06E3>
fullmatch() 문자열 전체가 정규식 패턴과 정확히 일치하는 경우에만 객체를 반환한다.
예시 re.fullmatch('Pomeranian', 'Pomegranate') >>> None
re.fullmatch('b(an)*a', 'banana') >>> <re.Match object; span=(0, 6), match='banana'>
split()  정규식 패턴을 구분자로 사용하여 문자열을 분할한다. 이때 ()를 사용하면 구분자를 포함한다.
예시 re.split('-', 'What-The-Hell') >>> ['What', 'The', 'Hell']
re.split('(-)', "What-The-Hell') >>> ['What', '-', 'The', '-', 'Hell']
sub() 문자열 전체에서 정규식 패턴과 일치하는 부분을 다른 문자열로 대체한다.
예시 re.sub(' ', '!OH!', 'friends like me') >>> friends!OH!like!OH!me
re.sub('o|g', '-', 'algorithmprogramming') >>> al--rithmpr--ammin-
subn() sub() 결과에 대체한 횟수도 함께 반환한다.
예시 re.subn(' ', '!OH!', 'friends like me') >>> (friends!OH!like!OH!me, 2)
re.subn('o|g', '-', 'algorithmprogramming') >>> (al--rithmpr--ammin-, 5)

위에서 설명한 메타 문자를 이용하여 원하는 문자열을 추출할 차례이다.

기본적으로 re(regular expression) 라이브러리를 호출해야 사용이 가능하다.

예를 들어서 'ab*'라는 정규식 패턴과 일치하는지 찾고 싶다고 해보자.

 

우선 pattern = re.compile('ab*')로 컴파일 객체 만들어야 한다.

그 다음 result = pattern.match('abbb')의 형태로 코드를 작성하면 result에 결과가 담기게 된다.

이 과정을 축약하여 result = re.match('ab*', 'abbb') 형태로 작성하는 것도 지원한다.

위의 표에서는 가독성을 위하여 축약 형태로 예시를 설명했음을 알린다.

 

    2.5. Match Method

Match 객체의 method
Method 용도 예시
group 문자열 반환 re.search('isThere', 'dvDNJisThereKMdcOnklm').group()
>>> isThere
start 문자열의 시작 위치를 반환 re.search('isThere', 'dvDNJisThereKMdcOnklm').start()
>>> 5
end 문자열의 끝 위치를 반환 re.search('isThere', 'dvDNJisThereKMdcOnklm').end()
>>> 12
span 문자열의 (시작, 끝)을 tuple로 반환 re.search('isThere', 'dvDNJisThereKMdcOnklm').span()
>>> (5, 12)

match, search, finditer 등의 메소드로 객체를 탐색할 경우, Match Object 형태로 결과를 반환한다.

하지만 일치하는 문자가 있는지? 시작은 어디인지? 끝은 어디인지?가 궁금한 것이다.

위의 4가지 메소드를 사용하면 match object 결과에서 원하는 부분만 return 할 수 있다.

 

    2.6. Compile Option

Compile Option
종류 설명 예시
IGNORECASE 또는 I 대소문자를 구분하지 않음 re.search('abc', 'ABC', re.IGNORECASE)
>>> <re.Match object; span=(0, 3), match='ABC'>
DOTALL 또는 S 줄바꿈 문자를 포함한 모든 문자와 매칭 re.search('a.b', 'a\nb', re.DOTALL)
>>> <re.Match object; span=(0, 3), match='a\nb'>
MULTILINE 또는 M 메타 문자 ^, $를 각 줄에 대해서 적용 re.search('^a', 'ba\na', re.MULTILINE)
>>> <re.Match object; span=(3, 4), match='a'>

re.search('k$', 'asd\n'apk', re/MULTILINE)
>>> <re.Match object; span=(5, 6), match='k'>
VERBOSE 또는 X 문자열에 사용한 공백과 주석 무시 re.compile(r'''
    \d+    # 숫자
    \.       # 소수점
    \d+    # 소수 부분
    ''', '3.141592', re.VERBOSE)
>>> <re.Match object; span=(0, 8), match='3.141592'>

컴파일 옵션(혹은 플래그)라고 부르는 동작 방식이 있다.

re 라이브러리에서 제공하는 패턴 객체 생성 시의 컴파일 옵션이다.

 

마지막 VERBISE 옵션에서 사용한 r'' 문법은 raw string 문법이다.

메타 문자를 활용하여 [\d+]라고 사용하지 않고 단 한 글자만 필요하여 \d라고 쓰고 싶다고 해보자.

이러면 역슬래시(\)를 메타 문자로 인식해버린다. 따라서 \d가 아닌 \\d라고 작성해주어야 한다.

이것을 r'\d'라고 작성할 수 있다.

예시에서 r''' '''로 3개씩 사용한 이유는 여러 줄에 걸쳐있는 경우이기 때문이다.

 

 

고민을 시작한 문제 출처 : https://school.programmers.co.kr/learn/courses/30/lessons/67257

참고한 문서 자료 : https://wikidocs.net/4308

참고한 문서 자료 : https://wikidocs.net/21703

참고한 웹 사이트 : https://nachwon.github.io/regular-expressions/

참고한 웹 사이트 : https://spidyweb.tistory.com/373

'Computer Science > 파이썬(Python)' 카테고리의 다른 글

Python Decorator  (0) 2023.11.12
Python Iterator  (0) 2023.11.11
Python list.pop(0) vs deque.popleft()  (0) 2023.10.03
Python float OverflowError  (0) 2023.09.19
Python Namespace(Scope)  (0) 2023.08.18

1. Prologue

    1.1. 함수(f)를 변수(x)에 '=' 명령어로 할당(assignment) 가능하다.

    1.2. 함수(f)를 iterable한 객체의 element로 지정 가능하다.

    1.3. 함수(f)에게 전달할 인자(argument)와 함수(f)의 return 값으로 호출 가능하다.

    1.4. 함수(f) 자체를 argument와 return으로 지정 가능하다.

2. Decorator 개념

    2.1. Decorator 응용

3. Epilogue

(작성 중)


1. Prologue

최근 넥슨 과제를 진행하면서 여러모로 많이 깨달았다.

개발자에게 CS 지식이 필요하지만, 내 직군에 맞는 CS 지식이 필요하다.

내 직군에 맞는 깊이와 경험이 필요하다는 걸 깊게 체감했다.

그리고 공부란, 내가 부족한 걸 채워나가는 거란 사실을 다시 느꼈다.

내게 부족한 decorator를 공부할 때가 온 것이다.

 

이전에 @lru_cache라는 것으로 속도 향상이 된다는 것을 보았다.

이것이 어떤 구조로 동작하는지 찾아보아도 나오는 설명들은 아래와 같다.

"@lru_cache는 표준 라이브러리 functools에 있는 decorator다.

같은 인수를 전달했던 호출 결과가 이미 캐시되어 있다면, 함수를 실행하지 않고 캐시 결과를 반환한다."

 

그래 캐싱을 이용한다는 건 알겠어. 그래서 @은 대체 뭐하는 친구지? 어떤 과정으로 동작하는 거지?

@lru_cache뿐만 아니라 다른 github나 소스 코드, 공식 문서를 보면 종종 @(at)이 튀어나온다.

결국 실력 향상을 위해서는 반드시 넘어야 할 하나의 퀘스트이다.

Decorator(데코레이터)를 이해하기 위해서는 사전 설명이 조금 필요하다.

몇 달동안  이를 모른 채 고민하다, 약간의 사전 설명을 보고 드디어 깨달았다.

 

print(type(10))         # <class 'int'>
print(type("string"))   # <class 'str'>
print(type([1, 2, 3]))  # <class 'list'>

Python의 모든 변수는 객체로 이루어진다.

숫자는 int class의 객체, 문자는 str class의 객체처럼 파이썬의 모든 변수는 객체다.

 

# Case 1
a = 10
print(a + 3)        # 13

# Case 2
a = 10
print(a.__add__(3)) # 13

위의 코드에서 Case 1을 살펴본다면, a라는 변수에 10을 넣는다.

그 다음 정수형 변수 a에 3을 더한 다음 출력한다. 라는 형태로 동작한다고 보인다.

하지만 실제 동작 과정은 Case 2처럼 이루어진다.

int class의 인스턴스로 a를 선언하고 값을 10으로 초기화한다. 

그 다음  __add__ 메소드를 이용하여 3의 값을 더해준다.

그래서 "+" 연산자가 아니라 __add__ 메소드로 실행해도 같은 결과를 뱉는다.

 

궁금한 사항이 있다면 int, str 같은 예약어를 따라가볼 수 있다.

VSC 기준으로  ctrl + LC(좌클릭)로 해당 정의를 따라가면서 클래스를 자세하게 살펴보자.

 

def foo():
    return


print(type(foo))   # <class 'function'>

그렇다면 함수는 어떨까?

Python의 모든 변수는 객체라고 했으니 함수도 객체일까?

맞다. def로 선언한 Python의 함수는 function class의 인스턴스가 된다.

위의 코드에서 잘보면 foo라는 함수의 타입을 확인할 때 foo()가 아니라 foo라고 작성했다.

함수 이름도 다른 객체(변수)처럼 동작하는 것이 가능하다는 이야기다.

 

변수를 사용하는 것처럼, 아래와 같은 방식으로 모두 함수를 사용할 수 있다.

1. 함수(f)를 변수(x)에 '=' 명령어로 할당(assignment) 가능하다.

2. 함수(f)를 iterable한 객체의 element로 지정 가능하다.

3. 함수(f)에게 전달할 인자(argument)와 함수(f)의 return 값으로 호출 가능하다. 

4. 함수(f) 자체를 argument와 return으로 지정 가능하다.

 

    1.1. 함수(f)를 변수(x)에 '=' 명령어로 할당(assignment) 가능하다.

두 매개변수를 더하여 값을 반환하는 foo()라는 함수를 작성한다.

그리고 foo의 인자로 1과 2를 던져주면 3을 반환한다.

사용자 정의 함수 foo()를 bar라는 변수에 넣는다.

bar의 인자로 1과 2를 던져주면 foo() 함수와 동일하게 3을 반환한다.

 

사용자 정의 함수로 직접 작성한 foo()와, 변수에 할당한 bar를 비교해본다.

type과 주소(id) 그리고 객체까지 전부 동일한 것을 알 수 있다. 

 

사용자 정의 함수뿐만 아니라 Python에서 제공하는 built-in function도 동일하다.

위의 예시처럼 print()라는 기본 함수를pooorint라는 변수에 넣어 사용하는 것도 가능하다. 

 

    1.2. 함수(f)를 iterable한 객체의 element로 지정 가능하다.

대표적으로 순회 가능한(iterable) 객체 list다.

위에서 선언한 foo와 bar를 func_list의 element로 넣어줬다.

또한 built-in function으로 확인할 range 함수도 추가로 같이 넣어줬다. 

 

func_list[0]은 foo에 해당한다.

따라서 func_list[0](1, 2)는 foo(1, 2)과 똑같은 것이기에, 3을 반환한다.

func_list[1]은 bar에 해당한다. bar는 foo()를 할당받은 변수다.

따라서 func_list[1](1, 2)는 bar(1, 2)가 되고, 이는 foo(1, 2)와 똑같기에 3을 반환한다.

func_list[2]는 range에 해당한다. 1.1.에서 언급한 것처럼 built-in function도 상관없다.

따라서 fun_list[2](10)은 range(10)에 해당하기에, 0부터 9까지 갖는 리스트를 반환한다.

 

또 다른 순회 가능한(iterable) 객체 dict이다.

func_list처럼 func_dict을 같은 element로 넣어줬다.

index로 호출하냐, key로 호출하냐의 차이가 있을 뿐, 동작 과정은 동일하다.

 

    1.3. 함수(f)에게 전달할 인자(argument)와 함수(f)의 return 값으로 호출 가능하다.

길어서 복잡해보이지만 어렵지 않다.

핵심은 변수를 사용하듯 함수를 사용할 수 있다는 것이다.

1.3.에 해당하는 내용은 실제로 현재 코드에서도 자주 사용하는 방법이다.

 

foo()라는 함수의 인자로 foo() 함수와 bar() 함수를 사용하는 것이다.

위의 코드에서 4가지 상황은 모두 동일하니 2번째 상황으로 설명한다.

첫 번째 인자로는 foo(1, 2)를 준다. foo(1, 2)는 3이다.

두 번쨰 인자로는 bar(3, 4)를 준다. bar(3, 4)는 foo(3, 4)이니 7이다.

그럼 결국 foo()의 인자로 3과 7을 주는 셈이다.

결과적으로 10을 반환한다.

 

특정 함수의 return으로 다른 함수를 호출하는 것 또한 자주 사용해서 익숙할 것이다.

foo 함수의 결과보다 10 큰 값을 반환하는 foo10() 함수를 정의했다고 해보자.

(이때 각 매개변수에 5씩 더하든, 하나에만 10을 더하든 상관없다.)

 

foo10() 함수는 결과를 반환할 때, foo() 함수를 다시 호출할 것이다.

foo10()을 호출할 때 1과 2를 인자로 넘겨주면, foo에 각각 5씩 더해서 다시 인자로 넘겨준다.

결국 foo()에는 6과 7을 넘겨주는 것이니, 결과적으로 13을 반환한다.

 

    1.4. 함수(f) 자체를 argument와 return으로 지정 가능하다.

사실상 이 부분이 핵심 개념이고, decorator로 가기 위한 초석이다.

이해할 때 막히지 않게끔, 최대한 단계별로, 조금씩 조금씩 어렵게 함수를 바꿔가면서 작성한다.

결국 이전에 작성한 중첩 함수에서의 쓰임도 이해할 수 있다.

최종적으로 데코레이터도 이해할 수 있다.

 

가장 단순한 형태부터 살펴보자.

return_foo()라는 사용자 정의 함수의 return은 foo다.

foo() 함수를 호출하는 것이 아니라, bar라는 변수에 할당하는 것처럼 foo를 반환한다.

그럼 return_foo()를 호출하면 isFoo에는 무엇이 들어갈까?

bar와 똑같이 foo가 들어가게 된다.

결국 foo와 bar와 isFoo는 전부 동일한 상태가 된다.

isFoo(1, 2)를 출력해보면 foo(1, 2)와 동일하므로 3을 출력한다.

 

아주 살짝 더 복잡하게 코드를 바꿔서 확인해보자.

이번에는 return_foo() 함수에 매개변수를 추가하고, return을 bar로 바꿨다.

이럴 때 출력문, return_foo(10, 20)(1, 2)는 어떻게 동작할까?

 

실은 return_foo()의 매개변수는 함정이다.

VSC의 색상 테마를 봐도 알 수 있지만, 흐린 하늘색이다. 사용하지 않는다는 말이다.

return_foo()가 어떤 매개변수를 받든, 위의 상황에서는 return이 bar다.

즉, 어떤 값을 넣어주든 bar를 반환한다는 말이다.

 

return_foo(10, 20)은? bar다.

return_foo(10,20)(1, 2)는 bar(1, 2)와 똑같은 코드다.

bar는 1.1.에서 설명했듯 foo() 함수를 할당한 변수다.

그럼 bar(1, 2)는 결국 foo(1, 2)와 동일하다.

결과적으로는 3을 출력한다. 

 

조금만 더 복잡하게 꼬아보자.

return_func() 사용자 정의 함수에 매개변수 f를 받는다.

new_func에 f를 할당하고, return한다.

이때 return_func(foo)를 한다면 new는 어떻게 될까?

 

return_func(foo)를 하게 되면 입력으로 foo 함수를 전달한다.

new_func = foo는 bar = foo와 동일한 코드다.

결국 new_func에는 foo가 할당된다.

return_func의 최종 반환값은 foo가 된다.

그럼 new는 결국 foo다.

new(1, 2)는 foo(1, 2)와 같으니, 결과적으로 3을 출력한다. 

 

사실 new_func = f라는 코드로 한 단계 거쳐갔을 뿐, 별 의미는 없다.

def return_func(f):를 한 다음, 바로 return f 하는 것과 동일한 코드다.

하지만 저런 식으로 입력받은 함수를 새롭게 할당하고 return이 된다는 게 중요하다.

 

foo랑 bar에 너무 매몰될까봐 다른 예시도 작성했다.

지금 상황에서 핵심은 '함수를 변수처럼 사용할 수 있다'이지 foo와 bar가 아니다.

위와 똑같은 return_func() 함수에 built-in function에 해당하는 len 함수를 넣어줬다.

그럼 new는 이제 len처럼 길이를 세는 함수가 된 것이다. 

 

익숙해졌으니 이젠 함수에 함수를 감싸보자.

INNER에 outer(1)을 할당하면 어떻게 될까?

 

outer() 사용자 정의 함수는 inner를 반환한다.

inner는 outer의 내부에 선언된 중첩 함수로, 외부에서 접근할 수 없다.

하지만 outer(1)을 한다면, def inner(IN): return 1 + IN이라는 형태가 되어, outer 함수의 반환값이 된다.

즉 INNER는 def inner(IN): return 1 + IN의 함수가 된 것이다.

이때 INNER(2)를 호출하면, inner(2)와 동일하고, 1 + 2가 되어 3을 출력한다.

만약 INNER(3)을 호출했다면, inner(3)과 동일하고, 1 + 3이 되어 4를 출력할 것이다.

 

이것이 저번 중첩 함수에서 예시로 들었던 함수다.

바로 직전의 코드를 이해했다면, 이 코드도 이해할 수 있을 것이다.

 

generate_power(2)를 호출하고, power를 반환한다.

그럼 power의 exponent(지수)에 2를 넣은 채로 power 함수를 반환하는 꼴이 된다.

즉 raise_two는 def power(base): return base ** 2의 함수가 된 것이다.

이때 raise_two(4)를 해주면 base에 4를 넣은 꼴이니, 4 ** 2를 하여 16을 출력한다.

 

2. Decorator 개념

함수(f)를 입력받고 그대로 반환하는 대신, 중간에 Hello, world!를 출력하는 noob() 함수가 있다.

입력받은 숫자(num)를 그대로 반환하는 just_return() 함수가 있다.

이때 noob() 함수에 just_return 함수를 넣고, 그것을 just_return에 할당하면 어떻게 될까?

 

just_return이 noob에 들어가면 Hello, world!를 출력하고 그대로 반환된다.

그대로 반환된 함수를 다시 just_return에 넣었으니 실질적인 변화는 없다.

여기서 중요한 것은 '함수를 인자로 받는 함수'다.

이 개념이 바로 Decorator다.

 

위에서 언급한 just_return = noob(just_return)이 바로 Decorator에 해당한다.

해당 부분(호출하고자 하는 함수명)을 @ 기호와 함께 작성하면 끝이다.

just_return 함수 위에 @noob을 작성하면, just_return = noob(just_return)와 같은 동작을 한다.

 

just_return() 함수를 출력하지 않고, decorator로 선언만 해보자.

그럼 noob() 함수의 print문을 출력하는 것을 볼 수 있다.

Decorator로 선언할 때 해당 함수를 실행하는 것을 알 수 있다.

 

    2.1. Decorator 응용

def deco(f):
    def deco_hello():
        print("*" * 20)    # 기능 추가
        f()                # 기존 함수 호출
        print("*" * 20)    # 기능 추가
    return deco_hello


@deco
def Hello():
    print("Hello")
    

# ********************
# Hello
# ********************
Hello()

중첩 함수를 사용하여 decorator를 사용할 수 있다.

동작 방식은 1.4.에서 설명한 generate_power() 함수를 decorator로 만들었다고 보면 된다.

Hello() 함수에 @deco를 하면 deco() 함수에 Hello를 넣는 것과 동일하다.

f()에 Hello() 함수 동작인 print("Hello")가 들어간 채로, deco_hello를 반환한다.

결과적으로 Hello()를 호출하면, 주석처럼 출력이 된다.

 

class KoreanTop:
    def __init__(self, n):
        print("check")
        self.n = n
        
    def __call__(self, f):
        print("HIPHOP")
        return f


@KoreanTop(3)
def SuperStarK2(n):
    return 2 * n

함수가 아니라 클래스에 대해서도 Decorator를 사용할 수 있다.

조금 복잡하여 이런 것이 있구나 하는 내용으로 보면 된다.

Decorator를 사용할 때 매개변수(괄호 안에 값)를 넘겨주는 경우, 대다수가 클래스이다.

(함수든 클래스든 매개변수를 줄 수도 있고 안 줄 수도 있어서, 코드에 맞게 해석해야 한다.)

__init__는 객체를 생성할 때 초기화하는 메소드고, __call__는 인스턴스가 호출됐을 떄 호출하는 메소드다.

 

@KoreanTop(3)의 코드는 SuperStarK2 = KoreanTop(3)(SuperStarK2)와 같다.

KoreanTop(3)을 실행할 때, __init__ 메소드를 실행한다.

(SuperStarK2)를 실행할 때, 인스턴스를 호출하니 __call__ 메소드를 실행한다.

2.에서 설명한 것처럼 @로 Decorator를 선언할 때, 해당 함수 혹은 클래스를 호출한다.

위의 코드에서는 따로 객체를 생성하거나 출력하는 코드가 없지만, 실행하면 check와 HIPHOP을 출력한다.

 

그밖에도 캐싱과 클래스 효율을 위해 사용하는 @lru_cache, @dataclass,

github 코드에서 종종 보이는 @staticmethod, @classmethod, @property,

조금 더 심화된 내용으로 @wraps, @contextmanager를 사용하기도 한다.

이런 다양한 Decorator의 심화 버전은 추후에 정리한다.

 

3. Epilogue

이번에 각잡고 Decorator를 공부했는데 쓰임이 너무나 다양했다.

함수와 클래스끼리 서로 사용이 가능하고, 인자를 주는 경우도 있고... 대충 봐도 8가지는 넘는다.

심지어 구조만 따졌을 때 저런 거지, 호출하는 함수와 클래스가 뭐냐에 따라서도 조합이 엄청나게 많아진다...

Decorator를 잘 사용하면 코드 재사용성이 높아질 것 같긴 한데... 조금 더 공부가 많이 필요해보인다.

또한, 이름이 비슷해서 자칫 헷갈릴 수 있는 Decorator, Iterator, Generator에 대해서도 정리를 할 필요가 있어보인다.

 

 

고민을 시작한 문제 출처 : https://miny-genie.tistory.com/227

참고한 문서 자료 : https://wikidocs.net/160127

참고한 문서 자료 : https://www.daleseo.com/python-decorators/

참고한 문서 자료 : https://realpython.com/primer-on-python-decorators/

참고한 웹 사이트 : https://blog.naver.com/PostView.naver?blogId=youndok&logNo=222060631620&proxyReferer=

참고한 웹 사이트 : https://ctkim.tistory.com/entry/데코레이터decorator

참고한 웹 사이트 : https://www.youtube.com/watch?v=7dMDSXx7TwE

참고한 웹 사이트 : https://www.youtube.com/watch?v=VY7akCnhQ9o

'Computer Science > 파이썬(Python)' 카테고리의 다른 글

Python 정규 표현식(Regular Expression)  (0) 2023.11.19
Python Iterator  (0) 2023.11.11
Python list.pop(0) vs deque.popleft()  (0) 2023.10.03
Python float OverflowError  (0) 2023.09.19
Python Namespace(Scope)  (0) 2023.08.18

1. Iterator

2. Iterator Protocol

3. Constraints

4. Iterator vs Iterable

5. Collection vs Sequence

6. Collections 라이브러리

7. Itertools 라이브러리

8. Appendix


1. Iterator

Iterator라는 개념은 Python 2.2 버전에서 PEP 234에 의해 추가되었다.

반복하는 프로세스에 대한 통일된 추상화 규격이 필요했기 때문이다.

 └ 반복하는 프로세스: 하드 코딩 반복, indefinite iteration(무기한 반복, while), definite iteration(기한 반복, for)

추상화를 통해 collections과 unordered collections에 대해서 확실한 규격을 갖게 되었다.

 └ 여기서 collections는 변수나 아이템들이 모여있는 그룹(추상화 데이터 타입)을 말한다.

 

결국 파이썬에서 Iterator는 data collections을 순회하는 객체다.

파이썬에서 Iterator는 디자인 패턴 중 Iterator Pattern으로 구현한다.

또한 Iterator Protocol이라는 내부 구조를 구현해야 한다.

이를 통해 list, tuple, dictionary, set 같은 자료 구조의 요소(element)에 접근할 수 있다.

 

Iterator의 핵심 행동은 다음과 같다.

1. 한 번에 하나의 아이템을 반환하기

2. 현재 방문한 아이템 추적하기

 

2. Iterator Protocol

https://realpython.com/python-iterators-iterables/

Iterator Protocol은 위의 두 메소드를 말한다.

파이썬의 공식 iterator든, 직접 만든 custom iterator class든, iterator protocol이 들어가야만 한다.

그래야 Iterator라고 부를 수 있다.

 

__iter__() 메소드는 일반적으로 self를 반환하는데, 이는 현재 객체인 iterator 자신에 대한 참조를 가진다.

__iter__()의 유일한 책임은 iterator 객체(self, 현재 인스턴스)를 반환하는 것이다.

 

__next__() 메소드는 데이터 스트림에서 다음 아이템을 반환한다.

더 이상 스트림에 아이템이 없을 때는 흐름 제어를 위해 StopIteration 예외를 발생시킨다.

 

class SequenceIterator:
    def __init__(self, sequence):
        self._sequence = sequence
        self._index = 0

    def __iter__(self):
        return self

    def __next__(self):
        if self._index < len(self._sequence):
            item = self._sequence[self._index]
            self._index += 1
            return item
        else:
            raise StopIteration
    
        
# Sample Code
si = SequenceIterator([1, 2, 3, 4])

print(si.__next__())    # 1
print(si.__next__())    # 2
print(si.__next__())    # 3
print(si.__next__())    # 4
print(si.__next__())    # StopIteration

해당 Iterator Protocol을 사용하여 직접 custom iterator class를 만들었다.

객체를 초기화할 떄, 특정 sequence 자료를 입력받고, index를 0으로 한다.

그리고 __next__()가 호출될 때마다, index에 1을 더하여 해당 index의 요소를 반환한다.

가장 처음 입력받는 인스턴스의 길이보다 index가 커지게 되면, StopIteration을 발생한다.

 

sequence = SequenceIterator([1, 2, 3, 4])

# Get an iterator over the data
iterator = sequence.__iter__()

while True:
    try:
        # Retrieve the next item
        item = iterator.__next__()
    except StopIteration:
        break
    else:
        # The loop's code block goes here...
        print(item)

객체를 생성하여 접근하는 코드는 위와 같다.

SequenceIterator로 [1, 2, 3, 4]를 넘겨준 뒤, sequence 인스턴스에 할당한다.

__iter__()로 SequenceIterator의 데이터를 가져온 뒤, 반복문으로 요소에 접근한다.

반복문에서는 try - except - else를 활용하여, except로 빠지지 않으면 else로 이동하여 코드를 실행한다.

iterator.__next__()로 다음 item을 받아온 뒤, 오류라면 break 아니라면 해당 아이템을 출력한다.

 

3. Constraints

# Sample Code
numbers_iter = SequenceIterator([1, 2, 3, 4])

# 1, 2, 3, 4를 한 줄마다 출력
for number in numbers_iter:
    print(number)
    
# 아무 것도 출력하지 않음
for number in numbers_iter:
    print(number)

위에서 작성한 SequenceIterator를 사용하여 두 번의 반복문을 돌려보자.

첫 번째 반복문에서는 numbers_iter에 넣어준 모든 요소를 정상적으로 출력하지만, 두 번째에서는 아니다.

이는 첫 번째 반복문과 두 번째 반복문이 이어지기 때문이다.

즉 첫 번째 반복문으로 해당 iterator는 모든 요소를 소진했다.

두 번째 반복문을 시작할 때 이미 전부 소진했기 때문에, StopIteration을 뱉고 종료한다.

 

# Sample Code
numbers_iter = SequenceIterator([1, 2, 3, 4])

# 1, 2, 3, 4를 한 줄마다 출력
for number in numbers_iter:
    print(number)
    
another_iter = SequenceIterator([1, 2, 3, 4])
    
# 1, 2, 3, 4를 한 줄마다 출력
for number in another_iter:
    print(number)

따라서 같은 요소에 대해서 여러 번 순회하고 싶다면, 똑같은 객체를 여러 번 재할당해야만 한다.

위의 코드는 numbers_iter에 [1, 2, 3, 4]를 할당, 순회한 뒤 다시금 할당, 순회를 하는 예제다.

 

numbers_iter = SequenceIterator([1, 2, 3, 4, 5, 6])

# 1, 2, 3을 한 줄마다 출력
for number in numbers_iter:
    if number == 4:
        break
    print(number)

# 5를 출력
next(numbers_iter)

# 6을 출력
next(numbers_iter)

# StopIteration
next(numbers_iter)

위에서 언급했듯이, Iterator는 현재 위치를 기억하고 아이템을 방문한다.

요소 개수가 명확하다면(위의 코드에서는 4), 조건에 도달하기까지 더 이상 아무 것도 하지 않아도 된다.

__next__() 메소드만 정의했다는 것은 바꾸어말하면, 이전으로는 돌아갈 수 없음을 뜻한다.

즉 __previous__() 메소드를 통하여 이전 index로 향할 수 없다.

 

만약 4에 도달했다고 해보자. 이때 numbers_iter는 앞으로 얼마나 많은 요소를 순회할 수 있을지 알까?

이전 상태로 되돌아갈 수 없다는 점 외에, 동시에 전체 길이를 파악할 수 없다는 점도 있다.

왜냐하면 Iterator는 한 번에 하나의 요소만을 저장하기 때문이다.

Iteraotr가 유한하다는 전제 하에, 전체 요소를 돌아야만 길이를 파악할 수 있다.

 

numbers_iter = SequenceIterator([1, 2, 3, 4, 5, 6])

numbers_iter[2]
'''
Traceback (most recent call last)
TypeError: 'SequenceIterator' object is not subscriptable
'''

numbers_iter[1:3]
'''
Traceback (most recent call last)
TypeError: 'SequenceIterator' object is not subscriptable
'''

또 다른 문제점으로는 접근 방법이다.

Iterator는 한 번에 하나의 요소에 순차적으로 접근한다는 특징을 가지고 있다.

그렇기에 특정 인덱스 위치로 접근하는 indexing이나, 범위를 추출하는 slicing이 불가능하다.

 

이처럼 Iterator 여섯 가지 제약 사항을 확인할 수 있다.

 특정 iterator에 대해서 한 번 이상 순회(반복)할 수 없다.

ㆍ iterator의 모든 요소를 소진했을 때, 재시작하도록 초기화할 수 없다.

ㆍ 오직 앞으로(forward)만 갈 수 있고, 되돌아(backward)갈 수 없다.

ㆍ 끝에 도달하기 전까지는 전체 길이를 알 수 없다.

ㆍ list, tuple처럼 대괄호[ ]를 사용한 indexing과 slicing이 불가능하다.

 

class ReusableRange:
    def __init__(self, start=0, stop=None, step=1):
        if stop is None:
            stop, start = start, 0
        self._range = range(start, stop, step)
        self._iter = iter(self._range)

    def __iter__(self):
        return self

    def __next__(self):
        try:
            return next(self._iter)
        except StopIteration:
            self._iter = iter(self._range)
            raise

물론 첫 번째와 두 번째 상황은 실제 Iterator에 적용되는 문제점은 아니다.

해결 방법은 위와 같은 코드를 사용하여, StopIteration에 걸릴 시 본래 값으로 초기화해준다.

위 코드에 존재하는 next() 함수는 built-in function으로 __next__() 메소드를 호출하는 함수다.

반복문 없이 iterator를 순회할 수 있기에, 무한이나 길이를 모르는 iterator를 다룰 때 유용하게 사용할 수 있다.

또한 next() 함수는 두 번째 매개변수를 갖는데, 이는 StopIteration이 발생했을 때 반환할 값을 말한다.

 └ Ex) next([1, 2, 3], 0) next를 통해 1, 2, 3 요소를 전부 순회하고 다시 next()로 접근한다면(StopIteration) 0을 반환한다.

 

4. Iterator vs Iterable

가장 첫 1. Iterator 챕터에서 설명했듯 iter라는 것은 '반복'이라는 뜻이다.

Iterator는 '모여있는 아이템을 순회하는 객체'라고 설명했다.

하지만 세간에는 Iterator라는 용어도 있고, Iterable이라는 용어도 있다.

Iterator가 '순회하는 객체'라면, 직관적으로 Iterable은 '순회 가능한'이라는 뜻이다.

사각형과 직사각형의 관계처럼, Iterable이 Iterator의 상위 개념처럼 보인다.

엄밀히 따지자면, 두 개념은 종속하지 않는 다른 개념이다.

 

https://realpython.com/python-iterators-iterables/#comparing-iterators-vs-iterables

Iterator와 Iterable를 구분하는 차이는 __next__() 메소드의 유무다.

클래스를 구현하는 내부 구조에 __next__() 메소드가 있으면 Iterator, 없으면 Iterable이다.

 └ 추가로 Iterable에는 __iter__() 또는 __getitem__() 메소드가 있어야 한다(PEP 234).

 └ __iter__()와 __getitem__()을 Iterable Protocol이라고 부른다.

하지만 실무에서는 iterable 안에 iterator가 포함하게끔 설계한다고 한다.

직접 클래스를 설계할 때도 iterator가 iterable에 포함하도록 설계하는 원칙을 따라간다고 한다.

 

밑에서 부연 설명을 하겠지만, Iterator에는 파일 객체, generator, itertools 내의 모듈 등이 해당한다.

놀랍게도 Iterable에는 list, tuple 등이 해당한다.

Iterable에는 __next__()가 없어서 __iter__()로 객체를 호출하더라도 늘 같은 값이 나온다.

왜냐하면 __iter__() 메소드는 '객체 자신을 반환'하는 책임밖에 없기 때문이다.

이와 반대로 Iterator는 호출자의 요구가 있을 때만 한 요소씩 반환한다.

그렇기에 Iterator가 Iterable보다 메모리 측면에서 효율적이다.

 

list 내부 구조(좌상단), dict 내부 구조(좌하단), tuple 내부 구조(우상단), set 내부 구조(우하단(

이런 이유로 Python에서 사용하는 list, dict, tuple, set은 전부 Iterable 객체다.

위 사진에 첨부하지는 않았지만, str도 Iterable 객체에 해당한다(__iter__(), __getitem__() 메소드가 전부 존재한다).

3. Constraints 챕터 마지막 부분에서 next() 함수는 __next__() 메소드를 호출하는 함수라고 했다.

고로 위에서 언급한 Iterable 객체들은 next()를 호출하는 것이 불가능하다.

이런 Iterable 객체를 iter() 함수로 감싸 순회하는 것이 가능하다. 이것이 range() 함수 동작 원리다.

Iterator라면 next()로 요소들을 하나씩 확인하고, Iterable이라면 iter()를 씌운 뒤 next()로 요소들을 하나씩 확인한다.

 

5. Collection vs Sequence

Iterable하다는 것은 '해당 클래스 내부 구조에 __iter__()나 __getitem__()이 있는 순회 가능한 객체'임을 알았다.

Iterable한 객체 형태에 따라서 두 가지 타입으로 나눌 수 있다.

하나는 Collection 타입이고, 다른 하나는 Sequence 타입이다.

 

Collection 타입

개념 : 데이터가 서로 관련이 없는, 여러 개의 요소를 가질 뿐인 데이터

종류 : set, dictionary, default dictionary, OrderedDict, Counter 등

ㆍ 멤버쉽 연산자(in)를 사용할 수 있음 ㆍ 크기(길이)가 있음 ㆍ 순회 가능

 

Sequence 타입

개념 : 여러 개의 요소가 있으며, 요소에 순서가 존재하는 데이터 타입

종류 : list, tuple, str

ㆍ 멤버쉽 연산자(in)를 사용할 수 있음 ㆍ 크기(길이)가 있음 ㆍ 순회 가능

ㆍ 순서가 유지 ㆍ 정수를 통한 indexing과 slicing 가능 ㆍ 복제 가능(*N) ㆍ 같은 타입은 잇는 것이 가능(s + s)

 

Collection과 Sequence 타입에서 가장 큰 차이는, '순서가 존재하느냐'이다.

각 요소에 대해 하나씩 반환은 가능하지만, 정해진 순서가 없는 set과 dict은 collection이다.

각 요소에 대해 하나씩 반환이 가능하며, 순서대로 나열되어 있는 list와 tuple은 sequence이다. 

 

이때 Collection은 가장 처음에 언급한 Collections하고는 다르다.

Collections는 변수나 아이템들이 모여있는 그룹을 말한다. 하나의 거대한 집합 같은 개념이다.

따라서 list, stack, queue, deque, priority queue, set, array, graph, tree 등이 전부 해당한다.

파이썬으로 코딩테스트를 준비하거나 알고리즘을 공부한 사람이라면 collections가 뭔가 낯익을 거다.

알고 있는 그 collections가 맞다.

 

6. Collections 라이브러리

collections는 어떤 아이템이 모여있는 그룹, 위에서 설명한 다른 자료구조들을 모아둔 라이브러리다.

위의 사진은 dir() 함수로 collections에서 호출할 수 있는 목록을 출력한 것이다.

ChainMap, Counter, OrderedDict, defaultdict, deque, namedtuple 등

어떤 아이템들에 대해서 모아두는 자료구조들만을 정리한 라이브러리다.

파이썬에서 자주 사용하는 두 라이브러리(collections, itertools) 중 하나이다.

itertools에서 iter라는 단어가 보인다. iterable과 어떤 연관이 있을까?

 

7. Itertools 라이브러리

itertools는 iterator를 위한 다양한 도구를 제공하는 라이브러리다.

위의 사진은 dir() 함수로 itertools에서 호출할 수 있는 목록을 출력한 것이다.

count, cycle, repeat 등의 무한한 sequence를 생성하는 iterable 객체 목록,

accumulate, chain, groupby, takewhile, dropwhile 등의 종료 조건이 있는 iterable 객체 목록,

permutations, combinations, product 등의 데이터 조합을 생성하는 목록.

이것이 itertools 구성이다.

 

말그대로 collections는 데이터들의 집합을 모아둔 라이브러리고,

itertools는 순회 가능한 함수를 모아둔 라이브러리에 해당한다.

앞으로는 헷갈리지 않고 적재적소에 사용할 수 있다. 

 

8. Appendix

참고한 문서 자료 : https://realpython.com/python-iterators-iterables/

참고한 문서 자료 : https://docs.python.org/3/whatsnew/2.2.html#pep-234-iterators

참고한 문서 자료 : https://peps.python.org/pep-0234/

참고한 문서 자료 : https://en.wikipedia.org/wiki/Collection_(abstract_data_type)

참고한 문서 자료 : https://en.wikipedia.org/wiki/Iterator_pattern

참고한 문서 자료 : https://docs.python.org/3/glossary.html#term-sequence

참고한 문서 자료 : https://docs.python.org/3/library/functions.html#next

참고한 문서 자료 : https://wikidocs.net/84391

참고한 웹 사이트 : https://lgphone.tistory.com/38

참고한 웹 사이트 : https://www.acmicpc.net/board/view/102619

참고한 웹 사이트 : https://velog.io/@ehdgus8054/파이썬-컬렉션-자료구조

참고한 웹 사이트 : https://tibetsandfox.tistory.com/27

참고한 웹 사이트 : https://born-dev.tistory.com/18

'Computer Science > 파이썬(Python)' 카테고리의 다른 글

Python 정규 표현식(Regular Expression)  (0) 2023.11.19
Python Decorator  (0) 2023.11.12
Python list.pop(0) vs deque.popleft()  (0) 2023.10.03
Python float OverflowError  (0) 2023.09.19
Python Namespace(Scope)  (0) 2023.08.18

dd

 

list / tuple method
  Operation Example Big-O Notes
1 Index l[i] O(1) 인덱스로 값 탐색
2 Store l[i] = 0 O(1) 인덱스로 데이터 저장
3 Length len(l) O(1) 리스트 길이 반환
4 Append l.appned(0) O(1) 리스트 가장 마지막에 데이터 저장
5 Pop l.pop() O(1) 리스트 가장 마지막 요소 꺼내기
6 Clear l.clear() O(1) 리스트 초기화, l = []와 동일한 연산
7 Slice l[a:b] O(b-a) 슬라이싱하는 요소 수에 비례한 시간 소모
8 Extend l.extend(...) O(len(...)) 확장 길이만큼 시간 소모
9 Construction list(...) O(len(...)) ...는 iterable object, 객체 크기만큼 시간 소모
10 check == , != list1 == list2 O(N) 전체 리스트 비교
11 Insert l[a:b] = ... O(N) 데이터 삽입
12 Delete del l[i] O(N) 데이터 삭제
13 Containment x in / not in l O(N) 포함 여부 확인
14 Copy l.copy() O(N) 데이터 복사
15 Remove l.remove(...) O(N) 데이터 삭제
16 Pop l.pop(i) O(N) i번째 데이터 삭제 후 모든 요소 한 칸씩 이동
17 Extreme value min(l) / max(l) O(N) 리스트 전체 확인
18 Reverse l.reverse() O(N) 리스트 역순 정렬
19 Iteration for v in l: O(N) 리스트 전체 확인
20 Sort l.sort() O(N log N) 기본 정렬 알고리즘
21 Multiply k * l O(k * N) k만큼 리스트 복사

ㅇㅇ

 

Frozen set / set method
  Operation Example Big-O Notes
1 Add s.add(0) O(1) 집합 가장 마지막에 데이터 추가 
2 Containment x in / not in s O(1) 포함 여부 확인
3 Remove s.remove(...) O(1) 데이터 삭제
4 Discard s.discard(...) O(1) 데이터 삭제(없어도 에러를 반환하지 않음)
5 Pop s.pop() O(1) 무작위 요소 pop(순서를 보장하지 않는 set)
6 Clear s.clear() O(1) 초기화
7 Construction set(...) O(len(...)) ...는 iterable object, 객체 크기만큼 시간 소모
8 check ==, != s != t O(len(s)) 전체 집합 비교
9 <= / < s <= t O(len(s)) 부분 집합 여부 확인
10 >= / > s >= t O(len(t)) 부분 집합 여부 확인
11 Union s, t O(len(s) + len(t)) 합집합
12 Intersection s & t O(len(s) + len(t)) 교집합
13 Difference s - t O(len(s) + len(t)) 차집합
14 Symmetric Diff s ^ t O(len(s) + len(t)) 여집합
15 Iteration for v in s: O(N) 집합 전체 확인
16 Copy s.copy() O(N) 복사

ㅇㅇ

 

defaultdict / dictionary method
  Operation Example Big-O Notes
1 Store d[k] = v O(1) 데이터 추가
2 Length len(d) O(1) 딕셔너리 길이
3 Delete del d[k] O(1) 데이터 삭제
4 get / setdefault d.get[k] O(1) k에 맞는 값 확인
5 Pop d.pop(k) O(1) 가장 마지막 데이터 pop
6 Pop item d.popitem() O(1) 무작위 요소 pop
7 Clear d.clear() O(1) 초기화
8 View d.keys() O(1) d.values(), d.items()
9 Construction dict(...) O(len(...)) ...는 iterable object, 객체 크기만큼 반복
10 Iteration for k in d: O(N) 딕셔너리 전체 확인

ㅇㅇ

 

https://velog.io/@juntree/Python-ListDictSet%EC%9D%98-%EC%8B%9C%EA%B0%84%EB%B3%B5%EC%9E%A1%EB%8F%84-Big-O

 

https://leo-bb.tistory.com/61

 

고민을 시작한 문제 출처 : https://miny-genie.tistory.com/304

참고한 문서 자료 : https://www.geeksforgeeks.org/difference-between-list-and-array-in-python/

참고한 문서 자료 : https://github.com/python/cpython/blob/v3.8.1/Modules/_collectionsmodule.c

참고한 문서 자료 : https://docs.python.org/3/library/stdtypes.html#list

참고한 문서 자료 :  https://docs.python.org/3/library/collections.html#collections.deque

참고한 웹 사이트 : https://stackoverflow.com/questions/32543608/deque-popleft-and-list-pop0-is-there-performance-difference

참고한 웹 사이트 : https://seoyeonhwng.medium.com/파이썬-리스트-내부-구조-f04847b58286

참고한 웹 사이트 : https://stackoverflow.com/questions/6256983/how-are-deques-in-python-implemented-and-when-are-they-worse-than-lists

참고한 웹 사이트 : https://stackoverflow.com/questions/34633178/why-is-the-big-o-of-pop-different-from-pop0-in-python

'Computer Science > 파이썬(Python)' 카테고리의 다른 글

Python Decorator  (0) 2023.11.12
Python Iterator  (0) 2023.11.11
Python float OverflowError  (0) 2023.09.19
Python Namespace(Scope)  (0) 2023.08.18
Python Nested function  (0) 2023.08.18

최근 소수(float) 문제를 다루는 경우가 많아 OverflowError를 보는 경우가 전보다 잦아졌다.

Overflow라는 것은, 한계를 넘어서서 발생하는 오류이다.

그럼 그 한계가 궁금해지는 것은 당연하다고 생각한다.

 float의 한계는 어디까지지? 한계를 넘어서면 어떻게 되지? 이런 궁금증으로 시작했다. 

 

https://miny-genie.tistory.com/225

Python3를 기준으로 int 자료형은 우선 Overflow가 발생하지 않는다.

저번 Overflow 글에서 살펴보았듯, arbitrary precision(임의 정밀도)라는 방법을 사용하기 때문이다.

물론 default_max_str_digits의 값을 변경하는 식으로, 직접 제한을 둔다면 OverflowError가 발생하긴 한다.

하지만, 일반적인 상황에서는 int 자료형에서 OverflowError가 뜨는 경우는 없다.

 

https://pythonnumericalmethods.berkeley.edu/notebooks/chapter09.02-Floating-Point-Numbers.html

Python3에서 정수를 표현하는 자료형이 int밖에 없는 것처럼, 실수를 표현하는 자료형은 float밖에 없다.

물론 Python2에서는 int와 long이 있었고, Python3로 넘어오는 과정에서 하나로 통합된 것이긴 하다.

하지만 Python2에서도 실수를 나타내는 자료형은 float 하나였다.

하나로 많은 부분을 관리하기 위해서는, 다른 언어에서 double에 해당하는 크기를 사용할 수밖에 없다.

그래서 Python은 실수를 나타내는 자료형이 float 하나이고, 이는 8Byte다.

Python2에서 직접 테스트해보고 싶다면 온라인 IDE를 참고해보자.

 └ https://www.jdoodle.com/python-programming-online/

 

Python에서도 다른 언어들과 마찬가지로 실수를 저장하는 방법은 부동 소수점 방법을 활용한다.

위의 그림은 8Byte 부동 소수점을 표현한 예시 그림이다.

8byte는 부호(sign) 1bit, 지수(exponent) 11bit, 가수(fraction) 52bit를 사용한다.

 

import sys
print(sys.float_info)

'''
sys.float_info(
    max=1.7976931348623157e+308,
    max_exp=1024,
    max_10_exp=308,
    min=2.2250738585072014e-308,
    min_exp=-1021,
    min_10_exp=-307,
    dig=15,
    mant_dig=53,
    epsilon=2.220446049250313e-16,
    radix=2,
    rounds=1
)
'''

sys 라이브러리에서 float_info로 float에 대한 정보를 확인할 수 있다.

float에 대한 다양한 정보가 있지만, 궁금한 max 값을 살펴보자.

max 값은 1.7976931348623157e308이다.

이 값을 직접 입력해도 되지만, sys.float_info.max로 값을 저장해도 된다.

 

sys.float_info.max를 넣어도 실제 max값과 같은지 확인한다.

a에는 직접 max 값을 넣어서 저장했고, x에는 sys 명령어로 값을 넣어주었다.

x를 출력해도 a와 같은 값이 나오면서, a == x는 True를 반환한다.

 

이제 궁금한 사항을 해결해보자. 최댓값에 숫자를 더하면 OverflowError가 발생할까?

놀랍게도 1을 더해도 OverflowError가 발생하지 않는다. 값이 그대로이다.

물론 0이 308개나 있기 때문에 변화하지 않은 것처럼 보일 수 있다.

그래서 x와 x + 1을 비교해보았다. 결과는 True를 반환한다.

 

https://stackoverflow.com/questions/35833340/python-float-overflow-what-happens-when-float-overflows

알 수 없는 이 현상을 파헤치기 위해, 진짜 많이 찾아다녔다.

그러던 중 나와 같은 수식을 작성한 Stackoverflow글을 발견했다.

질문의 글은 조금 다르긴 하지만, 어쨋든 위 수식에 대한 답을 확인할 수 있었다.

해당 글이 궁금하다면 직접 들어가서 확인해보는 것도 좋은 방법이다.

https://stackoverflow.com/questions/35833340/python-float-overflow-what-happens-when-float-overflows

 

https://pythonnumericalmethods.berkeley.edu/notebooks/chapter09.02-Floating-Point-Numbers.html

Stackoverflow에도 달린 답변이기는 하지만, 더 높은 신뢰와 출처를 위해 원서를 뒤져보았다.

그 결과 "PYTHON PROGRAMMING AND NUMERICAL METHODS"라는 책에서 같은 표현을 찾을 수 있었다.

 

"Numbers that are larger than the largest representable floating point number result in overflow,

and Python handles this case by assigning the result to inf."

해석하자면, "부동 소수점으로 표현 가능한 최대 범위보다 더 큰 값을 넣으면 overflow가 발생한다,

그리고 Python은 이런 경우에 inf로 표현하게끔 조절한다"는 뜻이다.

그러니까 엄밀하게 따진다면, int에도 float에도 OverflowError는 발생하지 않는다는 뜻과 같다.

하지만, 그럼에도 OverflowError는 분명 발생한다. 이 경우는 밑에서 알아본다.

추가로 308까지는 정상 범위지만, 309부터 inf를 반환하는 것은 float('inf')에서도 엿볼 수 있다.

Python inf 글 참고.

 

"Numbers that are smaller than the smallest subnormal number result in underflow,

and Python handles this case by assigning the result to 0."

해석하자면, "비정규 값(subnormal)으로 표현 가능한 최소 범위보다 더 작은 값을 넣으면 underflow가 발생한다,

그리고 Python은 이런 경우에 0으로 표현하게끔 조절한다"는 뜻이다.

Overflow를 설명한 문장을 읽었을 때, '그럼 underflow는 어떻게 되지?'라는 의문을 가졌다.

그리고 바로 다음 문장에서 이를 해결할 수 있을지 상상도 못했다.

Python이 이렇게까지 handling을 잘해주는 언어일 줄은 몰랐다...

 

"The Python float does not have sufficient precision to store the +2 for sys.float_info.max,
therefore, the operations is essentially equivalent to add zero."

해석하자면, "Python에서 sys.float_info.max가 +2를 더한다는 차이를 저장하기에는, 정밀도가 충분하지 않다.

따라서 시스템상으로 사실상 0을 더하는 것과 본질적으로 동일하다"는 뜻이다.

더하기는 하되, 차이를 인식하지 못하는 걸까? 실제로 store(더하기)하지도 않는 걸까?

그리고 인식할 수 있는 차이는 어디까지일까? 한 번 확인해보자.

 

여러 번 테스트하면서 얻은 결과이다. float가 인식 가능한 정밀도(precision) 차이는 16자리다.

a가 1.797... * 1e308이니, p1과는 17자리 차이, p2와는 16자리 차이가 난다.

(17자리 차이가 난다는 것은 그만큼 더 작은 수를 더해주었다는 이야기이다.)

이때 a와 p1은 같다고 인식하지만, a와 p2는 다르다고 인식한다.

p1을 출력해보면 a와 똑같이 나타나고, p2를 출력해보면 inf로 나타난다.

 

과연 그럼 실제로 더하기는 하는 걸까? 아니라면 더하지도 않는 걸까?

확인하기 위해 메모리 주소를 반환하는 id() 함수를 사용했다.

a의 메모리 주소 마지막 3자리는 720이고, p1의 메모리 주소 마지막 3자리는 768이다.

놀랍게도 값을 더한다. 정말 더하기는 하되, 인식을 하지 못하는 것이었다.

여전히 a == p1은 True를 반환한다.

 

a, p1, p2를 비교할 때 a + p2는 inf가 되어버려 정확한 비교가 어렵다는 생각이 들었다.

그래서 숫자를 낮춰, 1e18과 1e1, 1e2라는 아주 작은 숫자 단위에서 비교를 진행했다.

그랬더니 확실하게 16자리 차이까지는 인식하는 것을 볼 수 있다.

그리고 여전히 값을 더하기는 하되, 부동 소수점 방식 때문에 인식하지 못할 뿐이었다.

참고로, 1e1은 10인데 부동 소수점(float)이 아니라 int가 아니냐고 반문할 수 있다.

Python에서 e를 이용한 지수 표기법을 할 시, 10이 아닌 10.0의 float로 값을 저장한다.

 

그런데 왜 하필 기준이 16자리일까? 실은 정확하게 말하면 15자리이다.

엥? 1e292은 인식해서 더했고 값도 inf로 바뀌었는데? 16 = 308 - 292라서 16자리 맞는데?

1e291부터 인식 못 했고, 17 = 308 - 291잖아. 그니까 인식하는 최대 한도는 16 아니야?

 

헷갈릴 수도 있으니 다시 한 번 정리부터하고 가겠다.

17자리 차이가 날 때부터 인식을 못 한다는 말은, 16자리 차이가 나는 경우는 인식한다는 말이다.

즉, 1자리부터 16자리 차이는 인식한다는 말과 같다. 다시 말하면 1자리부터 16자리 차이가 '유효'하다는 말이다.

하지만 잘 생각해봐야 한다. 지수 표기법으로 적은 숫자는 소수점 위, 정수 관점이다.

이 글에서 지금 알아보고 있는 것은? 부동 소수점. 실수에 대한 이야기이다.

그러니 실수의 관점에서 바라봐야 한다.

 

정말 개략적으로 예를 들어서 100.0이라는 숫자가 있다고 해보자.

이걸 부동 소수점으로 표현한다면 어떻게 될까? 1.000 * 10^2이다.

소수점 앞에 정수 부분에 1만을 두게끔, 소수점을 가장 앞쪽까지 끌어오는 방식이다.

 

sys.float_info.max의 값이 무엇이었는가? 1.7976913...e+308

보다시피 이미 소수점이 가장 앞쪽까지 이동해있는 상태다.

위의 코드들에서 비교한 값은 무엇이었는가? 1e292

100000000...000.0 이제 차이를 알겠는가? 이 값은 아직 소수점을 앞으로 보내지 않은 상태다.

1.7976913...e+308과 비교하기 위해서는 같은 기준에 맞춰야 한다.

1e292는 1 뒤에 0이 292개 있다는 이야기이다 그리고 .0까지 합치면 0이 총 293개 있다.

소수점을 가장 앞쪽까지 이동했을 때 비로소 비교가 가능하다.

그럼 뒤 값의 차이는 1.7e+308과 1.0e+293으로 15(308 - 293)가 된다.

 

위의 코드도 살펴보자. 1.0, 10.0, 100.0처럼 계속해서 0을 늘려가면서 1을 더하는 코드다.

그리고 기존 값과 1을 더한 값이 다르면 계속 x를 출력한다.

더한 값이 같다고 인식할 때, 즉 유효하지 않으면 출력을 그만둔다.

결과를 출력해보면 15자리까지 유효한 것을 확인할 수 있다.

그럼 대체 왜 차이가 15자리인 것일까?

그 이유는 Python의 '유효 숫자(significand)'가 바로 소수점 아래 15자리이기 때문이다.

 

https://stackoverflow.com/questions/35833340/python-float-overflow-what-happens-when-float-overflows

위에서 본 Stackoverflow의 정확한 질문은 사실, 'float의 최댓값에 숫자를 더하면 어떻게 되나요?'가 아니다.

"when I do x=x*1.5 then I see inf as the output, (중략) what upper limit does it go from expected output to inf?"

이 문장에서 볼 수 있듯, x에 1.5를 곱했더니 inf가 출력됐습니다. 한계를 넘어서면 inf가 나오나요?라는 질문이다.

원서에서 살펴보았듯, Python handling으로 inf가 된다는 것을 이제 알았다.

하지만 밑에 달린 여러 개의 답변 중 신기한 답변 하나를 발견했다.

 

"x ** 2 get an error, but x * x does not."

이때 x는 float의 최댓값 sys.float_info.max를 의미한다.

x의 제곱은 에러를 반환하는데... x 곱하기 x는 에러가 아니라고...?

작은 수를 더했을 때 인식하지 못한다는 걸 확인했더니, 새로운 의문이 또 생겼다.

산 넘어 산이다. 바로 확인해보자. 

 

print(x * x)        # inf
print(x ** 2)       # OverflowError: (34, 'Result too large')

실제로 x * x를 하면 inf를 반환하는데, x ** 2는 OverflowError를 반환한다.

OverflowError를 반환하면서 같이 (34, "Result too large")라는 알 수 없는 숫자와 에러 문구도 같이 반환한다.

아마 여기서 Python OverflowError가 뜨는 이유라고 생각했다.

하지만, 이해할 수 없었다. 두 결과는 수학적으로 같은 연산이지 않은가?

프로그래밍 관점에서는 대체 어떤 차이가 있는 것이지?

 

이 이야기를 수학과 지인에게 물어보았다.

그랬더니 연산자 차이에서 오는 에러가 아니냐는 의견을 주었다.

어떤 수끼리 곱하는 것보다, 거듭제곱이 값이 훨씬 기하급수적으로 증가한다.

y = nx 그래프랑 y = x^n을 비교해보면 된다고 말이다. 그럴 듯한 가설이다.

 

print(a ** 1.000_000_000_000_000_02) # 1.7976931348623157e+308
print(a ** 1.000_000_000_000_000_2)  # OverflowError: (34, 'Result too large')

그전에 먼저 제곱을 하는 숫자에 문제가 있는 건가해서, 아주 작은 수를 거듭제곱했다.

위의 코드는 소수점 아래 17자리까지 내려가서 거듭제곱하였고, 아랫줄은 소수점 아래 16자리이다.

그랬더니 소수점 아래 16자리정도에서 OverflowError가 발생했다. 

(사실 이 코드는 그렇게 중요하지 않다. 궁금증에 한 번 테스트한 것이다.)

 

https://stackoverflow.com/questions/22955720/meaning-of-error-numbers-in-python-exceptions

이번에도 Stackoverflow에서 정답을 찾을 수 있었다.

곱하는 연산이 아니라 제곱(float_pow_function) 연산을 할 때, 코드 동작이 조금 다르다.

우선, OverflowError: (34, 'Result too large')에서 34는 에러 코드였다.

마치 HTTP 상태 코드로 200, 404 NOT FOUND를 반환하는 것처럼 말이다.

저 에러 코드(errno)가 0이 아닐 때, 조건에 따라 코드를 수행한다.

34번 Result too large인 경우에 OverflowError를 띄우고, 아닌 경우에는 ValueError를 띄운다.

정리하자면, 제곱을 해서 최댓값을 초과하는 경우에만 OverflowError가 뜨는 것이다.

 

t = 1 * 1e102
print(t*t*t)    # 1e+306
print(t ** 3)   # 9.999999999999999e+305

t = 1 * 1e103
print(t*t*t)    # inf
print(t ** 3)   # OverflowError: (34, 'Result too large')

t = 1e308
print(t*t*t*t*t*t*t*t*t*t)  # inf

경우를 나눠가면서 테스트 해보았다.

거듭제곱을 해도 범위를 초과하지 않는 경우, 거듭제곱 시 범위를 초과하는 경우,

마지막으로 최댓값에 가까운 수를 엄청나게 곱하는 경우.

분명 거듭제곱을 하여 계산하면 Overflow가 뜨지만, 곱셈을 통한 연산은 inf로 handling한다.

 

t = 1 * 1e103
print(t ** 3)       # OverflowError: (34, 'Result too large')
print(pow(t, 3))    # OverflowError: (34, 'Result too large')

참고로 ** 연산자가 아니라 pow() 함수로 계산해도 결과는 같다.

거듭제곱한다는 방식일 때 위와 같은 errno을 확인하는 구조다.

 

int에 이어서 float도 아주 자세하게 알아가는 시간이었다.

이제 웬만한 에러나, 자잘한 실패 사항들을 조금 더 빠르고 확실하게 검출할 수 있다.

 

P.S.1

https://pythonnumericalmethods.berkeley.edu/notebooks/chapter09.02-Floating-Point-Numbers.html

Stackoverflow랑 원서는 무적이야... 구글은 신이고...

 

 

고민을 시작한 문제 출처 : https://miny-genie.tistory.com/275

참고한 문서 자료 : https://pythonnumericalmethods.berkeley.edu/notebooks/chapter09.02-Floating-Point-Numbers.html

참고한 문서 자료 : https://runebook.dev/ko/docs/python/library/sys

참고한 문서 자료 : https://docs.python.org/3/library/functions.html#float

참고한 문서 자료 : https://docs.python.org/3/library/exceptions.html#OverflowError

참고한 문서 자료 : https://dawoum.ddns.net/wiki/Significand

참고한 문서 자료 : https://python.flowdas.com/tutorial/floatingpoint.html

참고한 문서 자료 : https://en.wikipedia.org/wiki/IEEE_754

참고한 문서 자료 : https://docs.oracle.com/cd/E19957-01/806-3568/ncg_goldberg.html

참고한 웹 사이트 : https://ssungkang.tistory.com/entry/python-Decimal-vs-Float-고정소수점과-부동소수점

참고한 웹 사이트 : https://stackoverflow.com/questions/35833340/python-float-overflow-what-happens-when-float-overflows

참고한 웹 사이트 : https://stackoverflow.com/questions/22955720/meaning-of-error-numbers-in-python-exceptions

참고한 웹 사이트 : https://stackoverflow.com/questions/449560/how-do-i-determine-the-size-of-an-object-in-python

참고한 웹 사이트 : https://www.codeit.kr/community/questions/UXVlc3Rpb246NjExZGY3MGQ1ODkzYjA3ODE4NjAwMmQx

'Computer Science > 파이썬(Python)' 카테고리의 다른 글

Python Iterator  (0) 2023.11.11
Python list.pop(0) vs deque.popleft()  (0) 2023.10.03
Python Namespace(Scope)  (0) 2023.08.18
Python Nested function  (0) 2023.08.18
Python Overflow 심화  (0) 2023.08.18

Namespace(Scope)

Scope(범위)는 프로그래밍 언어에서 객체가 유효한 범위를 나타낸다.

다른 말로 namespace라고 하며, 객체 범위 바깥에서 사용할 시, 사용할 수 없다는 오류가 발생한다.

변수(name)가 선언한 동안 살아있는(lifetime) 유효한 공간(space)이라는 뜻이다.

 

파이썬에는 4가지 범위가 존재한다.

넓은 범위부터 Built-in scope > Module scope > Enclosing scope > Local scope다.

Module scope에 해당하는 변수는 global이라고 부르며, Enclosing scope는 non-local이라고 부른다.

Local scope는 다른 말로 Function scope라고도 부른다.

여기서 다루는 규칙과 특징들은, 각 범위의 앞글자를 따 LEGB rule이라고 부른다.

 

Built-in namespace

dir(__builtins__)

 '''
['ArithmeticError', 'AssertionError', 'AttributeError',
 'BaseException','BlockingIOError', 'BrokenPipeError', 'BufferError',
 'BytesWarning', 'ChildProcessError', 'ConnectionAbortedError',
 'ConnectionError', 'ConnectionRefusedError', 'ConnectionResetError',
 'DeprecationWarning', 'EOFError', 'Ellipsis', 'EnvironmentError',
 'Exception', 'False', 'FileExistsError', 'FileNotFoundError',
 'FloatingPointError', 'FutureWarning', 'GeneratorExit', 'IOError',
 'ImportError', 'ImportWarning', 'IndentationError', 'IndexError',
 'InterruptedError', 'IsADirectoryError', 'KeyError', 'KeyboardInterrupt',
 'LookupError', 'MemoryError', 'ModuleNotFoundError', 'NameError', 'None',
 'NotADirectoryError', 'NotImplemented', 'NotImplementedError', 'OSError',
 'OverflowError', 'PendingDeprecationWarning', 'PermissionError',
 'ProcessLookupError', 'RecursionError', 'ReferenceError', 'ResourceWarning',
 'RuntimeError', 'RuntimeWarning', 'StopAsyncIteration', 'StopIteration',
 'SyntaxError', 'SyntaxWarning', 'SystemError', 'SystemExit', 'TabError',
 'TimeoutError', 'True', 'TypeError', 'UnboundLocalError',
 'UnicodeDecodeError', 'UnicodeEncodeError', 'UnicodeError',
 'UnicodeTranslateError', 'UnicodeWarning', 'UserWarning', 'ValueError',
 'Warning', 'ZeroDivisionError', '_', '__build_class__', '__debug__',
 '__doc__', '__import__', '__loader__', '__name__', '__package__',
 '__spec__', 'abs', 'all', 'any', 'ascii', 'bin', 'bool', 'bytearray',
 'bytes', 'callable', 'chr', 'classmethod', 'compile', 'complex',
 'copyright', 'credits', 'delattr', 'dict', 'dir', 'divmod', 'enumerate',
 'eval', 'exec', 'exit', 'filter', 'float', 'format', 'frozenset',
 'getattr', 'globals', 'hasattr', 'hash', 'help', 'hex', 'id', 'input',
 'int', 'isinstance', 'issubclass', 'iter', 'len', 'license', 'list',
 'locals', 'map', 'max', 'memoryview', 'min', 'next', 'object', 'oct',
 'open', 'ord', 'pow', 'print', 'property', 'quit', 'range', 'repr',
 'reversed', 'round', 'set', 'setattr', 'slice', 'sorted', 'staticmethod',
 'str', 'sum', 'super', 'tuple', 'type', 'vars', 'zip']
 '''

Built-in namespace는 파이썬의 모든 built-in 객체를 포함한다.

위에서 출력한 모든 built-in 객체는, 파이썬을 실행하는 동안 언제 어디서든 사용 가능하다.

여기에는 여러 에러(StopIteration, SyntaxError, TypeError, etc) 종류도 포함하며,

built-in 함수(divmod, enumerate, len, map, etc)들도 있고,

객체 타입(dict, int, list, str, etc)들도 존재한다.

Built-in namespace는 파이썬 인터프리터 동작 시 생성하며, 종료할 때까지 유지한다.

 

Global namespace

출처 : https://velog.io/@inyong_pang/Python-Scope-8yk42kog5x

Global namespace는 파이썬 main에서 정의한 모든 객체를 포함한다.

이 객체들은 main이 실행할 때 생성되며, 파이썬 인터프리터가 종료할 때까지 남아있는다.

또한 main의 객체들은 함수들의 내부에서도 유효하게 사용할 수 있다.

 

더 정확하게 말하면, global namespace는 파이썬에서 실행한 main 부분 하나만이 아니다.

프로그램에서 호출(import)한 각 모듈(module)에 대한 namespace도 만드는데, 이 또한 global namespace이다.

main과 module & package에 대한 자세한 개념은 realpython docs에서 확인하자.

 

Non-local namespace and Local namespace

# outer(), inner() 함수 입장에서 전역(global) 범위

def outer():

    # outer() 함수 입장에서 지역(local) 범위
    # inner() 함수 입장에서 비지역(nonlocal) 범위
    
    def inner():
    
        # inner 함수 입장에서 지역(local) 범위
        
        return inner
        
    return outer

이를 간단하게 코드로 표현하면 위와 같다.

Local namespace는 함수가 단 하나일 때만 존재하고, non-local namespace는 중첩 함수일 때 존재한다.

outer()라는 부모 함수와 inner()라는 중첩 함수가 있다고 가정해보자.

outer() 바깥 영역은 함수 영역 밖, 즉 main이라고 가정한다. 고로 global namespace에 해당한다.

outer() 안쪽 영역은 outer() 입장에서는 local 범위에 해당한다.

하지만, inner()는 중첩 함수로 inner 바깥이 outer()의 안에 해당한다. 즉 inner() 입장에서는 non-local이다.

각 영역(namespace)에 대해서, 인터프리터는 함수가 실행될 때 namespace를 생성하고, 함수 종료 시 삭제한다.

 

Variable Shadowing

 

Local => Enclosing => Global => Built-in

파이썬은 객체나 함수의 정의를 찾을때 위 scope 순서를 따라찾는다

가장 좁은 범위부터 가장 넓은 범위로 나아가며, 사용할 객체의 정의를 찾는다.
동일한 이름의 객체를 서로 다른 scope에서 선언하면, 좁은 범위부터 넓은 범위에 있는 객체를 찾아간다.

그러다 가장 처음 값을 찾게 되면 멈춘다. 즉, 더 넓은 범위의 값을 가리는 현상이 발생한다.

이것을 Variable Shadowing이라고 한다.

 

 

def variable_shadowing_example():
    a = 10
    print(a)		# 10
    
a = 1

variable_shadowing_example()

print(a)		# 1

 

 

global namespace에서 a로 1을 선언했다.

함수 내부에서 같은 이름으로 a를 10으로 다시 선언하고, 출력한다.

이러면 함수 내부에서는 10으로 출력하고, 바깥에서는 1을 출력한다.

 

def variable_shadowing_example():
    global a
    a = 10
    print(a)		# 10
    
a = 1

variable_shadowing_example()

print(a)		# 10

Variable shadowing을 해소하기 위해서는 global이나 nonlocal 키워드를 사용할 수 있다.

함수 내부에서 global a라고 선언하면, 이제 함수 내부에서 사용하는 a는 global 범위의 a로 취급한다는 뜻이다.

따라서 함수 내부에서 출력해도 10, 바깥에서 출력해도 10이 나온다. 

 

def variable_shadowing_example():
    a = 10
    print(a)		# 10
    
    def inner():
        global a
        a = 20
        print(a)    # 20
        
    inner()
    
a = 1

variable_shadowing_example()

print(a)		# 20

중첩 함수에서도 global 키워드를 사용할 수 있다.

부모 함수에서도 a에 접근하고, 중첩 함수에서도 a에 접근하여 값을 바꾼다.

이때 global로 선언한 inner() 함수에서 변경한 값만 적용된다.

 

def variable_shadowing_example():
    b = 1
    print(b)		# 1
    
    def inner():
        nonlocal b
        b = 10
        print(b)    # 10
        
    inner()
    print(b)        # 10
    
variable_shadowing_example()
print(b)		# NameError: name 'b' is not defined

Non-local namespace의 객체는 nonlocal 키워드로 접근할 수 있다.

Non-local 범위에 있는 값에 local 함수에서 접근한다.

이때 global 범위에서 b에 접근하면, nonlocal 범위에서 선언했기 때문에 NameError가 뜬다.

 

Python Namespace Dictionaries

type(globals())
# <class 'dict'>

globals()
'''
{'__name__': '__main__', '__doc__': None, '__package__': None,
'__loader__': <class '_frozen_importlib.BuiltinImporter'>, '__spec__': None,
'__annotations__': {}, '__builtins__': <module 'builtins' (built-in)>}
'''

파이썬에는 globals()와 locals() 함수가 bulit-in에 내장되어 있다.

둘 다 dictionary 구조 형태로 각각 global과 local 영역에 선언된 객체들을 담고 있다.

 

x = 'foo'

globals()
# {(생략), 'x': 'foo'}

실제로 main 영역에서 새로운 값을 선언하면, 이름은 key로 값은 value로 들어간다.

 

print(x)
# 'foo'

print(globals()['x'])
# 'foo'

print(x is globals()['x'])
# True

선언한 x를 그냥 출력해도 되고, globals()의 키로 x를 검색하여 value를 출력해도 된다.

결국 둘 다 같은 객체이기 때문이다. 정말 같은 객체인지 is 키워드로 확인할 수 있다.

 

globals()['y'] = 100

globals()
# {(생략), 'x': 'foo', 'y': 100}

print(y)
# 100

global 영역에서 선언한 값은 globals() dictionary에 저장한다는 사실을 알았다.

그렇다면 선언을 조금 특이한 방식으로 할 수 있다.

선언하려는 이름을 key로, 값을 value로 globals()에 직접 넣어주는 것이다.

실행하면 정상적으로 선언되었음을 알 수 있다.

 

def f(x, y):
    s = 'foo'
    print(locals())
    # {'s': 'foo', 'y': 0.5, 'x': 10}

f(10, 0.5)

locals()를 local 영역에서 실행하면 globals()처럼 local 영역의 상태를 알 수 있다.

이때 매개 변수로 넘겨받은 값도 전부 locals()에 들어가게 된다.

locals()를 main 영역에서 실행하면 globals()와 똑같은 형태로 동작하니 유의해야 한다. 

 

g = globals()
print(g)
# {(생략), 'g': {...}}

x = 100
print(g)
# {(생략), 'g': {...}, 'x': 100}

globals()와 locals()가 각 namespace의 값들을 dictionary로 출력한다는 점에서 유사하지만, 차이가 있다.

globals()는 말 그대로 현재 namespace를 반환해주고, locals()는 namespace를 복사하여 반화한다.

예를 들어서 g라는 객체에 globals()를 담고 출력하보고, x를 새로 선언하고 출력해본다.

당연히 나중에 출력한 globals()에는 정보가 갱신되어 나타날 것이다.

 

def foo():
    l = locals()
    print(l)	# {}
    
    v = 10
    print(l)	# {}

foo()

locals()는 이 당연한 동작 과정이 살짝 다르다.

위에서 말했듯, 값을 복사하여 반환하기 때문에, l에는 새롭게 선언한 v가 들어가지 않는다.

따라서 두 출력 결과가 같아지는 현상이 생긴다.

 

class Books:
    def __init__(self, name_1 = "Steve", name_2 = "Tom"):
        self.person1 = name_1
        self.person2 = name_2
        self.person3 = name_2
        
book = Books()
print(vars(book))
# {'person1': 'Steve', 'person2': 'Tom', 'person3': 'Tom'}

globals()와 locals() 비슷하게 클래스에 대해서 객체를 확인하는 vars() 함수도 있다.

 

 

참고한 문서 자료 : https://realpython.com/python-namespaces-scope/

참고한 문서 자료 : https://wikidocs.net/80519

참고한 웹 사이트 : https://velog.io/@inyong_pang/Python-Scope-8yk42kog5x

참고한 웹 사이트 : https://www.daleseo.com/python-global-nonlocal/

참고한 웹 사이트 : https://www.knowledgehut.com/blog/data-science/python-scopes

참고한 웹 사이트 : https://hcnoh.github.io/2019-01-30-python-namespace

'Computer Science > 파이썬(Python)' 카테고리의 다른 글

Python list.pop(0) vs deque.popleft()  (0) 2023.10.03
Python float OverflowError  (0) 2023.09.19
Python Nested function  (0) 2023.08.18
Python Overflow 심화  (0) 2023.08.18
Python Overflow  (0) 2023.08.17
def func(*args, **kwargs):
    ''' do something '''
    return

일반적으로 함수를 선언하면 작성하는 형태이다.

def로 함수를 선언하고, 그 안에 받아들인 매개변수를 작성해준다.

그리고 함수의 내부 코드를 작성한 다음, 경우에 따라 return을 진행한다.

 

def Calculator(A, op, B):
    if op == "+": return A + B
    if op == "-": return A - B
    if op == "*": return A * B
    if op == "/": return A / B

만약 계산기 함수를 구현하고 싶어, 위처럼 Calculator를 작성했다고 가정해보자.

'사칙연산을 수행'하는데 있어서 아무런 문제가 없는 코드이다.

만들고 싶은 것은 계산기 코드지, 사칙연산을 수행하는 코드가 아니다.

제곱 연산을 하려면? 정수와 실수를 구분하려면? 계산하려는 값이 2개가 아니라면?

이런 생각을 거쳐, 나아가 이 코드는 SRP를 지키지 않은 코드이다.

 

SRP(Single Responsibility Principle; 단일 책임 원칙)는 하나의 모듈이 하나의 책임만을 져야 한다는 의미다.

SRP는 다섯 가지 SOLID 애자일 원칙 중 하나이다. https://en.wikipedia.org/wiki/SOLID

쉽게 말하면 위의 계산기 코드는 사칙연산의 4가지 책임을 지는 코드라는 이야기이다. 

 

def outer_func():
    def inner_func():
        print("Hello, World!")
        
    inner_func()
    
outer_func()	# Hello, World!

이럴 때 Nested function를 사용할 수 있다.

 └ 단순히 SRP만을 위해 존재하는 개념도, SRP가 핵심 이유도 아니다.

 └ 흐름상 이렇게 설명하는 것이 자연스러워 빌드업을 한 것이니 참고만 하자.

 

Nested는 중첩했다는 의미로, 여러 개의 함수를 중첩하여 사용한다.

Nested function 대신 Inner function라고 표현하기도 한다.

outer_func()은 parent function이라 하고, inner_func()은 child function이라고도 한다.

Nested function은 다양한 이점이 있어 사용한다. 그 이유들을 살펴보자.

 

def func():
    dp = [1] * 10
    for i in range(2, 10):
        dp[i] = dp[i-1] + dp[i-2]
        
    ''' do something one '''
    
    dp = [1] * 10
    for i in range(2, 10):
        dp[i] = dp[i-1] + dp[i-2]
        
    ''' do something two '''
    
    dp = [1] * 10
    for i in range(2, 10):
        dp[i] = dp[i-1] + dp[i-2]
        
    ''' do something three '''

어떤 함수를 위처럼 작성했다고 가정해보자.

얼핏봤을 때는 문제가 없지만 똑같은 코드가 반복해서 나타나고 있다.

우리가 함수를 처음 사용한 이유를 생각해보자. 반복을 줄이기 위함이다.

 

def func():
    def maybeFibonacci():
        dp = [1] * 10
        for i in range(2, 10):
            dp[i] = dp[i-1] + dp[i-2]
      
      
    maybeFibonacci()
    ''' do something one '''
    
    maybeFibonacci()        
    ''' do something two '''
    
    maybeFibonacci()
    ''' do something three '''

1. 가독성이 뛰어나다.

반복적으로 나타나는 부분을 inner_function으로 만들 수 있다.

반복하는 부분을 줄여 코드 길이도 짧아져 가독성이 높아지고, 유지보수도 쉬워진다.

 

def Calculator():
    def add(): ''' code '''
    def sub(): ''' code '''
    def mul(): ''' code '''
    def div(): ''' code '''
    def pow(): ''' code '''

2. SRP를 만족한다.

첫 번째 이유와 비슷한 맥락이다. 함수를 작성하는 또 다른 이유는 기능 분할(모듈화)이다.

Calculator 함수 내부에서 자체적으로 코드를 구현하는 게 아니라 각각을 함수로 만들어준다.

이렇게 기능별로 코드를 나누다보면 자연스레 SRP를 만족하게 된다.

 

def increment(number):
    def inner_increment():
        return number + 1
    return inner_increment()


# Call inner_increment()
inner_increment()

'''
Traceback (most recent call last):
  File "<input>", line 1, in <module>
    inner_increment()
NameError: name 'inner_increment' is not defined
'''

3. Encapsulation을 제공한다.

말그대로 내부 선언 함수는 상위 함수에서만 호출이 가능하다는 특징이다.

increment 함수 내부에, 숫자를 1 늘려주는 inner 함수를 따로 작성한 뒤, return해준다.

어떤 수 N이 있을 때 increment(N)을 하면 N+1을 얻을 수 있다.

하지만 직접적으로 inner_increment()를 호출하는 것은 불가능하다.

 

def generate_power(exponent):
    def power(base):
        return base ** exponent
    return power
 
raise_two = generate_power(2)
raise_three = generate_power(3)

raise_two(4)	# 16
raise_two(5)	# 25

raise_three(4)	# 64
raise_three(5)	# 125

다른 말로 Closure를 제공한다고도 표현한다. Closure는 폐쇄, 금지 등의 뜻을 갖고 있다.

Closure의 예시를 잘 보여준 함수가 위에 있다.

generate_power라는 부모 함수는 exponent(지수)를 매개변수로 받는다.

하지만 exponent는 중첩 함수 power 내부에서만 가두어져 사용이 가능하다.

부모 함수의 정보를 외부에서 직접 접근하는 것을 막으면서, 연산은 가능하게 해주는 것. 이를 Closure라고 한다.

 └ 부모 함수의 매개변수 뿐만 아니라, 부모 함수의 변수를 중첩 함수에서만 변형 가능하다면 이 또한Closure다. 

 

generate_power(2)처럼 부모 함수에 인자(exponent = 2)를 넣어준다.

그리고 부모 함수가 담긴 변수(raise_two)에 중첩 함수 인자 값(base = 4)을 넣어주면 함수 return(16)을 구할 수 있다.

코드를 잘 보면 func()이 아니라 func을 return한다. 함수명을 그대로 반환한다는 점에 유의하자. 

그외 다양한 Closure 예제는 RealPython을 참고하자.

 

https://velog.io/@inyong_pang/Python-Nested-Function-2wk42jt94r

이해가 안 된다면 사진으로 이해하면 더 와닿는다.

위에서 설명한 코드와는 살짝 다른 코드지만, 전체적인 흐름을 보면 이해가 빠르다.

 └ 위의 코드는 RealPython의 코드로 부모 함수 매개변수가 exponent(지수)다.

 └ 사진의 코드는 타 블로그의 코드로 부모 함수 매개변수가 base(밑)다 

 

def add_messages(func):
    def _add_messages():
        print("This is my first decorator")
        func()
        print("Bye!")
    return _add_messages


@add_messages
def greet():
    print("Hello, World!")


greet()
# This is my first decorator
# Hello, World!
# Bye!

4. Decorator를 지원한다.

Decorator에 대해 조금 더 자세하게 다룰 필요가 있어 추후에 따로 작성한다.

간단하게 소개하자면, 함수를 매개변수로 받아, 중첩 함수를 실행하는 것이다.

중첩 함수(nested function)을 이용한 개념이기에 nested function을 return하는 함수만 decorator를 사용할 수 있다.

위 코드는 간단한 decorator 예제이다.

 

 

참고한 문서 자료 : https://realpython.com/inner-functions-what-are-they-good-for/

참고한 문서 자료 : https://en.wikipedia.org/wiki/SOLID

참고한 문서 자료 : https://en.wikipedia.org/wiki/Nested_function

참고한 문서 자료 : https://en.wikipedia.org/wiki/Single-responsibility_principle

참고한 문서 자료 : https://en.wikipedia.org/wiki/Closure_(computer_programming) 

참고한 웹 사이트 : https://velog.io/@inyong_pang/Python-Nested-Function-2wk42jt94r

참고한 웹 사이트 : https://softwareengineering.stackexchange.com/questions/232766/when-to-use-python-function-nesting

'Computer Science > 파이썬(Python)' 카테고리의 다른 글

Python float OverflowError  (0) 2023.09.19
Python Namespace(Scope)  (0) 2023.08.18
Python Overflow 심화  (0) 2023.08.18
Python Overflow  (0) 2023.08.17
Python Logical operator(AND, OR) 설명  (0) 2023.08.15

Python Overflow 글에서 말한, 왜 int는 기본으로 28 Byte를 차지하는가에 대한 분석이다.

기본적으로는 분석 블로그를 바탕으로 따라갔으며, 부족한 부분은 내 나름대로 보완하여 작성한다.

 

import sys

num = 10

print(sys.getsizeof(num))	# 28
print(type(num))		# <class 'int'>

이전 글에서 살펴보았듯, Python 3는 기본적으로 int가 28 Byte 크기를 갖는다.

이는 int 뿐만이 아니라 데이터를 설명하기 위한 데이터, 즉 메타 데이터도 포함하기 때문이다. 

 

https://github.com/python/cpython

Python interpreter 중 하나인 CPython 코드를 Github에서 다운 받아 확인한다.

가장 첫 줄에서 확인할 수 있듯, Python 3.13.0 버전 기준으로 현재 작성했음을 알 수 있다.

 

Search 탭에서 arbitrary precision 검색

검색해야 할 키워드가 'Arbitrary precision'임을 알고 있다.

따라서 좌측의 Search 탭으로 전체 프로젝트에서 어디어디에 키워드가 적혀있는지 살펴본다.

이때 Ctrl + F는 현재 파일에서 찾기지만, Ctrl + Shift + F를 하면 전체 프로젝트에서 탐색이 가능하다.

좌측 목록들을 보면 찾고 있는 파일이 "longobject.h"임을 알 수 있다.

 

cpython-main\Include\longobject.h

Include 폴더 내에 들어있는 헤더파일이다. 근데 잘못 찾아온 것 같다.

/* Long (arbitrary precision) integer object interface */라고 달려있는데, 구조가 궁금한 거지 인터페이스가 아니다.

 

https://docs.python.org/ko/3/c-api/long.html

그래서 docs의 힘을 빌린다. docs.python에서 Integer Objects에 대한 설명을 찾아본다.

All integers are implemented as "long" integer objects of arbitrary size라고 한다. 잘 찾아온 것 같다.

Python의 모든 정수는 PyLongObject로 구현한다고 한다.

 

// Forward declarations of types of the Python C API.
// Declare them at the same place since redefining typedef is a C11 feature.
// Only use a forward declaration if there is an interdependency between two
// header files.

#ifndef Py_PYTYPEDEFS_H
#define Py_PYTYPEDEFS_H
#ifdef __cplusplus
extern "C" {
#endif

typedef struct PyModuleDef PyModuleDef;
typedef struct PyModuleDef_Slot PyModuleDef_Slot;
typedef struct PyMethodDef PyMethodDef;
typedef struct PyGetSetDef PyGetSetDef;
typedef struct PyMemberDef PyMemberDef;

typedef struct _object PyObject;
typedef struct _longobject PyLongObject;
typedef struct _typeobject PyTypeObject;
typedef struct PyCodeObject PyCodeObject;
typedef struct _frame PyFrameObject;

typedef struct _ts PyThreadState;
typedef struct _is PyInterpreterState;

#ifdef __cplusplus
}
#endif
#endif   // !Py_PYTYPEDEFS_H

처음에 검색한 것과 마찬가지로 찾아보니 cpython-main\Include\pytypedefs.h에서 찾을 수 있었다.

L19에서 PyLongObject를 구조체 형태인 _longobject로 선언함을 볼 수 있다.

int가 아니라 long인 이유는 저번 글에서 설명했다.

Python 2에서 Python 3로 넘어오면서 PEP 237을 따랐고, int가 아닌 long 형식으로 통합했기 때문이다.

 

// Include/pytypedefs.h
// L19

typedef struct _longobject PyLongObject;

밑에서 살펴볼 코드들의 위치와 라인은 두 줄의 주석으로 작성해 두었으니 참고하자.

우리가 알고 싶은 28 Byte의 자료형은 PyLongObject이다. 이는 구조체 _longobject로 정의한다.

 

// Include/cpython/longintrepr.h
// L87 ~ L90

struct _longobject {
    PyObject_HEAD
    _PyLongValue long_value;
};

_longobject 또한 구조체이다. PyObject_HEAD와 _PyLongValue를 멤버로 갖는다.

 

// Include/object.h
// L78 ~ L79

/* PyObject_HEAD defines the initial segment of every PyObject. */
#define PyObject_HEAD                   PyObject ob_base;

PyObject_HEAD부터 살펴보자.

PyObject_HEAD는 모든 PyObject의 initial segment를 뜻한다. PyObject는 PyObject_HEAD의 타입이다.

 

// Include/pytypedefs.h
// L18

typedef struct _object PyObject;

PyObject는 또 다시 _object로 정의한다.

 

// Include/object.h
// L161 ~ L191

/* Nothing is actually declared to be a PyObject, but every pointer to
 * a Python object can be cast to a PyObject*.  This is inheritance built
 * by hand.  Similarly every pointer to a variable-size Python object can,
 * in addition, be cast to PyVarObject*.
 */
struct _object {
    _PyObject_HEAD_EXTRA
    /* skip */
    Py_ssize_t ob_refcnt;
    /* skip */
    PyTypeObject *ob_type;
};

_object 구조체는 다음과 같은 멤버 변수를 갖는다.

 

1. _PyObject_HEAD_EXTRA

같은 파일의 L65를 보면 Py_TRACE_REFS가 정의되지 않아 동작하지 않는다. 0 Byte이다.


2. Py_ssize_t ob_refcnt

ob_refcnt는 참조 횟수로 Garbage collector가 메모리 관리를 하기 위한 용도이다.

Py_ssize_t는 OS와 컴파일러에 따라 다르지만, "PC\pyconfig.h"에서 __int64로 정의한다. 8 Byte이다.


3. PyTypeObject *ob_type

Type 형식의 주소(64bit OS)를 저장한다. 8 Byte이다.

그래서 32bit의 Python 2에서는 int의 기본 사이즈가 24 Byte를 갖게 된다.

 

// Include/cpython/longintrepr.h
// L87 ~ L90

struct _longobject {
    PyObject_HEAD		// 0 + 8 + 8
    _PyLongValue long_value;
};

잠시 쉬어가보자. 결국 알고 싶은 것은 _longobject의 크기이다.

이 구조체는 2개의 멤버 변수를 갖고, 그 중 첫 번째는 총 16 Byte임을 알았다.

 

// Include/cpython/longintrepr.h
// L82 ~ L85

typedef struct _PyLongValue {
    uintptr_t lv_tag; /* Number of digits, sign and flags */
    digit ob_digit[1];
} _PyLongValue;

/*
struct {
    unsigned long length;
    uint32_t *digits;
} bignum;
*/

이제 _PyLongValue 차례이다. 저번 글에서 간단하게 살펴본 구조이다.

 

1. lv_tag

ob_digit의 배열 크기를 뜻한다. 참고한 블로그와는 다른 코드가 되었다.

Py_ssize_t ob_size로 선언한 것 같았는데, 버전 때문인지 uintptr_t lv_tag로 바뀌었다.

uintptr_t 자료형을 찾아보니 "Include/pymacconfig.h" L46에서 정의한다. 8 Byte이다.

 

2. ob_digit[1]

digit 배열 1개를 할당한다는 뜻이다. 같은 파일 L43에서 정의한다. uint32_t로 4 Byte이다.

ob_digit이 unsigned인데 음수는 표현할 수 없는 걸까? unsigned는 음수 범위는 없는데 말이다.

그럴 때는 lv_tag에 음수를 저장한다. num = -10이라고 하면, lv_tag = -1, ob_digit = 10이 된다.

 

// Include/cpython/longintrepr.h
// L87 ~ L90

struct _longobject {
    PyObject_HEAD		// 0 + 8 + 8
    _PyLongValue long_value;	// 8 + 4
};

드디어 _longobject에 대해서 모든 것을 살펴보았다. 정리를 해보자.

1. GC(가비지 콜렉터) 메모리 관리를 위한 참조 횟수 저장 공간, 8 Byte

2. OS Type을 가리키는 주소, 8 Byte

3. digit의 개수로 사용하는 저장 공간, 8 Byte

4. array로 관리하는 int 값 그 자체, 4 Byte

 

/* Comment skip */
/* Skip */

PyLongObject *
_PyLong_New(Py_ssize_t size)
{
    assert(size >= 0);
    PyLongObject *result;
    
    /* Skip */
    /* Comment skip */
    
    result = PyObject_Malloc(offsetof(PyLongObject, long_value.ob_digit) +
                             ndigits*sizeof(digit));
                             
    /* Skip */
    /* Comment skip */
    
    result->long_value.ob_digit[0] = 0;
    return result;
}

"cpython-main\Objects\longobject.c"에서 L134 ~ L170에 해당하는 코드이다.

저번에 사람이 직접 종이에 연산하는 방식으로 계산한다는 표현을 혹시 기억하는가?

그 부분에 해당하는 코드로, PyLongObject의 digits array의 크기를 PyObject_Malloc로 동적으로 키운다.

코드가 상당히 길어 전문을 첨부하기는 힘들어, 일부분만을 가져왔다.

 

num = int("9" * 4300)
print(num + 1)

# ValueError: Exceeds the limit (4300 digits) for integer string conversion;
# use sys.set_int_max_str_digits() to increase the limit

마지막으로 하나 재미있는 코드를 가져왔다. 그렇다면 Python에서 표현 가능한 숫자 한계는 어디일까?

재미있게도 메모리가 허용하는 한 MLE는 뜨지 않는다. 말그대로 표현의 문제이다.

결국 숫자를 출력하는 것도 글자수를 차지하기 마련이다.

Python은 이 글자수를 4300자로 제한하고 있다.

 

import sys
sys.set_int_max_str_digits(10000)

Python의 default 재귀(1000)을 해제하는 것처럼 sys로 한도를 늘려줄 수 있다.

sys.set_int_max_str_digits 함수를 사용하면 한도를 원하는 만큼 늘릴 수 있다.

 

글을 마무리하며...

최근 Python에 대해 궁금한 점이 꼬리에 꼬리를 물고 늘어지고 있다.

덕분에 공부를 많이 해서 도움은 되지만... 그 누가 Python이 쉽다고 하였는가...

어떤 언어든 이렇게까지 파고 들면 어렵지 않은 언어가 없겠지만은 정말이지 어질어질하다.

새삼스레 언어를 만든 개발자들이 정말 미친 사람들이었다는 걸 깨닫는다.

이건 어떻게 해결하지?! 하면 코드로 다 구현을 해놨다. 아니 글자수 제한은 뭘 생각하고 해제 코드를 넣은 거지.

뭐 오늘도, 지식이 늘었다!

 

 

참고한 문서 자료 : https://github.com/python/cpython

참고한 문서 자료 : https://docs.python.org/ko/3/c-api/long.html

참고한 문서 자료 : https://docs.python.org/ko/3/c-api/intro.html#c.Py_ssize_t

참고한 문서 자료 : https://peps.python.org/pep-0353/

참고한 웹 사이트 : https://stackoverflow.com/questions/61369007/expected-py-ssize-t-but-got-long

참고한 웹 사이트 : https://devocean.sk.com/blog/techBoardDetail.do?ID=165190&boardType=techBlog

참고한 웹 사이트 : https://kirkim.github.io/c/2021/02/05/size_t_ssize_t.html

참고한 웹 사이트 : https://devocean.sk.com/blog/techBoardDetail.do?ID=165190&boardType=techBlog 

 

'Computer Science > 파이썬(Python)' 카테고리의 다른 글

Python Namespace(Scope)  (0) 2023.08.18
Python Nested function  (0) 2023.08.18
Python Overflow  (0) 2023.08.17
Python Logical operator(AND, OR) 설명  (0) 2023.08.15
Python inf  (0) 2023.08.14

1. Prologue

2. Overflow

3. Python Overflow

    3.1. Aribitrary Precision Arithmetic

4. Epilogue


1. Prologue

Python inf에 대해 다룬, 하나의 코드에서 의문이 시작했다.

 

import sys

# 922_3372_0368_5477_5807
int_pos_inf = sys.maxsize

# -922_3372_0368_5477_5808
int_neg_inf = -(sys.maxsize + 1)

바로 sys.maxsize이다. 922경이라는 매우 큰 수를 반환하니 가히 무한이라 할 수 있다.

이때 양의 무한대가 아닌 음의 무한대를 표현하려면 1을 더하고 -1을 곱해준다.

1을 더해주는 이유는 당연히 다른 자료형과 마찬가지로 정확한 범위 설정을 위함이다.

그렇다면 int_pos_inf에 1을 더하면 오버플로우가 발생할 테니 똑같은 값이겠군!

 

어라? 왜 오버플로우가 발생하지 않는 거지?

그러고 보니 이상하다. -1을 곱하고 1을 더 빼는 게 아니라, 미리 1을 더하고 -1을 곱했네?

파이썬의 엄청난 구조 때문에 실은 몇 바이트 차지하지 않는다든가?

해서 메모리도 확인해보았는데 36 Byte나 차지하고 있었다.

 

심상치 않음을 감지하여 더 큰 수를 확인해보았다. 바로 무한대에 가까운 수를 제곱하였다.

...오버플로우가 일어날 기미가 전혀 없이 거뜬하게 값을 출력했다.

심지어 44 Byte로 늘어난 것을 알 수 있다.

 

의문을 갖던 중 내 궁금증을 최고로 만들어 준 대망의 계기이다.

백준의 어떤 문제를 풀다보니 꽤 간단한 DP로 해결 가능한 규칙을 찾았고, 테스트 해보았다.

이때 깜빡하고 나머지 연산을 수행하지 않고, 문제 조건의 최댓값을 넣어버렸다.

그랬더니 뭐라 불러야 할지도 모를 충격적인 숫자가 나왔다.

저 숫자가 차지하는 메모리 용량만해도 228 Byte다.

 

2. Overflow

파이썬의 구조에 대해 들어가기 전 Overflow에 대해서 간단하게만 짚고 넘어가자.

Overflow란, 값이 증가하면서 허용한 최댓값 초과로 올라가면서 발생하는 '오류'이다.

 └ 이와 반대로, 허용한 최솟값 미만으로 내려가 발생하는 오류는 Underflow라고 부른다.

 

요점은 특정 범위가 있고, 범위를 벗어날 때 발생하는 오류라는 거다.

통상 프로그래밍 언어에서 변수는 이름을 붙여 저장한다.

이때 변수 값을 저장하기 위해 메모리 공간을 확보하고, 그것을 '자료형(Data type)'이라고 부른다. 

 

가장 대표적으로 integer(정수) 타입에는 4 Byte를 할당한다.

그래서 최댓값은 '2 ^ 31 - 1 = 2,147,483,647'라는 21억의 값을 갖는다.

 └ 자바나 C 정수 타입 연산은 CPU architecture와 연관되어, 32bit 사용 시 2^31-1까지 표현한다.

 └ MSB는 부호를 표현하는 데 사용하여 31bit로만 표현한다.

이러한 자료형은 값을 계산하기 위해 중요하게 사용한다.

int로 부족한 공간은 long long을 사용하고, 실수의 경우 float가 아닌 double을 사용한다.

 

3. Python Overflow

num = 10
txt = "num이라는 변수는 10입니다"

하지만 Python에서는 자료형 없이 변수를 선언한다. 이는 파이썬의 모든 것을 객체(Object)로 구현하기 때문이다.

정수 값을 넣어준다면 정수 클래스 객체로, 문자열을 넣어준다면 문자열 클래스 객체로 선언한다.

Python에서는 변수가 자신만을 위한 메모리를 갖는 것이 아닌, 객체를 가리키는 것이다.

Python이 왜 느린지, 변수 관리 방식에 대한 글이다. 궁금하면 직접 더 읽어보자.

 └ http://jakevdp.github.io/blog/2014/05/09/why-python-is-slow/

 

https://peps.python.org/pep-0237/

하지만 이전 버전, 즉 Python 2에는 정수형 데이터 타입에 int와 long이 있었다.

int는 C에서의 정수형 데이터 타입과 같은 방식의 자료형이었고,

long은 Arbitrary precision이라는 것을 지원하는 데이터 타입이었다.

기본적으로 정수 연산 시 int를 사용하되, overflow가 발생할 것 같으면 자동으로 long을 사용하여 overflow를 막았다.

 

Python 3로 넘어오면서 많은 변화가 있었는데, 그중 하나가 PEP 237을 따른 것이다.

이에 따라 int를 long의 방식으로 통합하고 이름을 int로 함으로써 arbitrary precision을 지원하는 int만이 남게 되었다.

 └ 하지만 Python 3에서도 numpy, pandas 같은 패키지는 아직도 C 스타일을 유지해 overflow에 주의해야 한다.

 

    3.1. Arbitrary Precision Arithmetic

In computer science, arbitrary-precision arithmetic, also called bignum arithmetic, multiple-precision arithmetic, or sometimes infinite-precision arithmetic, indicates that calculations are performed on numbers whose digits of precision are limited only by the available memory of the host system

컴퓨터 과학에서, 임의 정밀도 산술은, 호스트 시스템의 사용 가능한 메모리 내에서 각각의 자릿수에 대한 연산을 수행하는 것을 말한다. 이는 큰 수 산술, 복수 정밀도 산술, 가끔은 무한 정밀도 산술이라고도 부른다.

 

그럼 대체 Arbitrary precision가 뭐길래 무한한 자료형을 제공하는 걸까?

위키피디아 정의를 가져온 것으로, 사람이 계산하는 방식을 그대로 따라하는 것이다.

그러니까 정해진 메모리를 사용하는, 기존의 Fixed-precision과 달리, 사용 가능한 메모리를 계속 끌어다 사용한다.

특정 값을 나타내는데 4 Byte로 부족하다면 5, 6... 이렇게 유동적(the available memory)으로 운용한다.

 

https://mortada.net/can-integer-operations-overflow-in-python.html

Python 3.4를 기준으로 아주 큰 정수를 표현할 때 사용하는 메모리의 크기를 그린 그래프이다.

 └ 따로 확인한 결과 Python 3.11에서도 같은 그래프를 그린다.

2^0부터 2^30-1까지는 28 Byte를 사용하다가, 특정 수를 넘길 때마다 4 Byte씩 증가하면서 수를 표현한다.

0이든 100이든 1000이든 28 Byte를 잡아먹는 건 overhead가 지나친 게 아닌가 싶다.

그렇게 생각하며 찾아보던 중 감사하게도 이를 분석해주신 분이 계시다.

 └ 분석 블로그 : https://tyoon9781.tistory.com/entry/python-int-size-28bytes

 

struct {
    unsigned long length;
    uint32_t *digits;
} bignum;

자세하게는 말고 정말 간단하게 어떤 방식으로 arbitrary precision arithmetic을 구현하는지 알아보자.

사용 가능한 메모리를 끌어다 쓰는 연산을 위해, 정수를 저장한 구조체를 C로 표현하면 위와 같다.

length에는 몇 자리 숫자인지, digits에는 각 자릿수를 저장한다.

 

bignum.length = 4

bignum.digits[0] = 4
bignum.digits[1] = 0
bignum.digits[2] = 0
bignum.digits[3] = 1

1004라는 숫자가 있으면 위와 같이 저장할 수 있다.

각 자릿수를 저장하고, 연산을 수행한다. 올림수가 발생하면 길이와 배열을 늘려주고, 올림값을 저장한다.

이런 방식을 이용해 사람이 사칙연산하는 것과 흡사한 알고리즘 구현을 할 수 있다.

우리가 종이에 계산할 때 오버플로우가 발생하는가? 그렇지 않다.

초반에 말한 "사람이 계산하는 방식을 그대로 따라하는 것"은 이걸 말한다.

 

하지만 그렇기에 Python은 느릴 수밖에 없는 언어가 됐다.

2진수로 표현한 비트 나열을, 하드웨어적으로 일일이 계산한다면 당연히 느려진다.

 

4. Epilogue

내가 생각한 것보다 더 깊게 들어간 느낌이다.

뭐 하나 허투루 만들어진 게 없고, 모든 것에는 다 이유가 있다.

생각해보면 21억이 넘어가는 수가 종종 나오곤 했지만, Python에 익숙해져 넘어간 적이 많다.

힘들지만, 하나하나 의심하고 생각해보는 습관이 중요해지는 요즘이다.

 

 

고민을 시작한 문제 출처 : https://miny-genie.tistory.com/222

참고한 문서 자료 : https://peps.python.org/pep-0237/

참고한 문서 자료 : https://en.wikipedia.org/wiki/Arbitrary-precision_arithmetic

참고한 문서 자료 : https://en.wikipedia.org/wiki/Fixed-point_arithmetic

참고한 웹 사이트 : https://yunreka.tistory.com/3

참고한 웹 사이트 : https://velog.io/@toezilla/1D1Q-001.-Python의-int-자료형은-어떻게-범위가-무제한일까

참고한 웹 사이트 : http://jakevdp.github.io/blog/2014/05/09/why-python-is-slow/

참고한 웹 사이트 : https://ahracho.github.io/posts/python/2017-05-01-everything-in-python-is-object-integer/

참고한 웹 사이트 : https://hbase.tistory.com/109

참고한 웹 사이트 : https://mortada.net/can-integer-operations-overflow-in-python.html

참고한 웹 사이트 : https://tyoon9781.tistory.com/entry/python-int-size-28bytes

'Computer Science > 파이썬(Python)' 카테고리의 다른 글

Python Nested function  (0) 2023.08.18
Python Overflow 심화  (0) 2023.08.18
Python Logical operator(AND, OR) 설명  (0) 2023.08.15
Python inf  (0) 2023.08.14
Python loop-else  (0) 2023.07.09

Logical operator(논리 연산자)는 논리식을 판단하여, 참과 거짓을 반환하는 연산자이다.

대표적으로 and와 or가 있다. 각각 &&, ||로 표기한다.

논리 연산자와 비트 연산자의 차이는 '식 자체로 판단하느냐', '비트 단위 계산을 수행하느냐'의 차이이다.

예를 들어서 A = 3 그리고 B = 7이라는 두 값이 있다고 가정해보자.

논리 연산자를 사용하여 A && B를 한다면, true와 true이기에 결과는 true가 된다.

비트 연산자를 사용하여 A & B를 한다면, 011과 111의 and 연산이기에 결과는 3(011)이 된다.

 

여기까지가 일반적으로, 그리고 내가 알고 있던 사실이었다.

 

return_home_dist = D[last][start] or infinite

''' 파이썬에서 or 연산자는 두 값 중 하나를 선택하도록 할 수도 있다. '''

TSP(여행하는 외판원 문제)에 대한 코드를 보다가 위와 같은 코드를 마주쳤다.

코드는 그렇게까지 중요하지 않다. 코드에 대한 설명이 내게 큰 의문을 넘겨주었다.

"파이썬에서 or 연산자는 두 값 중 하나를 선택하도록 할 수도 있다."

이게 대체 무슨 말이지? 두 값 중 하나를 선택할 수 있다고? or 는 True아니면 False 아닌가?

 

TSP에서 위의 코드는 돌아올 때 사용하는 코드이다.

모든 도시를 여행하고, 마지막으로 도착한 도시에서 출발지로 돌아올 수 있는지 확인한다.

만약 마지막 도시에서 출발지로 가는 길이 있다면(D[last][start]) 값을 더해주고,

없다면(infinite) 무한하게 값을 키워, 최단 경로로 계산할 수 없게끔 처리해주는 방식이다.

이걸 if 조건문이 아니라 or 논리 연산자로 처리할 수 있다고? 바로 확인해보았다.

 └ 파이썬에는 &&와 || 연산자가 없다. and와 or 연산자로 사용한다.

 

num = int(input())	# positive number
inf = float('inf')

print(num or inf)	# num

num과 inf를 논리합 연산을 했다. 당연히 True를 출력할 줄 알았다.

당연하지 않은가? 만약 if num or inf: '''do something''' 이라는 코드가 있다고 가정해보자.

그럼 'num이나 inf 둘 중 하나라도 참이라면, do something해라'라는 말과 같기 때문이다.

하지만 print()문은 num을 출력했다.

 

num = int(input())	# positive number
inf = float('inf')

print(inf or num)	# inf

이번에는 순서를 바꾸어 "inf or num"으로 작성했다. 그랬더니 이번에는 inf를 출력했다.

내가 예상한 동작과 전혀 다른 방식으로 움직이고 있다. 혼란스럽다.

그래서 확인하기 위한 몇 가지 코드를 더 작성해 보았다.

 

# ---------- Case 1 ----------
num = int(input())	# negative number
inf = float('inf')

print(num or inf)	# num

# ---------- Case 2 ----------
num = int(input())	# negative number
inf = float('inf')

print(inf or num)	# inf

# ---------- Case 3 ----------
zero = 0
inf = float('inf')

print(zero or inf)	# inf

# ---------- Case 4 ----------
zero = 0
inf = float('inf')

print(inf or zero)	# inf

음수를 받아서 위와 같은 방식으로 테스트했지만, 양수든 음수든 같은 양상을 보였다.

그리고 0이 들어갔을 때, 어떤 경우에든 inf를 출력했다. True가 아니라 inf를 말이다.

 

and: A is False(왼쪽), A is True(오른쪽)

그러다 Python wikidocs에서 해답을 찾을 수 있었다.

and 연산자는 둘 다 True일 때에만 True를 반환하는, 즉 실행하는 연산자이다.

A and B가 있을 때, A가 False라면? B는 중요하지 않다. 어쨌든 False니까.

그래서 False값인 A를 바로 return한다. 어쨋든 A는 False이니까.

이에 대한 소스 코드는 왼쪽의 그림이다.

 

만약 A가 True라면? 이때도 B는 그닥 중요하지 않다.

B가 True면 True가 되는 거고, B가 False라면 False가 되는 것이다. 즉, B 그 자체가 결과가 된다.

그래서 bool 객체가 아닌 B를 바로 return한다. 파이썬은 B의 참거짓따위 신경쓰지 않는다.

이에 대한 소스 코드는 오른쪽의 그림이다.

 

or: A is True(왼쪽), A is False(오른쪽)

그렇다면 or의 경우는 어떨까? 이 경우도 살짝 골때린다.

A or B가 있을 때 A가 True라고 해보자.

그럼 B는 볼 필요도 없이 A를 return한다. 왜? A 그 자체로도 True니까.

그래서 num or inf라는 식을 수행했을 때, num값 자체로도 True기에 num을 출력했다.

그 반대인 inf or num은? 당연히 inf도 True이니 inf를 출력한 것이다.

이때 num이 양수인지 음수인지 중요하지 않다. 0만이 False이고, 그외에 모든 값은 True니까.

 

만약 A가 False라면 어떨까? 마찬가지로 B는 크게 의미 없다.

B에 따라 'A or B' 식의 연산 결과가 정해지기 때문이다. 따라서 B를 return한다.

파이썬은 B가 True인지 False인지 신경쓰지 않는다.

 

https://docs.python.org/3/reference/expressions.html#boolean-operations

and와 or 연산에서 2가지 공통점이 있다.

첫째, 앞의 값과 연산자만을 보고 판단하여 return한다. 뒤의 값은 상대적으로 덜 중요하다.

둘째, True / False의 bool 객체가 아닌 자체의 값(혹은 수식 연산 결과)를 return한다.

이런 과정들은 내부적으로 계산을 덜 하기 위한 파이썬의 특별한 기능이라고 한다.

 

not을 설명하지 않았지만, not은 특정 표현식(변수나 조건식)의 반댓값을 return한다.

and, or와는 달리 결괏값은 항상 True 아니면 False 객체이다.

 

그밖에 언어들은 어떤 동작 과정을 거치나 궁금해서 추가로 찾아보았다.

cpp에서도 마찬가지로 0이 아닌 숫자는 true로 인식한다. 일반적으로 예상 가능한 진리값(1 또는 0)을 return한다.

Java는 논리 연산자의 피연산자로 무조건 진리값을 요구한다. 그렇기에 당연히 결과는 진리값(1 또는 0)만이 나온다.

 

https://velog.io/@louie/논리-연사자와-비트-연사자의-차이는

친절하게도 Java 코드로 어디까지 동작하는지 확인해주신 분이 있다.

Python과 같은 매커니즘을 갖지만, 입력과 결과는 진리값이라는 차이가 존재한다.

 

return_home_dist = D[last][start] or infinite

''' 파이썬에서 or 연산자는 두 값 중 하나를 선택하도록 할 수도 있다. '''

결론으로 돌아와보자. "파이썬에서 or 연산자는 두 값 중 하나를 선택하도록 할 수도 있다."

'할 수도 있다'라는 말은, 위에서 추적한 모든 의미를 함축해놓은 문장이었다.

TSP 알고리즘은 마지막 도시에서 출발지로 돌아오는 여정이 필요하다.

돌아오는 길(D[last][start])이 있으면, or 연산에서 무조건 True이니, 그 값을 저장할 것이다.

만일 돌아오는 길이 없다면(0), False이니 무한한 값(infinite)을 저장하여 최소 경로로 들어가지 않을 것이다.

 

최단 거리로 여행하는 외판원과는 달리 참 긴 여정이었다.

 

 

고민을 시작한 문제 출처 : https://shoark7.github.io/programming/algorithm/introduction-to-tsp-and-solve-with-exhasutive-search

참고한 문서 자료 : https://docs.python.org/3/reference/expressions.html#boolean-operations

참고한 문서 자료 : https://dojang.io/mod/page/view.php?id=1638#google_vignette 

참고한 문서 자료 : https://peps.python.org/pep-0285/

참고한 문서 자료 : https://wikidocs.net/22211

참고한 웹 사이트 : https://velog.io/@louie/논리-연사자와-비트-연사자의-차이는

참고한 웹 사이트 : https://velog.io/@kkiyou/py0040

참고한 웹 사이트 : https://homubee.tistory.com/45

'Computer Science > 파이썬(Python)' 카테고리의 다른 글

Python Overflow 심화  (0) 2023.08.18
Python Overflow  (0) 2023.08.17
Python inf  (0) 2023.08.14
Python loop-else  (0) 2023.07.09
Python Underscore(_)  (0) 2023.06.30

+ Recent posts