알아두면 좋은 파이썬 개념 - 2. 파이썬 언어의 기본 특징

11 minute read

‘알아두면 좋은 파이썬 개념’ 시리즈의 두번째 글로 파이썬의 기본 특징들에 대해 알아보겠다.

기본 특징

모든 것은 객체

파이썬을 공부하다보면 ‘파이썬의 모든 것은 객체’ 라는 말을 듣게 된다. 앞에서 말한 파이썬을 사용한 프로그래밍 방식 이전에 파이썬 자체가 구현된 방식에 대한 이야기로 어떤 방식으로든 파이썬을 이용해 코딩을 하게 되면 알아야 되는 개념이다. 파이썬은 기본 데이터 타입들조차 클래스 형태의 객체로 구현되어 있으며, 우리가 사용하는 코드들은 이 클래스들의 속성과 메소드를 기반으로 한다. 예시를 통해 알아보자.

파이썬 클래스 타입별 속성 및 메소드

tmp = [1, 'example', True, [3, 'a'], {1, 2, 3}, (1, 2), {'a': 1, 'b': 2}]
for item in tmp:
    print(type(item))
	print(dir(item))
<class 'int'>
['__abs__', '__add__', '__and__', '__bool__', '__ceil__', '__class__', '__delattr__', '__dir__', '__divmod__', '__doc__', '__eq__', '__float__', '__floor__', '__floordiv__', '__format__', '__ge__', '__getattribute__', '__getnewargs__', '__gt__', '__hash__', '__index__', '__init__', '__init_subclass__', '__int__', '__invert__', '__le__', '__lshift__', '__lt__', '__mod__', '__mul__', '__ne__', '__neg__', '__new__', '__or__', '__pos__', '__pow__', '__radd__', '__rand__', '__rdivmod__', '__reduce__', '__reduce_ex__', '__repr__', '__rfloordiv__', '__rlshift__', '__rmod__', '__rmul__', '__ror__', '__round__', '__rpow__', '__rrshift__', '__rshift__', '__rsub__', '__rtruediv__', '__rxor__', '__setattr__', '__sizeof__', '__str__', '__sub__', '__subclasshook__', '__truediv__', '__trunc__', '__xor__', 'bit_length', 'conjugate', 'denominator', 'from_bytes', 'imag', 'numerator', 'real', 'to_bytes']
<class 'str'>
['__add__', '__class__', '__contains__', '__delattr__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__getitem__', '__getnewargs__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__iter__', '__le__', '__len__', '__lt__', '__mod__', '__mul__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__rmod__', '__rmul__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', 'capitalize', 'casefold', 'center', 'count', 'encode', 'endswith', 'expandtabs', 'find', 'format', 'format_map', 'index', 'isalnum', 'isalpha', 'isascii', 'isdecimal', 'isdigit', 'isidentifier', 'islower', 'isnumeric', 'isprintable', 'isspace', 'istitle', 'isupper', 'join', 'ljust', 'lower', 'lstrip', 'maketrans', 'partition', 'replace', 'rfind', 'rindex', 'rjust', 'rpartition', 'rsplit', 'rstrip', 'split', 'splitlines', 'startswith', 'strip', 'swapcase', 'title', 'translate', 'upper', 'zfill']
<class 'bool'>
['__abs__', '__add__', '__and__', '__bool__', '__ceil__', '__class__', '__delattr__', '__dir__', '__divmod__', '__doc__', '__eq__', '__float__', '__floor__', '__floordiv__', '__format__', '__ge__', '__getattribute__', '__getnewargs__', '__gt__', '__hash__', '__index__', '__init__', '__init_subclass__', '__int__', '__invert__', '__le__', '__lshift__', '__lt__', '__mod__', '__mul__', '__ne__', '__neg__', '__new__', '__or__', '__pos__', '__pow__', '__radd__', '__rand__', '__rdivmod__', '__reduce__', '__reduce_ex__', '__repr__', '__rfloordiv__', '__rlshift__', '__rmod__', '__rmul__', '__ror__', '__round__', '__rpow__', '__rrshift__', '__rshift__', '__rsub__', '__rtruediv__', '__rxor__', '__setattr__', '__sizeof__', '__str__', '__sub__', '__subclasshook__', '__truediv__', '__trunc__', '__xor__', 'bit_length', 'conjugate', 'denominator', 'from_bytes', 'imag', 'numerator', 'real', 'to_bytes']
<class 'list'>
['__add__', '__class__', '__contains__', '__delattr__', '__delitem__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__getitem__', '__gt__', '__hash__', '__iadd__', '__imul__', '__init__', '__init_subclass__', '__iter__', '__le__', '__len__', '__lt__', '__mul__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__reversed__', '__rmul__', '__setattr__', '__setitem__', '__sizeof__', '__str__', '__subclasshook__', 'append', 'clear', 'copy', 'count', 'extend', 'index', 'insert', 'pop', 'remove', 'reverse', 'sort']
<class 'set'>
['__and__', '__class__', '__contains__', '__delattr__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__gt__', '__hash__', '__iand__', '__init__', '__init_subclass__', '__ior__', '__isub__', '__iter__', '__ixor__', '__le__', '__len__', '__lt__', '__ne__', '__new__', '__or__', '__rand__', '__reduce__', '__reduce_ex__', '__repr__', '__ror__', '__rsub__', '__rxor__', '__setattr__', '__sizeof__', '__str__', '__sub__', '__subclasshook__', '__xor__', 'add', 'clear', 'copy', 'difference', 'difference_update', 'discard', 'intersection', 'intersection_update', 'isdisjoint', 'issubset', 'issuperset', 'pop', 'remove', 'symmetric_difference', 'symmetric_difference_update', 'union', 'update']
<class 'tuple'>
['__add__', '__class__', '__contains__', '__delattr__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__getitem__', '__getnewargs__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__iter__', '__le__', '__len__', '__lt__', '__mul__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__rmul__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', 'count', 'index']
<class 'dict'>
['__class__', '__contains__', '__delattr__', '__delitem__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__getitem__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__iter__', '__le__', '__len__', '__lt__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__setitem__', '__sizeof__', '__str__', '__subclasshook__', 'clear', 'copy', 'fromkeys', 'get', 'items', 'keys', 'pop', 'popitem', 'setdefault', 'update', 'values']

우리가 자주 사용하는 int, str, bool, list, tuple, set, dict에 대해 type 함수를 적용해보면 모두 class로 구현되어 있음을 확인할 수 있다. 또, dir 함수 결과를 보면 각 클래스 내부에는 __ 로 감싸져있거나 그렇지 않은 다양한 이름들이 존재한다.

매직 메소드

__ 로 감싸져 있는 이름들은 매직 메소드로 출력이나 연산, 비교 등의 기본적인 클래스의 작동 방식을 정의한다. str, list, set, tuple, dict 에서 볼 수 있는 __contains__ 는 우리가 흔히 사용하는 in 을 통해 특정 원소가 포함되어 있는 지 확인할 때 어떤 방식으로 확인할 지를 정의한다. int에서 보이는 __gt__, __lt__ 등은 int 객체 간 값의 >, < 부등호로 값을 비교할 기본 방식을 정의한다. 이외에도 + 연산자 작동 방식에 쓰이는 __add__, 길이 측정에 사용되는 __len__ 등 다양한 매직 메소드가 존재한다. 이를 통해 우리가 자주 사용하는 if item in list_sample:, a > b, c = 1 + 2, len(d) 등의 코드가 작동되고 있는 것이다.

매직 메소드 외

__로 되어있은 이름들 외에도 다양한 메소드들이 존재한다. str 클래스의 경우, join, lower, lstrip 등의 함수는 문자형 데이터를 다룰 때 정말 자주 사용되는 메소드들인데 실제로 클래스 내에 구현되어 있음을 볼 수 있다. 다른 클래스에 join 이나 lower 등의 메소드는 정의되어 있지 않으므로 우리는 str 데이터에서만 해당 기능들을 사용할 수 있었던 것이다. str 클래스처럼 각 클래스들은 자신만의 메소드들이 존재하고 파이썬은 이렇게 정의된 많은 메소드들을 기반으로 다른 언어들보다 쉽게 코드를 작성할 수 있게 구현되어 있다.

이처럼 파이썬 내부가 구현된 방식을 알게 되면 그 이해도를 바탕으로 기본 데이터 타입 사용이나 사용 시에 발생하는 에러처리, 또는 새로운 클래스 구현을 효과적으로 수행할 수 있게 된다.

객체 참조, 얕은 복사(Shallow Copy), 깊은 복사(Deep Copy)

C 같은 언어와 달리 파이썬은 변수를 생성할 때마다 새로운 메모리 주소를 할당하지 않고 우항에 있는 객체를 참조해 변수를 할당한다. 이런 점 때문에 만들어진 객체를 다른 변수에서 다시 이용할 때 생각하지 않은 방식으로 코드가 작동할 때가 있다. 이런 문제는 데이터 타입에 따라 발생하기도 발생하지 않기도 하는데, 어떤 상황에서 문제가 발생하는 지 알아야 에러 없이 코드를 작성할 수 있다.

# Mutable Object (list, dict, set, ...)
print('list')
a = [1, 2]
b = a
print('a -- value: %s, id: %s' % (a, id(a)))
print('b -- value: %s, id: %s' % (b, id(b)))
a.append(3)
print('a -- value: %s, id: %s' % (a, id(a)))
print('b -- value: %s, id: %s' % (b, id(b)))
print('')

print('dict')
a = {1: 1, 2: 2}
b = a
print('a -- value: %s, id: %s' % (a, id(a)))
print('b -- value: %s, id: %s' % (b, id(b)))
a[3] = 3
print('a -- value: %s, id: %s' % (a, id(a)))
print('b -- value: %s, id: %s' % (b, id(b)))
print('')

print('set')
a = {1, 2}
b = a
print('a -- value: %s, id: %s' % (a, id(a)))
print('b -- value: %s, id: %s' % (b, id(b)))
a.add(3)
print('a -- value: %s, id: %s' % (a, id(a)))
print('b -- value: %s, id: %s' % (b, id(b)))
print('')

# Immutable Object (int, str, tuple, ...)
print('int')
a = 1
b = a
print('a -- value: %s, id: %s' % (a, id(a)))
print('b -- value: %s, id: %s' % (b, id(b)))
a = 2
print('a -- value: %s, id: %s' % (a, id(a)))
print('b -- value: %s, id: %s' % (b, id(b)))
print('')

print('str')
a = 'sample'
b = a
print('a -- value: %s, id: %s' % (a, id(a)))
print('b -- value: %s, id: %s' % (b, id(b)))
a = 'change'
print('a -- value: %s, id: %s' % (a, id(a)))
print('b -- value: %s, id: %s' % (b, id(b)))
print('')

코드를 보면 Mutable 객체와, Immutable 객체에 대한 예시가 나눠져 a 객체의 복사와 a 객체 값 변경을 통한 b 객체의 변화를 보는 코드가 작성되어 있다.

결론부터 말하면 mutable한 객체에 대해서 복사에 대한 문제가 발생하고 immutable한 객체에 대해서는 문제가 발생하지 않는다. list, dict, set 같은 객체들은 mutable한 객체로 분류되는데, 이들은 한 번 선언되면 메모리 값이 고정되어 내부 값을 변경해도 메모리 주소가 변하지 않는다. 이에 대한 부작용으로 만약 어떤 객체(e.g. b)가 mutable한 객체(e.g. a)의 값을 참조해 복사하고 있다면 mutable한 객체 내부 값이 변경(e.g. a.append(3))이 되었을 때 두 객체는 같은 메모리 주소를 참조하고 있기에 mutable한 객체를 참조한 객체의 값은 mutable한 객체의 값으로 변경되어버린다.

반면, immutable한 객체는 값 변경 시 메모리 주소가 바뀌어버려 참조하고 있던 객체(e.g. b)가 기존 주소를 가지고 변경된 객체가 새로운 주소를 가지는 방식으로 되어 별도의 값을 가지고 문제가 발생하지 않는다.

파이썬 코드를 짜게 되면 리스트나 딕셔너리를 정말 많이 다루게 되는데 이 문제는 리스트나 딕셔너리 사용시 필연적으로 발생하므로 사용에 주의가 필요하다.

물론, 해결 방법은 존재한다. 아래에서 나오는 얕은 복사와 깊은 복사에 대해 알아보자.

얕은 복사(Shallow Copy)

import copy

# Case 1
a = [1, 2]
b = copy.copy(a)
print('a -- value: %s, id: %s' % (a, id(a)))
print('b -- value: %s, id: %s' % (b, id(b)))
a.append(3)
print('a -- value: %s, id: %s' % (a, id(a)))
print('b -- value: %s, id: %s' % (b, id(b)))
print('')

# Case 2
a = [1, [2, 3]]
b = copy.copy(a)
print('a -- value: %s, id: %s' % (a, id(a)))
print('a -- %s' % [id(item) for item in a])
print('b -- value: %s, id: %s' % (b, id(b)))
print('b -- %s' % [id(item) for item in b])
a[1].append(4)
print('a -- value: %s, id: %s' % (a, id(a)))
print('b -- value: %s, id: %s' % (b, id(b)))
print('')

copy 모듈의 copy.copy() 를 활용하면 mutable한 객체 복사 시 새로운 메모리 주소에 객체를 할당해 이전 예시에서 봤던 상황에서 복사 관련 문제가 발생하지 않는다.

다만 이 방법도 한계가 존재한다. copy.copy() 는 얕은 복사로 바깥 객체의 주소만을 바꿔 복사를 수행하고 내부 객체의 주소를 바꾸지 않는다. 두번째 케이스의 경우 리스트 안에 또 다른 리스트가 선언되어 있다. 객체 내부에 mutable한 객체가 존재할 시 결국 a와 b의 원소는 또 같은 메모리 주소를 가리키고 있어 a[1].append(4) 를 통해 a 내부의 [2, 3] 값을 변경해버리면 b도 따라서 바뀌게 된다.

깊은 복사(Deep Copy)

import copy

a = [1, [2, 3]]
b = copy.deepcopy(a)
print('a -- value: %s, id: %s' % (a, id(a)))
print('a -- %s' % [id(item) for item in a])
print('b -- value: %s, id: %s' % (b, id(b)))
print('b -- %s' % [id(item) for item in b])
a[1].append(4)
print('a -- value: %s, id: %s' % (a, id(a)))
print('b -- value: %s, id: %s' % (b, id(b)))
print('')

이에 대한 해결책은 깊은 복사이다. 깊은 복사의 경우 객체 내부의 모든 값의 메모리 주소까지 모두 변경해 복사해 버린다. copy.deepycopy() 를 통해 깊은 복사를 수행하게 되면 내부의 원소가 어떤 방식으로 구성되어있든 모든 메모리를 새로 가지는 새로운 객체가 생성되어 완전한 복사가 이루어진다.

이런 내용을 고려해 변수를 사용한다면 이전 값을 참조 형태로 복사하다가 이전 변수를 잘못 조작해서 문제가 발생하는 경우들을 방지할 수 있다.

안전하게 무조건 deepcopy로 해야지라고 생각할 수도 있으나 deepcopy는 내부적으로 많은 로직이 포함되어 있어 속도를 중시하여 프로그램을 짤 때 방해요소가 된다. mutable, immutable 타입 등을 고려해 상황에 맞게 복사 방법을 선택해야 할 것이다.

네임스페이스

파이썬 코드를 보다 보면 if __name__ == '__main__': 이라는 특이한 코드가 계속 사용되는 것을 보게 된다. 네임스페이스라는 개념으로 모듈의 실행방법을 정의하는 중요한 요소이다. 하나의 .py 파일만으로 구성된 로직을 짜거나 jupyter notebook을 활용해 코드를 쓰다보면 잘 모를 수 있지만 여러개의 모듈이 얽혀있는 로직을 만들다 보면 반드시 알아야 되는 개념이다.

calc/calc.py

print("calc module __name__: %s" % __name__)

def simple_add(a, b):
    return a + b

def simple_mul(a, b):
    return a * b

if __name__ == '__main__':
    print(simple_add(10, 11))

test.py

from calc import calc

print("test module __name__: %s" % __name__)

def test(a, b):
    return calc.simple_add(a, b) + calc.simple_mul(a, b)

if __name__ == '__main__':
    print(test(3, 7))

calc.py 모듈의 함수를 import 해서 test.py에서 계산하는 간단한 예제 코드이다.

python test.py 실행 결과

calc module __name__: calc.simple
test module __name__: __main__
31

터미널에서 python test.py 를 치거나 개발환경을 이용해 test.py 모듈을 실행시키면 위와 같은 결과가 나온다.

먼저 test.py에서 calc.py 모듈을 import하면 print("calc module __name__: %s" % __name__) 가 실행되는데 그 결과는 calc module __name__: calc.calc 이다. 그 후 함수들이 test.py 모듈에서 사용될 수 있는 형태로 import 되고 print("test module __name__: %s" % __name__) 가 실행되어 test module __name__: __main__ 이 나온다. test.py 모듈에서 __name____main__ 이 맞으므로 test(3, 7) 이 실행되어 31이라는 결과값이 산출된다.

python calc/calc.py 실행 결과

calc module __name__: __main__
21

calc.py 에서 파이썬 코드를 실행시키면 위와 같은 결과가 나온다. 이 경우 calc 모듈은 __main__ 의 이름으로 실행되었고 앞의 케이스에서 실행되지 않던 print(simple_add(10, 11)) 이 실행되어 21이라는 값이 나오게 된다.

결국 __name__ 은 프로그램이 수행되는 곳에서는 __main__ , 아닌 곳에서는 모듈의 이름을 따르는 방식으로 작동된다. if __name__ == '__main__': 를 활용하면 import 되는 상황이 아니라 프로그램이 해당 모듈에서 실행되었을 때 어떤 식으로 동작할 지를 규정할 수 있다.

실제 프로그램 작성시에는 모듈이 어떤 경우에는 실행의 주체가 되고 어떤 경우에는 import의 대상이 되기도 한다. 따라서 실행의 주체가 되는 파이썬 모듈에 if __name__== '__main__': 을 사용해 작동 방식을 정의하면 실행 주체에 따라 원하는 방식으로 작동하는 파이썬 프로그램을 만들 수 있다.

Import

네임스페이스도 알았지만 막상 여러 모듈을 엮어서 프로그램을 짜려다 보면 모듈 간 import 되는 방식이 정확히 이해가 안 돼 골머리를 앓게 된다. 사실 import 라는 영어 단어가 특정 기능들을 단순히 불러온다는 느낌으로 해석되어 작동 방식을 헷갈리게 하는데, import가 수행하는 것은 실제 해당 모듈의 실행 + 정의된 객체를 포함한 모듈 클래스의 불러오기이다. 모듈이 실행되는 개념이므로 if __name__== '__main__': 으로 되어있지 않은 부분은 모두 실행되고, 그 과정에서 정의된 변수나 메소드 객체들은 모두 모듈 클래스로 패킹되어 메인 모듈에서 활용할 수 있게 된다.

calc/complex_calc.py

from . import var

const = var.c

def complex_add(a, b):
    return a + b + const

def complex_mul(a, b):
    return a * b + const

calc/var.py

c = 10

complex_calc.py 모듈에서 from . import var 로 var.py 모듈을 불러오게 되면 c = 10 이 실행되고 var라는 모듈 클래스로 complex_calc.py 에서 사용할 수 있게 된다. const = var.c 같은 형태로 import한 상수 객체를 사용할 수 있고 이를 통해 complex_add(), compledx_mul() 함수를 만들 수 있게 된다.

complex_test_1.py

from calc import complex_calc

def test(a, b):
    return complex_calc.complex_add(a, b) + complex_calc.complex_mul(a, b)

if __name__ == '__main__':
    print(complex_calc.const)
    c = 3
    d = 7
    print(test(c, d))

코드를 실행시키는 모듈로 돌아가보자. complex_test_1.py 모듈에서 from calc import complex_calc 라인을 통해 complex_calc.py 모듈이 실행되고 complex_calc이라는 모듈 클래스가 통째로 불러와진다. complex_add, complex_mul 함수를 사용해 코드를 구현할 수 있게 되고 complex_calc.py 모듈을 실행하며 만들어진 const에도 complex_calc.const를 통해 접근할 수 있게 된다.

complex_test_2.py

from calc.complex_calc import complex_add, complex_mul

def test(a, b):
    return complex_add(a, b) + complex_mul(a, b)

if __name__ == '__main__':
    # print(complex_calc.const)
    c = 3
    d = 7
    print(test(c, d))

이제 조금 다른 방법의 import를 살펴보자. from calc.complex_calc import comlex_add, complex_mul 처럼 import를 하게 되면 import 뒤에 규정된 특정 함수나 변수만을 최종적으로 불러오게 된다. 하지만 const를 활용하는 함수는 complex_calc를 실행하면서 선언되었기 때문에 const를 활용한 함수인 complex_add, complex_mul의 작동에는 아무런 문제가 없게 된다.

모듈을 불러와서 다른 모듈에서 사용할 때 위와 같이 실행 후에 불러와진다는 import의 방식을 이해한다면 다른 모듈에서 기능을 import 해올 때 헷갈리지 않고 코드를 구성할 수 있을 것이다.

추가적으로 import 사용 시 from calc.complex_calc import * 과 같은 코드를 사용한다면 기존에 정의되어 있는 이름과 겹칠 시 해당 이름을 덮어쓰게 되어 원하지 않는 방식으로 코드가 돌아갈 수도 있다. 겹치는 이름을 사용할 것 같으면 겹치지 않을 만한 이름으로 as imported_name 를 사용하거나 모듈 통째로 가져와서 complex_calc.complex_add 처럼 접근해 꼬이는 것을 방지하도록 하자.

유용한 내장 라이브러리

파이썬은 훌륭한 내장 라이브러리들을 가지고 있다. functools, itertools, collections 라이브러리들은 머신러닝이나 최적화 알고리즘 개발 등에서 활용할만한 기능들이 이미 구현되어 있다. 라이브러리의 몇 가지 함수들은 알아두어 모든 기능을 구현하지 말고 효과적으로 개발 하도록 하자.

References

Leave a comment