이 페이지는 외부 공간에 무단 복제할 수 없으며 오직 있는 그대로 게시되며 부정확한 내용을 포함할 수 있습니다. 법률이 허용하는 한 가이드 라인에 맞춰 게시 내용을 인용하거나 출처로 표기할 수 있습니다.This page is not to be distributed to external services; it is provided as is and may contain inaccuracies.
이번 파이썬 AI / 인공지능 교육 과정에 참여하기 전 파이썬 언어에 대해서 공부를 시도하고 있습니다. 이 언어를 공부하면서 다른 언어에 비해서 매우 간소화되어 있고 사용하기에 매우 편리했습니다. built 내장 기능들이 작업을 간소화 해 주고 편리한 부분이 많아 별개 기능을 구현할 필요가 전혀 없었습니다. 이 문서에서는 파이썬에서 유용하게 사용할 만한 내용들을 언급하며, 주로 사용되는 테크닉, 고려할만한 사항을 정리 해 보았습니다.
다른 언어와의 차이점 미리보기
파이썬 언어로 넘어가기 전, 간단하게 다른 언어와 비교 해 봅니다.
저는 주로 Java C#을 주로 사용하는데, 이 언어들을 이용해 기본적으로 데이터를 정의하고 정렬하려면 비교적 높은 시간 투자가 필요합니다.
예시를 들어보겠습니다. 다음은 배열 ["A","B","C","D","E"] 를 차례로 출력하고 정렬했을 때 제일 높은 글자를 뽑아내는 Java의 소스코드입니다. 작업 시간 : 2-3분
정렬
간단한 작업임에도 불구하고 제법 많은 작업이 필요합니다. 심지어 여기서는 "한 글자"만 처리할 수 있으며, 배열에 여러 글자가 섞인 경우는 직접 하나 하나 처리 해 줘야 합니다. (기본적으로는)
String 간의 비교는 당연히 불가하다.
The operator > is undefined for the argument type(s) java.lang.String, java.lang.String
> 연산자는 String, String 간의 연산에 정의되어 있지 않아요.
특히 String과의 비교는 당연히 불가합니다. 이건 데이터형이 비교할 수 없는 구조이기 때문에 당연히 불가합니다.
C# 언어도 마찬가지 입니다.
정렬
이 언어 역시 String간의 기본 비교 연산은 정의되어 있지 않습니다
String은 연산이 불가능하다.
네 좋습니다. 이 김에 Programming 언어하면 빠질 수 없는 C 언어도 가볼까요?
C언어에서의 구현
C 언어에서는 배열 할당을 할 때 가변크기로 구현하기 위해서는 기본적으로 직접 메모리 공간을 원하는 크기로 할당해서 그 공간에 데이터를 삽입해야 합니다.
그리고 개발자가 사소한 실수를 하여 크기 할당을 더 적게 한 경우, 오류를 띄우게 되는데 그 오류 역시 다른 언어에 비해 불친절합니다.
일부러 오류를 내보면
런타임 오류 발생
이 재밌는 친구는 오류가 어디서 발생한지는 커녕 정확한 오류 이름도 모릅니다. 그냥 단지 메모리 주소와 함께 해당 힙 처리에서 메모리 위치를 벗어나 쓰기를 했다고 오류를 띄우네요. 어떻게 보면 메모리 위치를 벗어났다는 것이 정확한 오류기도 합니다. 만약 의도하지 않은 부분에서 이러한 오류가 발생한다면 개발자는 한 줄씩 직접 디버깅하며 오류 위치를 찾고 오류가 발생한 메모리를 추적해야 합니다.
적어도, Java언어에서는 가상 런타임이 아래와 같이 오류를 띄워줍니다.
너 main:9번 줄에서 Array Index가 초과했어.
적어도 여기까지만 보면 웬만한건 직접 구현해야하고 내장된 함수를 사용한다고 하더라도 어느정도 복잡하게 사용해야 함을 알 수 있습니다. 물론, 여기서의 복잡은 파이썬 언어에 비해서 복잡할 뿐, 각 언어들도 간략화 문법이 대량 존재합니다. ^^
이 게시물의 주요 언어인 파이썬은 어떨까요?
정렬
허허.. 신기하네요. 심지어 한 글자가 아니라 여러 글자에 대해서도 내장 함수가 알아서 처리를 해줍니다.
String
심지어 String 간의 비교 연산도 가능합니다.
비교 연산
심지어 데이터를 그냥 내가 원하는대로 막 넣을 수 있습니다.
데이터 형
파이썬에서는 데이터를 가변으로 처리하기 때문에 데이터 형을 크게 신경 쓸 필요 없이 데이터를 넣고 관리할 수 있습니다. 예를 들어, 다른 언어에서는 데이터 형에 선언한 데이터만 넣을 수 있기 때문에 아래와 같이 컴파일 오류가 발생합니다.
다른 데이터 타입은 넣을 수 없다.
그렇기 때문에 다양한 데이터를 넣으려면 여러가지 데이터를 받아주는 하나의 보관함을 만들어야 합니다.
보관함 지정
보관함을 배열에 넣기
보관함을 만들고 비로소 배열에 보관함에 맞는 데이터 형으로 Packing 하여 데이터를 삽입할 수 있습니다. 파이썬에서는 3줄이면 되는 걸, 다른 언어에서는 그에 맞는 형식을 작업 해 주느라 시간을 조금 더 소모해야 합니다.
그래도... 각 언어만의 사용 용도와 장점은 확실합니다.
물론, 다른 언어들이 안좋다는 것은 절대 절대 절대 절대아닙니다. 각 언어들 마다 장단점이 존재하며 사용 용도가 분명한 경우가 많습니다.
C언어는 자원 소비를 매우 적게 하기 때문에 임베디드나 내부적인 처리에서 사용할 수 있으며, Java언어는 네트워킹 & 서버 처리 및 각종 다양한 처리에 특화되어 있어 전문적인 처리를 할 수 있으며, C#언어는 GUI 처리 및 윈도우 클라이언트 프로그래밍에 매우 특화되어 있어 소프트웨어를 개발할 때 자주 사용됩니다.
예를 들어, 저 같은 경우 C# 소프트웨어는 GUI 기반의 프로그래밍과 알고리즘적인 처리를 같이 병행해야할 때 자주 애용합니다. 아래는 개인 목적으로 사용하기 위해 C#언어로 개발한 클라우드 프로그램입니다.
창 구현
그렇기 때문에 다른 언어를 전혀 배제해서도 안된다고 생각합니다. 파이썬을 접하면서도 다른 언어를 같이 응용할 때가 분명히 있습니다. 웹이든 서버든...한편, 파이썬은 데이터 처리에 특화되어 있다고 생각합니다. 파이썬은 데이터를 수집하고 분류, 정렬, 관리하는데 별도 작업을 할 필요 없이 내장 함수 수준 또는 라이브러리 수준에서 손 쉽게 사용 가능하며 문법이 직관적이기 때문에 데이터 처리를 할 때 다른 부분에 시간을 소모할 필요가 없는 것이 매우 큰 장점으로 보입니다. 그래프를 출력하고 데이터를 시각화하는데, 다른 언어들은 라이브러리를 사용한다고 하더라도 데이터를 배치, 정렬하는데 복잡한 부분이 있는 반면, 파이썬 언어는 직관적인 문법으로 바로 처리할 수 있기 때문에 이 부분이 장점이 되리라 생각합니다.
이러한 배경을 알아두고 파이썬을 접근하니, 조금 더 재밌고 유용하게 공부를 진행할 수 있었습니다. 다음 섹션부터는 주요 테크닉들에 대해서 언급합니다.
저작권 고지
기본적으로 이 게시물에 언급된 소스코드, 방식, 코드 구성들은 문법만 숙지한 채 필자가 작성한 내용으로 다른 알고리즘 게시물 내용을 참조하지 않았습니다. 다른 내용이 참조된 경우 그에 대한 출처를 명시합니다.
참고 사항
필자는 다른 언어에 익숙한 상황이라 파이썬 명령 뒤에 세미콜론을 붙이는 습관이 있습니다. 옛날 예적 수업시간에 붙이지말라는 경고를 받았음에도 불구하고 아직도 습관을 버리지 못했습니다. 동작에는 문제가 없으므로... 양해부탁드리겠습니디.
가변 변수
기본적으로 데이터형을 지정하지 않습니다. 데이터를 지정하는 그 때에 해당하는 데이터로 정의되어 기록됩니다.
데이터형을 출력 하기 위해서는 type내장 함수를 사용합니다.
데이터 형의 출력
데이터 형의 비교 연산
변수에 담긴 데이터가 의도한 데이터형이 맞는지 확인하는 과정을 거치기 위해서는 is키워드를 사용하여 비교할 수 있습니다. 예를 들어 함수로 들어온 데이터가 String 연산을 처리하는데 String 값인지 확인하는 전 단계가 필요한 경우 type(item) is str로 비교합니다.
데이터 형의 명시적 변환
담긴 데이터의 형을 명시적으로 변환해야 할 상황이 생길 수 있습니다. 예를 들어 부동소수점을 표현하는 float데이터에 대해 소수점을 모두 제거한 int로 변환할 수 있습니다. 또는 정수만을 표현하는 int를 소수점을 표현할 수 있는 float으로 변환할 수 있습니다. 이를 캐스팅이라고 합니다만, 파이썬 언어에서는 뭐라고 부는지 모르겠네요.
캐스팅
위 이미지를 보면 알 수 있듯 3이라는 정수를 가지던 value를 강제로 float화 하여 3.0으로 변환, 3.0이라는 실수를 가지던 value를 강제로 int화 하여 3으로 변환하였습니다. 이외 str()을 사용하여 숫자적인 데이터를 String으로 (Parsing)변환할 수 있습니다.
String으로 parse
위 사진은 1과 3.3을 가진 정/실수 형태의 데이터를 String으로 Parsing하여 연결 - concatenation한 예시 입니다.
데이터 형의 변환 또는 파싱 이유
실제 데이터 처리 사례를 볼 때, 실수형 데이터를 문자열데이터로 변환함으로써 실수형에서는 처리할 수 없던 문제들을 처리할 수 있게 됩니다.
int는 데이터를 원소로 접근할 수 없습니다.
하지만 String으로 변환함으로써 원소 접근이 가능해집니다. 이는 숫자가 아닌 하나의 문자열 데이터 (1바이트)로 변환되기 때문입니다.
원소의 접근이 가능해짐
대량의 데이터 처리시 권장 안함보통 이러한 방법은 대량의 데이터를 처리하거나 메모리 등 자원이 민감한 상황에서 많은 리소스를 차지할 수 있거나 성능 저하 요인이 될 수 있습니다. 보통 int 데이터의 경우 숫자가 아무리 길어도 4바이트 범위로 표현하지만, String 은 한 글자당 적어도 1바이트를 차지하기 때문에 데이터의 리소스 점유율도 극도로 상승합니다. 따라서 대량의 데이터를 처리하는 구문 내에는 정석적인 방법을 사용하는게 좋다고 생각됩니다.
함수
파이썬에서는 함수를 정의하기 위해 def 키워드를 사용합니다. 함수는 어떤 데이터를 반환할 수 있으며 반환하지 않는 void로 정의할 수도 있습니다.
함수의 예시
함수는 개발자가 직접 호출하기 위해서만 사용하지 않습니다. 다른 라이브러리나 함수에 의해 호출되도록 하는 Callback함수로서 사용되기도 합니다.
콜백 함수의 예시
예를 들어 위 사진에서의 finish()는 직접 호출하지 않습니다. 어떤 처리자에게 함수를 넘겨주면 처리자가 작업을 끝냈을 때 신호 용도로 finish()를 대신 호출 해 줍니다. 어떤 작업이 얼마나 어떻게 된지 실시간으로 피드백이 필요할 때, 정보의 처리가 필요할 때 피드백의 용도로 사용되기도 합니다. Processor 내부의 소스코드를 건들지 않고, 넘겨주는 함수를 통해 원하는 기능을 연결하는 방식으로 작업을 보다 유연하게 진행할 수 있게 됩니다. 이러한 방식은 파이썬의 내부 함수에서도 많이 애용됩니다. 뒷 부분 내용에서는 그러한 예시들을 찾아보고 활용 방법을 언급합니다.
익명 함수
함수 하나를 선언하기 위해 많은 시간과 노력이 필요합니다. 함수의 이름도 지정 해야 할 뿐더러 적어도 2줄 이상은 무조건 차지합니다. 위와 같은 콜백 함수에서 사용할 함수를 만들기 위해 소스코드를 2-3줄 이상 만드는 것은 별로 같아 보입니다. 이럴 때 사용할 수 있는 것이 바로 익명 함수입니다.
위 소스코드를 보면, 함수 정의가 다음과 같이 구현되어 있습니다.
a = lambda a : a
이렇게 보니 다시 봐도 헷갈립니다. 하지만 이러한 함수 정의는 다음과 100% 동일합니다.
위 함수와 100% 동일하다.
익명 함수는 lambda 파라메터들 : 반환 데이터로 구성되며, 대상 변수에 할당함으로써 함수의 이름을 정의합니다. 즉 대상 변수에 할당하지 않음으로써 함수의 이름을 정의하지 않을 수 있습니다. 이러한 예시는 다음과 같습니다.
익명 함수의 예시
위 익명 함수의 예시는 첫 번째의 경우 function이라는 변수에 함수를 할당하였고, 두번 째 익명 함수의 예시는 생성하는 즉시 파라메터를 넘김으로써 호출하였습니다. 두 번째 방법과 같이 호출하게 되면 익명 함수는 내부적으로 처리하고 참조나 처리가 끝나면 보통 소멸됩니다.
함수 적용 - 데이터 정렬
위와 같은 함수를 활용해서 손 쉽게 데이터를 정렬할 수 있습니다. 기본적으로 데이터를 정렬하기 위해서는 자료 구조 시간에 배우는 순차 정렬 과 같은 다양한 기법을 활용하여 직접 구현해야 합니다.
직접 정렬하는 예제
위 예제는 2중 반복을 활용하여 데이터를 정렬하는 예시입니다. [30, 20, 10] 이라는 데이터가 존재한다면, 첫 번째 데이터인 0번 인덱스"30"을 기준으로 뒤에 있는 모든 데이터와 비교를 하여, 제일 작은 데이터가 있으면 서로 교체를 합니다.
예를 들어, 0번 인덱스"30"을 기준으로 둔 채, 새로운 for을 시작합니다. "20" 값이 "30"보다 작은 경우 서로 교체하여 [20,30,10]이 됩니다. 지금 기준으로 0번 인덱스는 더 이상 "30"이 아닌 "20"입니다. 아직 for문이 끝나지 않았습니다. 다음 값인 10과 현재 0번 인덱스의 값을 비교하여 제일 작은 값이 발견되면 교체합니다. [10,30,20]
아직 뒤에 값이 엉망이 되었지만 상관이 없습니다. 한번 for문이 끝났기 때문에 현재 기준으로 맨 앞 0번 인덱스는 그 어떤 원소보다도 작은 값을 가지고 있습니다. 다음으로 1번 인덱스를 기준으로 새로운 for문을 시작합니다. 다만, 이 for문은 1번 인덱스 이후로부터 비교를 시작합니다. [10,30,20] 에서, 30과 20을 비교합니다. 20이 더 적으므로 교체 합니다. 결과 : [10,20,30] 이로써 모든 프로세스가 완료되었습니다.
하지만 내장된 함수와 사용자 정의 함수를 활용하면 이렇게 직접 정렬 알고리즘을 구현 할 필요가 없습니다.
List에 존재하는 sort 함수를 사용하면 데이터를 손 쉽게 정렬할 수 있습니다.
정렬
여기서 알아둬야 할 것은 이 사용 방법은 sort의 기본 사용 예 입니다. 이 함수는 직접 key 를 지정할 수 있는데, 직접 정렬한 데이터를 넘겨줌으로써 원하는 방법대로 정렬할 수 있습니다.
사용자 정의 정렬
위 예시에서는 정렬을 할 때 직접 데이터의 원소를 넘겨준 방법입니다. 여기서 알 수 있는 건, sort를 호출 할 때 key에 정렬 기준점을 담은 함수를 넘겨줄 수 있습니다. sort의 함수에서 넘겨받은 함수를 내부에서
호출함으로써 비교할 대상을 반환합니다. 즉 위 코드에서 func1의 파라메터 x는 하나의 원소 값을 의미합니다.
위 예시에서는 하나의 원소는 30, 20, 10과 같은 정수 하나를 의미하고 있습니다. 보통 실전에서 정렬하는 데이터는 원소가 적어도 두개의 쌍을 가지고 있는 경우가 많습니다.
정렬
[("감자",30),("고구마",20),("더덕",10)] 의 데이터를 정렬하면 하나의 원소는 ("감자",30)가 됩니다. 이를 정렬 요청할 때 통채로 넘겨주면 파이썬에서는 자동으로 첫 번째 인덱스 데이터인 "감자"를 정렬 비교 대상으로 처리합니다.하지만 숫자를 기준으로 처리하고 싶은 경우 숫자 원소 정보를 넘겨줘야 합니다. 이를 함수에 직접 구현합니다.
비교 대상 지정
func1함수의 x 하나는 하나의 원소를 의미합니다. data 리스트에서 하나의 원소는 Tuple데이터인 ("감자",30)를 의미합니다. 여기서 반환 되는 값을 [1]인덱스로 정의함으로써 30을 반환하도록 합니다.
def func1(x):
return x[1];
이를 넘겨주게 되면 sort함수는 정의된 내부 함수를 통해 비교 대상을 삼고 정렬하게 됩니다.
익명 함수의 적용
특히 위 단락에서 알아본 익명 함수를 적용하여 한줄로 수행할 수 있습니다. lambda x:x[1]를 직접 정의하여 key의 파라메터 변수에 직접 참조시킴으로써 함수 이름을 정의하지 아니하고 함수의 참조 주소만 전달할 수 있습니다.
익명 함수의 사용
함수 적용 - 데이터 필터링
리스트에 담긴 원소들 중 조건에 미달되는 데이터를 제거하고자 하는 경우 원시적인 방법은 다음과 같습니다.
자격 조건 미달인 데이터 제거
모든 데이터를 돌아가면서 길이가 3보다 작은 데이터는 제거합니다. 위 예시에서 참고할 사항은 데이터를 처리할 때 반복문 처리 대상인 리스트를 제거하거나 순서를 건들게 되는 경우 순환 처리 런타임 오류가 발생할 수 있습니다. 따라서 리스트의 복사본을 만들어주고 그 복사본에서 데이터를 지워나감으로써 새로운 데이터를 만듭니다. 결과적으로 받아온 데이터를 다시 items 변수에 대입함으로써 이전 원본 데이터 리스트는 참조 대상이 없어지므로 런타임에 의해 자동으로 소멸됩니다. 따라서 최종적으로 남는 건 복사본의 정리된 리스트만 남게 됩니다.
하지만, 이 역시 파이썬의 내장 함수인 filter를 사용해 리스트 정렬 처럼 손 쉽게 구현할 수 있습니다. filter은 조건 사항에 만족되는 데이터만 걸러서 남겨주는 함수입니다.
여기서는 길이가 3보다 같거나 작은 데이터는 소멸하고 있으므로 결과적으로 남기는 데이터는 길이가 3보다 초과인 데이터입니다.
데이터의 비교
filter함수는 쉽게 2개의 파라메터를 요구합니다. 첫 번째 파라메터는 기준 정보를 담은 함수의 참조 주소, 두 번째 파라메터는 처리 할 리스트입니다.
func1함수는 정렬 때와 동일합니다. x 값은 하나의 원소를 의미하며, 해당 원소를 기준으로 len을 계산하여 길이가 3을 초과하는 경우 True을 내보냅니다. 해당 함수가 반환는 데이터가 True인 경우에 데이터를 남기게 됩니다.
여기서 고려할 사항은, filter을 호출하게 되면, 이는 필터 정보를 담은 filter 클래스로 반환됩니다. 그렇기 때문에 list로 다시 씌움으로써 filter 조건 사항에 맞게 순회처리를 받을 수 있습니다. 즉 list()로 filter을 씌움으로써 그 때서야 실제 데이터의 순회 및 필터링이 발생합니다.
익명 함수의 적용
이 함수 역시 익명 함수를 적용할 수 있습니다. 원소 하나에 대한 조건을 계산한 뒤 조건을 그대로 반환하여 조건에 만족하는 데이터만 필터링되도록 합니다.
익명 함수식으로 구현
직접 구현
위 filter함수를 직접 구현할 수도 있습니다.
함수의 구현
filterItems함수는 직접 함수의 원형과 리스트 목록을 받아 리스트를 순회하면서 받은 함수 원형을 콜백 형태로 호출하면서 조건의 만족 여부를 판단합니다.
함수 특징
이 소 단락에서는 파이썬 언어의 함수 특징에 대해서 언급합니다.
파라메터 복사
기본적으로 함수의 파라메터로 넘겨지는 데이터 및 변수들은 어떤 함수 내에서 원형을 = 연산을 통해 건든다고 하더라도 데이터는 변하지 않습니다. 해당 공간에서 다시 재 참조가 발생합니다.
예시
위 예시에서는 items 을 함수 내부에서 None으로 대입하고 있지만 밖에서는 여전히 바뀌지 않았습니다. 함수 내부의 items 자체는 기존 메모리를 건드는 것이 아닌 새롭게 None을 참조하게 됩니다. 다만, List 같은 데이터의 경우 원소를 건들 수 있는데 이 경우는 말이 다릅니다.
메모리 데이터의 변경
함수 안에서 List 내부를 조작하면 외부에서도 변경된 것을 확인할 수 있습니다.
기본적으로 파이썬에서는 변수를 대입하면 대입 연산자가 실제 메모리 내용을 모두 수정하는 것이 아닌, 그 데이터에 대해서 방향을 참조하게 됩니다. 하지만 그 데이터의 내부를 건들게 되면 참조되는 데이터의 실제 값을 변경하게 됩니다. 그렇기 때문에 만약 위 예시에서도 items=None 을 한다고 해도 밖에서 None으로 적용되지는 않습니다. 이는 파이썬 말고도, Java, C#같은 객체지향 언어에서 동일한 부분입니다.
global
그러나, 때론 의도한 대로 함수 내에서 밖의 변수 내용을 조작 할 필요가 생기기도 합니다. 이럴 때에는 함수의 맨 위에 global 변수을 선언함으로써 해당 변수는 밖의 변수에 직접적으로 접근할 수 있습니다. 이러한 경우는 파라메터로 데이터를 전달받지 않습니다.
global의 사용
참고 사항
이 부분은 파이썬 메모리 처리가 어떻게 되는지 파악 해본 내용을 언급합니다.
참고만 하세요.
이 부분은 개인적인 생각을 서술하기에 굳이 읽고 넘어갈 필요는 없습니다.
예시
위 사진에서는 초기 50 데이터를 담은 number을 할당하지만 사실상 50이라는 데이터를 가진 값이 메모리 어딘가에 존재하는 데이터의 주소를 참조하게 됩니다. 이 값을 함수 파라메터로 넘기더라도 동일한 참조를 나타내는 메모리가 전달됩니다. 기본적으로 C나 C++을 기준으로 변수로 int 같은 데이터를 전달하는 경우 메모리 자체가 복사됩니다.
참고 자료
파이썬에서는 메모리를 확인 할 방법이 없어 참조만 복사되는지 메모리 주소 자체가 그대로 똑같이 복사되는지 확인 할 방법은 없어보입니다. id 값이 변수 자체의 메모리 주소를 의미한다면 메모리 주소는 그대로 전달되고, 반면 참조 대상의 주소를 의미한다면 C언어에서의 포인터를 전달하는 것과 유사하겠습니다. 다만, id는 실제 메모리 주소를 반환하는 것은 아니므로 이 부분에 대해서는 정확히 파악하기는 어려워보입니다. 그러나 저의 추측으로는 id는 메모리 자체의 주소보다는 참조 대상의 고유 번호를 반환하는 것으로 파악됩니다.
a=1234564321;
print(hex(id(a)));
위 파이썬 소스코드에 대해서, 초기 메모리를 할당하고 출력 받은 값은 0x24bc2212790라는 64bit 기반 (8바이트) 메모리 주소 값 같아보이지만, 실제 메모리 에디터를 통해 해당 프로그램 프로세스의 메모리 영역을 조회 해 보면 실제 데이터는 없습니다.
4바이트가 02 00 00 00 으로 변수 값을 의미하지는 않아보인다.
데이터의 검색
실 데이터를 Hex 값으로 검색 해 보면, Little-Endian 메모리 정렬 기준 "1234564321"을 의미하는 49 95 F4 E1는 다른 메모리 위치에 저장되어 있습니다. 그것도 3개씩이나 있는 것 봐서는 파이썬 언어 내부에서 유동적으로 처리하고 실제 사용자에게는 불필요한 정보를 은닉하여 효율적으로 관리하고 있어보입니다.
변경 가능한 데이터와 불가능한 데이터
결과적으로 대입하는 데이터들은 참조 형태로 데이터가 할당됩니다. 기본적으로 = 연산을 통해 데이터를 대입할 때, 데이터를 참조하는 형태로 정의된다고 보면 됩니다. 여기서 변경 불가능한 데이터는 어떤 방법으로도 실제 데이터 내용을 변경할 수 없기 때문에 어차피 프로그래밍 상 값을 변경하려면 "="을 통해 다른 값으로 참조를 하게 될 것입니다.반면, 변경 가능한 데이터는 내부 데이터의 변경이 가능하기 때문에 참조된 주소의 내부 데이터를 변경할 수 있고 이는 다른 참조에서도 그대로 반영됩니다.
변경 불가능한 데이터 String, int, float, ...
변경 가능한 데이터 Dictonary, List ...
예시
즉 number의 list 자체의 참조 주소는 그대로 동일하고, 이 참조 내부의 데이터를 변경하게 됩니다.
다만 = 연산을 사용하면 데이터가 쓰기가 되는 것이 아닌 참조가 변경됩니다. 보통 이렇게 참조를 함으로써 접근 불가에 빠지는 메모리 데이터가 생기기 마련입니다.
a=["Hello"];
a=["Bye"]
위 소스코드에서 초기 a 값인 ["Hello"]의 참조는 더 이상 사라졌기 때문에 런타임에 의해 보통 자동으로 Collecting 수거됩니다.
너무 신경쓰지는 마세요..
파이썬 실전에서는 이러한 메모리 처리를 의식할 필요는 거의 없다고 합니다. 특히 파이썬 인공지능의 처리에서 라이브러리가 내부적으로 처리 해주고, 모델 구성 입장에서는 큰 고려 사항은 아닐 수 있다고 사전 시간에 피드백을 받을 수 있었습니다. 특히 대부분 런타임이 메모리를 자동으로
Collecting 소거 해주고 정리 해주는 것 같아 보입니다.
단지 함수로 넘어온 변수 그 자체는 변경되지 않고, 변수의 내부를 건들면 참조된 데이터를 바꾸기 때문에 변경된다는 사실만 기억하면 좋아보입니다.
Return 의 활용
Return은 함수를 끝냄과 동시에 데이터를 호출자에게 반환할 수 있는 키워드입니다.
함수
파이썬 기준 return 명령은 필수가 아닙니다. 하지만 데이터를 반환하고자 하는 경우 주로 마지막에 사용합니다.
return
return은 데이터를 호출자에게 반환함과 동시에 해당 함수를 메모리로부터 정리합니다. 그렇기 때문에 해당 명령 지점에서 더 이상 수행되지 않습니다. 실무에서는 이러한 원리를 이용하여 함수의 흐름을 제어하는 용도로 많이 사용됩니다. 예를 들어, 플레이어가 3명 이상임과 동시에 플레이어의 모든 닉네임에 'p' 문자가 들어간 경우에만 게임이 진행되는 함수가 있다고 가정하겠습니다.
함수 조건
이러한 방법을 분기문으로 구현하기 위해서는 적어도 2개의 분기문을 중첩하거나 and 하는 방식으로 표현하며, 그에 따라 들여쓰기가 필수적으로 늘어나게 됩니다. 이는 분기의 개수가 많아짐에 따라 들여쓰기는 더 늘어나게 됨과 동시에 실수 할 여지를 남기게 됩니다.하지만 함수의 return을 사용하면 다음과 같이 구현할 수 있게됩니다.
return의 사용 예
함수 내에서 순차적으로 진행하다가 분기문의 조건 사항에 미달인 경우 return을 수행하여 더 이상 함수가 진행되지 않고 블럭을 빠져나옵니다. 이렇게 되면 분기가 몇 개든 앞에 순차적으로 명시하면 되기 때문에 들여쓰기 개수가 늘어날 일도 없습니다
때로는 return에 그러한 사유를 반환하여 함수를 호출한 호출자위치에서 어떤 처리를 진행할 수도 있습니다.
return 에 사유를 반환
이러한 방법을 통해서 함수 안에서 모든 것을 처리하지 않고 작업하는 파트를 적절히 분배하여 모듈화가 가능합니다. 예를 들어 어떤 데이터를 처리하여 반환하는 함수에 사용자에게 출력하는 기능을 넣는 것은 적절하지 않습니다. 데이터를 처리하는 함수는 데이터를 처리하는 기능으로 마무리 짓는 것이 제일 완벽하며, 데이터를 출력하는 건 호출자가 진행하는 것이 적절합니다.
작업 영역의 분할
재귀 함수
파이썬에서의 재귀 함수는 다른 언어와 거의 유사합니다. 재귀 함수는 함수 안에서 자신의 함수를 다시 호출함으로써 파고 들어가는 구조입니다.
재귀 함수의 예제
재귀 함수는 기본적으로 함수 안에서 함수를 다시 파고 들어가기 때문에 적당한 분기문이 없으면 무한 재귀에 빠져 프로그램의 Stack 메모리 exceed가 발생할 수 있습니다. 위 예시에서는 재귀함수를 하되 카운팅하는 넘버를 함수에 다시 넘겨줌으로써 10번 카운팅 째 더 이상 재귀 하지 않도록 합니다. 위 예시를 그림으로 나타내면 다음과 같습니다.
스택
여기서 유의할 사항은 재귀함수를 호출할 때 자기 자신의 함수가 끝나고 함수가 호출되는 것이 아닙니다. 자신의 함수는 아직 끝나지 않은 상태에서 새로운 자기 자신의 함수가 호출되기 때문에 반복하면 할 수록 스택에 함수의 주소가 쌓이게 됩니다.
print를 아래로 내린 경우
위 예제에서는 print를 단순히 다음 data()를 호출한 뒤에 호출하였습니다. data() 함수를 실행하기 위해 이 쓰레드에서 다음 작업을 미룬 채 다음 data()의 내부로 진입하게 됩니다. 이 같은 상황이 반복되다가 끝 지점에서 하나씩 끝나면서 마지막에 쌓인 재귀 함수부터 하나씩 출력이 되기 시작합니다.
진행 순서
이러한 재귀 함수가 호출이 무한히 반복되면 일정 이상에서 런타임 오류가 발생합니다.
런타임 오류
이 예시에서는 약 2969번 만에 중지됩니다. 생각보다 적은 양의 반복 횟수입니다.
응용 예시
이러한 재귀 함수는 실제 발생하는 문제를 해결하는데 응용할 수 있습니다. 예를 들어 리스트가 존재할 때 이 리스트를 뽑을 수 있는 모든 원소의 조합은 어떻게 구할까요?
items=[2,5,6,7];
2를 뽑은 상태에서, 나머지 [5,6,7] 중 5를 뽑은 상태에서 , [6, 7] 중 6을 뽑은 상태에서 남은 [7]을 뽑을 경우 => [2,5,6,7]
2를 뽑은 상태에서,
5를 뽑은 상태에서, 남은 [6, 7] 중 6을 뽑은 상태에서 [7]이 남았지만 안뽑을 경우 => [2,5,6]
모든 경우의 수를 돌아가면서 하나씩 뽑아봐야 합니다.
만약 위와 같은 경우는 for으로 표현하면 아래와 같이 풀이할 수 있습니다.
for문을 사용한 방법
위 방법은 원소 하나를 중심으로 두면서 계속 다음 for문에서 다음 원소를 탐색함으로써 조합할 수 있는 모든 경우의 수를 찾는 예제입니다. 각 for끝 마다 해당하는 원소 범위까지의 데이터를 result에 삽입함으로써 모든 경우의 수를 탐색할 수 있습니다.
하지만, 위 방법은 리스트의 원소가 4개 인 경우로 한정되며 원소가 5개 또는 6개 인 경우 그만큼 for문을 중첩 해야 합니다.
이러한 경우에는 단순한 중첩 방법을 통해서는 문제를 해결할 수 없으며, 재귀 함수나 깊이가 있는 탐색을 통해 작업을 진행해야 합니다.
재귀 함수를 사용해 깊이가 있는 형식의 탐색 구현
def depthSearch(result, items, latestItems, index):
if(index!=-1):
latestItems.append(items[index]);
result.append(latestItems);
for i in range(index+1, len(items)):
depthSearch(result,items,latestItems.copy(),i);
items=[2,5,6,7]
result=[];
depthSearch(result,items,[],-1);
print(result);
depthSearch함수를 실행하면 처음에는 -1 인덱스로 넘어옵니다. 이런 경우에는 제외하고 단순히 for문만 한 번 진행합니다. 첫 번째 인덱스부터 끝까지 하나씩 선택하면서 해당하는 인덱스와 함께 다음 깊이로 넘어갑니다. 2번째 깊이에서는 각각 하나의 데이터가 [2], [5], [6], [7]들어온 상황입니다. 이 상태에서는 결과에 우선 들어온 값을 한번 추가 해주고, 다음 for로 넘어가 현재 자신 값에서 추가할 수 있는 원소들을 모두 다시 자신의 재귀 함수로 추가 호출 해줍니다. 이러한 작업을 들어가면서 계속 좁혀나가면서 반복합니다.
데이터의 순회 순서
여기서 주의할 것은, 특정 원소를 기준으로 다음 인덱스만 판단할 뿐, 이전 인덱스로 돌아가서 판단하지는 않습니다. 그렇게 되는 경우 조합이 아닌 이미 뽑은 자료가 순서만 다른 채로 중복하여 뽑게 됩니다.
이러한 방법들은 실 생활에서, 내부의 깊이를 모를 때 사용할 수 있으며, 예를 들어 디렉터리 내의 구조를 파악하거나 모든 데이터를 순회할 때 사용할 수도 있습니다.
디렉터리 구조 예시
실제 알고리즘 문제에서 사용하는 경우도 제법 있는 데, 이러한 방법은 모든 경우의 수를 직접 탐색하기 때문에 각 문제의 의도에 따라 효율적으로 구현해야 합니다. 리스트가 조금만 늘어나도 경우의 수는 기하급수적으로 늘어나기 때문에 특정 연산에서 매우 비효율적일 수 있습니다.
연산 횟수 : 33554431, 걸린 시간 : 20초
또한 이러한 방법은 멀티쓰레딩 등 동시 처리 작업이 어렵거나 불가능하므로 일부 실무에서는 비효율적인 방법으로 분류될 수 있습니다. (구현하는 방법에 따라 멀티쓰레딩이 가능할 수 있음)
String
이번 섹션에서는 String을 처리하고 분석하는 부분에서 자주 사용되는 테크닉을 언급합니다.
데이터의 분할 - split
데이터 분할
기본적으로 데이터를 분할하기 위해서String데이터 형에 탑재된 split을 사용할 수 있지만 내장된 re 모듈에 제공되는 Regex표현을 사용하면 패턴을 적용하여 데이터를 분류할 수 있습니다.
만약 data 리스트에 대해 다음과 같은 상황에서 =, 로 나누고자 하는 경우 다음과 같은 방법을 고려할 수 있습니다.
data="Key=UserName, Password=a1234, FilePassword=b1234";
데이터를 모두 나누기
하지만, 위와 같은 방법은 동적인 문자열이 들어가거나 띄어쓰기 등 상황에 따라 오류를 발생할 가능성이 매우 높기 때문에, 이런 경우에는 정규 표현식을 사용합니다.
정규표현식
split 정규 표현식은 import re;로 모듈을 임포트하며, re.split(정규식, 데이터)로 정규 표현식을 적용해 split할 수 있습니다. 예를 들어 정규 표현식 "[=]|, "는 =와 같은 문자 또는 ", " 같은 문자로 나눕니다.
다양한 split의 적용
import re;
data="Key=UserName, Password=a1234(, )FilePassword=b1234[, ]Items";
print(re.split('[=]|\\(, \\)|\\[, \\]|, ',data));
사실 위 같은 경우는 ", " "(, )" "[, ]"로 각각 split를 해주면 되기에 정규식만의 장점을 보지 못합니다. 그러나 만약 괄호나 구분점 안에 임의의 데이터가 들어가고, 이러한 데이터 관련 없이 잘라내야 하는 경우 매우 유용합니다.
[=]|\\(, \\)|\\[, \\]|,
= 또는 (, ) 또는 [, ] 또는 , 를 기준으로 자릅니다. 맨 앞의 [=]|는 =| 로도 표현할 수 있습니다.
정규 표현식 설명
여기서 ( 또는 [ 앞에 \\가 붙는 이유는 ( 및 [ 는 문자 그자체로 처리하고 싶기 때문에 붙입니다. 정규식에는 (, [ 자체는 정규식 문자로 인식하기 때문에 글자로 인식하지 않고 정규식 패턴 (프로그래밍 언어로 따지면 문법)으로 파악하게 됩니다. 따라서 문자열 그 자체인 ( 및 [로 인식시키게끔 ([ 앞에 \\를 붙여 문자 그 자체로 정의합니다.
예시
import re;
data="Key=UserName(유저이름 을 입력하세요.),Password=a1234(비밀번호를 입력하세요.)Items=a12345";
print(re.split('\\([^)]*\\),{0,1}',data));
위 코드는 data의 형태가 매우 뒤죽박죽으로 엉망이지만, 정규식 규칙에 의해 잘 잘렸고 문제 없이 필요한 데이터만 분류되었습니다. 이 처럼 입력을 예측하기 힘들고, 다양한 경우의 수가 생길 수 있는 입력에서 데이터를 자르거나 파싱의 작업이 필요한 경우 정규식을 적용해 자를 수 있습니다.
데이터의 치환 - replace
기본적으로 어떤 문자열안에서 원하는 원본 문자열을 대상 문자열로 변경할 때 사용합니다.데이터에서 바꾸고 싶은 문자열을 특정 내용으로 모두 바꿀 수 있습니다.
"문자열".replace(원본문자열, 대상문자열);
replace 사용
이 예시에서는 특정 문자열에서 띄어쓰기를 모두 제거하고 있습니다.
이 예시에서는 (잘라)를 모두 ,로 바꾸고 있습니다.
기본 형태의 replace 는 유용하지만 패턴을 적용할 수 없습니다. 정규식을 지원하는 replace 를 사용하기 위해서는 re 모듈의 sub 메소드를 사용합니다.
sub 메소드
print(re.sub('\\([^)]*\\),{0,1}',',',data));
위 예시는 \\([^)]*\\),{0,1} 에 해당하는 패턴들을 모두 ,로 바꾸고 있습니다.
\\([^)]*\\),{0,1}
( 로 시작하면서,안에 어떠한 내용을 담고 있으며 )로 끝나되 끝에 ,이 붙을 수도 있고 안 붙을 수도 있는 패턴들을 의미합니다.
정규식 예시
replace 의 활용예
replace 의 사용 예시
기본적으로 replace 는 각종 다양한 언어에서 중요한 구성요소를 표시할 때 대체할 수 있습니다. 언어 데이터는 내부 언어 리소스에 적재해 둔 뒤, 이를 사용자의 언어에 맞게 가져와서 사용하되 필요로한 이름 요소 등의 플레이스홀더 들을 대체하여 출력합니다.
eng ="properties of <ItemName> : ItemType : <ItemType>, DamageSize : <Damage>, ProtectionSize : <FirewallSize>";
kor = "<ItemType>:<ItemName>은 공격력 <Damage>을 가지며 방어력 <FirewallSize>을 가집니다."
jap = "<ItemType>:<Itemname>は攻撃力<Damage>を持ち、防御力<FirewallSize>を持ちます。"
게임에서 옥의 티를 찾아보세요.가끔 해외에서 개발되어 국내에 번역된 게임 등에서 플레이스 홀더가 적용되지 않아 그대로 화면에 표시되는 경우를 가끔 볼 수 있습니다. 예를 들어, 아이템, 스킬에 마우스를 가져다 댔는데, 아이템, 스킬 설명에 "지속시간 <skill_cooltime> 초 동안 적용됩니다." 등이 표시되는 경우가 있습니다.
리스트
이 섹션에서는 리스트 자료형에 대해서 자세히 알아봅니다.
다양한 데이터형
다양한 데이터형을 리스트 원소로 삽입할 수 있으며 그 제한은 크게 없습니다.
2차원
흔히 위와 같은 형태는 2차원 배열로 볼 수 있지만 다른 언어와 동일하게 2차원 배열 그 자체는 아닙니다. 하나의 원소에 3개의 원소를 가지고 있는 리스트를 담고 있는 큰 하나의 리스트라고 파악하는 것이 추후 데이터 이동 및 관리에 편리합니다.
가변 인덱스
특히 주의할 것은, 파이썬의 리스트는 흔히 알려진 배열 방식의 고정 인덱스가 아님에 유의합니다. 원소의 순서는 원소의 배치에 따라 유연하게 변경될 수 있으며, 앞의 원소가 제거되는 경우 뒤의 원소들의 인덱스는 차례로 앞당겨 집니다.
설명
즉 여기서의 배열로 인식되는 리스트는 C# 또는 자바 언어의ArrayList 와 유사하다고 보며, C++의 std::Vector 와 유사하다고 볼 수 있습니다. 배열이라고 부르기 보다는 리스트라고 보는 것이 좋아보입니다. 따라서 초기 선언할 때 리스트의 크기를 직접 제한하지 않습니다. 즉 원소는 다른 데이터를 참조하는 방식이며 리스트를 여러개 담는 경우 다른 리스트를 참조하고 그 안에서 다른 리스트를 참조하는 방식입니다.
참조 대상의 내부 데이터 변경
참조 대상의 변경 및 참조 대상의 내부 데이터 변경
위 두 개의 소스코드는 다른 점이 있습니다. 첫번 째 단락의 소스코드는 value로 "a"를 가져왔지만 이를 변경하더라도 data 리스트에 적용되지 않고, 두번 째 단락의 소스코드는 value 로 ["a"]를 가져와 이 내부를 수정하였더니 원본data의 데이터도 적용된 것을 볼 수 있습니다.
이 게시물 첫 섹션에서 언급된 함수 변수의 참조 정보와 동일합니다.
첫 번째 단락의 소스코드의 경우 data 리스트 내부의 첫 번째 원소인 "a"를 value라는 이름으로 가져왔고, 현재 value 는 "a"를 참조하고 있는 상황입니다. 하지만 이를 =를 통해 변수 그 자체의 대입 연산을 함으로써 내부의 데이터가 변경되는 것이 아닌 value가 참조하는 대상을 새로 변경하게 됩니다. 즉 value가 가르키는 참조 대상이 변경된 것 입니다.
한편 두 번째 단락의 소스코드의 경우 data 리스트 내부의 첫 번째 원소인 ["a"]을 value라는 이름으로 가져왔고, 현재 value는 ["a"]를 참조하고 있는 상황입니다. 여기서 변수 그 자체의 = 연산이 아닌 0번의 원소의 내부에 접근하여 0번 원소의 내부 리스트에 대한 0번 째 참조 대상을 "c"로 바꾸고 있습니다. 즉 참조 대상의 내부의 값을 변경하고 있기 때문에 원본에서도 반영되어 보이는 것 입니다.결과적으로 두 번째 단락의 소스코드에서도 변수 그 자체를 =연산한다면 동일하게 바뀌지 않을 것 입니다.
위 경우에는 바뀌지 않는다.
이는 리스트뿐만이 아니라 딕셔너리의 value, 셋 등에서도 동일한 내용입니다.
인덱스 슬라이싱
파이썬에서는 저수준 언어와 다르게 데이터 조작 및 추출을 손 쉽게할 수 있도록 리스트 등에 대해 슬라이싱을 지원합니다.
슬라이싱
hello[]를 통해 접근할 때 인덱스를 정수 하나가 아닌 <시작점>:<끝 직전 지점>:<증감 크기> 데이터를 원하는 규칙에 따라 한번에 새로운 리스트로 가져올 수 있습니다.
증감 크기는 생략할 수 있으며 생략시 1로 처리됩니다. 특히 <끝 직전 지점>은 해당 숫자의 전 인덱스까지 조회합니다.
예시
위 예시에서는 0번 인덱스부터, 4번 미만까지 하되 간격을 2로 두고 있습니다. 0,2 인덱스가 선택되어 [1,3]이 출력됩니다.
역방향
또는 역 방향으로도 가능합니다.
딕셔너리
딕셔너리는 Key과 Value을 쌍으로 가지는 데이터 형태입니다. 순서를 가지지 않으며 Key에 대한 데이터를 기록할 때 사용합니다.기본적으로 딕셔너리 없이도 리스트로만 구현 할 수 있습니다.
딕셔너리 구현
위 소스코드는 클래스 없이 딕셔너리를 구현 한 예시 입니다. getValue()를 통해 키 값을 가져오고, setValue()를 통해 키 값에 대한 value를 등록하고 중복이 있으면 그 key의 value를 변경하고, 마지막으로 getItems()를 통해 순회하거나 반복문에서 받아서 처리할 수 있도록 순회 형태로 반환해주고 있습니다.하지만 파이썬에서는 자체적으로 이러한 도구를 제공해주기에 직접 구현할 필요는 없습니다.
딕셔너리 사용
딕셔너리의 정의는 {}로 하며, 데이터는 [] 를 통해 접근할 수 있습니다.
데이터의 유무 파악
데이터가 존재하는지 여부에 대해서는 in키워드를 통해서 사용할 수 있으며, 딕셔너리 자체에 대해 in을 조회하더라도 키 값을 기준으로 존재하는지 확인합니다. 이외 keys()를 통해 key의 목록만 가져오거나, values()를 통해 value의 목록만 가져올 수 있습니다.
이를 반복문에서 사용할 때는 다양한 방법으로 가져와서 사용할 수 있습니다. items()를 통해 쌍을 한 번에 Unpack하여 불러오거나 keys()를 통해 Key만 가져와서 key로 접근하거나 아예 인덱스로 접근하여 Key에 접근해 value를 가져올 수 있습니다만, 보통 쌍 또는 Key만 가져와서 처리를 합니다.
예시
원하는 방법을 골라서 사용하면 됩니다. 하지만 마지막 소스코드의 items.keys()의 경우, dict_keys 클래스 자료형으로 직접 인덱스 접근이 불가능하기 때문에 keys를 list자료형으로 변경 한 뒤 인덱스로 접근해야 합니다.
셋
set은 어떤 자료에 대해 순서도 없고, 중복되지 않는 자료 공간입니다. 순서도 중요하지 않고 어떤 자료를 보관하되 중복되지 않아야 하는 자료를 보관할 때 유용합니다.
append 가 없다.
주의 할 점은 set은 append로 리스트에 데이터를 추가하지 아니하며 add 메소드를 사용합니다. 왜 이름이 다 제각각 다른지는 모르겠습니다 ?
사용 예
remove를 통해 데이터를 지울 수 있으며 안에 데이터가 있는지는 리스트 등과 동일합니다. 모든 데이터를 열람할 때는 단순히 for x in data형태로 조회할 수 있습니다.
클래스
파이썬은 객체 지향 언어로 협업, 관리, 유지보수, 재사용성이 가능한 소스코드를 만드는 것이 중요합니다. 그에 대해 클래스를 활용하는 방법에 대해서 간단하게 설명합니다.
기본 형태
클래스
기본적으로 클래스를 표현하기 위해 아래와 같은 형태로 정의합니다.
class 클래스이름:
def __init__(self):
#생성자 코드
def 함수들(self):
#함수코드
클래스는 틀을 만드는 행위입니다. 공장으로 비유하자면 공장 기계의 틀입니다. 이 틀을 통해서 틀 모양에 맞는 객체를 만들게 되고 이를 보통 언어 기준 Instance인스턴스라고 합니다. 위 소스코드에서 생성자 코드는 클래스가 인스턴스화되었을 때 호출될 소스코드 입니다. 보통 생성자에서 자기 클래스의 데이터를 초기화하고 정의합니다.
이러한 인스턴스를 만들면 해당하는 데이터나 그룹에만 의존하여 처리할 수 있게 됩니다. 예를 들어, 위의 클래스는 딱히 그룹된 데이터가 없습니다. 그러나 아래의 예시를 보겠습니다.
클래스 예시
위 클래스에서는 생성자에서 x,y 값을 추가로 받고, play() 및 stop()에는 그 데이터를 다시 받아와 연산을 하고 있습니다. 즉 저런식으로 틀을 만들어두면 해당 인스턴스에 대해 명령을 호출하면 내부에서 알아서 작업을 처리하고 결과만 받아볼 수 있습니다. 예시에서는 123와 456에 대한 데이터를 처리 했지만, 필요에 따라 인스턴스를 더 많이 생성하고 호출할 수 있습니다.
예시
위 예시에서는 연산 하고자 하는 두 숫자에 대해 매번 클래스인스턴스를 만들면서 결과만 받아서 처리하고 있습니다. 퍼포먼스 이슈 우려 하지만 위 예는 저런식으로도 가능한 예를 보여줄 뿐, 실제 극대용량의 연산을 처리하는 과정에서 클래스를 인스턴스화 하고 호출하는 행위는 성능 차이가 발생할 수 있습니다.
참고 문헌, 자료
코드나 전반적인 내용 구성은 직접 작성되었으나, 원리나 문법에 대해 헷갈려 일부 참고한 문서를 포함합니다.
파이썬 문서
https://docs.python.org/ko/3/howto/regex.html
https://docs.python.org/ko/3/library/copy.html
https://docs.python.org/ko/3.9/tutorial/classes.html
정규 표현식 관련 정보
https://hamait.tistory.com/342
위 자료는 어디까지나 필자의 헷갈리는 부분을 해소하기 위해 검색, 검증, 참고한 자료 정보로 원본 자료를 인용하지는 않았으며, 참고만 한 자료로 출처를 표기합니다.