Python 강좌 : 제 6강 - 비트 연산
비트 연산(Bitwise)
컴퓨터는 모든 정수를 0과 1의 나열인 2진수로 저장합니다. 2진수의 한 자리를 비트(Bit)라 하며, 비트 연산은 정수를 하나의 숫자가 아닌 비트들의 나열로 보고 각 자리를 직접 계산하는 연산입니다.
비트 연산은 여러 개의 켜짐/꺼짐 상태를 정수 하나에 담는 플래그(Flag), 색상 값이나 네트워크 데이터에서 일부 비트만 꺼내는 마스크(Mask), 2의 거듭제곱 곱셈과 나눗셈 등에 사용됩니다. 이번 강좌에서는 비트 연산자의 동작과 음수가 비트로 표현되는 방식인 2의 보수, 진법 표현, 그리고 여러 참·거짓 값을 한 번에 판단하는 all(), any(), 자료형을 확인하는 isinstance()를 다룹니다.
| 연산 | 의미 |
|---|---|
| x | y | or |
| x & y | and |
| x ^ y | xor |
| x « y | left shift |
| x » y | right shift |
| ~x | not |
비트 연산을 통하여 2진법으로 구성된 값을 계산할 수 있습니다.
각 자릿수를 서로 비교하여 결과를 반환합니다.
or: 둘 중 하나의 값이 1일 경우 1을 반환and: 둘 다 값이 1일 경우 1을 반환xor: 두 값이 서로 다를 경우 1을 반환left shift: 좌측으로 y회 비트 밀기right shift: 우측으로 y회 비트 밀기not: 반전
제 5강의 논리 연산 or, and, not이 값 전체의 참과 거짓을 판단한다면, 비트 연산 |, &, ~는 각 비트를 하나의 참(1)과 거짓(0)으로 보고 자리마다 따로 같은 규칙을 적용합니다.
x = 0b0110
y = 0b1010
print(bin(x | y), x | y)
print(bin(x & y), x & y)
print(bin(x ^ y), x ^ y)
print(bin(x << 1), x << 1)
print(bin(y >> 1), y >> 1)
print(bin(~x), ~x)- 결과
- 0b1110 14
0b10 2
0b1100 12
0b1100 12
0b101 5
-0b111 -7
2진법은 0b를 포함하여 0과 1로 구성된 진법입니다. 맨 우측 자릿수부터 차례로 2^0, 2^1, 2^2, … 을 의미합니다. 즉, 우측에서 n번째 자릿수는 2^(n-1)을 의미합니다.
0b0110의 경우 0 / 1 / 1 / 0 이므로 2^2 + 2^1입니다. 즉, 0b0110=6을 의미합니다.
0b1010의 경우 1 / 0 / 1 / 0 이므로 2^3 + 2^1입니다. 즉, 0b1010=10을 의미합니다.
or, and, xor은 각 자릿수들을 비교하여 반환하며 결과에서 확인 가능합니다. bin()은 앞쪽의 0을 생략하여 출력하므로 0b0010이 0b10으로 출력됩니다.
shift는 y회 만큼 방향으로 이동하며 결과에서 확인 가능합니다. x << n은 x * 2**n, x >> n은 x // 2**n과 같습니다.
not의 경우 0 → 1로 1 → 0으로 반전시킵니다. 하지만 여기서 -6이 아닌 -7이 나오는데, 음수를 2의 보수 표현법으로 다루기 때문입니다.
- Tip :
2의 보수란 모든 비트를 반전시킨 값에+1을 더해 음수를 표현하는 방식입니다. 따라서 비트를 반전만 한~x는-x - 1과 같으며,~6은-7이 됩니다. Python의 정수는 크기 제한이 없으므로bin()은 2의 보수 비트열 대신-0b111처럼 부호와 절대값으로 출력합니다.
자리 맞추어 보기
x = 0b0110
y = 0b1010
print(f"{x:04b}")
print(f"{y:04b}")
print(f"{x | y:04b} or")
print(f"{x & y:04b} and")
print(f"{x ^ y:04b} xor")- 결과
- 0110
1010
1110 or
0010 and
1100 xor
비트 연산은 같은 자리의 비트끼리 계산하므로, 자릿수를 맞추어 세로로 놓고 보면 결과를 쉽게 이해할 수 있습니다. f"{x:04b}"는 x를 4자리 2진수로 출력하는 표현으로, 아래의 진법 표현에서 설명합니다.
or: 위아래 중 하나라도1인 자리가1이 되어1110(14)입니다.and: 위아래 모두1인 자리는 오른쪽에서 두 번째 자리뿐이므로0010(2)입니다.xor: 위아래가 서로 다른 자리가1이 되어1100(12)입니다.
xor는 같은 값을 두 번 적용하면 원래대로 돌아오는 성질((x ^ y) ^ y == x)이 있어, 특정 비트를 켰다 껐다 반전하는 데 사용됩니다.
시프트 연산의 원리
print(5 << 3, 5 * 2 ** 3)
print(45 >> 2, 45 // 4)
print(1 << 100)- 결과
- 40 40
11 11
1267650600228229401496703205376
10진수에서 숫자를 왼쪽으로 한 칸 밀고 뒤에 0을 붙이면 10배가 되듯이, 2진수에서는 왼쪽으로 한 칸 밀면 2배가 됩니다. 5(101)를 왼쪽으로 3칸 밀면 101000이 되어 5 × 2³ = 40입니다.
오른쪽으로 밀면 맨 오른쪽 비트가 밀려나 사라지므로 나눗셈의 나머지가 버려집니다. 45(101101)를 오른쪽으로 2칸 밀면 1011이 되어 45 // 4 = 11과 같습니다.
C/C++에서는 정수의 비트 수가 정해져 있어 왼쪽으로 많이 밀면 비트가 넘쳐 사라지지만, Python의 정수는 크기 제한이 없으므로 1 << 100처럼 밀어도 비트가 사라지지 않고 2 ** 100과 같은 정확한 값이 됩니다.
2의 보수
음수를 2진수로 표현하는 가장 단순한 방법은 맨 앞 비트를 부호로 쓰는 것이지만, 그렇게 하면 +0과 -0이 따로 생기고 덧셈 회로도 복잡해집니다. 그래서 컴퓨터는 2의 보수(Two's Complement) 방식을 사용합니다.
8비트를 예로 들면, 2의 보수에서 음수 -n은 2^8 - n(= 256 - n)을 2진수로 쓴 값으로 표현합니다. 이렇게 하면 n + (-n)을 계산했을 때 256이 되고, 8비트를 넘친 맨 앞의 1이 버려져 정확히 0이 되므로 덧셈 회로 하나로 뺄셈까지 처리할 수 있습니다.
for n in [6, -6, -7, -1, 127, -128]:
print(n, format(n & 0xFF, "08b"))- 결과
- 6 00000110
-6 11111010
-7 11111001
-1 11111111
127 01111111
-128 10000000
n & 0xFF는 n의 하위 8비트만 남기는 연산으로, 음수가 8비트 2의 보수로 어떻게 저장되는지 보여줍니다. 0xFF는 11111111이므로 and 연산을 하면 아래 8자리만 그대로 남습니다.
결과를 보면 다음과 같은 규칙을 확인할 수 있습니다.
- 양수는 맨 앞 비트가
0, 음수는 맨 앞 비트가1입니다. -1은 모든 비트가1입니다.1을 더하면 모든 자리에서 올림이 일어나0이 되기 때문입니다.- 8비트로 표현할 수 있는 범위는
-128(10000000)부터127(01111111)까지입니다.
x = 6
print(format(x & 0xFF, "08b"))
print(format(~x & 0xFF, "08b"))
print(format((~x + 1) & 0xFF, "08b"), -x)- 결과
- 00000110
11111001
11111010 -6
6의 모든 비트를 반전한 ~6은 11111001이며, 이는 위 표에서 -7의 비트열과 같습니다. 여기에 1을 더한 11111010이 -6입니다. 즉, 비트 반전 + 1 = 부호 변경이므로, 비트 반전만 한 ~x는 -x - 1이 됩니다.
Python의 정수는 크기 제한이 없으므로, 음수는 왼쪽으로 1이 끝없이 이어진 2의 보수로 생각할 수 있습니다. 그래서 ~, &, |, ^가 음수에도 일관되게 동작하며, 위처럼 & 0xFF로 원하는 비트 수만큼 잘라 보면 C/C++의 8비트 정수와 같은 비트열을 얻을 수 있습니다.
x = 6
print(~x, -x - 1)
print(-7 >> 1)
print(1 << 10)- 결과
- -7 -7
-4
1024
음수의 right shift도 //와 같이 음의 무한대 방향으로 내림하므로 -7 >> 1은 -4가 됩니다.
-7은 2의 보수로 ...11111001이며, 오른쪽으로 한 칸 밀면 왼쪽에 부호 비트 1이 채워져 ...11111100이 됩니다. 이 비트열은 -4입니다. 이처럼 부호를 유지하며 미는 방식을 산술 시프트(Arithmetic Shift)라 하며, 결과가 항상 x // 2**n과 같습니다.
비트 연산의 활용
READ = 0b100
WRITE = 0b010
EXECUTE = 0b001
perm = READ | WRITE
print(format(perm, "03b"))
print(bool(perm & WRITE), bool(perm & EXECUTE))
perm = perm & ~WRITE
print(format(perm, "03b"))
perm = perm ^ EXECUTE
print(format(perm, "03b"))- 결과
- 110
True False
100
101
여러 개의 켜짐/꺼짐 상태를 비트 하나씩 맡겨 정수 하나에 저장하는 방식을 비트 플래그(Bit Flag)라 합니다. 파일의 읽기·쓰기·실행 권한이 대표적인 예입니다.
READ | WRITE: 두 비트를 모두 켜서110이 됩니다.perm & WRITE: 해당 비트만 남겨0이 아니면 켜져 있는 것입니다. 쓰기는 켜져 있고(True), 실행은 꺼져 있습니다(False).perm & ~WRITE:~WRITE는 쓰기 비트만0이고 나머지는 모두1이므로,and하면 쓰기 비트만 끄고 나머지는 유지하여100이 됩니다.perm ^ EXECUTE: 실행 비트를 반전하여101이 됩니다. 한 번 더 적용하면 다시 꺼집니다.
color = 0xFF8800
r = (color >> 16) & 0xFF
g = (color >> 8) & 0xFF
b = color & 0xFF
print(r, g, b)- 결과
- 255 136 0
색상 0xFF8800은 빨강(FF), 초록(88), 파랑(00)이 각각 8비트씩 이어진 값입니다. 원하는 색상을 꺼내려면 >>로 해당 8비트를 맨 오른쪽으로 옮긴 뒤 & 0xFF로 나머지 비트를 지웁니다. 이처럼 필요한 비트만 남기는 데 사용하는 값(0xFF)을 마스크(Mask)라 합니다.
for n in [4, 7, -3]:
print(n, n & 1, n % 2)- 결과
- 4 0 0
7 1 1
-3 1 1
2진수의 맨 오른쪽 비트는 2^0 = 1의 자리이므로, 이 비트가 1이면 홀수, 0이면 짝수입니다. n & 1은 맨 오른쪽 비트만 남기므로 홀수와 짝수를 판단할 수 있습니다. Python에서는 음수도 2의 보수로 다루므로 -3 & 1은 1이 되어 % 2와 결과가 같습니다.
비트 연산의 주의사항
a = 5
b = 3
print(a > 0 & b > 0)
print((a > 0) & (b > 0))
print(a > 0 and b > 0)
print(1 << 2 + 1, (1 << 2) + 1)- 결과
- False
True
True
8 5
비트 연산자 &, |, ^는 비교 연산자보다 우선순위가 높습니다. 그래서 a > 0 & b > 0은 의도와 달리 a > (0 & b) > 0, 즉 a > 0 > 0이라는 연결 비교로 해석되어 False가 됩니다.
조건을 조합할 때는 and, or를 사용하고, &를 사용해야 한다면 각 비교를 괄호로 감싸야 합니다. 또한 시프트 연산자는 +, -보다 우선순위가 낮으므로 1 << 2 + 1은 1 << 3 = 8입니다.
print(True & False, True | False, True ^ True)
print(type(True & False))
print(3 and 5, 3 & 5)- 결과
- False True False
<class ‘bool’>
5 1
True, False끼리 비트 연산을 하면 결과도 bool로 반환됩니다. 하지만 정수에서는 and와 &의 결과가 전혀 다릅니다. 3 and 5는 제 5강의 단락 평가에 따라 뒤의 값 5를 반환하고, 3 & 5는 011과 101의 같은 자리 비트를 계산하여 001인 1을 반환합니다.
and, or, not은 값 전체의 참·거짓, &, |, ~는 각 비트를 다룬다는 점을 구분하여 사용합니다.
진법 표현
같은 정수라도 2진수, 8진수, 10진수, 16진수 등 여러 진법으로 적을 수 있습니다. 값은 같고 표기 방법만 다를 뿐입니다. 2진수는 비트를 직접 보여주지만 길어지므로, 2진수 4자리를 한 글자로 줄인 16진수와 3자리를 한 글자로 줄인 8진수도 자주 사용합니다.
x = 0b0110
print(bin(x))
print(format(x, '04b'))
print(f"{x:08b}")
print(int('0110', 2))
print(0o17, 0xff, hex(255), oct(15))- 결과
- 0b110
0110
00000110
6
15 255 0xff 0o17
format(x, '04b')나 f"{x:08b}"를 사용하면 0b 없이 지정한 자릿수만큼 앞을 0으로 채운 2진수 문자열을 얻을 수 있습니다.
int(문자열, 2)는 2진수 문자열을 정수로 변환합니다.
8진수는 0o, 16진수는 0x를 앞에 붙여 표현하며, oct(), hex()로 변환할 수 있습니다.
'04b'는 0(빈자리를 0으로 채움), 4(최소 4자리), b(2진수)를 의미합니다. b 대신 o는 8진수, x는 16진수, d는 10진수입니다.
bin(), oct(), hex(), format()의 결과는 모두 문자열이며, 0b0110, 0o17, 0xff처럼 코드에 직접 적은 값은 정수입니다. print(0o17)이 15로 출력되는 것은 어떤 진법으로 적었든 저장되는 값은 같은 정수이고, print()는 정수를 10진수로 출력하기 때문입니다.
print(0b1111_0000, 0xFF_FF)
print(f"{255:#x} {255:#o} {255:#b} {255:X}")
print(int("0xff", 16), int("17", 8))
try:
int("12", 2)
except ValueError as err:
print(err)- 결과
- 240 65535
0xff 0o377 0b11111111 FF
255 15
invalid literal for int() with base 2: ‘12’
긴 2진수나 16진수도 제 4강의 10진수처럼 밑줄(_)로 자릿수를 구분하여 읽기 쉽게 적을 수 있습니다.
형식 지정에 #을 붙이면 0x, 0o, 0b 같은 접두사를 포함하여 출력하고, 대문자 X는 16진수를 대문자로 출력합니다.
int()에 진법을 지정하면 해당 진법의 문자열을 정수로 변환하며, 16진수는 0x 접두사가 있어도 변환됩니다. 2진수에는 0과 1만 사용할 수 있으므로 "12"처럼 다른 숫자가 있으면 ValueError가 발생합니다.
비트 수와 1의 개수
n = 0b101101
print(n.bit_length())
print(n.bit_count())- 결과
- 6
4
정수.bit_length()는 부호를 제외하고 정수를 표현하는 데 필요한 비트 수를 반환합니다.
정수.bit_count()는 2진수에서 1의 개수를 반환합니다. (Python 3.10 이상)
0b101101은 앞쪽의 0을 제외하면 6자리이므로 bit_length()는 6, 1이 네 번 나오므로 bit_count()는 4입니다. bit_length()는 어떤 값을 저장하는 데 최소 몇 비트가 필요한지 확인할 때, bit_count()는 비트 플래그에서 켜져 있는 항목의 개수를 셀 때 사용합니다.
- Tip :
bit_count()는bin(n).count("1")과 같은 결과를 더 빠르게 계산합니다. 음수는 절대값의 1의 개수를 반환합니다.
print(~0, ~-1, ~~5)
print(bin(-6))
print((-6).bit_length(), (-6).bit_count())- 결과
- -1 0 5
-0b110
3 2
~0은 -0 - 1 = -1이며, 앞에서 본 것처럼 -1은 모든 비트가 1입니다. ~를 두 번 적용하면 원래 값으로 돌아옵니다.
음수에서는 bin(), bit_length(), bit_count() 모두 절대값을 기준으로 동작합니다. -6은 -0b110으로 출력되고, 6과 같이 비트 수 3, 1의 개수 2를 반환합니다. 음수의 2의 보수 비트열이 필요하다면 앞에서처럼 & 0xFF 등으로 비트 수를 정해 확인합니다.
함수 사용
비트 연산이 한 정수 안의 여러 비트를 한꺼번에 다룬다면, all()과 any()는 여러 개의 값의 참과 거짓을 한꺼번에 판단합니다. 여러 조건이 모두 만족되는지, 하나라도 만족되는지 확인할 때 and, or를 길게 이어 쓰는 대신 사용합니다.
L1 = [True, True, False]
print(L1)
print(all(L1))
print(any(L1))
L2 = [1, 1, 0]
print(L2)
print(all(L2))
print(any(L2))
L3 = [3 > 0, 3 > 2, 2 == 2]
print(L3)
print(all(L3))
print(any(L3))
print(all([]), any([]))- 결과
- [True, True, False]
False
True
[1, 1, 0]
False
True
[True, True, True]
True
True
True False
all(목록)을 사용하여 목록 내의 모든 원소가 참일 경우 True를 반환합니다.
any(목록)을 사용하여 목록 내의 원소 중 하나라도 참일 경우 True를 반환합니다.
빈 목록의 경우 all([])은 True, any([])는 False를 반환합니다.
L1은 False가 하나 있으므로 all()은 False, True가 있으므로 any()는 True입니다. L2의 1, 0도 각각 참, 거짓으로 판단되어 같은 결과가 나옵니다. L3은 비교 연산이 먼저 계산되어 [True, True, True]가 되므로 둘 다 True입니다.
빈 목록의 결과는 all()이 “거짓인 원소가 하나도 없는가”, any()가 “참인 원소가 하나라도 있는가“를 확인한다고 생각하면 이해할 수 있습니다. 빈 목록에는 거짓인 원소가 없으므로 all([])은 True, 참인 원소도 없으므로 any([])는 False입니다.
- Tip :
논리 형식이나상수형,조건식등True나False로 판단할 수 있는 값은 사용이 가능합니다.
def show(v):
print("check", v)
return v
print(all(show(v) for v in [1, 0, 1]))
print(any(show(v) for v in [0, 2, 0]))
scores = [72, 88, 95, 64]
print(all(s >= 60 for s in scores))
print(any(s >= 90 for s in scores))- 결과
- check 1
check 0
False
check 0
check 2
True
True
True
all()과 any()도 제 5강의 and, or처럼 결과가 정해지는 순간 확인을 멈춥니다. all()은 거짓인 0을 만나자마자 False를 반환하여 세 번째 원소를 확인하지 않고, any()는 참인 2를 만나자마자 True를 반환합니다.
s >= 60 for s in scores처럼 쓰면 각 점수에 대한 비교 결과를 차례로 만들어 전달할 수 있습니다. “모든 점수가 60점 이상인가”는 all(), “90점 이상이 하나라도 있는가”는 any()로 표현합니다. 이 표현 방식은 이후 강좌에서 자세히 다룹니다.
numb = 33.2
L = [1, 2, 3, 4]
print(isinstance(numb, int))
print(isinstance(numb, float))
print(isinstance(L, list))
print(isinstance(L[0], list))- 결과
- False
True
True
False
isinstance(객체, 클래스)를 사용하여 객체가 클래스의 객체인지를 확인합니다.
numb는 실수이므로 int에 대해서는 False, float에 대해서는 True입니다. L은 리스트이므로 True이지만, L[0]은 리스트의 첫 번째 원소인 정수 1이므로 list에 대해서는 False입니다.
제 3강의 동적 타이핑 때문에 함수에 어떤 자료형의 값이 전달될지 미리 알 수 없는 경우가 있습니다. isinstance()는 이럴 때 자료형에 따라 다르게 처리하거나 잘못된 값을 걸러내는 데 사용합니다.
numb = 33.2
print(isinstance(numb, (int, float)))
print(isinstance(numb, int | float))
print(isinstance(True, int))
print(type(True) == int)- 결과
- True
True
True
False
isinstance()의 두 번째 인수에 튜플을 전달하면 여러 클래스 중 하나에 해당하는지 확인합니다.
Python 3.10부터는 int | float처럼 |를 사용하여 여러 클래스를 표현할 수도 있습니다.
isinstance()는 상속 관계도 확인하므로 int를 상속한 bool의 값 True는 int의 객체로 판단됩니다. 반면 type()은 정확한 클래스만 비교합니다.
int | float의 |는 이 강좌의 비트 연산 or와 같은 기호이지만, 클래스 사이에 사용하면 “둘 중 하나의 자료형”이라는 의미가 됩니다. 같은 연산자라도 피연산자의 자료형에 따라 다른 동작을 하는 것입니다.
일반적으로 자료형을 확인할 때는 type() ==보다 isinstance()를 사용합니다. 상속받은 자료형도 원래 자료형처럼 사용할 수 있는 경우가 대부분이기 때문입니다. 다만 위처럼 bool도 int로 판단되므로, 논리값을 제외해야 한다면 bool인지 먼저 확인해야 합니다.
정리
| 이름 | 설명 | 반환값/특징 |
|---|---|---|
| x | y | 비트 or | 비트 켜기 |
| x & y | 비트 and | 비트 확인, 마스크로 비트 남기기 |
| x ^ y | 비트 xor | 비트 반전, 두 번 적용하면 원래대로 |
| ~x | 비트 not | -x - 1 |
| x « n | 왼쪽 시프트 | x * 2**n |
| x » n | 오른쪽 시프트 | x // 2**n |
| bin(), oct(), hex() | 진법 문자열 변환 | 음수는 부호와 절대값으로 표시 |
| int(문자열, 진법) | 진법 문자열을 정수로 변환 | 잘못된 글자는 ValueError |
| bit_length() | 필요한 비트 수 | 부호 제외 |
| bit_count() | 1의 개수 | Python 3.10 이상 |
| all(), any() | 모두 참인지, 하나라도 참인지 | 결과가 정해지면 확인 중단 |
| isinstance() | 자료형 확인 | 상속 관계 포함 |
비트 연산은 정수를 비트의 나열로 보고 자리마다 계산하는 연산이며, 음수는 2의 보수로 다루어지므로 ~x가 -x - 1이 됩니다. 비트 연산자는 비교 연산자보다 우선순위가 높으므로 조건을 조합할 때는 and, or를 사용하는 것이 안전합니다.
댓글 남기기