들어가며
이전 글에서는 scope, 디버깅, 객체지향 프로그래밍, 그리고 PyPI에 대해 간단하게 살펴봤습니다. 그중에서 객체지향은 "클래스가 객체를 만드는 틀이다" 정도로 개념만 맛봤었는데요. 솔직히 그땐 "그래서 이걸 직접 어떻게 만드는 거지?" 하는 느낌이 남아 있었습니다.
그래서 이번 글에서는 직접 클래스를 만들어보면서 손에 익혀보려고 합니다. 클래스를 만들고, 생성자와 메소드를 추가하고, 거기에 더해 외부 라이브러리를 가져와 쓰는 방법(import)과, 공부하면서 새로 알게 된 문법(튜플 등)까지 정리해봤습니다.
저는 마크업과 프론트엔드 작업을 하다 보니 이번에도 자바스크립트와 비교하면서 보게 되는 부분이 많았는데요, 그런 부분들은 같이 적어두겠습니다.
1. 클래스 만들기
클래스를 만드는 건 생각보다 간단합니다. class 키워드 뒤에 이름을 붙여주면 끝이에요.
class Dog:
pass
pass는 "아직 안에 넣을 내용이 없으니 일단 비워둔다" 는 뜻입니다. 클래스 틀만 만들어두고 내용은 나중에 채우겠다는 거죠.
여기서 이름 짓는 규칙을 하나 짚고 가려고 합니다. 파이썬에서는 클래스 이름은 첫 글자를 대문자로 시작하는 파스칼 케이스(PascalCase) 로 짓습니다. (예: Dog, UserAccount) 반대로 변수나 함수 이름은 스네이크 케이스(snake_case), 즉 단어 사이를 밑줄로 잇는 방식을 자주 씁니다. (예: my_dog, user_name)
자바스크립트에서는 변수나 함수에 userName처럼 카멜 케이스를 즐겨 썼는데요, 파이썬에서는 카멜 케이스를 거의 쓰지 않는다는 점이 조금 달랐습니다. 클래스는 파스칼, 그 외는 스네이크. 이렇게 기억하시면 됩니다.
2. 생성자(__init__)와 self
이제 이 틀로 실제 강아지 객체를 만들려면, 생성자(constructor) 라는 것을 이해해야 합니다.
생성자는 클래스라는 청사진(blueprint)의 일부로, 객체가 만들어질 때 무슨 일이 일어나야 하는지를 정해두는 곳입니다. 보통은 객체가 가질 값(속성)의 시작 값을 여기서 지정해요. 이걸 어려운 말로 "객체 초기화(initialization)"라고도 합니다.
그런데 처음엔 생성자랑 __init__이 서로 다른 건가 싶어서 조금 헷갈렸습니다. 쉽게 정리하면 이렇습니다.
-
생성자(constructor) 는 "객체가 만들어질 때 자동으로 실행되는 함수" 라는 역할(개념)의 이름이고,
-
__init__은 파이썬에서 그 역할을 맡는 함수의 실제 이름입니다.
즉 둘은 다른 게 아니라, "생성자라는 역할을 파이썬에서는 __init__이라는 이름으로 쓴다" 고 이해하시면 됩니다. 사실 이건 언어마다 이름만 다른데요, 자바스크립트에서는 클래스 안에 constructor()라고 대놓고 적었잖아요? 파이썬은 그 자리를 __init__이 대신한다고 보시면 됩니다.
// 자바스크립트
class Dog {
constructor() {
// 객체가 만들어질 때 실행
}
}
# 파이썬
class Dog:
def __init__(self):
# 객체가 만들어질 때 실행
pass
이 __init__이 특별한 함수인 이유는, def로 만드는 건 똑같지만 이름 양옆에 밑줄이 두 개씩(__) 붙어 있기 때문입니다. 이건 파이썬 인터프리터(우리가 작성한 코드를 한 줄씩 읽고 실행해주는 프로그램)에게 "이건 특별한 기능을 가진 메소드야" 라고 알려주는 표시예요. (참고로 에디터에서 def __init__까지 치면 자동완성이 떠서 편하게 선택할 수 있습니다.)
self는 뭘까요?
그런데 __init__을 보면 항상 맨 앞에 self라는 게 들어가 있죠. 저도 이게 뭔가 싶었습니다.
self는 쉽게 말하면 "지금 만들어지고 있는 그 객체 자신" 을 가리키는 말입니다. 클래스는 어디까지나 틀 일 뿐이고 실제로 쓰는 건 그 틀로 찍어낸 객체잖아요. 그래서 self.name = name이라고 쓰면 "방금 만들어진 이 객체의 name 속성에 값을 넣어라" 가 됩니다. self.이 붙은 건 "이 객체에 딱 붙어 있는 값" 이라고 보시면 돼요.
자바스크립트를 해보셨다면 this와 똑같다고 생각하시면 편합니다. this.name = name 하던 걸 파이썬에서는 self.name = name이라고 쓰는 거예요. 이름만 this → self로 바뀐 셈입니다.
한 가지 다른 점은, 파이썬에서는 이 self를 메소드의 첫 번째 매개변수로 직접 적어줘야 한다는 거예요. 자바스크립트의 this는 따로 안 적어도 알아서 쓸 수 있었는데, 파이썬은 def __init__(self, ...)처럼 self를 꼭 맨 앞에 써준다는 점이 조금 낯설었습니다.
이제 실제로 만들어볼게요.
class Dog:
def __init__(self, name, age):
self.name = name # 밖에서 받은 name을 이 객체의 속성으로 저장
self.age = age # 밖에서 받은 age를 이 객체의 속성으로 저장
self.energy = 100 # 모든 강아지는 에너지 100에서 시작
my_dog = Dog("콩이", 3) # 객체를 만들면서 "콩이", 3을 넘겨줍니다
print(my_dog.name) # 콩이
print(my_dog.energy) # 100
여기서 기억해야 할 중요한 점(중요!!)은, 이 클래스로 새 객체를 만들 때마다 __init__이 자동으로 호출된다는 거예요. 그래서 Dog("콩이", 3)이라고 객체를 만드는 순간 __init__이 실행되면서 name, age, energy 속성이 채워집니다.
그리고 self.name = name 이 한 줄을 풀어보면,
-
오른쪽
name→ 밖에서 받아온 값 -
왼쪽
self.name→ 이 객체에 저장해 둘 자리
이렇게 "받아온 값을 객체 자신에게 붙여준다" 는 의미가 됩니다. 또 self.energy = 100처럼 밖에서 받지 않고 시작 값을 직접 정해두는 속성도 만들 수 있어요. 모든 강아지가 에너지 100부터 시작하는 것처럼요.
3. 메소드 추가하기
속성이 객체가 가지고 있는 값(데이터) 이라면, 메소드는 객체가 할 수 있는 동작 입니다. 메소드는 어렵게 생각할 것 없이 클래스 안에 들어있는 함수예요. 똑같이 def로 만들고, 첫 매개변수로 self를 적어준다는 점만 기억하면 됩니다.
class Dog:
def __init__(self, name, age):
self.name = name
self.age = age
self.energy = 100
def bark(self):
print(f"{self.name}: 멍멍!") # 속성을 읽어서 사용
def walk(self):
self.energy -= 20 # 산책하면 에너지(속성)가 줄어듭니다
print(f"{self.name}가 산책을 했어요. 남은 에너지: {self.energy}")
def is_tired(self):
return self.energy < 30 # 값을 돌려주는 메소드 (True/False)
my_dog = Dog("콩이", 3)
my_dog.bark() # 콩이: 멍멍!
my_dog.walk() # 콩이가 산책을 했어요. 남은 에너지: 80
print(my_dog.is_tired()) # False
메소드를 보면 몇 가지 패턴이 보이는데요,
-
bark()처럼 속성을 읽기만 할 수도 있고 (self.name), -
walk()처럼 속성 값을 바꿀 수도 있고 (self.energy -= 20), -
is_tired()처럼 값을 돌려줄(return) 수도 있습니다. (이return은 2편에서 봤던 그return이 맞습니다.)
여기서 한 가지 더, 메소드 안에서 다른 메소드를 부를 수도 있어요. 이때도 self.을 붙여서 "내 메소드를 호출해" 라고 알려줍니다.
def play(self):
self.bark() # 메소드 안에서 다른 메소드를 호출
self.energy -= 30
이렇게 속성(데이터)과 메소드(동작)가 하나의 클래스 안에 같이 묶여 있다는 것이 객체지향의 핵심인 것 같습니다.
4. 클래스로 여러 객체 만들기
클래스는 어디까지나 틀 이라서, 같은 클래스로 객체를 여러 개 찍어낼 수 있습니다. 그리고 그렇게 만든 객체들을 1편에서 봤던 리스트에 담아두면 다루기 편해요.
dogs = [
Dog("몽이", 3),
Dog("토리", 5),
Dog("콩이", 1),
]
for dog in dogs:
dog.bark()
# 몽이: 멍멍!
# 토리: 멍멍!
# 콩이: 멍멍!
리스트 안의 요소가 전부 같은 Dog 클래스의 객체라서, 반복문으로 하나씩 꺼내 똑같은 메소드를 호출할 수 있습니다.
이렇게 데이터와 기능을 클래스로 나눠두면 뭐가 좋을까요? 데이터(어떤 강아지가 있는지)와 기능(짖기, 산책하기)을 따로 관리할 수 있다는 점이 좋습니다. 강아지가 늘어나거나 바뀌어도 기능 코드(
bark,walk)는 손댈 필요가 없거든요. 프론트엔드로 비유하면, 컴포넌트(기능)와 props(데이터)를 분리해두는 것과 비슷하다고 느꼈습니다. 데이터만 갈아끼우면 같은 기능이 그대로 재사용되니까요.
5. 모듈을 가져오는 여러 가지 방법
2편에서 모듈과 PyPI를 잠깐 봤었는데요, 이번엔 모듈을 가져오는(import) 방법이 여러 가지라는 걸 알게 됐습니다. 1편에서 썼던 random 모듈로 비교해볼게요.
# 1) 모듈을 통째로 가져오기
import random
random.choice([1, 2, 3]) # random. 을 붙여서 호출 (어디서 온 기능인지 명확)
# 2) 모듈에서 특정 기능만 콕 집어 가져오기
from random import choice
choice([1, 2, 3]) # 바로 choice() 로 호출 (random. 안 붙임)
# 3) 모듈 안의 모든 것을 가져오기
from random import *
choice([1, 2, 3]) # 전부 바로 호출 가능
# 4) 별칭(alias)을 붙여서 가져오기
import random as r
r.choice([1, 2, 3]) # 짧은 이름 r 로 호출
핵심은 어떻게 가져오느냐에 따라 호출하는 방법이 달라진다는 거예요. import random으로 가져오면 random.choice()처럼 출처를 붙여야 하고, from random import choice로 가져오면 그냥 choice()로 쓸 수 있습니다.
3번 from random import *은 _"전부 다(_) 가져오기"* 인데, 편해 보이지만 어디서 온 기능인지 안 드러나고 이름이 겹칠 위험이 있어서 실무에서는 잘 쓰지 않는 방식이라고 합니다. 4번 별칭은 모듈 이름이 길거나 겹칠 때 짧게 줄여 쓰는 용도예요. (데이터 분석 쪽에서 import pandas as pd처럼 쓰는 게 대표적인 관례라고 합니다.)
표준 라이브러리 vs 외부 패키지
그런데 random처럼 파이썬에 기본으로 들어있는 표준 라이브러리는 그냥 import하면 바로 쓰이지만, 파이썬에 없는 외부 패키지는 먼저 설치를 해줘야 합니다. 설치 안 된 걸 그냥 import하면 이런 에러가 나요.
import colorgram
# ModuleNotFoundError: No module named 'colorgram'
이럴 땐 PyPI에서 패키지를 받아와야 합니다. 터미널에서 pip install colorgram.py로 설치하거나, PyCharm 같은 에디터에서는 에러 옆에 뜨는 "Install package" 버튼으로 바로 설치할 수도 있어요. (내부적으로는 똑같이 pip install이 실행됩니다.) 즉 외부 패키지는 "설치 → import → 사용" 순서라는 게 표준 라이브러리와의 차이입니다.
6. 외부 패키지 활용하기
마침 위에서 클래스와 메소드를 직접 만들어봤으니, 이번엔 반대로 외부 패키지의 클래스를 가져다 쓰는 경험을 해봤습니다. 예시로 쓴 건 이미지에서 색을 뽑아주는 colorgram이라는 패키지인데요, 개발하다 보면 "이미지에서 주요 색상을 추출" 해야 하는 경우가 실제로 종종 있어서 꽤 실용적이었습니다.
import colorgram
# 이미지에서 색을 최대 10개 추출합니다
colors = colorgram.extract("sample.jpg", 10)
rgb_list = []
for color in colors:
r = color.rgb.r # 객체의 속성을 점(.)으로 꺼냅니다
g = color.rgb.g
b = color.rgb.b
rgb_list.append((r, g, b)) # (r, g, b) 형태로 묶어서 리스트에 추가
print(rgb_list)
# [(34, 28, 22), (210, 200, 190), (158, 75, 49), ...]
여기서 color.rgb.r 부분을 보시면, 이 color도 결국 colorgram이 만들어서 돌려준 객체라서, 우리가 위에서 my_dog.name으로 속성을 꺼냈던 것과 똑같은 방식(.)으로 속성을 꺼내 쓰게 되어있습니다.
참고로 이렇게 추출한 결과는 한 번 print해서 나온 값을 복사해 코드에 직접 넣어두고 재사용하기도 합니다. 색 추출을 매번 다시 할 필요는 없으니까요.
7. 튜플(tuple)
위 colorgram 코드에서 (r, g, b)처럼 소괄호로 묶은 것이 나왔는데요, 이게 1편에는 없었던 새로운 자료형 튜플(tuple) 입니다.
리스트와 비슷하게 여러 값을 담는데, 모양이 조금 달라요.
my_tuple = (1, 3, 8) # 튜플: 소괄호 ( )
my_list = [1, 3, 8] # 리스트: 대괄호 [ ]
읽는 건 리스트와 똑같이 인덱스로 꺼냅니다.
print(my_tuple[1]) # 3
그런데 결정적인 차이가 하나 있어요. 튜플은 한 번 만들면 값을 바꿀 수 없습니다. (이걸 "불변, immutable"이라고 합니다.) 리스트는 값을 바꿀 수 있었지만, 튜플은 바꾸려고 하면 에러가 납니다.
my_tuple[1] = 100
# TypeError: 'tuple' object does not support item assignment
# ("튜플은 항목 변경을 지원하지 않는다"는 뜻)
그래서 "바뀌면 안 되는 값들" 을 묶어둘 때 튜플을 쓰면 좋습니다. 자바스크립트에는 딱 들어맞는 게 없어서, 저는 "바꿀 수 없는 리스트" 정도로 이해했습니다.
8. 그 외 알아두면 좋은 것들
마지막으로, 공부하면서 새로 알게 된 자잘한 것들을 모아봤습니다.
① list() — 타입 변환 내장함수
튜플은 못 바꾼다고 했는데, 그래도 바꿔야 한다면 리스트로 변환해서 쓰면 됩니다. list()로 감싸주면 됩니다.
my_tuple = (1, 3, 8)
my_list = list(my_tuple) # 튜플 → 리스트로 변환
my_list[1] = 100 # 리스트니까 이제 변경 가능
print(my_list) # [1, 100, 8]
1편에서 리스트 자료형을 살펴봤고, int(), str() 같은 타입 변환 함수도 다뤘었는데요. list()도 같은 종류의 내장함수입니다. 이미 익숙한 방식이라 크게 어색하지 않으실 것 같습니다.
② % — 나머지 연산자
1편에서 나눗셈(/), 정수 나눗셈(//), 제곱(**)은 봤었는데, 나머지를 구하는 % 는 안 다뤘었네요. %는 "나눈 나머지" 를 구해줍니다. 자바스크립트에서도 똑같이 %로 사용하기 때문에 익숙하실 것 같습니다.
print(10 % 3) # 1 (10을 3으로 나누면 나머지가 1)
print(10 % 2) # 0 (나누어떨어지면 나머지는 0)
이게 은근 자주 쓰이는데요, 예를 들어 "몇 번째마다 한 번씩" 같은 처리를 할 때 유용합니다.
for i in range(1, 11):
if i % 3 == 0: # i를 3으로 나눈 나머지가 0이면 = 3의 배수면
print(f"{i}: 3의 배수!")
# 3: 3의 배수!
# 6: 3의 배수!
# 9: 3의 배수!
③ 언더스코어(_) — 안 쓰는 변수
반복문에서 횟수만 필요하고 변수 값은 쓰지 않을 때, 변수 이름을 그냥 밑줄(_) 하나로 두는 경우가 있습니다. 처음엔 "변수 이름이 왜 밑줄이지?" 싶었는데, "이 값은 안 쓸 거예요" 라는 파이썬식 표현이라고 보시면 됩니다. 자바스크립트에서 lodash 라이브러리를 import _ from 'lodash'처럼 _로 불러와 쓰는 것과는 전혀 다른 의미이니 혼동하지 않으시면 될 것 같습니다.
for _ in range(3):
print("안녕하세요")
# 안녕하세요
# 안녕하세요
# 안녕하세요
마치며
이번 글에서는 클래스를 직접 만드는 법부터 시작해서 생성자(__init__)와 self, 메소드 추가하기, 그리고 모듈을 가져오는 여러 방법과 외부 패키지(colorgram) 활용, 마지막으로 튜플 같은 새로운 문법까지 살펴봤습니다.
자바스크립트에서 클래스를 써본 적은 있었지만, 파이썬에서 직접 만들어보니 속성과 메소드가 어떻게 연결되는지 조금 더 구체적으로 와닿는 부분이 있었고, 외부 패키지의 객체를 다룰 때도 같은 방식으로 접근할 수 있다는 점이 자연스럽게 연결되는 것 같았습니다.
공부를 계속하면서 새로 알게 되는 내용이 있으면 이어서 정리해보겠습니다.
부족한 글 읽어주셔서 감사합니다.🙇🏻♀
그럼 안녕히…👋
