하드웨어 수준까지 내려가 보면, 트랜지스터는 켜지거나 꺼지는 두 가지 상태만 가질 수 있어요.
이 두 상태는 전통적으로 1과 0으로 나타내요.
이것이 binary digits이고, 줄여서 bits라고 해요.
bits와 binary에 대한 감각은 저수준 언어로 작업하는 시스템 프로그래머에게 특히 중요해요.
하지만 컴퓨팅 역사의 대부분 동안 프로그래밍의 우선순위는 이 이진법적 현실을 점점 더 정교하게 _추상화_하는 방법을 찾는 것이었어요.
Python(그리고 다른 많은 고수준 프로그래밍 언어)에서는 int, float, string을 비롯해 정의된 여러 _타입_을 다뤄요. 오디오와 비디오 형식까지 포함해서요.
모든 것을 (결국에는) 비트로 변환하는 일은 Python 내부가 알아서 처리하도록 맡겨요.
그럼에도 비트 연산자와 비트 연산을 사용하면, Python처럼 고수준 언어에서도 속도와 메모리 효율에서 상당한 이점을 얻을 수 있어요.
당연하게도 Python은 사용자와 십진수로 소통하지만, 프로그래머는 이 기본값을 바꿀 수 있어요.
실제로 Python은 binary, hexadecimal, octal 형식의 int를 곧바로 받아들이고, 그들 사이의 수학 연산도 문제없이 수행해요.
자세한 내용은 binary-octal-hexadecimal 개념을 참고해요.
이진수는 0b 접두사를 붙여 입력해요. 16진수에 0x를 쓸 수 있는 것과 같고(16진수는 4비트 묶음을 간결하게 나타내는 방법이에요), 8진수에는 oct를 쓸 수 있어요.
정수를 이진 문자열로 변환하는 방법은 여러 가지가 있어요. 0b 접두사를 포함하는지, 0으로 왼쪽을 채우는 패딩을 지원하는지에 따라 달라요.
# Binary entry.
>>> 0b10111
23
# Converting an int display to binary string, with prefix.
>>> bin(23)
'0b10111'
>>> number = 23
# Binary without prefix, padded to 8 digits.
>>> format(number, '08b')
'00010111'
# Same format, but using an f-string.
>>> f"{number} in decimal is {number:08b} in binary and {number:x} in hex"
'23 in decimal is 00010111 in binary and 17 in hex'
Bitwise Logicbools 개념에서 불리언(True와 False) 값에 사용하는 논리 연산자 and, or, not에 대해 다뤘어요.
비트를 다룰 때도 같은 논리 규칙이 적용돼요.
다만 논리 연산자에 대응하는 비트 연산자 & (and), | (or), ~ (not), ^ (XOR)는 이진 표현의 각 _비트_에 적용되며, 1을 True("켜짐"), 0을 False("꺼짐")으로 취급해요.
비트 연산자 &의 예를 보면 더 분명해질 거예요.
>>> x = 0b01100110
>>> y = 0b00101010
>>> format(x & y, '08b')
'00100010'
두 입력 숫자 _모두_에서 1인 위치만 출력에서 1이 돼요.
비트 연산자 &는 앱의 사용자 설정처럼 True/False 값을 압축해 담은 집합에서 개별 비트를 골라내는 방법으로 흔히 쓰여요.
이렇게 하면 개별 비트의 값으로 프로그램 로직을 제어할 수 있어요.
>>> number = 0b0110
>>> number & 0b0001 > 0
False
>>> number & 0b0010 > 0
True
비트 연산자 |(or)의 경우, 입력 _둘 중 하나_에 1이 있으면 출력에 1이 설정돼요.
>>> x = 0b01100110
>>> y = 0b00101010
>>> format(x | y, '08b')
'01101110'
^ 연산자는 비트 단위 배타적 논리합(xor)을 뜻해요. 입력 _둘 중 하나_에는 1이 있지만 _둘 다_에는 없을 때 출력에 1이 설정돼요.
이 기호는 sets 개념에서 본 적이 있을지도 몰라요. 거기서는 set의 _대칭 차집합_에 쓰이는데, 이는 집합에 적용한 xor와 같아요.
xor ^가 낯설게 느껴진다면, 이것이 암호학에서 가장 흔히 쓰이는 연산이라는 점을 알아 두세요.
>>> x = 0b01100110
>>> y = 0b00101010
>>> format(x ^ y, '08b')
'01001100'
마지막으로 ~ 연산자(tilde 문자)가 있어요. 이는 입력 하나를 받아 _모든 비트를 반전_시키는 비트 not으로, 예상했던 결과가 아닐 수도 있어요!
표현에서 각 1은 0으로, 0은 1로 바뀌어요.
자세한 내용은 아래 섹션을 봐요.
십진 표현에서는 숫자 왼쪽에 +나 - 기호를 붙여 양수와 음수를 구분해요.
이 기호를 이진 수준에서 쓰는 것은 디지털 컴퓨팅에 비효율적이었고, +0이 -0과 같지 않다는 문제를 낳았어요.
모든 현대 컴퓨터는 -와 + 대신 음수를 two's complement 표현으로 나타내요. 실리콘 칩 수준까지 말이죠.
즉, 왼쪽 끝 비트("최상위 비트", 줄여서 MSB라고도 해요)가 1이면 모든 비트가 반전되고 그 수가 _음수로 해석된다_는 뜻이에요.
양수는 MSB가 0이에요.
이 표현은 0이 한 가지 형태만 존재한다는 장점이 있어서, 프로그래머가 -0과 +0을 따로 관리할 필요가 없어요.
이렇게 음수와 양수를 표현하는 방식은 Python에 한 가지 복잡한 문제를 더해요. 핵심 언어 내부에는 int32나 int64 같은 유한 정수 개념이 없어요.
'현대' Python에서 int는 크기 제한이 없고(오직 하드웨어 용량으로만 제한돼요), 음수나 비트가 반전된 수는 왼쪽으로 (이론상) 무한히 많은 1을 가져요. 양수가 무한히 많은 0을 갖는 것과 같아요.
그래서 bitwise not의 유용한 예를 들기가 어려워요:
>>> x = 0b01100110
>>> format(x, '08b')
'01100110'
# This is a negative binary (not twos-complement display).
>>> format(~x, '08b')
'-1100111'
# Decimal representation.
>>> x
102
# Using the Bitwise not, with an unintuitive result.
>>> ~x
-103
이것은 고정 크기 정수를 쓰는 언어에서 볼 수 있는 0b10011001이 아니에요.
~ 연산자는 부호 없는 바이트나 정수 타입, 또는 고정 크기 정수 타입에서만 기대한 대로 동작해요.
이런 숫자 타입은 NumPy, pandas, sympy 같은 서드파티 패키지에서 지원하지만, 핵심 Python에서는 지원하지 않아요.
실제로 Python 프로그래머는 &, |, ^와 아래에서 설명할 시프트 연산자를 양수에만 사용하는 경우가 꽤 많아요.
음수를 대상으로 한 비트 연산은 훨씬 드물어요.
한 가지 방법은 음수 값에 2**32 (or 1 << 32)를 더해 int를 부호 없는 값으로 만드는 것이지만, 관리하기가 어려워져요.
또 다른 방법은 ctypes 모듈을 사용해 C 스타일 정수 타입을 쓰는 것이지만, 이것도 마찬가지로 다루기 번거로워요.
Shift operators왼쪽 시프트 연산자 x << y는 x의 모든 비트를 y자리만큼 왼쪽으로 옮기고, 새로 생긴 빈자리는 0으로 채워요.
이것이 수를 (2**y)로 곱하는 것과 산술적으로 같다는 점에 주목해요.
오른쪽 시프트 연산자 x >> y는 반대로 동작해요.
이것은 정수 나눗셈 x // (2**y)와 산술적으로 같아요.
Python에서 음수를 시프트할 때 생기는 함정에 대해서는 앞 섹션의 음수 설명을 떠올려 봐요.
>>> x = 8
>>> format(x, '08b')
'00001000'
# A left bit shift.
>>> x << 2
32
>>> format(x << 2, '08b')
'00100000'
# A right bit shift.
>>> format(x >> 2, '08b')
'00000010'