Python 심화 - 2

closure

  • 함수 내부에 상태 정보를 가지고 있다.

  • 상태 정보 = 관련 있는 데이터(함수가 처리하려는 데이터)

  • class를 사용할 수 없을 때 쓴다.

    • 계좌 만들기 예제

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      31
      32
      33
      34
      35
      def account(clnt_name, balance):  # 상태 정보 - free variable
      def change_money(money):
      nonlocal balance
      balance += money
      return (clnt_name, balance)
      return change_money

      # global variable - 맨 바깥쪽
      # local variable - 함수 내
      # free variable - 상태 정보

      my_acnt = account('greg', 5000)
      your_acnt = account('john', 3000)

      type(my_acnt)
      >> function

      my_acnt.__name__ == your_acnt.__name__
      >> True

      my_acnt.__name__
      >> 'change_money'

      # input 값 + 상태 정보 = output이 나온다.
      my_acnt(1000)
      >> ('greg', 6000)

      your_acnt(1000)
      >> ('john', 4000)

      # cell_contents : 담겨 있는 상태 정보를 불러옴
      cells = my_acnt.__closure__
      for cell in cells:
      print(cell.cell_contents, end = ' ')
      >> 6000 greg

문자, 문자열

  • character set

    • 문자 집합

    • 문자(character)를 모아둔 것

    • ex) 라틴 문자

  • character encoding

    • 문자 인코딩

    • 문자 집합을 메모리에 저장 · 통신하기 위해 부호화하는 방식

    • ex) 모스 부호

  • code point

    • 문자 하나에 정수 하나를 매핑해둠

    • ex) ASCII 코드의 ‘a’는 97

  • ASCII CODE

    • 0 ~ 127의 7bit로 모두 표현 가능

    • 128가지 밖에 표현하지 못함

    • ASCII table 참고

  • UNICODE

    • 0x0000 ~ 0xFFFF의 다국적 기본평면(BMP)에서부터 16번 평면까지 총 17개 존재

    • 기본 평면(BMP)은 한글과 한중일 통합 한자들로 이루어져 있음

  • ASCII CODE와 UNICODE

  1. ASCII CODE

    • ‘a’ = 97 ==메모리에 저장될 때==> 0b01100001 = 1byte로 표현
  2. UNICODE

    • ‘가’ = 0xac00 =(메모리에 저장될 때)=> 0b1010110000000000 = 2byte로 표현

    • 기본 평면(BMP)이 아닌 2번째 평면부터는 0xFFFF가 넘어감 = 3byte를 사용

    • 2byte + 4bit = 3byte를 쓰기보다는 padding byte를 추가해서 4byte로 쓰는 것이 좋음

    • 다양한 인코딩 방식(UTF-8, UTF-16, UTF-32)이 생겨남

유니코드 부호화 방식

  • UTF-8

    • 영어 때문에 1byte(8bit)를 기반으로 보내는 가변 길이 인코딩 방식

    • 한글을 보낼 때는 3byte 사용

    • UTF-8이 기준

    • UTF-8 구조

      • U+0800 - U+FFFF ==> 1110XXXX 10XXXXXX 10XXXXXX의 3byte 구조

      • ‘가’의 유니코드 ==> U+AC00 ==> 1010 1100 0000 0000 ==> 1010 110000 000000

      • X자리에 넣기 ==> 11101010 10110000 10000000 ==> 0x EA B0 80

      • 즉, 3byte로 인코딩됨

  • UTF-16

    • 1차 평면(BMP)에서는 2byte로 표현

    • 2차 평면 이상부터는 앞에 “0000”을 붙여서 4byte로 표현

    • 다국적 기본 평면(BMP) : 16bit = 2byte

    • 다국적 보충 평면(SMP) 이상 : 32bit = 4byte

  • UTF-32

    • 모두 4byte로 보내는 인코딩 방식
  • CP949

    • 통합형 한글 코드(Unified Hangul Code)

    • 마이크로 소프트 사에서 만듦

    • 현대의 모든 한글 수용

  • 유니코드 사용 방법(ASCII 호환)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    '\u0041'
    >> A

    # 한글의 범위 U+AC00 ~ U+D7AF
    '\uac00'
    >> '가'

    a = '가'
    b = a. encode('utf-8')

    type(b)
    >> bytes

    print(b)
    >> b'\xea\xb0\x80'

    # 0x ea b0 80의 3byte가 나온다.
    # 통신할 때는 1byte씩하여 ea, b0, 80으로 나눠서 보내진다.
  • little endian과 big endian

    • CPU에 따라 다르다.
    1. big endian

      • ‘1234’의 수에서 most significant disit은 맨 앞의 1이 된다.

      • Sparc / RISC CPU 계열

    2. little endian

      • ‘4321’의 수에서 most significant disit은 맨 뒤의 1이 된다.

      • Intel CPU 계열

    • big endian과 little endian 사이에서 0xac00을 보낸다고 할때,

      ac 00 순으로 가는건지, 00 ac 순으로 가는건지 기준이 뭘까?

    • Networking에서의 protocol 기준은 ‘big endian’으로 한다.

    • UTF-8은 ‘endianless’이다. = little endian이건 big endian이건 상관없다.

    • ex) 0x ea b0 80은 80 b0 ea로 보낸다고 생각하겠지만, 변환하지 않는다.

      1
      2
      3
      4
      5
      6
      c = a.encode('utf-16')
      print(c)
      >> b'\xff\xfe\x00\xac'

      # 00 ac로 나오는 것을 볼 수 있다.
      # little endian으로 읽는 Intel 계열 CPU이기 때문이다.
  • 마이크로소프트 사가 도입한 코드 - CP979

    1
    2
    3
    a = '가'
    a.encode('cp949')
    >> b'\xb0\xa1'
  • 부호화

    1
    2
    3
    4
    a = '가'
    b = a.encode() # UTF-8이 default
    b
    >> b'\xea\xb0\x80'
  • 복호화

    1
    2
    b.decode()
    >> '가'
  • CP949는 UTF-8이 아니므로 복호화할 시, 오류가 난다.

    1
    2
    3
    4
    5
    6
    c = a.encode('cp949')
    c
    >> b'\xb0\xa1'

    c.decode()
    >> UnicodeDecodeError: 'utf-8' codec can't decode byte 0xb0 in position 0: invalid start byte
  • UTF-8 방식이 아닐 경우, 복호화할 수 있는 방법

    • CP949가 아닌 다른 인코딩 방식일 수도 있기 때문에 찾아야 하는 경우가 올 수 있다.

      1
      2
      3
      4
      5
      6
      7
      try:
      d = c.decode()
      except UnicodeDecodeError:
      d = c.decode('cp949')

      d
      >> '가'

Call by value

  • 값에 의한 부름

  • stack frame 그림을 직접 그려보자.

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    // C language code
    include <stdio.h>

    void change_value(int x, int val) {
    x = val;

    printf("x : %d in change_value \n", x);
    }

    int main(void) {
    int x = 10;
    change_value(x, 20);
    printf("x : %d in main \n",x);
    }

    // 실행 결과
    >> x : 20 in change_value
    x : 10 in main
    • 두 결과값이 모두 x가 20이 나오지 않는 것을 알 수 있다.

    • stack frame에서 main 함수의 x와 change_value 함수의 x는 다르다.

    • change_value의 x는 main 함수의 x 값을 복사해오기만 한다.

    • 그리고 결과적으로 change_value 함수 안에서는 x 값이 20이 된 상태로 나온다.

    • 하지만 change_value 함수가 끝나면 함수가 사라진다.

    • 그러면 main 함수의 x값은 여전히 10으로 남아있다.

Call by reference

  • 참조에 의한 부름

  • stack frame 그림을 직접 그려보자.

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    // C language code
    include <stdio.h>

    void change_value(int * x, int val) {
    *x = val;
    printf("x : %d in change_value \n", *x);
    }

    int main(void) {
    int x = 10;
    change_value(&x, 20);
    printf("x : %d in main \n",x);
    }

    // 실행 결과
    >> x : 20 in change_value
    x : 20 in main
    • 두 결과값이 모두 x가 20이 나온다.

    • change_value 함수를 호출할 때에는 주소 연산자(&)를 붙여준다.

    • change_value 함수 선언에서는 매개변수에 *를 붙여준다.

    • stack frame에 쌓일 때 change_value의 x가 main 함수 x의 메모리 주소 처음을 가리키게 된다.

    • change_value 함수 내부 지역변수 x에서도 *을 붙여줘서 main 함수 x에 접근할 수 있게 만들었다.(dereference: 역참조)

    • 접근할 수 있게 되면서 main 함수 x의 값을 20으로 바꿀 수 있다.

Call by object reference

  • Call by assignment

  • 객체 참조에 의한 호출

  • stack frame 그림을 직접 그려보자.

    1. 변경할 수 없는 객체(immutable)의 경우

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      def change_value(num, new_num):
      num = new_num
      print('%d in change value' % num)

      # 파이썬에서 "="의 의미는 할당(assignment)
      # num은 new_num이 가리키는 값을 가리킨다.

      num = 10 # immutable(변경할 수 없는 객체)
      # num은 10을 가리킨다.
      # 10을 객체(object)라고 한다.

      change_value(num, 20)
      print(num)
      >> 20 in change value
      10
    • 두 결과값이 모두 num이 20이 나오지 않는다.

    • change_value 함수의 num은 글로벌 변수 num의 값을 복사한 것이 아니다.

    • change_value 함수의 num은 글로벌 변수 num의 값을 가리키는 것이다.

    • 따라서 call by value는 아니다.

    • num = new_num이 되면 num은 10을 가리키다가 20을 가리킨다.

    • change_value 함수가 종료되면 stack frame에서 사라진다.

    • 글로벌 변수 num에는 여전히 10이 남아있다.

    • 파이썬 함수 안에서는 call by reference처럼 해결할 수는 없다.

    • 하지만 다음처럼 해결할 수 있다.

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      # 반환값을 num으로 받음
      # num에 반환값을 할당해서 num이 20을 가리키도록 함

      def change_value(num, new_num):
      num = new_num
      print('%d in change value' % num)
      return num

      num = 10
      num = change_value(num, 20)
      print(num)
      >> 20 in change value
      20
    1. 변경할 수 있는 객체(mutable)의 경우

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      # list는 변경할 수 있는 객체(mutable)
      li = [1, 2, 3]

      def change_elem(li, idx, new_num):
      li[idx] = new_num
      print(li)

      change_elem(li, 1, 100)
      print(li)
      >> [1, 100, 3]
      [1, 100, 3]
    • 이번에는 두 결과값이 다 같다.

    • li[idx]도 상수 객체이기 때문에 imutable이다.

    • 여기서 중요한 것은 li가 []의 object를 가리키는 것은 맞다.

    • 하지만 [] 내부 각 자리에서 또 1, 2, 3을 각각 가리킨다.

    • li[idx] = new_num이 실행되면, li[1] = 100이므로, li[1]이 2가 아닌 100을 가리키게 된다.

    • change_elem 함수가 종료되면, 함수는 사라진다.

    • 기존 li[1]도 100을 가리키고 있기 때문에 [1, 100, 3]이 된다.

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      li = [1, 2, 3]

      def change_elem(li, a, b, c):
      li = [a, b, c]
      print(li)

      change_elem(li, 1, 100, 3)
      print(li)
      >> [1, 100, 3]
      [1, 2, 3]
    • 위와 같은 경우는 바뀌지 않았다.

    • global 영역의 list li는 []을 가리키고 [] 안 각 자리는 1, 2, 3을 가리킨다.

    • change_elem 함수 내부에서도 object [ ]가 생성되게 된다.

    • global 영역의 1과 3 그리고 새로 지정한 100을 [] 안 각 자리가 가리키게 된다.

    • 파이썬에서는 자동적으로 같은 object를 가리키도록 해준다.

    • 그래서 change_elem 함수 내부 li는 내부에서 생성된 object []를 가리키게 된다.

    • 따라서 함수가 사라지면 기존 global li의 값은 변하지 않게 되는 것이다.

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      # tuple은 변경할 수 없는 객체(immutable)
      tu = (1, 2, 3)

      def change_elem(tu, a, b, c):
      tu = (a, b, c)
      print(tu)
      return tu

      tu = change_elem(tu, 1, 100, 2)
      print(tu)
      >> (1, 100, 2)
      (1, 100, 2)
    • tuple의 요소 값은 바꿀 수는 없다.

    • 그리고 또한 함수 내부에서도 바꾸는건 불가능하다.

    • 따라서 위처럼 tu를 반환시킨다.

    • return으로 받은 tu를 tu에다가 할당함으로 tu가 새로운 tu를 가리키도록 해준다.

Call by value 의미

  • C 언어에서는 reference라는 개념 자체가 없다.

  • C 언어에서는 값을 복사하는 call by value 개념 밖에 없다.

  • C++에서 참조자 개념으로 등장한 것이 Call by reference이다.

  • Call by value는 stack frame 안 변수에 값이 바뀌더라도 stack frame 바깥쪽 변수에 영향을 주지 않는 것을 보장한다.

  • 주소값을 복사하는 Call by address라는 말도 된다.(원본 주소의 “값”을 복사하는 Call by value의 형태)

  • Call by address vs Call by reference?

    • 둘 다 원본 변수의 값을 변경한다는 동일성을 가지고 있다.

    • 하지만, 포인터 변수는 주소값을 가지고 있는 변수로서 4byte의 메모리 공간을 차지

    • 참조자(레퍼런스) 변수는 메모리 공간을 차지하지 않고 원본 변수의 별명으로 붙음

Share