파이썬을 공부하면서 기본적인 데이터형과 데이터 구조에 대해 나름 정리하고자 이 글을 쓴다.
함수와 메서드 차이
함수 : 여러 개의 처리를 기능별로 모아 놓은 것
- max(), min()과 같은 명령은 특정 요소에 관련돼 있지 않고 원하는 때에 호출할 수 있음
- 특정 요소(= 객체)에 관련된 함수를 메서드라고 함
예를 들어 append와 insert는 아래처럼 쓰일 수 있다.
weekdays.append("hello")
weekdays.insert(1, "world")
반면 del은 아래와 같다.
del items[2]
append나 insert는 조작 대상이 명확하다. 따라서 이러한 함수가 메서드다.
이와 반대로 del은 파이썬이 원래부터 준비하고 있는 명령이다.
배열
리스트 | 튜플 | 사전 | |
작성 방법 | [] 대괄호 | () 소괄호 | {} 중괄호 |
데이터 구조 | 시퀀스(나열) | 시퀀스(나열) | 사전 |
접근 방법 | 변수[번호] | 변수[번호] | 변수[키] |
특징 | 뮤터블 | 이뮤터블 | 번호는 없음 |
튜플은 변경이 불가하고 리스트는 변경이 가능하므로 언뜻 보기에 '그럼 더 유동적인 리스트로 다 선언하면 되지 않나?' 싶을 수 있다. 튜플은 값을 변경할 수 없는 대신, 메모리 소비가 훨씬 적다. 따라서 튜플은 좌표계(X,Y)와 같이 고정된 값을 사용하는 로직에 자주 사용된다.
어떤 변수에 리스트와 튜플을 저장했다고 가정하자. 그 변수를 다른 변수에 대입해도 그 리스트 자체가 복사 되는 것은 아니다. 아래와 같다.
a = [1, 2, 3]
b = a
a[2] = 9
a
Out[30]: [1, 2, 9]
id(a)
Out[31]: 3172553456896
b
Out[32]: [1, 2, 9]
id(b)
Out[33]: 3172553456896
a의 특정 값만 바꿨는데 b 변수의 특정 값도 같이 바뀌었다. 이는 a와 b가 같은 [1, 2, 3]이 저장된 공간을 참조하고 있기 때문이다. 그렇다면 그 리스트 자체는 어떻게 복사할까? 아래처럼 하면 된다.
a = [1, 2, 3]
b = a.copy()
a[2] = 9
a
Out[35]: [1, 2, 9]
id(a)
Out[36]: 3172553625856
b
Out[37]: [1, 2, 3]
id(b)
Out[38]: 3172552639680
조건문
파이썬에서는 조건식 위치에 값이 기술되면, 그 값에 따라 조건의 성립 여부를 결정하는 규칙이 정해져 있다. 아래와 같다.
조건식이 성립하지 않는다 | 조건식이 성립한다 | |
수치 | 0 | 0이 아닐 때 |
문자열 | 빈 문자열 ''나 "" | 왼쪽 항의 조건 이외일 때 |
리스트 | 빈 리스트 [] | 왼쪽 항의 조건 이외일 때 |
튜플 | 빈 튜플 () | 왼쪽 항의 조건 이외일 때 |
테스트를 해보면 다음과 같다.
a = 0
if a:
print("a is not zero")
else:
print("a is zero")
a is zero
if ():
print("it is ture value")
else:
print("it is false value")
it is false value
함수
거의 같은 값을 사용하지만, 때로는 다른 값을 지정하고 싶을 때도 있다. 이 때는 기본값을 아래와 같이 사용하면 된다.
def say_hello(name="Alex"):
print("Hi! " + name)
say_hello()
Hi! Alex
say_hello("june")
Hi! june
함수를 정의한다(=def로 선언하고 함수명을 붙이고 인수명을 정해서 실제 처리를 기술한다) 정도는 아니지만 약간의 작업으로 함수를 사용하고 싶을 때도 있을 것이다. 이럴 때 아래와 같이 람다 함수를 사용한다.
# 람다 함수를 선언하고 이를 호출하기 위해 변수에 대입
is_even = lambda x: x % 2 ==0
# ()를 붙여 함수를 호출함
is_even(2)
Out[10]: True
is_even(3)
Out[11]: False
# 마찬가지로 람다 함수를 선언하고 이를 호출하기 위해 변수에 대입
say_hi = lambda name: print("Hi! " + name)
# ()를 붙여 함수를 호출함
say_hi("Ken")
Hi! Ken
하지만 위처럼 변수에 담아 호출하는 것은 람다 함수의 정체성과 맞지 않다. 람다 함수는 일회성이다. 위처럼할 바엔 def로 함수를 정의해서 재사용하는 것이 더 좋다. 아래와 같은 예가 좋은 예다.
# map 함수를 사용할 때
list(map(lambda x: x*2, [1, 2, 3]))
Out[16]: [2, 4, 6]
# filter 함수를 사용할 때
list(filter(lambda a: a%2==0, [0, 1, 2, 3, 4, 5]))
Out[17]: [0, 2, 4]
# sorted 함수로 정렬을 하되 key 파라미터에 무엇을 기준으로 나열할지를 지정하는 함수를 람다로 표현
sorted(["bread", "rice", "spaghetti"], key=lambda x: len(x), reverse=True)
Out[23]: ['spaghetti', 'bread', 'rice']
외부 py 확장자 파일의 script import
외부 py 확장자 파일의 코드는 아래와 같다. test_import.py로 저장한다.
# -*- coding: utf-8 -*-
"""
Created on Thu Jun 22 19:09:30 2023
@author: june
"""
import random
for _ in range(5):
print(random.randint(0, 5))
print("done")
이 것을 어떻게 실행할까? 불러오는 파일과 같은 경로라면 아래처럼 심플하다.
# py 확장자명은 제외
import test_import
만약 경로가 다르다면 어떻게 해줘야 할까? 경로를 불러와 지정해주고 import를 아래처럼 해준다.
import sys
sys.path.append('C:/Users/june/.spyder-py3')
import test_import
3
2
1
2
5
done
변수
전역 변수는 간편하고 편리하다. 하지만 여러 곳에서 값을 수정하면 누가 언제 어떤 값을 써 넣은 것인지 알 수 없게 되고, 버그의 온상이 되기가 쉽다. 따라서 파이썬에선 아래와 같은 사양으로 정의 돼 있다.
- 함수 안에서 전역 변수 참조(Read)는 문제없이 실시할 수 있다.
- 함수 안에서 전역 변수와 같은 이름의 변수에 대입(Write)하면 전역 변수를 다시 쓰는 것이 아니라 같은 이름의 지역 변수가 작성된다. 즉, 전역 변수를 다시 쓰지 않는다.
아래 코드와 결과를 참조해보자.
# -*- coding: utf-8 -*-
"""
Created on Mon Jun 26 08:13:02 2023
@author: june
"""
""" scope3.py """
message = "Hello"
def say():
message = "Hi"
print("say:message="+message)
obj_id = id(message)
print("say:id(message)={0:d}".format(obj_id))
def main():
say()
print("main:message="+message)
obj_id = id(message)
print("main:id(message)={0:d}".format(obj_id))
if __name__ == '__main__':
main()
runfile('C:/Users/june/.spyder-py3/scope3.py', wdir='C:/Users/june/.spyder-py3')
say:message=Hi
say:id(message)=3172551671088
main:message=Hello
main:id(message)=3172551358448
하지만 전역 변수를 의도적으로 함수 내에서 다시 쓰고 싶은 경우도 있다. 이 때엔 global 명령어를 아래처럼 쓰면 된다.
# -*- coding: utf-8 -*-
"""
Created on Mon Jun 26 08:28:03 2023
@author: june
"""
""" scope4.py """
message = "hello"
def say():
# 전역 변수 message를 함수 안에서 변경한다
global message
message = "Hi"
print("say:message="+message)
obj_id = id(message)
print("say:id(message)={0:d}".format(obj_id))
def main():
say()
print("main:message="+message)
obj_id = id(message)
print("main:id(message)={0:d}".format(obj_id))
if __name__ == '__main__':
main()
runfile('C:/Users/june/.spyder-py3/untitled3.py', wdir='C:/Users/june/.spyder-py3')
say:message=Hi
say:id(message)=3172551671088
main:message=Hi
main:id(message)=3172551671088
객체
일상에서의 용어 | 객체지향에서의 용어 |
물건 | 객체 또는 인스턴스 |
물건의 특징 | 프로퍼티 |
물건의 조작 | 메서드 |
객체지향에서는 아래의 개념이 있다.
- 클래스 : 객체로부터 공통적인 특징만을 모아놓은 추상적인 개념
- 생성자 : 객체를 만들기 위한 전용 함수
- 상속 : 어떤 특징을 이어 받는 것
- 메서드 : 오브젝트(객체)를 조작하는 함수(혹은 기능)
- 프로퍼티 : 오브젝트(객체)가 가지고 있는 고유한 특징(혹은 속성)
- 인터페이스 : 메서드를 추상화한 것
클래스 설계
게임을 개발한다고 할 때, 클래스 설계에 대한 접근법으로는 다음과 같다.
- 명사(게임 중에서 움직이는 것)는 클래스로 할 수 있는 것이 많다.
- 비슷한 특징을 가진 것은 같은 클래스 또는 상속관계를 만들 수 있는지 생각한다.
- 반드시 클래스로 하면 좋은 것은 아니다(함수가 간단할 수도 있다).
- 상속을 무리하게 사용하는 것보다 클래스를 조합하도록 한다.
뛰어난 설계를 카탈로그로서 합한 것을 디자인 패턴이라고도 한다. 관심이 있다면 디자인 패턴 서적을 참고해 보자. 다음으로 클래스를 만들고 그 안에 아래와 같이 함수를 선언한다.
class Person:
def __init__(self, name):
self.name = name
he = Person("smith")
she = Person("alice")
객체지향 언어에서는 객체를 만드는 함수를 생성자라고 하며 __init__은 생성자에 해당한다. 이 함수의 첫 번째 변수에는 객체 자신이 전달된다. 첫 번째 인수는 파이썬이 설정해주므로, 우리가 그것을 명시적으로 지정하지는 않는다. 관습적으로 self란 인수명을 많이 사용한다. 이처럼 __init__는 인수로 주어진 객체를 초기화하는 기능을 하므로 "객체를 만든다"라기 보다는 "객체를 초기화한다"고 보는게 더 정확할지도 모른다.
클래스 도(diagram)라는 것이 있다. 클래스에 대한 설계도를 의미하는데, 보다 복잡한 설계가 있지만 간단하게 클래스명, 프로퍼티명, 메서드명을 네모 박스에 그려 표현한다. 아래 코드를 보자
class Pen:
def __init__(self, length, color):
self.length = length
self.color = color
def write(self, how_namy_hours):
self.length -= how_namy_hours / 10
my_pen = Pen(10, "black")
my_pen.write(3)
my_pen.length
Out[23]: 9.7
Pen에 대한 클래스를 정의하고, 이를 my_pen 변수에 담아서 사용하고 있다. 이 것을 클래스 다이어그램으로 표현하면 다음과 같다.
이상으로 파이썬의 기초 중 데이터형과 구조에 대해 알아보았다. 파이썬을 입문하는데 도움이 되었으면 한다.
* 참고문헌
- 다카나 겐이치로 저, "게임으로 배우는 파이썬"
'프로그래밍 언어 > Python' 카테고리의 다른 글
[Python] cave 게임 소스 코드 분석 (0) | 2023.07.03 |
---|---|
[Python] 어쩌면 FPS가 부하를 일으켰을 수도 있다. (0) | 2023.06.22 |