카테고리 없음

스파르타 AI-8기 TIL(10/8) -> 파이썬 중급, 고급

kimjunki-8 2024. 10. 8. 21:26
오늘은 한국어로 써보자...

들어가기 전에 알아야 할 것들

 

파이썬의 중급과 고급은 초급보다는 훨씬 어렵습니다. 초급에서 클래스와 함수를 배웠다면, 중급과 고급에서는 더 복잡한 "자료" 구조, 고급 함수형 프로그래밍, 예외 처리, 메타프로그래밍, 동시성, 병렬 처리, 메모리 관리 등, 강의에서 배우지 못한 더 복잡하고 여러운 개념들을 포함합니다. 하지만 앞으로 배울 강의들을 생각하면, 그 필요성은 매우 높죠. 아 참고로 오늘 TIL은 강의와 개인 과제까지 끝난 시점에서, 인터넷에서 배운 정보들을 바탕으로 제작되었습니다.^^

 

목차.
  • 여러 고급 함수
  • 예외 처리
  • 메타프로그래밍(10/9)(coming soon)
  • 동시성(10/10)(coming soon)
  • 병렬 처리(10/11)(coming soon)
  • 메모리 관리(10/11)(comsing soon)

 

 

 

 

 

 

1. 여러 고급 함수


1. 데코레이터 (Decorator)

 

데코레이터는 살짝 어려울지도? 하지만 필요하다!
예:
def my_decorator(func): -> 함수를 인자로 받는 코드 입니다. 일단은 이렇게 두자구요!
    def wrapper(): -> 안에 또다른 함수가 있습니다. 이것도 이렇게 놨둡시다!
        print("before!")
        func() -> 이것은 나중에 인자로 받을 함수가 시행될 때 쓰입니다!
        print("after!")
    return wrapper
@my_decorator -> 나중에 인자로 받을 함수가 실행되면 대신 my_decorator가 실행되게 됩니다.
def say_hello():
    print("hi!")
say_hello()
출력: 
before!
hi!
after!
엄청 복잡해보입니다. 하지만 조금만 들여다보면 가능!
일단 밑에 보면 @my_decorator가 선언이 되었고 밑에 def say라는 함수가 있습니다. 하지만 아까 말했듯이 my_decorator가 대신 실행이 되기 때문에, 결국 def say_hello()는 인자로 받을 func으로 대신 들어가게 됩니다. 즉,
def my_decorator(say_hello())가 되는것이죠! (그렇기에 @를 써서 선언이 필요)
그리고 안에 있던 함수 wrapper()가 그대로 받아서 안에 있는 func()를 실행시키는 겁니다!
그러면 wrapper()함수에 있던 모든 것들이 출력을 하게되고, 그 값들이 다시 wrapper로 반환되게 되는겁니다.

2. 재귀 함수 (Recursive Function)

 

재귀 함수는 기본적인 개념 구조 이해가 중요해요!
재귀 함수는 자기 자신을 부르는 함수라고 생각하시면 됩니다. 어떤 문제를 풀 때, 그 문제를 같은 방식으로 더 작께 쪼개서 풀 수 있을 때 사용합니다! for문이랑 비슷하다고 생각하면 편합니다, 하지만 종료 코드를 넣지 않으면 코드가 무한으로 반복을 하기 때문에, 종료 코드는 무.조.건 넣어야 합니다!
예:
def facto(a):
    if a == 0 or a == 1: -> a가 0이나 1이 되면 종료되게 만드는 종료 코드를 작성했습니다
        return 1
    else: -> 만약 a가 0이나 1이 아닐경우, 그 코드는 계속 실행되도록 만들었습니다.
        return a * facto(a-1)
출력: 120
자, 이 코드를 설명하면, 함수를 호출하고 인자로 5를 넣게 될 경우, a는 0이나 1이 아니기 때문에, 먼저
return 5 * factorial(5)가 됩니다. 하지만 factorial을 return 값으로 넣었기 때문에, 계속해서 반복하는 과정을 거칩니다.
그렇기에 간단하게 펼치면,
return 5 * 4 * 3 * 2 * 1 * 1이 됩니다.(1*1은 a가 -1로 계속해서 감소하다가 1과 0이 되었을때 자동으로 1로 반환하는 코드 때문입니다)
그렇게 값은 자동으로 120이 반환됩니다.

3. Lambda 함수 

 

람다 함수의 기본 구조: 
lambda 인자들(인자1, 인자2, ....) : 표현식

인자들: 함수가 받은 값들(일반 함수의 매개변수와 같습니다 + 인자를 사용하지 않는 경우도 있습니다!)
만약 인자가 없는 경우 -> lambda: 표현식


표현식: 계산 결과로 반환될 값입니다. 함수의 동작을 결정하는 아주 중요한 부분입니다.
Lambda 함수는 이름 없이 정의할 수 있는 간단한 익명 함수입니다. 다른 함수들은 def로 정의하지만, lambda는 바로 사용할 수 있죠.
예 ->
기본 함수:
def add(a, b):
      return a + b ->  이렇게 함수를 호출할 때, a와 b를 더해서 반환하는것이 기본 함수입니다.

lambda 함수:
lambda a, b: a + b -> 정말 간단하죠? 이렇게 a, b를 받으면 a + b를 해서 반환해줍니다.

Tip!
람다 함수는 '익명 함수'입니다, 즉 이름이 없기 때문에, 보통 변수에 할당해서 사용합니다!
예:
adding = lambda a, b : a + b
게다가 람다 함수는 다른 함수에 매개변수로 전달할 때 유용히 사용됩니다!

4. 일급 함수 (First-Class Function)
일급 함수는 사실 구조보다는 개념이 따로 있습니다! (조금 어려워요!)
일급 함수는 클래스에서 상속과 비슷한 개념이라고 생각하시면 편합니다.
예로 들어서 설명해볼게요:
def add (a, b):
      return a + b -> 기본적으로 함수를 호출하면 a + b를 반환하는 함수를 생성했습니다.
def adding(func, a, b, c):
      return a + b + c
print (adding(add, 1, 2, 3)) 
-> 약간 복잡하죠? 이제 뜻을 알려드릴게요
출력: 6
자, 여기서 먼저 add함수는 a와 b를 받으면 a + b 형태로 반환합니다. 그리고 adding에서는 매개변수로 func, a, b, c가 있습니다.
func는 함수를 받을것이고, a, b, c를 각각 받을 것입니다. 이렇게 되면, 함수에 있는 기능을 그대로 사용하면서, 기본적으로 있던 a + b형태로 반환을 해줌과 동시에 adding에서의 기본 반환인 (a+b) + c 까지 같이 하게된다는 뜻이죠. 즉,
add -> 1, 2를 인자로 받음->  1 + 2를 반환(=3)

그럼 그 동시에 adding에서 add의 형태 그대로 '상속' 받는다 생각하시면 됩니다. 그래서 나온 3(1+2)에서 c의 값인 3까지 추가로 더해줌으로써, 6이 출력이 됩니다. 그렇기 때문에 상속을 받는 함수는 기존에 있던 함수의 연산를 더해서 추가 연산을 해줄 수 있습니다. (*, -, %, **...)

5. 클로저 (Closure)
클로저 또한 개념 이해가 중요합니다. 함수 안에 함수가 존재하기 때문이죠.
여기서 중요한 3가지 클로저의 구성을 설명하겠습니다.
1. 중첩 함수: 함수 내부에 정의된 또 다른 함수를 뜻합니다.
2. 외부 함수: 제일 바깥에 존재하는 함수(중첩 함수를 포함하는 함수라 생각하세요^^)
3. 자유 변수: 중첩 함수에서도 쓸 수 있는(참조) 외부 함수의 변수를 뜻합니다.
예:
def out(a): -> 외부 함수(out)와 자유 변수(a)
    def inner(b): -> 중첩 함수(inner)
        return a + b -> 자유 변수와 inner함수의 매개변수인 b을 더하는 것을 반환
    return inner
out_no = out(3) ->여기서 외부 함수에서, 자유 변수에 3을 기입
inner_no = out_no(5) -> 중첩 함수에 5를 최종적으로 기입
출력: 8

사실, 중첩 함수에 매개변수를 지정하지 않아도 됩니다. 하지만 파이썬의 특성상, 코드가 여러울수록, 중첩 함수에도 매개변수를 지정하실 일이 많을겁니다...
여기서 궁금증이 생기실 겁니다. 왜 변수를 두번 할당하는거지?
사실, 
out_no = out(3) -> 이렇게 하시면 외부 함수에서 a에는 3이라는 값이 저장됩니다. 여기서 기억해야 할 점은,
inner_no = out_no(5)를 함으로써 a에 다시 5가 할당되는 것이 아닌, 두번 째에 있는 b에 할당이 된다는 것입니다.
왜냐하면 3은 이미 중첩 함수에 들어가서 a에 기억되었기 때문이죠. 5가 담긴 상자에 3이라는 상자를 넣어도 사라지지 않는것과 같다고 생각하시면 됩니다.

 


6. Map 함수
집중! Map() 함수는 파이썬에서 매우 유용하게 쓰이는 함수이므로 이해가 무조건 필!!요!!
map()함수는 리스트나, 튜플 등, 주어진 함수를 반복하는데 적용하는 함수입니다! 
그렇기에 주로 데이터를 변환하거나 처리할 때 사용됩니다!
기본 구조로는:
map(function, interable,...)
function -> 각 요소에 적용할 함수를 적으면 됩니다! 하나 이상의 인자를 받을 수 있습니다!
iterable -> 설명에 적힌 리스트나 튜플등을 말합니다!
그렇기에 map함수는 람다 함수와 자주 쓰입니다!
예: 람다 함수 포함
a = [1,2,3,4,5]
b = [5,6,7,8,9]
result = map(lambda x, y:x + y, a, b)
-> 여기서 a와 b에 있는 리스트 정보들을, a는 x에, b는 y로 넣어서 하나씩 x + y로 변환한 후 반환해준다는 뜻입니다.
print(list(result))
출력: [6,8,10,12,14]

잠깐, 잠깐, 저 list는 뭐고 어떻게 돌아가는거죠? 지금 바로 설명드리겠습니다.
일단 map함수에는 하나의 함정이 있습니다. 바로 map함수에 있는 값을 다 계산하면, 그 반환 값은 map object라고 해서 따로 사용이 불가합니다. 그렇기 때문에 list함수('리스트나, 튜플의 요소로 변환해준다' 라고 생각해주세요^^)를 사용합니다.
그렇게 하나씩 계산된 a에 있던 값들은 전부 list의 형식으로 나옵니다.

어? 그런데 다른 함수를 사용하면 어떻게 되나요? 이렇게 사용할 수 있습니다
def add(x,y): -> 여기서 함수를 지정하여 x와 y를 받아 x + y 형태로 반환합니다.
    return x + y
a = [1,2,3,4,5]
b = [5,6,7,8,9]
result = map(add, a, b) -> 여기서는 map함수의 구조인 add함수와 a,b 리스트들이 들어갑니다(function, interable)
print(list(result))
출력: [6,8,10,12,14]

그렇기 때문에 map은 다양한 형태로 쓰일 수 있죠!

7. Filter 함수
필터 함수는 map과 비슷한 구조를 가지고 있지만, 용도가 다릅니다!
필터 함수는 주어진 함수와 요소를 필터링하여 조건에 만족하는 요소만 남기도록 합니다!
그렇기에 구조도 매우 단순합니다!
filter(function, iterable....) 
function -> 여기서 적용할 요소에 대해 True 또는 False을 반환하는 함수가 필요합니다. True를 반환하는 요소만 필러팅 됩니다.
iterable -> 설명에 적힌 리스트나 튜플등을 말합니다!
그리고 람다 함수와 자주 사용됩니다!
a = [1,2,3,4,5] 
result = list(filter(lambda x: x < 4, a)) ->람다 함수를 사용해 for문과 비슷하게 조건과 반환값을 받습니다!
print(result)
출력: [1,2,3]

보시는 바와 같이, 람다 함수를 써서, a리스트에서 x가 4보다 큰 값들만 x에 담아 반환값으로 받습니다. 
참고로 str타입도 필터가 가능합니다.
a = ['you', 'love','the man','happy']
result = filter(lambda x: len(x) > 4, a) 
print(list(result))
출력:['the man',' 'happy']
이번에는 a 리스트에 여러가지 단어들을 넣었고, lambda 함수를 사용해 단어의 길이가 4보다 큰 단어들만 나오도록 필터를 했습니다! 이렇듯, 필터는 특정 조건하에만 필터를 해주는 함수입니다.

 

8. Reduce 함수
Reduce함수는 이해만 하면 쉬운 함수!
Reduce함수는 단순하게 받은 요소를 전부다 더해준다고 생각하면 편해요!
그래서 구조는 단순함에서 +1이 됬다고 생각합시다~
reduce(function, iterable, [initializer])
function -> 두 개의 인자를 받아서 결합하는 함수입니다!
iterable -> 설명에 적힌 리스트나 튜플등을 말합니다!
initializer -> 어렵게 생겼지만 사실은, 초기값을 설정해주는 녀석입니다!(없어도 됩니다. 없으면 리스트나 튜플의 첫번째 값이 초기값으로 자동 설정됩니다).
예:
from functools import reduce -> reduce함수를 쓰기 위해서는 from functools에서 가져오셔야 합니다!

def add(x , y):
    return x + y
a = ['I ', 'love ','you']
result = reduce(lambda x, y : x + y, a)
print(result)
출력: I love you
람다 함수와, 리스트의 형식은 전부 다 비슷하지만, reduce로 인해 사용되는 방식은 전혀 다릅니다.
처음 I와 love가 각각 x와 y에 할당되며, I 와 love가 더해지고, 그런다음 다시 I love와 you가 x와 y에 할당되면서, I love you가 만들어지는 겁니다!

I love you


9. 함수의 기본 인수값 (Default Argument)

 

이건 강의에서 배운 내용입니다!
예:
def add(x = 1, y = 2):
    return x + y
print(add())
출력: 3
이렇게 x = 1, y = 2을 매개변수로 설정하고, 나중에 함수를 호출할 때, 인자값으로 아무것도 안 넣으면 자동으로 기본값이 되는것을 말합니다~

 


2. 예외 처리

예외 처리는 간단하게 초급, 중급, 상급으로 하겠습니다!
예외 처리는 프로그램이 실행되는 동안 발생할 수 있는 에러를 대처하는 방법입니다. 
예외 처리를 잘 사용하면 문제가 생기는 것을 방지하고, 
문제가 생길 것 같으면 미리 정의하여 처리할 수 있습니다

 

초급
간단하게 try, except을 사용합니다.
try -> 에러가 발생할 수 있는 부분
except -> 대처할 방법
자, 간단하게 예시를 만들어 봅시다.
try:

    name = int(input('나이를 알려주세요: '))
    print(f'당신의 나이는: {name}살입니다.')
except ValueError:
    print("wrong index, please try again")
출력:

나이를 알려주세요: 몰라
wrong index, please try again
자, 위에 코드를 보면 int로 input이 가려져 있습니다. 즉, input함수로 받은 값을 무조건 int형이여야 합니다. 하지만 str타입으로 처리를 하게되면, 에러가 뜨게됩니다.
그렇기에 만약에 저렇게 에러가 뜨게되면, except 코드가 실행되며, except 코드에 안에 있는 wrong index를 띄우는 것입니다.
즉, try에 있는 코드가 실행되지 못하면, except 코드가 실행되게 되는 것입니다.

하지만 저 위에 있는 ValueError는 뭔지 궁금해 하신다면, 이것은 바로 에러의 한 종류입니다. except ValueError는 ValueError에 맞는 에러가 나타나면 실행하라는 의미이기도 합니다.
그래서 제가 어? 그럼 다른 에러들도 있는데 하나하나 어떻게 알아야 할까요?

이렇게 해볼 수 있습니다:
try:
    number = int("abc")  # 숫자가 아닌 문자열을 숫자로 바꾸려 해서 에러 발생
except Exception as e:
    print(f"에러가 발생했습니다: {e.__class__.__name__}")
자, 여기서 우리가 알아야 할 것은
1. Exception
2. __class__
3. __name__
이렇게 3가지가 있습니다.
첫번째로 Exception입니다. 아까 말했던 ValueError 기억하시나요? 이것은 에러에 한 종류이라고 하면, Exception은 모든 에러들을 담아놓은 거대한 클래스 상자라고 생각하시면 됩니다. 즉, Exception을 쓰면 모든 에러를 뜻한다고 해도 무방합니다.

두번째로 __class__입니다. 만약에 e(에러).__class__는 어떤 에러의 종류인지 찾아주는 역할을 합니다. 즉, e(as e)는 Exception을 쓰고 있기에 모든 종류의 에러를 나타낸다고 했지만, 정확이 어떤 에러인지 알 수 없죠. 그렇기에 .__class__를 사용하여 어떤 에러인지 찾아주는 겁니다. 

세번째로 __name__입니다. 이제 __class__로 어떤 에러인지 알았으면 이제 그것을 텍스트로 나타내 사용자에게 보여져야겠지요? 맞습니다. e.__class__는 에러를 찾아주지만 그 클래스 그대로를(에러의 형태만 가지고 있다고 생각해 주세요) 가지고 있을 뿐, 문자로 나타내야 합니다. 그것을 바로 __name__이 하는 일입니다. 요약하자면,
e.__class__ -> 앗! 이 에러는 ValueError야!
__name__ -> 알았어! 이제 이것을 내가 ValueError라고 띄울게!
이제 이해가 가시나요?

중급
이제 try와 except을 알았으니, elsefinally를 써볼 시간입니다.
else -> 만약 에러가 나지 않았을 때 실행할 문을 적는 곳입니다.
finally -> 에러가 나든 안 나든 마지막으로 무조건 이 부분이 실행되는 부분입니다.

이해가 잘 안 가시나요?
else는 에러가 나지 않을 때 나타내는 문을 적는곳이지만, else에 적을 코드를 try에 적어도 상관 없습니다. 어차피 에러가 안 나면 실행 될 문이니까 말이죠. 즉, 
try:
    name = int(input('나이를 알려주세요: '))
    print(f'당신의 나이는: {name}살입니다.')

try:
    name = int(input('나이를 알려주세요: '))

else:
    print(f'당신의 나이는: {name}살입니다.')
는 똑같습니다.

finally는 어떻게 하든 마지막에 "무조건" 실행되는 문입니다.
예:
try:
    number = int(input("숫자를 입력하세요: "))
except ValueError:
    print("숫자만 입력해 주세요!!")
else:
    print(f"입력한 숫자는 {number}입니다.")
finally:
    print("감사합니다.")
에서 보시다시피, finally는 마지막에 에러가 나든 말든 '감사합니다'를 띄움니다.
그러면 궁금증이 생길 수 있는데, '그럼 finally는 어떤 때에 쓰나요?'


예를 들어보겠습니다. 은행에 가서 키오스크에 잘못된 메뉴에 들어가 처음부터 다시 시작해도 끝에는 '감사합니다'를 띄우죠? 그런 상황과 비슷하다고 생각하시면 됩니다!

 

상급
자, 이제 상급입니다. 여기서에서는 raise라는 것을 한번 써보도록 하겠습니다.
raise -> '의도적'으로 에러를 발생시키는 코드
왜 의도적으로 발생을 시키냐구여?
예시를 한번 봅시다.
class Error_Exception(Exception):
    pass
def if_not(number):
    if number < 0:
        raise Error_Exception('제대로 된 값을 넣어주세요')
    return number
try:
    age = int(input('나이를 입력해 주세요'))
except Error_Exception as E:
    print(f'Error:{E}')
except ValueError:
    print('제대로 된 값을 넣어주세요')
else:
    print(f'입력한 숫자는 {if_not(age)}입니다.')

어우...보기만해도 어질 어질....
자 하나하나 살펴봅시다.
class Error_Exception(Exception):
    pass
이건 뭘까요? 자 제가 아까 Exception은 모든 에러를 담아놓는 클래스 상자라고 했죠? 거기에 힌트가 있습니다. 바로 Exception 클래스를 제가 만든 Error_Exception 클래스에 '상.속'을 시키는 겁니다
그러면 Error_Exception에 들어가서, Exception처럼 쓸 수 있는겁니다.

그렇다면 저 pass는 뭘까요? 그냥 간단히 답해서 말 그대로 패스시킨다 그런 겁니다. 그냥 아무것도 없이 대기 상태라고 생각하시면 편합니다. 클래스를 빈속으로 놨둔다고 생각하세요!(그냥 아무 기능 없어요...)

자, 그런다음 def if_not(number) 함수를 만들고 만약에 인자로 받은 값이 음수라면 바로 'raise'를 사용해서 의도적으로 에러를 발생시키는 겁니다. 이제 왜 'raise'가 필요한지 알겠죠? 그냥 Exception을 쓰지 못하니, 따로 클래스에 상속시켜, '제대로 된 값을 넣어주세요'를 넣고, 그것을 그냥 pass해서 내보낸다.....이제 감이 오시나요? 


그렇게 밑에 코드를 적고, 만약 -1을 적으면 '제대로 된 값을 넣어주세요'라고 에러가 뜹니다.
이렇게 예외 처리가 끝이 났습니다!

정말 쉽죠.....?

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

 

감사합니다.