클래스와 인스턴스

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

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

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

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

>>> 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

 

수학 연산

  • 숫자 데이터에 ** 연산자를 사용할 경우 지수 승을 구할 수 있다. x ** y == x^y

  • 숫자 데이터에 // 연산자를 사용할 경우 floor division 연산을 수행할 수 있는데, 이 연산은 나눗셈 후 소수점 밑으로 버리고 정수부분만 남기는 것이다.

>>> 2**4
16
>>> 1.2 // 0.2
5.0
>>> 1.2 / 0.2 # 실제 소수점 연산은 결과가 정수 연산과 다를 수 있으므로 유의해야 한다.
5.999999999999999

 

Comprehension

  • 짧은 문법만으로 반복 가능한 객체를 생성하게 해주는 기능으로, 시퀀스 형(List, Dictionary, Set)에 대해서만 사용가능하다.

>>> [x**2 for x in range(10)]		# List
[0, 1, 4, 9, 16, 25, 36, 49, 64, 81]
>>> {x:x+4 for x in range(1,10,2)}	# Dictionary
{1: 5, 3: 7, 5: 9, 7: 11, 9: 13}
>>> {x for x in range(10)}		# Set
{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}
  • 또한, 중첩 반복문과 조건문까지 사용가능하다.

>>> [x**2 for x in range(10) if x**2 & 1 == 0]
[0, 4, 16, 36, 64]
>>> [x*y for x in range(4) for y in range(4)]
[0, 0, 0, 0, 0, 1, 2, 3, 0, 2, 4, 6, 0, 3, 6, 9]
  • 괄호를 이용하면 제너레이터(Generator) 객체를 생성할 수 있는데, 제너레이터란 시퀀스 값을 만들어내는 객체로 메모리 공간을 절약하게 해주는 매우 유용한 기능이다.

    • 메모리 절약의 의미는 시퀀스 형의 모든 원소를 만들어서 메모리에 올리는게 아니라, 각 원소를 한 번씩 순회하기 때문에 메모리에 모든 원소가 올라갈 필요가 없다는 뜻이다.

    • next() 함수를 써서 다음 값을 계속 꺼내올 수 있는데 끝에 도달하면 StopIteration 예외를 던진다.

    • 제너레이터 객체는 함수 형태로 yield 라는 키워드를 return 대신 사용하는 방식으로 많이 작성되며, 더 자세한 정보는 여기(파이썬 코딩 도장)를 참고하면 좋다.

>>> y = (x**2 for x in range(4))
>>> y
<generator object <genexpr> at 0x10c26d580>
>>> next(y)
0
>>> next(y)
1
>>> next(y)
4
>>> next(y)
9
>>> next(y)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
StopIteration
>>> def gen():
...    for x in range(4):
...        yield x**2
...
>>> y = gen()
>>> y
<generator object gen at 0x10c18f660>
>>> next(y)
0
>>> next(y)
1
>>> next(y)
4
>>> next(y)
9
>>> next(y)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
StopIteration
>>> z = gen()
>>> list(z)	# == [i for i in gen()]
[0, 1, 4, 9]
>>> for i in gen():
...     print(i, end=' ')
...
0 1 4 9

 

Pack vs Unpack

  • * 연산자를 이용해서 시퀀스형 데이터들을 쉽고 간편하게 묶거나(packing) 풀거나(unpacking) 할 수 있다.

  • 주로 함수의 매개변수에 사용되는 경우가 있다. 매개변수에 *args**kwargs 를 사용할 경우 인자를 보다 유연하게 선별할 수 있다.

    • args 는 리스트로 받은 것을 함수에서 튜플로 관리한다. args[0] 은 첫번째 인자이다.

    • kwargs 는 딕셔너리로 받아서 함수에서 키워드 기반으로 인자를 관리한다. kwargs[키워드] 는 해당 인자를 값으로 갖는다.

>>> def fn(word, count):
...     print(word * count)
...

>>> args = ['monkey', 4]
>>> fn(*args)
monkeymonkeymonkeymonkey

>>> kwargs = {'count': 3, 'word': 'best'}
>>> fn(**kwargs)
bestbestbest

>>> a, *b, c = [i for i in range(1,7)]
>>> print(a, b, c)
1 [2, 3, 4, 5] 6
>>> v = [1,2,3,4]
>>> print(*v)
1 2 3 4
>>> x = '123'
>>> print(*x)
1 2 3
>>> a, *b = x
>>> a
'1'
>>> b
['2', '3']

 

자주 쓰이는 내장형 함수

  • 내장형 함수(Built-in Functions)는 별도의 import 없이 인터프리터에서도 사용가능한 함수들이다.

  • int(), float(), str() 등의 타입 변환 함수도 자주 쓰지만, 아래의 함수들도 타입 변환 함수만큼이나 매우 유용하다.

  • map(function, iterable, ...): 첫번째 인자로 함수를 받고 두번째 인자로 iterable 객체(값을 차례대로 꺼낼 수 있는 객체)를 받는다. 단, 넘기는 함수의 인자는 객체의 각 원소를 받을 수 있는 형태여야 하며, 원소의 함수 실행결과를 iterator 객체로 반환한다.

>>> x = map(int, ['1','2','3'])
>>> x
<map object at 0x10c1907c0>
>>> next(x)
1
>>> next(x)
2
>>> next(x)
3
>>> next(x)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
StopIteration

>>> x = map(int, ['1','2','3'])
>>> list(x)
[1, 2, 3]
>>> a, b, c = map(int, ['1','2','3'])
>>> print(a, b, c)
1 2 3
  • filter(function, iterable): 첫번째 인자로 함수를 받고 두번째 인자로 iterable 객체(값을 차례대로 꺼낼 수 있는 객체)를 받는다. 각 원소를 함수의 인자로 넘겼을 때 나온 결과가 참(True)인 것들만 걸러내서 iterator 객체로 반환한다.

>>> x = filter(lambda x: x % 2 == 0, [1,2,3,4,5])
>>> x
<filter object at 0x10c26bd60>
>>> next(x)
2
>>> next(x)
4
>>> next(x)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
StopIteration
>>> x = {'absdf': 23, 'jiower': 4342, 'dog': 994, 'cat': 9212, 'duck': 1}
>>> sorted(x) # 키를 기준으로 오름차순 정렬
['absdf', 'cat', 'dog', 'duck', 'jiower']
>>> sorted(x, key=lambda k: x[k]) # 값을 기준으로 오름차순 정렬
['duck', 'absdf', 'dog', 'jiower', 'cat']
>>> sorted(x, key=lambda k: x[k], reverse=True) # 값을 기준으로 내림차순 정렬
['cat', 'jiower', 'dog', 'absdf', 'duck']

>>> x = [('absdf', 23), ('jiower', 4342), ('dog', 994), ('cat', 9212), ('duck', 1)]
>>> sorted(x, key=(lambda x: x[1]), reverse=True) # 원소의 두번째 값을 기준으로 내림차순 정렬
[('cat', 9212), ('jiower', 4342), ('dog', 994), ('absdf', 23), ('duck', 1)]
  • zip(*iterables): 여러 개의 iterable 객체를 받으면, 인덱스 단위로 묶어서(aggregates) 제너레이터 객체로 반환한다.

    • 여러 개의 iterable 중 가장 짧은 길이의 것에 맞춰져서 나오기 때문에 길이가 맞지 않으면 중간에 데이터가 짤릴 수 있다.

    • 반복문에서 둘 이상의 시퀀스 형의 원소를 나란히 처리할 때 자주 쓰인다.

>>> x = zip([2*i+1 for i in range(5)], [i**2 for i in range(8)])
>>> x
<zip object at 0x10c2bde80>
>>> list(x)
[(1, 0), (3, 1), (5, 4), (7, 9), (9, 16)]
>>> x = zip([2*i+1 for i in range(5)], [i**2 for i in range(8)], [1,2,3,4])
>>> list(x)
[(1, 0, 1), (3, 1, 2), (5, 4, 3), (7, 9, 4)]
  • enumerate(iterable, start=0): iterable 객체를 start 번째 부터 순환하는데 인덱스도 함께 반환한다. 반복문 내에서 원소의 인덱스가 필요할 때 유용하게 쓰인다.

>>> for idx, el in enumerate(['a','b','c']):
...     print('idx =', idx, ', element =', el)
...
idx = 0 , element = a
idx = 1 , element = b
idx = 2 , element = c
  • range(start, stop[, step]): stop 만 인자로 받기도 하지만, 그 함수는 이미 유명하므로 생략. 세번째 인자로 step 값을 주면 step 단위로 start와 stop 사이의 값을 리스트 형태로 갖는다. 반환형은 range 클래스이기 때문에 반복문 밖에서 쓸 때는 list(range(10)) 이런식으로 한 번 형변환을 해줄 필요가 있다.

  • next(iterator[, default]): iterator 객체의 다음 원소를 반환하는 함수로, 내부적으로 iterator 객체의 __next__() 를 호출한다. iterable 객체(시퀀스형 데이터들, list, str 등)의 경우 iter() 함수를 사용해서 iterator 객체로 바꿀 수 있다.

>>> x = 'best'
>>> y = iter(x)
>>> next(y)
'b'
>>> next(y)
'e'
>>> next(y)
's'
>>> next(y)
't'
>>> next(y)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
StopIteration
  • input([prompt]): 표준 입력받을 때 사용되는 함수로, 알고리즘 문제 풀 때 다음과 같이 많이 이용된다.

>>> a, b, c = map(int, input().rstrip().split()) # 1줄에 개별 원소 입력 받기
3 6 9
>>> print(a, b, c)
3 6 9
>>> x = [int(input()) for _ in range(3)] # 여러 줄로 개별 원소 입력 받기
99
102
33
>>> x
[99, 102, 33]

 

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

+ Recent posts