저번 시간에는 객체를 만드는 법을 배우며 클래스와 인스턴스, 인스턴스 변수, 인스턴스 메소드 등의 개념을 알아보았습니다.

이번 시간에는 또 다른 변수인 클래스 변수와 데코레이터라는 개념, 그리고 메소드의 세 가지 유형 중 클래스 메소드와 정적 메소드에 대해 함께 알아보겠습니다.

🟨 클래스 변수

인스턴스 변수특정 인스턴스만의 속성이라고 배웠습니다. 그런데 변수 중에는 여러 인스턴스들이 공유하는 속성도 있습니다.

지난 시간에 사용했던 예시인 User 클래스를 다시 예로 들어보겠습니다. User 클래스 안에 사용자들의 수를 세는 count라는 속성이 있다고 합시다. 이 count라는 속성은 여러 인스턴스 즉, user1과 user2, user3 등이 함께 공유합니다. 그리고 어떤 User 인스턴스라도 동일한 count 값을 가져야 합니다.

Python에서는 이러한 속성을 클래스 변수라고 합니다. 클래스 변수는 같은 클래스의 인스턴스들이 서로 공유하는 값입니다.

코드로 한 번 구현해볼까요? User 인스턴스의 총 개수를 나타내는 count를 만들어보겠습니다.

class User:
    count = 0

위와 같이 클래스 변수는 클래스 이름 아래 정의하면 됩니다.

User.count = 1
print(User.count)
1

인스턴스 변수가 특정 인스턴스 이름 뒤에 변수명을 적었던 것과 유사하게 클래스 변수는 클래스 이름 뒤에 변수명을 적어주면 됩니다. 사용법일반 변수와 거의 유사하죠. 클래스 이름이 앞에 붙는다는 것만 빼구요.

클래스 변수 count가 User 인스턴스 개수를 정확히 나타내도록 하려면 User 인스턴스가 생성될 때마다 count의 값을 1씩 늘리면 됩니다. 이 문장이 익숙하신가요?

지난 시간에 우리는 User 인스턴스가 생성될 때마다 자동으로 호출되는 이닛 메소드를 배웠습니다. 이 말은 곧, 이닛 메소드에서 count 값을 늘려주면 반복문을 따로 쓸 필요 없이 새로운 인스턴스가 생성될 때마다 count 수를 늘려줄 수 있습니다.

def __init__(self, name, id, pwd):
    self.name = name
    self.id = id
    self.pwd = pwd
    
    User.count += 1

이제 클래스 변수 count가 정상적으로 동작하는지 보기 위해 인스턴스를 세 개 생성해보고 프로그램을 실행해보겠습니다.

user1 = User("타키탸키", "tataki26", "123456")
user2 = User("파이리", "firedragon12", "654321")
user3 = User("차정원", "carthegarden", "741852")

print(User.count)
3

잘 실행되네요 :)

🟨 클래스 변수 읽기/설정하기

만약 클래스 변수명 앞에 인스턴스 이름을 쓰면 어떻게 될까요?

print(user1.count)
print(user2.count)
print(user3.count)
3
3
3

정상적으로 출력이 됩니다. 그럼 인스턴스 이름을 가지고 클래스 변수의 값설정하는 것도 가능할까요?

user1.count = 5

이렇게 값을 설정한 후 위의 print함수를 동일하게 호출하면 이번에는 user1 인스턴스의 값만 5로 바뀌고 나머지 인스턴스의 값들은 3으로 출력됩니다.

자세히 살펴보면 user1.count인스턴스 변수를 설정할 때와 같은 문법입니다. 따라서, 이런 식으로 적으면 user1 인스턴스에 count라는 인스턴스 변수가 생기고 그 값이 5로 설정됩니다. 다시 말해, 이 코드는 클래스 변수의 값을 설정하는 것이 아닌 인스턴스 변수의 값을 설정하는 것입니다.

이처럼 같은 이름의 클래스 변수와 인스턴스 변수가 있으면 인스턴스 변수가 읽어집니다. 이럴 경우 변수명을 헷갈릴 수 있기 때문에 클래스 변수에 값을 설정할 때는 클래스 이름으로만 설정해야 합니다.

다시 코드를 고쳐서 User.count=5로 실행하면 이번에는 모든 인스턴스들의 count 값이 5로 출력됩니다.

정리하자면, 클래스 변수의 값을 읽을 때클래스 이름.클래스 변수 이름 혹은 인스턴스 이름.클래스 변수 이름으로 코드를 작성하면 됩니다. 그러나, 클래스 변수의 값을 설정할 때꼭 클래스 이름을 통해서만 작성해야 합니다.

🟨 데코레이터

Python에는 데코레이터(decorator)라는 개념이 있습니다. 데코레이터는 꾸민다라는 뜻을 가지고 있는데요. Python에서는 어떤 함수를 꾸며서 새로운 함수를 만들 수 있습니다. 이때 사용하는 것이 바로 데코레이터입니다.

def print_hi():
    print('안녕!')

print_hi인사 메세지를 출력하는 아주 간단한 함수입니다. 이제 이 함수를 꾸며서 새로운 함수를 만들고 싶은데요.

def add_print_to(original):
    def wrapper():
        print('함수 시작')
        original()
        print('함수 끝')
    return wrapper

함수를 꾸며주는 특이한 함수 하나를 만들었습니다. 이 함수는 파라미터로 또 다른 함수를 받습니다. 함수를 파라미터로 받는 경우는 처음 본 것 같습니다. 이 함수가 특이한 점은 또 하나 있는데요. 함수 안에 또 함수를 정의한 것이죠. 그리고 반환값이 바로 새롭게 정의한 함수입니다.

정리하자면 add_print_to 함수는 파라미터로 다른 함수를 받고 또 다른 함수를 리턴합니다. 예시를 함께 볼까요?

add_print_to(print_hi)

위 코드를 보면 add_print_to의 파라미터print_hi 함수를 받았습니다. 그럼 print_hi 함수가 original로 들어가면서 두 함수가 동일해지겠죠?

다음으로 wrapper 함수에 들어가서 첫번째 print 함수를 실행한 후, original 즉, print_hi 함수를 호출합니다. 그럼 함수 정의에 따라 인사 메세지를 출력하겠죠? 다음으로 또 다른 print 함수를 실행합니다.

original 함수를 통해 print_hi 함수를 호출한 후, 그 앞뒤로 부가 기능(print함수 두 개)을 추가했습니다. 결과적으로 print_hi 함수를 데코레이팅 한 것이죠.

add_print_to 함수의 역할다른 함수를 꾸며주는 것이기 때문에 이 함수는 데코레이터 함수라고 할 수 있습니다.

그런데 이 코드를 실행해보면 아무 것도 실행되지 않습니다. add_print_to 함수의 반환값은 wrapper 즉, 함수입니다. 함수를 실행하는 것이 아니라 리턴만 하는 것이죠. 리턴된 wrapper를 실행하려면 코드 뒤에 괄호를 붙여줘야 합니다. 함수를 호출하는 문법이 함수명()이기 때문입니다.

add_print_to(print_hi)()
함수 시작
안녕!
함수 끝

위 코드를 좀 더 깔끔하게 정리해보겠습니다.

print_hi = add_print_to(print_hi)
print_hi()

출력 내용은 동일합니다.

사실 두 코드 중 위 코드를 생략하고도 print_hi 함수를 꾸며줄 수 있는데요. 우선, 함수 정의 순서를 바꿔야 합니다. add_print_to 함수를 먼저 정의하고 그 다음에 print_hi 함수를 정의하는 것이죠.

그리고 print_hi 함수 위@add_print_to추가해줍니다. 이렇게 하면 print_hi 함수를 add_print_to 함수로 꾸며달라는 뜻이 됩니다.

이제 print_hi를 호출하면 원래의 print_hi가 아닌 꾸며진 모습으로 출력됩니다. 따라서, 변수를 저장하는 윗 문장이 필요 없게 되는 것이죠.

그렇다면 데코레이터는 어떨 때 사용하는 걸까요? 만약 print_hi와 같은 함수가 세 개 있고 각 함수마다 앞뒤로 부가 기능을 넣어야 한다면 어떨까요? 꽤 복잡해지겠죠? 중복이 많아 코드가 지저분해지기도 합니다. 이럴때 데코레이터를 쓰는 것이죠.

중복되는 부분을 모두 데코레이터 함수에 넣고 @를 붙여 데코레이터 함수 명을 원래 함수 위에 적어주면 코드가 아주 깔끔히 정리됩니다. 이후에도 같은 기능을 가진 함수를 생성하고 싶으면 데코레이터 함수를 쓰면 됩니다.

객체 지향 프로그래밍에서는 이 데코레이터 함수가 정말 많이 쓰입니다. 앞으로 함수 위에 @가 보이면 데코레이터 함수를 사용해서 기존의 함수에 새로운 기능을 추가했다고 보면 됩니다.

🟨 클래스 메소드

인스턴스 변수의 값을 읽거나 설정하는 메소드는 인스턴스 메소드입니다. 그럼 클래스 변수의 값을 읽거나 설정하는 메소드는 무엇일까요? 바로 클래스 메소드입니다.

앞서 클래스 변수 count를 정의했던 것 기억하시나요? 이제 이 count를 출력하는 메소드를 만들어봅시다. 우선, num_of_users라는 클래스 메소드를 하나 작성하겠습니다.

@classmethod
def num_of_users(cls):

클래스 메소드는 방금 배운 데코레이터 함수 중 하나이기 때문에 함수 헤더 위@classmethod와 같이 작성합니다.

클래스 메소드 작성 시에 꼭 알아야할 규칙이 있습니다. 인스턴스 메소드의 첫 번째 파라미터에는 인스턴스 자신이 들어갔죠? 이와 유사하게 클래스 메소드의 첫 번째 파라미터에도 클래스가 자동으로 전달됩니다. 이때, 클래스는 cls라는 이름으로 전달됩니다.

cls 말고 다른 단어로 작성해도 동작에는 문제 없지만 인스턴스 메소드의 self와 같이 Python 세계의 약속이므로 반드시 cls를 적어주도록 합시다.

이제 count를 num_of_users에 가져와야 하는데요. 어떻게 할 수 있을까요? cls는 User 클래스를 나타냅니다. 따라서, User.countcls.count동일합니다.

@classmethod
def num_of_users(cls):
    print(f"총 유저 수는: {cls.count}입니다.")

이렇게 하면 총 유저 수 count를 출력하는 클래스 메소드를 완성할 수 있습니다.

클래스 메소드는 클래스로 호출할 수도 있고 인스턴스로 호출할 수도 있습니다. 두 경우 모두 cls로 클래스가 자동 전달됩니다.

인스턴스는 앞서 사용했던 user1, user2, user3를 그대로 사용하겠습니다.

User.num_of_users()
user1.num_of_users()
총 유저 수는: 3입니다.
총 유저 수는: 3입니다.

그럼 인스턴스 메소드와 클래스 메소드의 차이는 뭘까요? 먼저, User 클래스의 say_hi 인스턴스를 봅시다.

User.say_hi(user1)
user1.say_hi()

인스턴스 자신이 첫 번째 파라미터로 자동 전달되기 때문에 변수명 앞에 인스턴스 이름을 적으면 파라미터인스턴스를 적지 않아도 되었습니다.

User.num_of_users()
user1.num_of_users()

그러나 클래스 메소드의 경우 두 가지 방법 모두 첫 번째 파라미터로 클래스가 자동 전달되기 때문에 파라미터 부분을 비워둬야 합니다. 이 공간을 비워두지 않으면 파라미터가 중복으로 들어가서 에러 메세지가 뜹니다.

이때, 클래스가 자동 전달되는 이유클래스 메소드 데코레이터로 num_of_users를 클래스 메소드로 만들었기 때문이라는 것을 잊지 마세요.

🟨 인스턴스 메소드 vs. 클래스 메소드

사실 num_of_users를 인스턴스 메소드로도 작성할 수 있습니다.

def num_of_users(self):
    print(f"총 유저 수는: {User.count}입니다.")

인스턴스 메소드에서도 클래스 변수 countUser.count라고 써서 가져올 수 있기 때문이죠.

User.num_of_users(user1)
user1.num_of_users()

결과는 위와 동일합니다.

그럼 왜 처음부터 인스턴스 메소드로 만들지 않고 클래스 메소드로 만든 걸까요? 그 이유인스턴스 변수를 사용하지 않았기 때문입니다. 다시 말해, num_of_users는 인스턴스 변수의 값을 읽거나 설정하지 않습니다.

위 코드만 봐도 알 수 있는데요. 파라미터에 self를 적었음에도 불구하고 함수 내에는 self를 사용한 적이 없습니다. 인스턴스 변수를 사용할 필요가 없기 때문이죠.

사용하고 있는 것은 User.count 즉, 클래스 변수뿐입니다. 이처럼 인스턴스 변수 말고 클래스 변수만 사용하는 경우라면 동작에 문제가 없더라도 클래스 메소드로 작성해야 합니다.

그럼 만약 클래스 변수와 인스턴스 변수 둘 모두를 사용하는 경우에는 어떤 메소드를 써야 할까요? 바로 인스턴스 메소드를 사용해야 합니다.

인스턴스 메소드는 인스턴스 변수와 클래스 변수를 모두 사용할 수 있기 때문이죠. 인스턴스 변수는 self를 통해, 클래스 변수는 클래스 이름에 점을 붙여 가져오면 됩니다.

그러나 클래스 메소드는 인스턴스 변수를 사용할 수 없습니다. cls를 통해 클래스 변수를 가져올 수는 있지만 인스턴스 변수를 가져올 방법은 없기 때문입니다.

때로는 인스턴스 없이 필요한 정보가 있는 경우가 생깁니다. 이 경우에는 무조건 클래스 메소드를 사용해야 합니다. 우리가 앞서 사용했던 User.count는 인스턴스가 하나도 없더라도 필요한 정보가 있습니다. User 인스턴스 개수를 0개라고 출력할 수 있으니까요.

이 말은 곧, User.count를 사용하는 클래스 메소드 num_of_users 또한 User 인스턴스가 하나도 없더라도 필요하다는 뜻입니다.

생성했던 인스턴스 user1, user2, user3를 모두 지우고 클래스 메소드를 실행해보세요. 그럼 다음과 같은 결과가 나옵니다.

총 유저 수는: 0입니다.

이처럼 User 인스턴스가 하나도 없어도 사용할 수 있는 가능성이 있으면 클래스 메소드로 만들어야 합니다.

🟨 정적 메소드

메소드에는 총 세 가지가 있다고 했는데요. 인스턴스 메소드와 클래스 메소드를 배웠으니 이제 나머지 메소드인 정적 메소드(Static Method)에 대해 알아봅시다.

정적 메소드는 인스턴스 변수, 클래스 변수를 전혀 다루지 않는 메소드인데요.

만약 아이디에는 반드시 -가 들어가야 한다고 가정해봅시다. 그럼 다음과 같은 메소드를 만들 수 있는데요.

@staticmethod
def is_valid_id(id):
    return "-" in id

이 함수가 바로 정적 메소드의 예입니다. 정적 메소드는 메소드 정의 위에 @staticmethod 데코레이터를 표시해야 합니다. is_valid_id 메소드는 파라미터 id로 받은 문자열에 -가 있는지 확인합니다.

정적 메소드는 인스턴스 메소드의 self클래스 메소드의 cls과 같은 자동으로 전달되는 파라미터가 없습니다.

그리고 아래 코드와 같이 인스턴스, 클래스 두 가지 모두를 통해 사용이 가능합니다.

print(User.is_valid_id("takityaki"))
print(User.is_valid_id("taki-tyaki"))

print(user1.is_valid_id("takityaki"))
print(user1.is_valid_id("taki-tyaki"))
False
True
False
True

앞서 인스턴스 메소드인 던더 str 메소드는 인스턴스 변수인 self.name과 self.id를 사용하고 클래스 메소드 num_of_users는 클래스 변수인 cls.count를 사용했습니다.

그렇다면 정적 메소드는 언제 사용할까요? is_valid_id 메소드를 보면 아무 변수도 사용하고 있지 않네요. 이 말은 곧, 인스턴스 변수나 클래스 변수 중 아무것도 사용하지 않는 메소드라면 정적 메소드로 만들면 된다는 말입니다.

다시 말해, 어떤 속성을 다루지 않고 단지 행동(기능)의 역할만 하는 메소드를 정의할 때 사용하는 것이 정적 메소드입니다.


이번 시간에는 클래스 변수와 데코레이터, 클래스 메소드와 정적 메소드에 대해 알아봤습니다.

지난 시간과 같이 많은 개념들을 한꺼번에 배우느라 혼란스러울 것 같은데요. 차근차근 복습을 통해 개념을 익히도록 합시다.

다음 시간에는 Python으로 객체 지향 프로그래밍을 할 때 유의해야 할 점에 대해 알아보겠습니다.

* 이 자료는 CODEIT의 '객체 지향 프로그래밍' 강의를 기반으로 작성되었습니다.
profile
There's Only One Thing To Do: Learn All We Can

0개의 댓글