클래스와 인스턴스

  • 내장형 데이터 외에 새로운 타입을 만들 때 클래스를 선언한다.

    • 파이썬에서는 클래스가 정의되면 하나의 독립적인 네임스페이스를 생성한다.

    • 네임스페이스란 변수가 객체를 바인딩(대입 연산자를 사용해서 저장하는 것)할 때 그 둘 사이의 관계를 저장하고 있는 공간을 의미한다.

    • 클래스 내에 정의된 변수(=클래스 변수)는 생성된 네임스페이스 안에 딕셔너리 타입으로 저장된다.

>>> dir()
['__annotations__', '__builtins__', '__doc__', '__loader__', '__name__',
'__package__', '__spec__']

>>> class Obj:
...     number = 1
...

>>> dir() # 클래스가 생성되었다.
['Obj', '__annotations__', '__builtins__', '__doc__', '__loader__', '__name__',
'__package__', '__spec__']

>>> Obj.__dict__ # 네임스페이스에 추가된 변수를 확인할 수 있다.
mappingproxy({'__module__': '__main__',
'number': 1,
'__dict__': <attribute '__dict__' of 'Obj' objects>,
'__weakref__': <attribute '__weakref__' of 'Obj' objects>,
'__doc__': None})

>>> Obj.number # 클래스 변수에 접근하기
1
  • 클래스는 객체의 설계도이며, 인스턴스는 이 설계도를 기반으로 메모리에서 생성되어 여러 작업을 수행한다. 이는 하나의 클래스로 여러 객체가 생성될 수 있음을 의미한다.

  • 객체를 생성하는 과정을 인스턴스화라고 하며, 생성된 객체를 인스턴스라고 부른다.

    • 파이썬은 인스턴스마다 별도의 네임스페이스를 가진다. 즉, 클래스의 네임스페이스와 인스턴스의 네임스페이스는 메모리 상에 다른 위치에 있다.

    • 어떤 인스턴스의 멤버에 접근할 때는 인스턴스의 네임스페이스를 먼저 찾아보고 없으면 클래스의 네임스페이스를 찾는다. 만약 여기에도 없다면 AttributeError 예외를 던진다.

    • 참고: 인스턴스 외부에서 멤버를 추가하는 행위를 몽키 패치(monkey patch)라고 한다.

>>> o1 = Obj()
>>> o2 = Obj()
>>> o1.__dict__
{}
>>> o2.__dict__
{}
>>> o1.number = 4 # 인스턴스 변수 생성
>>> o1.__dict__
{'number': 4}
>>> o2.__dict__
{}
>>> o2.number # 클래스 변수에 접근
1
>>> Obj.number # 클래스 변수에 접근
1
>>> o2.x
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AttributeError: 'Obj' object has no attribute 'x'
  • 인스턴스는 런타임(run-time) 환경에서 메모리 상의 서로 다른 주소를 가진다. 즉, 클래스는 같더라도 인스턴스는 다를 수 있으며, isinstance(변수명, 클래스명) 함수로 해당 변수에 바인딩된 인스턴스의 클래스 타입을 확인할 수 있다.

    • 동일 객체라는 말은 메모리 주소가 같다는 의미이다.

>>> o1 = Obj()
>>> o2 = Obj()
>>> o1 is o2
False
>>> id(o1), id(o2) # 다른 인스턴스이므로 다른 주소값이 나온다.
(4518943184, 4519522896)
>>> isinstance(o1, Obj)
True
>>> isinstance(o2, Obj)
True

 

클래스 변수와 클래스 메소드

  • 클래스 네임스페이스에 저장된 변수와 메소드로 모든 인스턴스에 대해 독립적이다.

  • 클래스 메소드는 다음과 같이 @classmethod 를 써서 선언하며, 첫번째 인자로 cls 를 받는다.

    • 참고로 첫번째 인자는 파이썬에서 자동으로 넘겨주기 때문에 별도로 매개변수로 넣지 않아도 된다.

class Obj:
    number = 3
    
    @classmethod
    def print_number(cls):
        print('{}: number = {}'.format(cls.__name__, cls.number))
  • 클래스 메소드 내부에서는 cls.변수명 또는 cls.메소드명(...) 으로 클래스 멤버에 접근할 수 있다.

  • 클래스 외부에서는 클래스 이름으로 접근이 가능하다. 

  • 인스턴스에 영향을 받지 않고, 모든 인스턴스가 공유할 필요가 있다면, 클래스 변수 및 메소드로 선언하는 것이 좋다.

>>> class Obj:
...     number = 3
...
...     @classmethod
...     def print_number(cls):
...         print('{}: number = {}'.format(cls.__name__, cls.number))
...
>>> o1 = Obj()
>>> o1.print_number()
Obj: number = 3
>>> o2 = Obj()
>>> o2.print_number()
Obj: number = 3
>>> Obj.number = 10
>>> o1.print_number()
Obj: number = 10
>>> o2.print_number()
Obj: number = 10
>>> o1.number = 1 # 인스턴스 변수에 접근 (클래스 변수가 아니다)
>>> o1.print_number()
Obj: number = 10
>>> o1.number
1
  • 클래스에 종속적이란 의미는 상속 시 상위 클래스와 하위 클래스를 구분한다는 의미이다. 상위 클래스에 선언된 클래스 메소드라 할지라도 하위 클래스의 클래스 변수에 접근한다.

 

인스턴스 변수와 인스턴스 메소드

  • 인스턴스 네임스페이스에 있는 변수와 메소드로, 인스턴스에 종속적이며, 보통 인스턴스 메소드를 일반 메소드라고 한다.

  • 인스턴스에 종속적이기 때문에 인스턴스마다 변수가 가지고 있는 값도 다르며 멤버의 메모리 주소도 다르다.

    • 따라서 인스턴스끼리 구분해서 사용하는 변수나 메소드는 인스턴스 멤버로 선언해야 한다.

  • 클래스 메소드는 클래스 멤버에만 접근 가능하며, 인스턴스 메소드는 인스턴스 멤버와 type(self) 로 클래스 멤버에도 접근할 수 있다. 즉, 클래스 정의 시 변수의 범위(scope)를 먼저 정하고, 메소드가 인스턴스 멤버에 접근해야 한다면 인스턴스 메소드로, 클래스 멤버에만 접근한다면 클래스 메소드로 선언하는 것이 좋다.

    • 예를 들어, 사람마다 이름과 나이가 다르니 이름과 나이는 인스턴스 변수로, 이름을 말하는 행위는 인스턴스 메소드로 선언해야 한다. 사람은 모두 지구에 살고 있기 때문에 거주중인 행성 이름은 클래스 변수로, 다른 행성으로 이사가는 행위는 클래스 메소드로 선언해야 할 것이다.

  • 인스턴스 메소드에서는 첫번째 인자로 self 를 받으며, 메소드 내부에서는 self.변수명 또는 self.메소드명(...)으로 접근한다.

    • 메소드 외부에서는 객체를 바인딩한 변수 이름으로 인스턴스 멤버에 접근한다.

    • self 변수를 통해 현재 메소드를 실행할 인스턴스를 구분한다.

>>> class Obj:
...     def method1(self, name):
...             print(id(self))
...             print('Hi!', name)
...
>>> o1 = Obj()
>>> o1.method1('best')
4519961600
Hi! best
>>> o2 = Obj()
>>> o2.method1('Lisa') # 인스턴스의 주소가 다르게 나온다!
4519522896
Hi! Lisa
  • 접근 제한자: 파이썬에서 공식적인 접근 제한자(Access Modifier)는 없지만, 멤버의 이름에 언더스코어를 추가하여 이를 구분한다. 일반적으로 언더스코어 1개는 private 이며, 2개는 protected, 없으면 public 이다.

    • private 은 현재 인스턴스에서만 사용 가능한 것

    • protected 는 현재 인스턴스가 상속한 부모 인스턴스에도 접근할 수 있는 것

    • public 은 외부로부터 인스턴스 멤버에 접근을 허용하는 것

 

Magic Method

  • 종류가 많기 때문에 전부 설명하기 보다는 자주 사용되는 것들 위주로 나열해보겠다. 자세한 설명은 문서를 참고.

  • __init__(self), __del__(self) 은 생성자와 소멸자, 선언하지 않으면 self만 인자로 받는 메소드가 생성된다. 따라서, 생성할 때 추가로 인자가 필요하다면 두 메소드를 다시 선언해야 된다.

>>> class Obj:
...     def __init__(self, name):
...             self.name = name
...             print('hi', name)
...     def __del__(self):
...             print('bye', self.name)
...
>>> o = Obj('Lisa')
hi Lisa
>>> del o
bye Lisa
  • __iter__(self)이터레이터(Iterator) 객체(=자기자신)를 반환하는 메소드이다. 이 메소드를 선언하면 해당 인스턴스는 이터레이션을 제공하는 이터러블(itarable) 객체가 된다.

  • __next__(self) 은 이터레이터를 제공하는 경우 다음 원소를 반환하는 메소드이다. __iter__(self) 와 함께 선언되어 자주 쓰인다. 이 메소드를 선언하는 해당 인스턴스는 이터레이터(iterator) 객체가 된다.

>>> class IterableObj:
...    def __init__(self, end):
...        self.number = 0
...        self.end = end
...    
...    def __iter__(self):
...        return self
...    
...    def __next__(self):
...        if self.number < self.end:
...            num = self.number
...            self.number += 1
...            return num
...        else:
...            raise StopIteration
...
>>> o = IterableObj(3)
>>> next(o)
0
>>> next(o)
1
>>> next(o)
2
>>> next(o)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 1, in __next__
StopIteration

>>> for i in IterableObj(3):
    print(i, end=' ')
...
0 1 2
  • __call__(self) 은 C언어에서 operator() 함수와 동일한 역할을 한다. 파이썬의 함수는 모두 객체이며 function 클래스 타입에서 __call__ 메소드가 실행된 것이다.

>>> class Obj:
...     def __call__(self):
...             print('__call__({})'.format(id(self)))
...
>>> o = Obj()
>>> o()
__call__(4519170592)
  • __enter__(self), __exit__(self, exc_type, exc_val, exc_tb)Context Manager 로 쓰이는 with 구문을 위한 메소드이다. with 문은 프로그램에서 리소스를 사용 후 자동으로 닫아주기 때문에 파일 읽기/쓰기 작업 시 사용이 권장된다. enter은 with 문으로 열기(open) 상태일 때 exit은 닫기(close) 상태일 때 각각 호출된다.

    • 대용량 데이터를 처리할 때, 중간에 끊기는 상황이 자주 생긴다. 이때 처음부터 다시 처리하지 않고 중간부터 시작하길 원한다면 __enter__ 메소드에서 시작할 위치를 찾는 코드 및 처리결과를 기록하는 파일을 여는 코드를 작성하고 __exit__ 메소드에서 처리결과 파일을 close 하도록 처리하면 될 것이다.

>>> class Obj:
...     def __enter__(self):
...         print('__enter__({})'.format(id(self)))
...
...     def __exit__(self, exc_type, exc_val, exc_tb):
...         print('__exit__({})'.format(id(self)))
...
>>> with Obj() as o:
...     print('This is inside with statement!')
...
__enter__(4557148704)
This is inside with statement!
__exit__(4557148704)

 

정적 메소드

  • 클래스 메소드는 cls를 인자로 받아서 호출한 클래스의 네임 스페이스에 접근할 수 있다. 메소드 주소를 확인해보면 동일한 주소값이 나온다.

>>> class Obj:
...     name = 'Lisa'
...
...     @classmethod
...     def get_name(cls):
...         print(id(cls))
...         print('hi', cls.name)
...
>>> o1 = Obj()
>>> o1.get_name()
140513752745968
hi Lisa
>>> o2 = Obj()
>>> o2.get_name()
140513752745968
hi Lisa
>>> Obj.get_name()
140513752745968
hi Lisa
  • 정적 메소드는 클래스와 인스턴스 모두에 독립적인 연산을 수행할 때 사용되는 메소드이며, 첫번째 인자로 아무것도 받지 않는다. 따라서 위와 같이 클래스를 구분하는 행위를 할 수 없다. 마찬가지로 인스턴스 멤버에도 접근할 수 없다.

  • 정적 메소드가 쓰일 때는 클래스 멤버나 인스턴스 멤버에 영향을 주지 않는 연산을 수행하는 경우이다. 단, 클래스 멤버의 경우 외부에서 클래스 이름으로 접근할 수 있듯이 정적 메소드에서 클래스 이름으로 접근할 수는 있다.

    • 정적 메소드도 마찬가지로 외부에서 클래스 이름으로 접근할 수 있다.

  • 메소드 선언 시 위에 @staticmethod 를 쓴다.

>>> class Parent:
...     number = 42 # 42는 Parent 클래스의 네임스페이스에 저장된다.
...
...     @staticmethod
...     def print_number():
...         print(Parent.number) # 항상 Parent 클래스의 네임스페이스에 접근한다.
...
>>> class Child(Parent): # 자식은 부모 클래스의 네임 스페이스에 접근 가능하므로
...     number = 24 # 24는 Child 클래스의 네임스페이스에 저장된다.
...
>>> Child.print_number() # 자식으로 부모 클래스의 정적 메소드를 호출 할 수 있다.
42

 

추상 메소드

  • 추상 클래스는 메소드를 정의만하고 실제 구현은 상속받은 하위 클래스들에게 위임하는 클래스이다.

  • 다음과 같이 abc 모듈을 import 하여 추상 베이스 클래스를 상속받아 구현할 수 있다.

  • 위임할 메소드 위에는 @abstractmethod를 쓴다.

  • 상속받은 하위 클래스들은 반드시 추상 메소드를 구현해야 한다.

>>> from abc import ABC, abstractmethod
>>> class BaseClass(ABC):
...     @abstractmethod
...     def fn(self):
...         raise NotImplementedError
...

>>> class MyObject(BaseClass):
...     def fn(self):
...         pass
...
>>> o = MyObject()
>>> o.fn()

>>> class MyObject(BaseClass):
...     pass
...
>>> o = MyObject()
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: Can't instantiate abstract class MyObject with abstract methods fn
  • 주로 여러 인스턴스가 공통된 행위를 해야할 때 추상 클래스에 해당 행위(=메소드)들을 모아놓는다.

  • 여러 디자인 패턴을 구현하는데 많이 활용되며 자바의 인터페이스와 같은 역할을 한다.

  • 흔히 OOP에서는 인스턴스에서 자주 변하는 부분에 대해 느슨한 결합을 강조하는데 이는 메소드의 구현을 강제하면서 동시에 실제 구현을 하위 클래스에 위임하는 성질과 찰떡궁합이다.

>>> import abc
>>> class Person(abc.ABC):
...     @abc.abstractmethod
...     def greet(self):
...         pass
...
>>> class Doctor(Person):
...     def __init__(self, name):
...         self.name = name
...
...     def greet(self):
...         print('Hi, I am', self.name, 'and I am a doctor!')
...
>>> d = Doctor('Lisa')
>>> d.greet()
Hi, I am Lisa and I am a doctor!

>>> class Student(Person):
...     def __init__(self, age):
...             self.age = age
...
>>> s = Student(17) # 추상 메소드를 정의하지 않을 경우 TypeError 발생
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: Can't instantiate abstract class Student with abstract methods greet

 

LEGB 규칙

  • 파이썬에서는 변수의 영역(scope)을 다음과 같이 나누며, 함수에서의 우선순위는 지역변수 > 외부 함수의 지역변수 > 전역변수 > 내장형 이다.

    • Local: 함수 안의 범위

    • Enclosed function locals: 내부 함수 밖에, 외부 함수 안의 범위

    • Global: 모듈 범위로, .py 파일 내부의 모든 범위

    • Built-in: 인터프리터 전역적으로 사용가능한 내장형 함수 및 타입을 의미

 

클로저(Closure)

  • 함수 안에 함수를 선언하는 것으로, 바깥 함수를 외부 함수(outer), 안쪽 함수를 내부 함수(inner) 또는 클로저(closure)라고 한다.

  • 내부 함수에서는 외부 함수의 지역변수(매개변수 포함)에 접근할 수 있다.

    • 단, 내부 함수에서 외부 변수의 쓰기 작업을 수행하려면 nonlocal 키워드를 사용해야 한다.

    • 읽기는 자유롭게 가능하다.

>>> def outer(num):
...     num += 3
...     def inner():
...             nonlocal num
...             num *= 3
...             return num
...     return inner
...
>>> f = outer(1)
>>> f()
12
>>> def outer(num): # nonlocal 키워드를 쓰지 않을 경우 다음과 같은 예외가 발생한다.
...     num += 3
...     def inner():
...             num *= 3
...             return num
...     return inner
...
>>> f = outer(1)
>>> f()
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 4, in inner
UnboundLocalError: local variable 'num' referenced before assignment
  • 메모리 관점: 외부 함수가 호출되면 외부 함수 자체는 메모리에 남아있지만(함수 주소) 변수들은 모두 제거된다. 그러나 내부 함수는 외부 함수 범위의 변수들을 사용하면 자신의 영역에 별도로 저장하기 때문에 f = outer(1) 이후에도 nums 변수는 메모리(내부 함수 영역)에 남아 있는 것이다.

    • 파이썬에서 함수는 일급 객체로 실제로 type(outer) 를 해보면 <class 'function'>이 출력된다. 따라서, 위와 같은 정보를 확인하려면 다음과 같이 객체의 멤버 변수나 함수를 이용하면 된다.

>>> type(outer)
<class 'function'>
>>> f.__closure__ # cell 이라는 객체로 튜플 타입으로 저장되어 있다.
(<cell at 0x1061e49d0: int object at 0x10607bbe0>,)
>>> type(f.__closure__[0])
<class 'cell'>
>>> f.__closure__[0].cell_contents # 변수에 저장된 값이 메모리에 남아있다!
12
  • 외부 함수에는 여러 개의 내부 함수(closure)를 선언할 수 있고 내부 함수끼리 서로 호출이 가능하다.

    • 그러나, 외부 함수의 closure 변수는 모든 내부 함수의 정보를 가지고 있지 않다.

    • 다음과 같이 내부 함수가 여러 개일 경우 호출되는 순서에 따라 내부 함수들이 저장되며, 내부 함수 A가 내부 함수 B를 호출하면 내부 함수 A의 __closure__ 에 내부 함수 B가 저장된다.

    • 사용되지 않는 내부 함수는 저장되지 않는다.

>>> def fn1():
...     def inner():
...             return 1
...     def inner2():
...             n = inner()
...             return n * 3
...     def inner3():
...             n = inner2()
...             return n + 5
...     return inner3
...

>>> x = fn1()
>>> x()
8
>>> x
<function fn1.<locals>.inner3 at 0x106303820>
>>> x.__closure__
(<cell at 0x1062dd400: function object at 0x106303940>,)
>>> x.__closure__[0].cell_contents # inner3 함수 객체 안에 inner2 함수 객체가 저장되어 있다.
<function fn1.<locals>.inner2 at 0x106303940>
>>> x.__closure__[0].cell_contents.__closure__
(<cell at 0x1062dd430: function object at 0x106303af0>,)
>>> x.__closure__[0].cell_contents.__closure__[0].cell_contents # 마찬가지.
<function fn1.<locals>.inner at 0x106303af0>

 

데코레이터(Decorator)

  • 클로저가 가장 많이 사용되는 코딩 패턴으로는 데코레이터(Decorator)가 있다. 이름에서 알 수 있듯이 코드에서 무언가를 꾸며주는 역할을 한다. 실제로 객체 지향 패턴에도 데코레이터 패턴이라는게 있는데 파이썬에서는 클래스로 선언하는 것보다 함수로 선언하는 것이 많이 사용된다.

  • 특징은 함수를 인자로 받아서 내부 함수로 한번 감싸서(wrapping) 반환을 한다는 점이다.

  • 보통 둘 이상의 함수에 대해 추가적인 작업이 선행되어야 하는 경우 자주 쓰인다.

    • 예를 들어, 데이터를 넘기기 전 공통된 전처리가 필요한 경우, 로깅, 리다이렉트, 성능 테스트

def decorator_fn(original_fn):
    def wrapped_fn(*args, **kwargs):
        if len(args) == 2:
        	args = (args[0] + args[1], args[0] * args[1])
        return original_fn(*args, **kwargs)
    return wrapped_fn

@decorator_fn
def fn1():
	print("HI")

@decorator_fn
def fn2(a, b):
	print(a, b)

fn1() # HI
fn2(2, 3) # 5, 6
  • 위에서 @을 썼는데 이는 fn = decorator_fn(fn1) 과 동일하게 동작하나 후자는 가독성이 낮아 잘 쓰이지 않는다.

  • 클래스에서는 __call__() 이라는 메소드를 이용해서 다음과 같이 선언할 수 있다. 위와 동일하게 동작한다.

class DecoratorClass:
	def __init__(self, original_fn):
    	self.fn = original_fn
        
    def __call__(self, *args, **kwargs):
    	if len(args) == 2:
        	args = (args[0] + args[1], args[0] * args[1])
        return self.fn(*args, **kwargs)
        
@DecoratorClass
def fn1():
	print("HI")

@DecoratorClass
def fn2(a, b):
	print(a, b)

fn1() # "HI"
fn2(2, 3) # 5, 6

 

Numbers

  • 파이썬은 동적으로 타입이 변하기 때문에, 정수를 저장한 변수에 실수도 저장할 수 있다. 물론 다른 타입도 가능하다.

>>> x = 1
>>> type(x)
<class 'int'>
>>> x = 0.5
>>> type(x)
<class 'float'>
  • 변수의 크기는 지정되지 않으므로 C언어의 long long 과 int 크기의 숫자도 같은 변수에 저장된다.

    • 단, 값이 매우 클 경우 inf 또는 지수 형태(예시: 1e+123)로 표현된다.

>>> x = 1e123
>>> x
1e+123
>>> '%d' % x
'999999999999999977709969731404129670057984297594921577392083322662491290889839886077866558841507631684757522070951350501376'
  • immutable: 불변 객체(생성 후 상태를 바꿀 수 없는 객체), 다른 변수로 동일한 메모리 주소에 쓰기 작업 수행 시 복사되어 원본이 변하지 않는다.

    • y = x 라인까지는 두 변수 모두 같은 메모리를 참조하지만, y에 연산을 수행할 경우 메모리가 복사되어 원본 데이터가 변경되지 않는다.

>> x = 3
>> y = x
>> y += 1
>> x
3
>> y
4

 

String

  • 문자열 타입으로, 큰 따옴표와 작은 따옴표 모두 문자열로 표현된다. "blabla" 와 'blabla' 는 같다.

>>> x = "abcd"
>>> type(x)
<class 'str'>
>>> x == 'abcd'
True

 

  • immutable: 불변 객체(생성 후 상태를 바꿀 수 없는 객체), 다른 변수로 동일한 메모리 주소에 쓰기 작업 수행 시 복사되어 원본이 변하지 않는다.

>> x = 'abcd'
>> y = x
>> y += 'e'
>> x
'abcd'
>> y
'abcde'
  • 파이썬에서는 시퀀스 형에 대해 슬라이싱(slicing) 기능을 제공하는데, 앞으로 설명할 튜플과 리스트, 그리고 문자열은 시퀀스 형에 해당된다. 단, 문자열은 리스트와 달리 특정 위치에 값을 할당할 수 없으며, 수정하길 원한다면 다음과 같이 슬라이싱을 활용할 수 있다.

    • 파이썬에서는 슬라이싱과 관련해서 유용한 연산들을 많이 할 수 있는데, 대표적인 예로 문자열 뒤집기가 있다.

>>> x = 'helloempty'
>>> y = 'emptyworld'
>>> x + y
'helloemptyemptyworld'
>>> x[:5] + y[-5:]
'helloworld'
>>> x[::-1] # 문자열 뒤집기
'ytpmeolleh'
  • 지원하는 메소드는 다음과 같고, 호출 시 원본은 바뀌지 않는다.

>>> x.
x.capitalize(    x.isalpha(       x.ljust(         x.split(
x.casefold(      x.isascii(       x.lower(         x.splitlines(
x.center(        x.isdecimal(     x.lstrip(        x.startswith(
x.count(         x.isdigit(       x.maketrans(     x.strip(
x.encode(        x.isidentifier(  x.partition(     x.swapcase(
x.endswith(      x.islower(       x.replace(       x.title(
x.expandtabs(    x.isnumeric(     x.rfind(         x.translate(
x.find(          x.isprintable(   x.rindex(        x.upper(
x.format(        x.isspace(       x.rjust(         x.zfill(
x.format_map(    x.istitle(       x.rpartition(
x.index(         x.isupper(       x.rsplit(
x.isalnum(       x.join(          x.rstrip(
메소드 이름 용도  
capitalize() 첫 글자만 대문자로 바꾼 문자열을 반환
isalnum() 문자열에 숫자, 알파벳로만 구성되어 있으면 true, 아니면 false
isdigit() 문자열이 숫자로만 구성되어 있으면 true, 아니면 false
islower() 문자들이 모두 소문자이면 true, 아니면 false
isupper() 문자들이 모두 대문자이면 true, 아니면 false
isnumeric() 문자들이 모두 유니코드 문자이면 true, 아니면 false
isspace() 문자들이 모두 공백이면 true, 아니면 false
join([...]) 배열 원소가 모두 문자열이면, 지금 문자열을 구분자로 하나로 합쳐서 반환
ljust(width, fillchar=' ') 문자열이 width보다 짧으면 좌측으로 정렬 후 나머지를 fillchar로 채워서 반환
rjust(width, fillchar=' ') 문자열이 width보다 짧으면 우측으로 정렬 후 앞부분을 fillchar로 채워서 반환
lower() 모든 대문자를 소문자로 바꿔서 반환
upper() 모든 소문자를 대문자로 바꿔서 반환
lstrip(ch=' ') 왼쪽에 ch를 모두 제거해서 반환. ex) " a cde" -> "a cde"
rstrip(ch=' ') 오른쪽에 ch를 모두 제거해서 반환. ex) "acd e " -> "acd e"
strip(ch=' ') 양쪽에 ch를 모두 제거해서 반환
replace(old, new[, max]) 최대 max개까지 old를 new로 변경, max가 없으면 모두 변경한 후 반환
swapcase() 대문자는 소문자로, 소문자는 대문자로 바꾼 것을 반환

 

List

  • 리스트 타입으로, 다른 타입의 데이터를 함께 저장할 수 있다.

>>> x = [1,2,3,'hi',0.5]
>>> x
[1, 2, 3, 'hi', 0.5]
>>> type(x)
<class 'list'>
  • mutable: 가변 객체(생성 후에도 상태를 변경할 수 있는 객체), 다른 변수로 동일한 메모리 주소에 쓰기 작업 시 복사되지 않고 원본이 수정된다. 따라서 원본을 유지하려면 깊은 복사(deepcopy)를 해야 한다.

    • y = x 이후에도 두 변수는 모두 같은 메모리 주소를 참조한다.

>> x = [1,2]
>> y = x
>> y.append(3)
>> x
[1,2,3]
>> y
[1,2,3]
>> x = [1,2]
>> y = x.copy() # 깊은 복사를 할 경우 immutable 하게 사용 가능.
>> y.append(3)
>> x
[1,2]
>> y
[1,2,3]
  • 동일한 타입의 데이터를 사용하길 원한다면 array 모듈을 사용해야 한다.

>>> import array
>>> A = array.array('i', [1,2,3,4])
>>> A.
A.append(       A.fromfile(     A.itemsize      A.tolist(
A.buffer_info(  A.fromlist(     A.pop(          A.tostring(
A.byteswap(     A.fromstring(   A.remove(       A.tounicode(
A.count(        A.fromunicode(  A.reverse(      A.typecode
A.extend(       A.index(        A.tobytes(
A.frombytes(    A.insert(       A.tofile(
  • 지원하는 메소드는 다음과 같다.

>>> x.
x.append(   x.copy(     x.extend(   x.insert(   x.remove(   x.sort(
x.clear(    x.count(    x.index(    x.pop(      x.reverse(
메소드 이름 용도  
append(element) 리스트의 끝에 원소를 추가
copy() 리스트를 복사하여 반환
extend([...]) 리스트의 끝에 새 리스트를 추가
insert(index, element)   해당 위치에 원소를 삽입
remove(element) 원소를 삭제
sort() 리스트를 오름차순으로 정렬
clear() 리스트의 모든 원소를 제거
count(element) 리스트에 저장된 원소의 개수를 반환
index(element) 원소의 인덱스를 반환
pop() 리스트의 끝에서 원소를 제거한 후 반환
reverse() 리스트를 뒤집음
  • 리스트의 원소를 지우는 방법은 del 키워드를 사용해서도 가능한데, 주로 원소를 모르고 인덱스로 연산할 때 유용하다.

>>> x = [1,2,3]
>>> x
[1, 2, 3]
>>> del x[2]
>>> x
[1, 2]
  • 시퀀스 형(리스트, 문자열, 튜플 등)은 + 연산자* 연산자를 이용해서 합칠 수 있다. 매우 유용하다.

    • 단, 다른 시퀀스 형끼리는 연산할 수 없다.

>>> [1,2,3] + [4,5,6] + [7,8,9]
[1, 2, 3, 4, 5, 6, 7, 8, 9]
>>> [1,2] * 3
[1, 2, 1, 2, 1, 2]
>>> [[1,2,3], [4,5,6]] + [1,2,3]
[[1, 2, 3], [4, 5, 6], 1, 2, 3]
>>> "hello" + "world"
'helloworld'
>>> "A" * 12
'AAAAAAAAAAAA'
>>> (3,)*13 # 튜플은 원소가 1개일 때 뒤에 콤마(,)를 써야한다. 안그러면 스칼라 곱셈으로 계산된다.
(3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3)

 

Tuple

  • 튜플 타입으로, 리스트와의 차이점은 다음과 같다.

    • 리스트는 []을, 튜플은 ()을 사용해서 선언한다.

    • 리스트는 크기나 원소를 동적으로 변경할 수 있으나, 튜플은 읽기만 가능(read-only)하다.

    • 리스트는 튜플보다 속도가 느리다.

  • immutable: 불변 객체(생성 후 원본이 변하지 않는 객체), 다른 변수가 동일한 메모리 주소에 쓰기 작업 수행 시 복사되어 원본이 수정되지 않는다.

>> x = (1,2)
>> y = x
>> y += (3,)
>> x
(1,2)
>> y
(1,2,3)
  • 지원하는 메소드는 다음과 같으며, count(element) 는 해당 원소의 개수를 반환하고, index(element) 는 해당 원소의 인덱스를 반환한다.

>>> x.
x.count(  x.index(

 

Dictionary

  • 딕셔너리 타입으로, key와 value 쌍을 원소로 저장한다.

    • key는 unhashable type(해쉬 불가능 타입)의 데이터는 해당될 수 없다. 주로 문자열, 숫자, 커스텀 객체가 해당된다.

>>> x = {'k1':1, 'k2':2, 3:"hello"}
>>> type(x)
<class 'dict'>

[+] 객체를 키로 쓰는 경우 메모리 주소로 동일한 키인지 구분한다.

>>> class Obj:
...     pass
...
>>> o = Obj()
>>> o
<__main__.Obj object at 0x10c1fddc0>
>>> x = {}
>>> x[o] = 1
>>> x
{<__main__.Obj object at 0x10c1fddc0>: 1}
  • mutable: 가변 객체(생성 후에도 원본이 변경되는 객체), 다른 변수에서 동일한 메모리에 쓰기 작업 수행 시 복사되지 않고 원본이 수정된다. 따라서 원본을 유지하려면 깊은 복사(deepcopy)를 해야 한다.

>> x = {'k1':2}
>> y = x
>> y['k2'] = 4
>> x
{'k1':2, 'k2':4}
>> y
{'k1':2, 'k2':4}
  • 지원하는 메소드는 다음과 같다.

>>> x.
x.clear(       x.get(         x.pop(         x.update(
x.copy(        x.items(       x.popitem(     x.values(
x.fromkeys(    x.keys(        x.setdefault(
메소드 이름 용도  
clear() 딕셔너리의 원소들을 모두 제거
get(key) 키에 해당되는 값을 반환
pop(key) 키에 해당되는 값을 제거해서 반환
update(dictionary) 현재 딕셔너리에 새로운 딕셔너리를 합침. 동일한 키가 있으면 새로운 값으로 수정된다.
copy() 딕셔너리를 복사해서 반환
items() 키와 값을 튜플 형태의 리스트로 반환. 주로 반복문에서 키와 값을 참조할 때 사용된다.
단, dict_items 라는 객체이며 리스트가 아니다.
>>> y = x.items()
>>> type(y) == type([])
False
>>> type(y)
<class 'dict_items'>
popitem() 마지막 아이템을 제거해서 반환
values() 값들만 리스트 형태로 반환하는데 dict_values 라는 객체이며 리스트는 아니다.
>>> y = x.values()
>>> type(y) == type([])
False
>>> type(y)
<class 'dict_values'>
fromkeys(seq[, value]) 시퀀스형의 원소들을 키로 갖는 딕셔너리를 반환하며, value는 기본값으로 쓰이는데 없으면 None을 값으로 갖는다.
keys() 키만 리스트 형태로 반환하는데 dict_keys 라는 객체이며 리스트는 아니다.
>>> y = x.keys()
>>> type(y) == type([])
False
>>> type(y)
<class 'dict_keys'>
setdefault(key[, value]) 해당 키에 대한 디폴트 값을 설정하는데, 키가 이미 있다면 바뀌지 않는다. value를 넘기지 않으면 None 이 설정된다.

 

set

  • 집합 자료형으로, set() 또는 {} 으로 선언할 수 있다.

>>> x = set({1,2,3})
>>> y = {1,2,3}
>>> type(x), type(y)
(<class 'set'>, <class 'set'>)
>>> x == y
True
  • 내부적으로 해쉬 테이블로 구현되어 있어 삽입/삭제/검색 시 O(1)의 비용이 든다.

  • 데이터는 순서 없이 저장되므로, 삽입한 순서와는 다르게 저장된다.

  • 인덱싱이나 슬라이싱이 불가능하다.

  • mutable: 가변 객체(생성 후에도 원본이 변경되는 객체)

  • 지원하는 메소드는 다음과 같다.

>>> x.
x.add(                          x.issubset(
x.clear(                        x.issuperset(
x.copy(                         x.pop(
x.difference(                   x.remove(
x.difference_update(            x.symmetric_difference(
x.discard(                      x.symmetric_difference_update(
x.intersection(                 x.union(
x.intersection_update(          x.update(
x.isdisjoint(
  • 집합 자료형으로는 frozenset 도 있는데 둘의 차이는 해시가능(hashable)의 유무이다.

    • set은 가변형이며 add() 나 remove() 메소드로 원소를 추가하거나 삭제할 수 있다. 원소에 대한 해시값이 따로 존재하지 않는다.

    • frozenset은 불변형이며 원소를 수정할 수 없다. 단, 원소에 대한 해시값이 별도로 존재한다.

    • frozeonset 은 딕셔너리의 key로 사용할 수 있다.

>>> x = { set([1,2]):1 }
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: unhashable type: 'set'
>>> x = { frozenset([1,2]):1 }
>>> x
{frozenset({1, 2}): 1}
  • frozenset 이 지원하는 메소드는 다음과 같다.

>>> x = frozenset()
>>> x.
x.copy(                  x.isdisjoint(            x.symmetric_difference(
x.difference(            x.issubset(              x.union(
x.intersection(          x.issuperset(

 

타입 변환 함수

  • int() – converts any data type into integer type

  • float() – converts any data type into float type

  • ord() – converts characters into integer

  • hex() – converts integers to hexadecimal

  • oct() – converts integer to octal

  • tuple() – This function is used to convert to a tuple.

  • set() – This function returns the type after converting to set.

  • list() – This function is used to convert any data type to a list type.

  • dict() – This function is used to convert a tuple of order (key,value) into a dictionary.

  • str() – Used to convert integer into a string.

  • complex(real,imag) – This functionconverts real numbers to complex(real,imag) number.

  • 모두 내장 함수(built-in function)이며, 인자로 주는 시퀀스형이 구조가 일치하지 않으면 예외를 일으킬 수 있다.

>>> int('123aa')
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
ValueError: invalid literal for int() with base 10: '123aa'
>>> int('123')
123

 

그 외

 

'Programming Language > Python' 카테고리의 다른 글

[정리 04] 파이썬 함수: LEGB 규칙, Closure, Decorator  (0) 2021.02.06
[정리 03] 유용한 기능들  (0) 2021.02.05
[정리 01] Python 기초  (0) 2021.02.04
간단한 알고리즘  (0) 2019.06.30
Tip (Reference)  (0) 2019.05.15

언어적 특징

  • 인터프리터(Interpreter) 언어로 별도의 컴파일 없이 실행된다.

  • 별도로 타입 명시를 하지 않으며, 실행 중에 타입이 변경 가능(Dynamically typed)하다. 

  • 객체 지향적(Object-oriendted) 언어로, 접근 제한자를 가지지 않지만 네이밍(Naming)의 언더스코어(_)로 접근 제한을 구분한다.

    • 이름 맨 앞에 언더스코어 1개이면 protected, 2개이면 private 이며, 그 외는 public 으로 구분한다.

  • 함수(function)와 클래스(class)는 일급 객체(The first-class objects)로, 모든 변수(매개변수, 로컬변수 등)에 할당 가능하며 일반적으로 다른 객체에 적용 가능한 연산을 모두 지원한다.

  • PYTHONPATH인터프리터가 파이썬 프로그램이 실행될 때 import된 모듈들이 실제 디스크에 어디에 위치했는지 알기 위해 사용하는 환경변수. 따라서 프로그램이 내장 모듈에 대해 ImportError 를 일으키면 가장 먼저 살펴봐야하는 부분이다.

  • PEP는 Python Enhancement Proposal의 약자로, 가독성 높은 파이썬의 코딩 스타일 중 하나이며, PEP8이 대중적이다.

  • 주석은 # comment""" comment """ 가 있는데, 후자는 docstring 용도로 사용된다.

    • 참고: Docstrings are not actually comments, but, they are documentation strings. These docstrings are within triple quotes. They are not assigned to any variable and therefore, at times, serve the purpose of comments as well.

  • 파이썬 인터프리터에서는 help() 와 dir() 이라는 함수를 사용할 수 있는데, 라이브러리를 잘 모를 경우 굉장히 유용하다.

    • The help() function is used to display the documentation string and also facilitates you to see the help related to modules, keywords, attributes, etc.

    • The dir() function is used to display the defined symbols. (클래스의 어떤 메소드가 있는지 확인할 때 매우 편하다.)

  • 파이썬 패키지(package)는 여러 모듈을 포함하는 네임스페이스의 집합이다. 최상위 패키지 디렉토리 안에 패키지 디렉토리가 있을 수 있는데 이들 안에 __init__.py 를 작성하면 해당 디렉토리 패키지임을 명시하는 것이다. 따라서 다른 디렉토리에서 다음과 같이 해당 모듈을 사용할 수 있다.

"""
예시: from 패키지명 import 모듈명

~/mypkg $ tree
.
├── utils
│   └── __init__.py
│   └── mini_module.py
│   └── large_module.py
└── subpkg
    ├── __init__.py
    └── new_module.py

mypkg 를 상위 디렉터리로 하고 위와 같은 구조를 가질 때,
subpkg의 new_module.py에서는 아래의 형태로 utils 의 모듈을 사용할 수 있다.
"""

from utils import mini_moudle
from utils.mini_module import *
from utils.large_moudle import MyObject

 

메모리 관리

  • 파이썬은 내부적(C언어로 작성되었음)으로 PyObject 라는 객체를 생성해서 데이터를 관리하는데, 데이터들은 Python Private Heap Space 라는 곳에 저장된다. 일반적으로 지역 변수는 스택에 저장된다고 배웠으나, 곧 살펴볼 파이썬의 내장 타입들은 모두 객체이며 힙 영역에 저장된다.

>>> a = 1
>>> type(a)
<class 'int'>
  • Private Heap 은 파이썬이 스스로 관리하는 곳이기 때문에 파이썬 코드로는 프로그래머가 직접적으로 메모리를 할당하거나 해제할 수 없다. 직접 살펴보고 싶다면 파이썬 C 라이브러리를 사용해야 한다.

  • PyObject는 멤버로 참조 카운트(reference count)를 가지는데, 이는 객체가 참조될 때 1씩 증가하고 참조되지 않을 경우 1씩 감소한다. 그리고 reference count = 0 이 되면 메모리는 해제된다.
    • sys 모듈의 getrefcount() 함수를 이용해서 count를 직접 확인해볼 수 있다.

    • 아래의 첫 출력 결과가 2인 것은 sys.getrefcount() 함수의 인자로 참조되었기 때문에 1이 아니라 2로 출력되는 것이다.

>>> import sys
>>> a = []
>>> sys.getrefcount(a)
2
>>> b = a
>>> sys.getrefcount(a)
3
>>> del a
>>> sys.getrefcount(b)
2
  • 그러나, 아래와 같이 순환 참조(Circular References)가 발생할 경우 레퍼런스 카운팅 기법으로는 메모리를 관리할 수 없다.

    • 순환 참조란 서로 다른 객체가 서로를 참조하는 경우를 의미한다. 여기서 두 객체가 삭제될 경우 참조 횟수는 0보다 큰 상태이지만 객체를 이미 삭제해버려서 해당 객체들에 접근할 수 없다는 문제가 발생한다.

>>> class Obj:
...     def __init__(self):
...         print('Hello,', id(self))
...
...     def __del__(self):
...         print('Bye,', id(self))
...
>>> o1 = Obj()
Hello, 4470886656
>>> o2 = Obj()
Hello, 4470654432
>>> sys.getrefcount(o1)
2
>>> sys.getrefcount(o2)
2
>>> dir() # 선언한 클래스와 생성된 인스턴스가 네임스페이스에 존재한다.
['Obj', '__annotations__', '__builtins__', '__doc__', '__loader__', '__name__',
'__package__', '__spec__', 'o1', 'o2']
>>> o1.x = o2
>>> o2.x = o1
>>> sys.getrefcount(o1)
3
>>> sys.getrefcount(o2)
3
>>> del o1 # o2.x 로 참조되므로 o1의 reference count = 1
>>> del o2 # o1.x 로 참조되므로 o2의 reference count = 1
>>> dir() # 삭제되었기 때문에 네임 스페이스에는 없으나 실제로 __del__() 메소드는 호출되지 않았다.
['Obj', '__annotations__', '__builtins__', '__doc__', '__loader__', '__name__',
'__package__', '__spec__']
  • 순환 참조와 같은 이슈를 해결하기 위해 파이썬은 가비지 콜렉터(Garbage Collector) 기능을 제공한다. 가비지 콜렉터는 gc 모듈을 import 해서 수동으로 관리할 수 있지만 파이썬에서는 자동으로 관리할 것은 권장한다.

  • 가비지 콜렉팅은 다음과 같이 동작한다.

    • 0~2세대로 객체들을 구분(이를 Generational Garbage Collecting 이라 한다.)하는데, 처음 객체가 생성되면 0세대로 분류된다. 그리고 각 세대별로 저장된 객체가 임계값(threshold, 최대로 저장할 객체의 개수)에 이르면 쓰레기 수집(collect)이 진행된다.

    • 한 번 수집기가 실행될 때마다 해당 프로그램은 중단되므로 성능에 큰 영향을 미친다.

    • 수집기가 실행되고 각 세대별로 살아남은 객체는 다음으로 옮겨진다. 0세대에서 살아남은 객체는 1세대로, 1세대에서 살아남은 객체는 2세대로 옮겨진다.

    • 임계값은 세대마다 다르며, Generational Hypothesis(최근에 생성된 객체일수록 빨리 제거되며, 최근에 생성된 객체는 오래된 객체를 참조할 가능성이 적다는 가설)에 따라 0세대의 임계값이 가장 높다.

  • gc 모듈을 이용해서 임계값을 바꾸거나 쓰레기 수집 기능을 직접 실행할 수 있다.

    • 임계값 변경 함수: gc.set_threshold(threshold0[, threshold1[, threshold2]])

    • 쓰레기 수집 기능 함수: gc.collect()

>>> import gc
>>> gc.get_threshold()	# 0세대는 700, 1세대는 10, 2세대는 10개까지 보관한다.
(700, 10, 10)
>>> gc.get_count()	# 0세대는 71개의 객체가 있다.
(71, 0, 0)
>>> gc.collect()	# 순환 참조로 삭제되지 않았던 객체들이 삭제되었다.
Bye, 4470886656
Bye, 4470654432
4
>>> gc.get_count()
(22, 0, 0)
  • 그 외에 참조 카운트로 메모리를 관리하다보니 Global Interpreter Lock(이하 GIL) 과 관련된 이슈도 존재한다. GIL은 파이썬 인터프리터 전역에 락 변수(lock variable)를 설정해놓은 것으로, 멀티 쓰레딩 시 임계 영역(critical section)에서는 단일 쓰레드만이 실행되도록 한다.

    • 임계 영역이란 둘 이상의 쓰레드가 동시에 접근했을 때 프로그램의 실행에 critical 하게 영향을 줄 수 있는 부분을 뜻한다.

    • 한 프로세스에서 실행중인 쓰레드들은 스택(+레지스터)을 제외한 나머지 모든 영역을 공유한다.

  • GIL 을 도입한 이유는 파이썬의 모든 객체가 힙에 저장되다 보니 멀티쓰레딩 환경에서 reference count 변수에 경쟁 조건(race condition)이 발생하는 것을 막기 위해서이다. 만약 reference count 변수를 멤버로 가지는 파이썬 객체마다 락 변수를 걸게 되면 데드락(deadlock)이 발생할 수 있으며 성능에 좋지 않기 때문에 파이썬 인터프리터 전역적으로 막아버린 것이다. 즉, 파이썬에서 단일 쓰레드만이 특정 객체 접근할 수 있도록 했다. 따라서 threading 모듈을 이용해서 멀티쓰레딩을 하더라도 실제로는 1개의 CPU가 여러 쓰레드를 번갈아가면서 실행시키다보니 컨텍스트 스위칭(context switching)이 빈번하게 발생하여 단일 쓰레드보다 성능이 떨어질 수 있다. (물론 이와 관련된 해결책으로 비동기 프로그래밍, 멀티프로세싱 등이 있는데 다음에 후술하겠다.)

 

Built-in Types

  • Integers

  • Floating-point

  • Complext numbers

  • Strings

  • Boolean

  • Built-in functions: 파이썬 인터프리터에 내장된 함수들로 위에서 설명했던 help()와 dir()이 이에 해당된다. 별도의 모듈을 import하지 않고 사용가능하다.

    • type() vs isinstance(): 파이썬의 데이터들은 모두 객체로 저장되지만, 내장형 타입(표준 데이터 타입)을 검사하기 위해 type(변수명) 함수를 사용하고 커스텀 클래스로 만든 객체들은 isinstance(변수명, 클래스명)로 클래스 타입을 검사한다. 즉, 파이썬 환경에서 인스턴스(=객체)와 내장형 타입을 구분하기 위해 제공하는 함수라고 볼 수 있다. 물론 표준 데이터 타입에 대해서도 isinstance 함수를 사용할 수 있다. 예를 들어, x = 1; isinstance(x, int); 의 출력 결과는 True 이다.

  • 내장형 타입 관련 연산

  • 비교 연산자

    • 파이썬에서는 직접 변수의 값을 비교할 때는 == 연산자를 사용하지만, 메모리 주소를 비교할 때(=동일 객체인지 등)는 is 연산자를 사용한다. 변수에 저장된 메모리 주소를 확인하려면 id(변수명) 함수를 사용하면 된다.

>>> a = 1000
>>> b = 1000
>>> a == b
True
>>> a is b
False
>>> id(a), id(b)
(4398724144, 4398724176)

 

+ Recent posts