본문 바로가기
공부/Deep Learning : 네부캠 AI Tech

부스트캠프 1주차 #2 - 파이썬 문법, 클래스, 모듈과 패키지

by 도리언 옐로우 2023. 11. 8.

1. 서

부스트캠프 2일차의 회고이다. 생각보다는 주어진 강의의 양이 많아서 매일 정리하려면 좀더 바쁘게 살아야 할 것 같다. 어제 첫번째 회고글을 쓰면서, 회고를 공부계의 명언으로 시작하는것이 의외로 마음에 들었다. 앞으로도 그때그때 생각나는 공부명언을 써보려하는데, 오늘의 명언은 "내일부터 열심히해서는 안된다. 오늘만 열심히 해야한다."이다.

카이지 - 요즘은 질질 끌고있지만 명대사가 많다

 

 

2. 공부내용 리뷰 - 파이썬 문법정리, 클래스, 모듈과 패키지

(1) 파이썬 문법정리 - list comprehension, lambda, iterator & generator, asterisk, closure와 decorator

강의에서 파이썬의 문법 관련 많은 내용을 다루었는데, 그 중에서 많이 쓰일 것 같거나 내가 아직 미숙한 부분을 선정해서 정리해본다.

 

1) List comprehension

 

파이썬에서 리스트를 간결히 생성하기 위한 문법이다. 통상적으로는 반복문과 조건문을 사용하여 리스트를 생성하게 되는데, 이러한 과정을 한 줄로 표현할 수 있게 된다. 이른바 pyhonic code의 하나로, 가독성이 좋아 간결한 코드 작성을 도와준다. 또한 이러한 pythonic code는 효율이 좋다고 하는데, 리스트 컴프리핸션 또한 for + append 보다 속도가 빠르다고 한다.

[표현식 for 항목 in iterable if 조건식] 으로 표현하게 되는데, 다음과 같은 예시를 보면 이해가 더 편하다.

pairs = [(x, y) for x in range(1, 4) for y in range(1, 4) if x != y]
print(pairs)  # 출력: [(1, 2), (1, 3), (2, 1), (2, 3), (3, 1), (3, 2)]

 

 

2) Lambda 함수

 

람다함수는 익명함수를 생성하는 방법인데, 익명함수는 한번 사용하고 버리는 간단한 함수 작성시 유용하게 사용된다. 람다함수의 일반적인 구조는 lambda 매개변수 : 표현식 이고, 주로 함수를 인자로 받는 함수(map()함수 등)나 리스트 컴프리핸션과 같은 문맥에서 사용된다. 다만 복잡한 로직이나 여러줄의 코드를 담는데는 적합하지 않다.

# 예시1
add = lambda x, y: x + y
result = add(3, 5)
print(result)  # 출력: 8

# 예시2 - map() 함수 : map(함수, iterable)
numbers = [1, 2, 3, 4, 5]
squared = list(map(lambda x: x**2, numbers))
print(squared)  # 출력: [1, 4, 9, 16, 25]

# 예시3 - 리스트 컴프리핸션
numbers = [1, 2, 3, 4, 5]
even_numbers = [x for x in numbers if (lambda num: num % 2 == 0)(x)]
print(even_numbers)  # 출력: [2, 4]

# 예시4 - reduce() 함수 : functools.reduce(함수, iterable, 초기값(생략가능))
from functools import reduce
numbers = [1, 2, 3, 4, 5]
sum_result = reduce(lambda x, y: x + y, numbers)
print(sum_result)  # 출력: 15

 

 

3) Iterator & Generator

 

이터레이터는 next함수 호출시 다음 값을 리턴(더는 리턴할 값이 없다면 StopIteration예외가 발생한다)하는 객체이다. 리스트와 같이 iterable한 요소라 하더라도 이터레이터인 것은 아니고, iter함수를 통해 이터레이터로 만들어 줄 수 있다.

 

이터레이터는 한번에 모든 값을 로드하지 않고 필요한 만큼 값을 생성하기 때문에, 큰 데이터 처리시 메모리를 효율적으로 관리할 수 있다. 이터레이터를 생성하는 방법으로 이터레이터 클래스를 구현하는 방법, 제너레이터 함수를 이용하는 방법, iter()함수를 이용하는 방법이 있다.

# 제너레이터 함수
def count_up_to(n):
    i = 0
    while i <= n:
        yield i # 제너레이터 함수는 return 대신 yield 키워드를 사용한다 
        i += 1

# 제너레이터 객체 생성
generator = count_up_to(5)

# 값 출력
print(next(generator))  # 출력: 0
print(next(generator))  # 출력: 1
print(next(generator))  # 출력: 2
print(next(generator))  # 출력: 3
print(next(generator))  # 출력: 4
print(next(generator))  # 출력: 5

 

제너레이터를 생성하는 방법으로 리스트 컴프리핸션과 유사한(다만 대괄호 대신 그냥 괄호를 사용한다) 제너레이터 컴프리핸션이 있다. 제너레이터 컴프리핸션은 필요한 값들을 지연평가(lazy evaluation)하여 필요한 만큼 생성하므로 메모리 관리면에서 효율적이다.

generator = (x for x in range(5) if x % 2 == 0)

for value in generator:
    print(value)  # 출력 결과: 0, 2, 4​

 

 

4) Asterisk(*)

 

파이썬에서 *은 기본적으로 곱셈 연산자로 사용되지만, 사용방식에 따라 이외의 다양한 기능을 수행할 수 있다. 구체적으로 가변 인자,  리스트/튜플 언패킹,  키워드 가변 인자 등 이 있는데, 예시로 살펴보자.

def my_function(*args):
    for arg in args:
        print(arg)

my_function(1, 2, 3)  # 출력: 1, 2, 3

numbers = [4, 5, 6]
my_function(*numbers)  # 출력: 4, 5, 6​

 

가변인자를 사용할때의 예시이다. 가변인자는 함수 정의시 인자(parameter)의 개수가 가변적일 수 있는 경우를 말한다. 따라서 parameter의 개수를 미리 알 수 없는 경우 등에 활용할 수 있다. *을 파라미터 이름 앞에 붙이면 가변 인자로 동작하게 된다. 가변인자를 사용하는 함수는 호출 시 전달된 인자들을 iterable 한 객체인 튜플로 받아들이고, 함수 내에서는 순회하거나 인덱스를 통해 개별 요소에 접근할 수 있다.

바로 아래에는 리스트 언패킹의 예시를 볼 수 있다. 리스트인 numbers앞에 *을 붙여 언패킹해주어 my_function(4,5,6)과 동일하게 동작하게 되는데, 만약 *을 붙여주지 않았다면 4,5,6이 아닌 [4,5,6]이 출력될 것이다.

def my_function(**kwargs):
    for key, value in kwargs.items():
        print(key, value)

my_function(name='Alice', age=25)  # 출력: name Alice, age 25

info = {'name': 'Bob', 'age': 30}
my_function(**info)  # 출력: name Bob, age 30​

 

키워드 가변인자를 사용할 경우의 예시이다. 함수 정의시 매개변수 앞에 **을 붙여 딕셔너리 형태로 전달되는 인자를 받을 수 있게 한다. **info의 경우, 딕셔너리 info를 언패킹 해준 것이다. 딕셔너리의 경우 *을 단독으로 사용하여 언패킹시 구문 오류가 발생한다.

 

numbers = [1, 2, 3, 4, 5]
a, b, *rest = numbers
print(a, b)  # 출력: 1, 2
print(rest)  # 출력: [3, 4, 5]

a, *middle, c = numbers
print(a)      # 출력: 1
print(middle) # 출력: [2, 3, 4]
print(c)      # 출력: 5
 

언패킹을 확인 할 수 있는 예시이다. 만약 위 예시에서 *rest 대신 rest 썼다면 ValueError: too many values to unpack (expected 3)가 발생하게 된다.

 

 

5) Closure와 Decorator (feat. 일급객체)

 

파이썬에서는 함수도 일급 객체로 취급되어, 함수를 다른 객체와 마찬가지로 변수에 할당하거나, 다른 함수의 인자로 전달하거나, 함수의 반환값으로 사용할 수 있다. 내가 한동안 묘하게 궁금했던 부분은 함수명에 괄호를 붙여쓰는 경우와 그렇지 않은 경우가 언제인지 여부이다. 정리하면, 함수를 변수에 할당하거나 반환값으로 사용할 때에는 함수 이름 뒤에 괄호를 붙이지 않는다. 함수이름뒤에 괄호를 붙이는 것은 함수를 호출하기 위함이다.

 

클로저란 함수 내부에서 정의된 함수로서, 외부 함수의 변수를 참조할 때 생성된다. 클로저의 핵심적인 특징은 변수 유지인데, 외부함수의 실행이 끝난 이후에도 외부함수의 변수에 대한 참조를 유지하는 것을 말한다. 따라서 클로저라고 하기 위해서는 외부함수를 호출할 때 내부 함수가 호출되거나 반환되어야 한다.

 

일반적인 내부함수는 외부함수의 실행이 끝남과 동시에 해당 함수의 변수에 접근할 수 없으나, 클로저는 외부함수의 변수에 대한 참조를 유지하여 변수에 접근할 수 있게 한다.

def outer_function():
    x = 10

    def inner_function():
        print(x)
    
    return inner_function

closure = outer_function()
closure()  # 출력: 10 -> outer_function의 실행종료 이후에도 변수 x에 접근할 수 있다​

 

데코레이터는 파이썬에서 함수나 클래스를 수정하거나 감싸는 기능을 제공하는 특별한 문법으로, 기존의 함수나 클래스를 수정하지 않고도 추가적인 동작을 수행할 수 있게 한다. 데코레이터는 일반적으로 클로저 형태로 구현된다. 즉, 데코레이터 함수 내부에서 다른 함수를 정의하고, 이 함수가 외부 함수를 감싸는 형태로 클로저를 생성하게 된다.

def hello_decorator(func): # 데코레이터 역할을 수행하는 함수
    def wrapper(): # 데코레이터로 감싸진 함수의 실행 전후 추가적인 동작 수행
        print("함수 실행 전")
        func()
        print("함수 실행 후")
    return wrapper

@hello_decorator  # @를 이용하여 감싸려는 함수 위에 적용하여 사용
def hello():
    print("안녕하세요!")

hello()

위 예시에서 hello함수를 데코레이터가 감싸고 있는데, 이는 hello = hellp_decorator(hello)와 같은 의미로 볼 수 있다. 이 과정을 통해 hello변수는 수정된 동작을 갖는 새로운 함수가 되는 것이다. 참고로 이렇게 내부 함수 정의에서 임의의 인자를 파라미터로 받을 수 있도록 하기 위해 def inner(*args, **kwargs): 의 형태로 정의하기도 하는 것도 기억하자.

(2) 클래스

클래스(class)는 객체지향 프로그래밍(OOP, Object-Oriented Programming)의 핵심 개념중 하나이다. 클래스는 객체의 설계도 내지 탬플릿으로 사용된다. 직관적 이해를 위해 클래스는 붕어빵틀, 객체는 해당 틀로 찍어낸 붕어빵이라는 것이 흔히 드는 예시이다. 객체를 클래스와의 관계에서 인스턴스라 한다. 클래스는 중요해보이는 포스에 비해 저번에 점프투 파이썬에서 공부한 이후로 딱히 사용해본 적이 없어 거의 잊고 있었는데 이 기회에 복습하여 내 것으로 만들면 좋을것같다.

 

1) 클래스의 구성

 

클래스는 속성(attribute)와 메서드(method)로 구성된다. 속성이 클래스를 구성한다는 말은 속성이 클래스 내에서 정의된다는 것을 의미하는데, 이 속성은 해당 클래스의 각각의 객체가 가질 수 있는 데이터로 객체의 상태를 나타낸다. 클래스 내에서 속성은 __init__ 메서드를 통해 초기화되고 self를 통해 객체 자체에 속성을 할당하게 된다. 메서드는 클래스 안에서 구현한 함수를 말한다.

 
class Animal:  # Animal 클래스 정의
    def __init__(self, name):
        self.name = name
# __init__ = 생성자(constructor), 클래스의 초기화 메서드 : 객체가 생성될때 자동으로 호출되는 메서드
    def speak(self):
        print("동물이 소리를 내고 있습니다.")
# 객체호출시 호출한 객체 자신이 전달되기 때문에 메서드의 첫번째 매개변수 이름은 관례적으로 self를 사용(다른이름도 ok)
my_pet = Animal("Marco")

print(my_pet.name) # Marco - 속성(객체변수) 접근 : 다른 객체의 속성과는 독립적

my_pet.speak() # 동물이 소리를 내고 있습니다. - 메서드 호출 : 매개변수중 self는 생략해서 호출해야 한다.
Animal.speak(my_pet) # 위와 달리 클래스를 이용해서 메서드 호출 가능 : 이 경우 객체 my_pet을 self에 전달해야 한다

위 예시에서 객체 my_pet의 name Marco는 my_pet에 속하는 변수인 객체변수이다. 이러한 객체변수는 다른 객체들의 영향을 받지 않고 독립적으로 값을 유지한다.

 

예시에서는 정확히 확인할 수 없으나, 파이썬 클래스의 이름은 CamelCase의 방식으로 짓는것이 관행이다. 함수명을 snake_case의 방식으로 짓는것과 구별된다.

 

클래스안에 변수를 선언하여 생성하는 클래스변수는 객체변수와는 성격이 다른데, 클래스로 만든 모든 객체에서 공유하게 된다. 그러나 실무에서는 객체변수를 훨씬 사용을 많이한다고 점프투 파이썬에서 설명하고 있다. 즉, 그리 중요하지 않다.

 

2) 클래스의 상속

 

클래스의 상속이란 기존의 클래스를 기반으로 새로운 클래스를 생성하는 방법을 말한다. 상속은 코드의 재사용성과 확장성(ex 오버라이딩 - 부모클래스의 메서드를 재정의하여 변경할 수 있다) 면에서 장점을 갖는다. 위의 예시코드에서 이어서 상속의 예시를 살펴보자.

class Dog(Animal):
    def speak(self):
        print("멍멍!")

class Cat(Animal):
    def speak(self):
        print("야옹!")

dog = Dog("멍멍이")
dog.speak()  # 멍멍!

cat = Cat("야옹이")
cat.speak()  # 야옹!

dog객체와 car객체는 각각 speak 메서드 호출시 다른 결과를 출력하고 있다. 부모클래스인 Animal 클래스를 상속하면서 speak 메서드를 재정의하여 다른 소리를 내도록 변경한 것인데, 이를 메서드 오버라이딩 이라 한다.

 

3) 매직 메서드(Magic Method)

 

매직 메서드는 파이썬에서 특별한 기능을 가진 메서드로 __(이름)__ 로 표현되는 특징이 있다. 앞서 살핀 생성자 __init__도 매직 메서드에 해당하는데, 객체의 초기화를 위해 사용된다. 이외에도 예를들어 string 객체를 +연산으로 합칠 수 있다는 점이나, 함수 이름에 ()을 붙여 함수를 호출할 수 있다는 점이나, 객체에 .을 찍어 객체에 접근할 수 있게 되는 것 등은 매직 메서드에 의한 것이라고 한다.

 

(3) 모듈과 패키지

모듈은 다른 파이썬 프로그램에서 import하여 사용할 수 있도록 만든 파이썬 파일이라고 한다. 에디터로 만들어 왔던 .py파일들이 바로 모듈인 것이다. 다만 이런식으로 import하기 위해서는 원칙적으로 동일한 디렉토리에 있어야 한다.

import box
box.things() # 모듈 이름 뒤에 .연산자를 붙여 things 함수를 쓴다

from box import things
things() # 이런 식으로 함수를 직접 import한다면 모듈 이름을 붙일 필요가 없다

from box import * # box 모듈의 모든 함수를 불러와 사용한다​
 

모듈을 다룰때 해당 파일을 직접 실행할 때에만 실행하려는 코드가 있다면 다음과 같은 구문을 추가하면 된다.

def things():
    print("empty")

if __name__ == "__main__":                                             
    print("ok")

해당 파일을 직접 실행하는 경우에 __name__ 변수에 __main__값이 저장되기 때문에 조건식을 충족하면서 이하 식이 실행되는 원리이다. 이와 달리 import 되는 경우에는 모듈이름이 __name__ 변수에 저장되어 조건식이 거짓이 된다.

 

다른 디렉토리의 파일을 import하려면 파이참 기준으로 다음의 과정을 거쳐야 한다.

 
import sys
sys.path.append('/다른_디렉토리_경로')                                                      

# 다른 디렉토리의 파일 import
from 파일_이름 import 함수_이름

이외에도 상대경로를 사용하여 import할 수도 있다고 한다.

 

패키지는 관련된 모듈을 모아놓은 것으로 모듈을 구조적으로 관리하기 위해 사용된다. 패키지는 하나의 프로그램으로 보면 된다. 패키지를 생성하려면 관련된 모듈을 하위 디렉토리에 저장하고 해당 디렉토리에 __init__.py 파일을 생성해 패키지로 인식하도록 해야 한다. 다만 이는 3.3버전부터는 불필요하다고 한다. 이외에 별도로 정리할 만한 사항은 없어보이는데, 직접 부딪치면서 만들어보는 과정을 거치는 것이 좋을 듯 하다.

 

3. 느낀 점

일단 정리가 생각보다 훨씬 오래 걸리고 많은 노력이 들어간다는 점을 깨닫게 되었다. 그래도 차차 요령이 생기면서 나아지지 않을까 기대하고 있다. 오늘 정리한 분량이 좀 많았는데, 앞으로는 분량을 좀더 컴팩트하게 쓰려한다. 뭔가 자꾸 일기 느낌이 되는것 같기도 하는데 정리의 목적을 항상 기억해야 할 것 같다. 마지막으로 특히 클래스 같은 경우 많이 써보면서 익숙해지는 것이 필요해 보인다. 다행인지 모르겠는데 그 기회는 앞으로 매우 많을 것 같다.