명제와 논리 - 조건문을 정확하게 쓰기
이 장에서 배우는 것
조건문을 고치다가 괄호 하나, and 하나 때문에 허용해야 할 요청을 거절한 경험이 있을 수 있다. 코드를 짧게 만드는 일보다 먼저 할 일은 조건의 뜻을 정확하게 정하는 일이다. “둘 다 필요하다”와 “하나라도 있으면 된다”를 구별하고, 수정한 조건이 원래 조건과 같은 결과를 내는지 확인해야 한다.
이 장에서는 참과 거짓을 다루는 작은 계산 도구를 만든다. 처음에는 조건을 문장으로 적고, 다음에는 Python 식으로 옮긴다. 마지막에는 가능한 입력을 모두 넣어 두 조건식의 결과를 비교한다. 외워 둔 법칙을 믿고 넘어가는 대신, 법칙이 어떤 입력에서 성립하는지 직접 확인하는 방식이다.
- 참인지 거짓인지 판단할 수 있는 문장을 명제로 구분한다.
not,and,or의 뜻을 진리표로 확인한다.- 함의와 대우를 구별하고, 방향을 뒤집은 문장이 달라지는 사례를 찾는다.
- 드모르간 법칙으로 조건을 바꾸고 두 표현의 동치를 검사한다.
- 조건식이 다를 때 그 차이를 보여 주는 입력을 출력한다.
문제 상황
작업 관리 도구에서 문서를 보관함으로 옮기는 기능을 만든다고 하자. 잠긴 문서는 옮길 수 없다. 검토 중인 문서도 옮길 수 없다. 두 상태가 모두 아니라야 옮길 수 있다. 담당자는 이 규칙을 다음과 같이 적었다.
locked = False
under_review = True
can_archive = not (locked or under_review)
print(can_archive)
# False
다른 개발자가 괄호를 없애 읽기 쉽게 만들려 한다. 다음 두 후보 중 어느 쪽이 같은 규칙을 나타낼까.
candidate_a = (not locked) and (not under_review)
candidate_b = (not locked) or (not under_review)
print(candidate_a)
print(candidate_b)
# False
# True
잠기지는 않았지만 검토 중인 문서에서 두 후보의 결과가 갈린다. 두 번째 후보는 “두 장애물이 모두 없어야 한다”를 “장애물 하나만 없어도 된다”로 바꾼다. 변수 이름이나 코드 길이만 살펴서는 이 차이를 놓치기 쉽다.
지금 확인한 입력 하나로 두 번째 후보가 틀렸다는 것은 알 수 있다. 그러나 첫 번째 후보가 모든 입력에서 맞는지는 아직 확인하지 않았다. 두 상태가 각각 참과 거짓을 가질 수 있으므로 확인할 조합은 네 가지다. 이 네 가지를 빠짐없이 나열하는 표가 이 장의 핵심 도구다.
명제와 논리 연산을 표로 확인하기
참과 거짓을 정할 수 있는 문장
“문서가 잠겨 있다”는 특정 문서와 시점이 정해지면 참인지 거짓인지 판단할 수 있다. 이런 문장을 전공 수업에서는 명제(proposition)라고 부른다. “문서를 잠가라”는 명령이므로 참과 거짓을 붙이는 대상이 아니다. “이 문서는 편리하다”처럼 판단 기준이 정해지지 않은 문장은 프로그램 조건으로 쓰기 전에 기준부터 구체화해야 한다.
“파일 크기가 100 이하이다”도 파일 크기가 정해지면 명제가 된다. 반면 값이 정해지지 않은 size <= 100은 그 자체로 참이나 거짓이 확정되지 않는다. 값을 넣으면 참과 거짓이 결정되는 이런 조건을 전공 수업에서는 술어(predicate)라고 부른다. 여기서는 변수에 값을 넣은 뒤 얻은 결과를 다룬다.
size = 80
small_enough = size <= 100
print(small_enough)
print(type(small_enough).__name__)
# True
# bool
type은 값의 자료형을 확인하는 내장 함수다. 뒤의 .__name__은 그 자료형의 이름을 읽는다. 출력된 bool은 참과 거짓 두 값을 나타내는 불리언 자료형이다. Python에서는 참을 True, 거짓을 False로 쓴다. 이 장의 논리 변수에는 이 두 값만 넣는다.
부정, 논리곱, 논리합
명제의 참과 거짓을 뒤집는 연산을 부정(negation)이라고 부른다. Python의 not p는 p가 참이면 거짓, 거짓이면 참이다. 두 조건이 모두 참이어야 참인 연산은 논리곱(conjunction)이며 p and q로 적는다. 하나 이상이 참이면 참인 연산은 논리합(disjunction)이며 p or q로 적는다.
일상에서 “차 또는 물”은 하나만 고르라는 뜻으로 쓰이기도 한다. 논리합의 뜻은 다르다. p와 q가 둘 다 참이어도 p or q는 참이다. “정확히 하나만 참”이라는 규칙이 필요하다면 별도의 식으로 표현해야 한다.
입력의 가능한 조합과 식의 결과를 함께 적은 표를 진리표(truth table)라고 부른다. 아래 표에서는 거짓을 0, 참을 1로 표시한다. 왼쪽 두 열이 입력이고, 오른쪽 두 열이 계산 결과다.
| p | q | p and q | p or q |
|---|---|---|---|
| 0 | 0 | 0 | 0 |
| 0 | 1 | 0 | 1 |
| 1 | 0 | 0 | 1 |
| 1 | 1 | 1 | 1 |
표의 네 행은 중첩 반복문으로 만든다. 바깥 반복문이 p를 고정하면 안쪽 반복문이 q의 두 값을 차례로 넣는다. int는 여기서 False를 0, True를 1로 바꾸어 표를 간결하게 만든다.
print("p q not_p and or")
for p in [False, True]:
for q in [False, True]:
print(int(p), int(q), int(not p),
int(p and q), int(p or q))
# p q not_p and or
# 0 0 1 0 0
# 0 1 1 0 1
# 1 0 0 0 1
# 1 1 0 1 1
모든 행에서 참인 식을 항진명제(tautology), 모든 행에서 거짓인 식을 모순명제(contradiction)라고 부른다. 참과 거짓만 다루는 지금의 규칙에서는 p or not p가 전자이고, p and not p가 후자다. 이름을 암기하기보다 다음 두 행의 결과를 확인하면 된다.
for p in [False, True]:
print(int(p), int(p or not p), int(p and not p))
# 0 1 0
# 1 1 0
함의는 방향이 있는 조건이다
“이면”을 위반하는 경우 찾기
“발급되었다면 승인되었다”라는 규칙을 생각하자. p는 발급되었다는 명제, q는 승인되었다는 명제다. “p이면 q이다”라는 연결을 전공 수업에서는 함의(implication)라고 부르고, p → q로 적는다. 여기서 p는 가정, q는 결론이다.
이 규칙을 위반하는 경우는 발급되었는데 승인되지 않은 경우다. 발급되지 않은 요청은 승인 여부와 관계없이 이 규칙의 위반 사례가 아니다. 따라서 함의는 p가 참이고 q가 거짓인 행에서만 거짓이다. “가정이 거짓인데 왜 전체는 참인가”가 낯설다면, 전체 결과를 “이 조합은 규칙을 위반하지 않는다”로 읽어 보면 된다.
Python에는 명제의 함의를 직접 적는 연산자가 없다. 대신 (not p) or q로 계산한다. 이는 “발급되지 않았거나, 승인되었다”라는 뜻이다. 다음 표에서 마지막 열은 뒤에서 확인할 대우의 결과다.
| p | q | p → q | not q → not p |
|---|---|---|---|
| 0 | 0 | 1 | 1 |
| 0 | 1 | 1 | 1 |
| 1 | 0 | 0 | 0 |
| 1 | 1 | 1 | 1 |
def implies(p, q):
return (not p) or q
for p in [False, True]:
for q in [False, True]:
print(int(p), int(q), int(implies(p, q)))
# 0 0 1
# 0 1 1
# 1 0 0
# 1 1 1
함의는 사건의 원인이나 발생 순서를 설명하지 않는다. “발급되었다면 승인되었다”는 문장만으로 승인 때문에 발급되었다고 결론 내릴 수 없다. 또한 Python의 if는 조건에 따라 실행할 코드를 고르는 문장이다. 참과 거짓을 결과로 갖는 함의 식과 역할이 다르다.
대우와 역을 구별하기
p → q에서 양쪽을 부정하고 방향을 뒤집은 not q → not p를 대우(contrapositive)라고 부른다. 앞의 규칙에 적용하면 “승인되지 않았다면 발급되지 않았다”가 된다. 원래 규칙과 대우가 금지하는 상황은 둘 다 “발급되었고 승인되지 않았다”다. 그래서 두 식은 모든 행에서 같은 결과를 낸다.
방향만 뒤집은 q → p는 역(converse)이라고 부른다. “승인되었다면 발급되었다”는 원래 규칙에서 따라오지 않는다. 승인되었지만 아직 발급하지 않은 요청을 생각하면 된다. 이때 원래 규칙은 위반되지 않지만 역은 거짓이다.
p = False # 아직 발급되지 않았다.
q = True # 승인은 끝났다.
print(implies(p, q))
print(implies(not q, not p))
print(implies(q, p))
# True
# True
# False
모든 입력에서 결과가 같은 두 식을 논리적으로 동치(logical equivalence)라고 한다. p → q와 그 대우는 동치다. 반면 원래 함의와 역은 방금 입력 하나에서 결과가 다르므로 동치가 아니다. 주장을 성립하지 않게 하는 이런 구체적인 사례를 반례(counterexample)라고 부른다.
또 다른 표현도 구별해 둘 필요가 있다. p → q에서 q는 p가 성립할 때 반드시 필요한 조건이므로 필요조건이다. p는 그것만 성립해도 q를 보장한다는 뜻에서 q의 충분조건이다. 예의 규칙에서는 승인이 발급의 필요조건이고, 발급 사실은 승인되었다고 판단하기 위한 충분조건이다. 다만 이 관계는 주어진 규칙이 지켜진다는 전제 아래에서 읽어야 한다.
드모르간 법칙으로 조건을 바꾸기
문제 상황의 not (locked or under_review)로 돌아가자. 바깥의 부정은 “잠김 또는 검토 중”이라는 묶음 전체를 뒤집는다. 이 조건은 “잠기지 않았고, 검토 중도 아니다”로 풀어 쓸 수 있다. 식으로 쓰면 (not locked) and (not under_review)다.
이처럼 부정이 묶음 안으로 들어갈 때 or와 and가 서로 바뀌는 관계를 드모르간 법칙(De Morgan's laws)이라고 부른다. 두 형태를 함께 살펴보면 “둘 다 아니다”와 “둘 다인 것은 아니다”의 차이를 구별하기 쉽다.
| 원래 식 | 같은 결과를 내는 식 | 읽는 방법 |
|---|---|---|
| not (p or q) | (not p) and (not q) | 둘 다 거짓이다 |
| not (p and q) | (not p) or (not q) | 하나 이상 거짓이다 |
두 번째 규칙도 코드로 확인한다. 표의 마지막 두 열이 네 행에서 모두 같아야 한다. 특히 마지막 행에서는 둘 다 참이므로 두 식 모두 거짓이 된다.
for p in [False, True]:
for q in [False, True]:
left = not (p and q)
right = (not p) or (not q)
print(int(p), int(q), int(left), int(right))
# 0 0 1 1
# 0 1 1 1
# 1 0 1 1
# 1 1 0 0
Python의 연산 우선순위는 이 장에서 쓰는 세 연산 사이에서는 not, and, or 순이다. 따라서 not p or q는 (not p) or q로 해석된다. 그렇더라도 규칙을 설명하는 코드에서는 부정할 범위가 보이도록 괄호를 쓰는 편이 좋다. 괄호는 결과뿐 아니라 읽는 사람의 해석도 돕는다.
실무에서 만나는 곳
조건의 동치 검사는 입력 검사, 기능 노출, 요청 허용 여부를 정리할 때 쓸 수 있다. 예를 들어 오류 안내를 띄우는 조건을 먼저 작성했다면, 정상 진행 조건은 그 전체를 부정해서 얻을 수 있다. 다만 부정을 안으로 옮길 때는 연결 연산도 함께 바꾸어야 한다.
missing_name = False
invalid_date = False
show_error = missing_name or invalid_date
can_continue = (not missing_name) and (not invalid_date)
print(show_error)
print(can_continue)
# False
# True
조건을 단순화한다는 말은 글자 수를 줄인다는 뜻으로만 이해하지 않는다. “오류가 없다”가 업무 용어에 맞으면 not show_error가 읽기 좋을 수 있고, 각 요구 사항을 드러내야 하면 두 부정의 논리곱이 나을 수 있다. 먼저 결과가 같은지 검사한 뒤, 규칙을 더 잘 드러내는 표현을 고른다.
이 검사는 각 상태가 이미 참이나 거짓으로 계산되었다는 전제를 둔다. 실제 프로그램에서는 “날짜가 유효한가”를 계산하는 부분 자체가 잘못될 수도 있다. 논리식이 맞는지와 개별 조건을 올바르게 계산했는지는 나누어 확인해야 한다.
완성 코드
다음 내용을 main.py에 저장한다. 프로그램은 드모르간 법칙, 함의와 대우, 함의와 역을 차례로 비교한다. 각 비교에서 네 입력을 모두 출력하고, 결과가 다르면 처음 발견한 입력을 함께 보여 준다. 외부 패키지나 입력 파일은 필요하지 않다.
def implies(p, q):
return (not p) or q
def original(p, q):
return not (p or q)
def rewritten(p, q):
return (not p) and (not q)
def contrapositive(p, q):
return implies(not q, not p)
def converse(p, q):
return implies(q, p)
def compare(title, left, right):
print(title)
print("p q left right")
differences = []
for p in [False, True]:
for q in [False, True]:
left_value = left(p, q)
right_value = right(p, q)
print(int(p), int(q), int(left_value), int(right_value))
if left_value != right_value:
differences.append([p, q])
if differences:
p, q = differences[0]
print(f"판정: 다름 / 첫 반례 p={int(p)}, q={int(q)}")
else:
print("판정: 동치")
print()
def main():
compare("드모르간 법칙", original, rewritten)
compare("함의와 대우", implies, contrapositive)
compare("함의와 역", implies, converse)
if __name__ == "__main__":
main()
줄별 해설
implies의 첫 두 줄은 함의의 계산 규칙을 정의한다. 호출할 때 받은 p와 q에 대해 (not p) or q를 반환한다. 앞으로 함의를 사용해야 할 때 같은 식을 반복해서 쓰지 않고 이 함수를 호출한다.
original은 묶음 전체를 부정한 원래 조건이다. rewritten은 각 조건을 부정하고 논리곱으로 연결한 조건이다. 함수 안에서는 상태 이름 대신 p, q를 쓴다. 이렇게 하면 문서 상태뿐 아니라 두 불리언 값으로 표현되는 다른 상황에도 같은 표를 적용할 수 있다.
contrapositive는 implies에 not q와 not p를 그 순서로 전달한다. 부정과 순서 변경이 모두 필요하다. converse는 부정 없이 순서만 바꾼다. 두 함수의 코드 차이가 원래 함의와 맺는 관계의 차이를 보여 준다.
compare는 제목과 두 함수를 전달받는다. Python에서는 함수도 변수에 담거나 다른 함수의 인자로 전달할 수 있다. compare를 호출할 때 original처럼 괄호 없이 이름을 쓰면 함수 자체를 전달한다. 함수 안의 left(p, q)에서 비로소 전달받은 함수를 실행한다.
처음 두 print는 비교 제목과 열 이름을 출력한다. left와 right는 각각 왼쪽 조건식과 오른쪽 조건식의 결과다. differences = []는 결과가 달랐던 입력을 담을 빈 리스트를 만든다.
중첩 반복문은 거짓·거짓, 거짓·참, 참·거짓, 참·참 순서로 입력을 만든다. 두 식에 같은 입력을 넣고 결과를 각각 저장한다. 결과를 변수에 저장하므로 표에 출력할 때와 차이를 검사할 때 함수를 다시 호출할 필요가 없다.
left_value != right_value는 두 결과가 다른지 검사한다. 다르면 append로 현재 입력을 리스트 끝에 추가한다. 예를 들어 [False, True]는 p가 거짓이고 q가 참인 입력을 나타낸다. 차이를 발견해도 반복을 끝내지 않아 진리표의 모든 행을 출력한다.
if differences:는 리스트가 비어 있지 않은지 검사한다. 차이가 하나라도 있으면 differences[0]으로 첫 입력을 꺼낸다. p, q = ...는 두 값을 순서대로 두 변수에 나누어 담는 문법이다. 그 뒤 f"..."는 중괄호 안의 계산 결과를 문자열에 넣어 반례를 표시한다.
리스트가 비어 있으면 네 행에서 결과가 같았으므로 동치라고 출력한다. 여기서 검사한 식은 불리언 입력 두 개만 사용하므로 네 행이 전체 입력이다. 일부 표본만 맞춘 것과 달리, 이 범위에서는 빠진 조합이 없다.
main은 세 비교를 실행 순서대로 모은 함수다. 마지막 조건문은 파일을 직접 실행했을 때 main을 호출하는 관용적인 형태다. __name__은 Python이 정하는 이름이며, 이 파일을 직접 실행하면 값이 "__main__"이 된다. 출력 순서는 리스트에 적힌 값과 함수 호출 순서로 정해지므로 실행할 때마다 같다.
실행 결과
터미널에서 파일을 저장한 디렉터리로 이동한 뒤 다음 명령을 실행한다. Python 3.12 이상에서 표준 기능만 사용하며, 실행 후 입력을 기다리지 않고 끝난다.
python3 main.py
예상 출력은 다음과 같다. 각 판정 다음에는 빈 줄이 하나씩 출력된다.
드모르간 법칙
p q left right
0 0 1 1
0 1 0 0
1 0 0 0
1 1 0 0
판정: 동치
함의와 대우
p q left right
0 0 1 1
0 1 1 1
1 0 0 0
1 1 1 1
판정: 동치
함의와 역
p q left right
0 0 1 1
0 1 1 0
1 0 0 1
1 1 1 1
판정: 다름 / 첫 반례 p=0, q=1
마지막 비교에는 서로 다른 행이 두 개 있다. 프로그램은 표에서 두 행을 모두 보여 주되, 판정 문장에는 먼저 발견한 행만 적는다. 동치가 아님을 확인하는 데는 그중 하나면 충분하다. 반대로 동치라고 판정하려면 검사 대상의 전체 입력에서 결과가 같아야 한다.
실무에서 자주 틀리는 것
부정만 옮기고 연결 연산을 그대로 두기
묶음 앞의 not을 두 변수 앞으로 옮기면서 or를 유지하면 다른 조건이 된다. 다음의 잘못된 코드는 검토 중인 문서를 허용한다.
# 틀린 코드
locked = False
under_review = True
can_archive = (not locked) or (not under_review)
print(can_archive)
# True
두 상태가 모두 거짓이어야 하므로 논리곱으로 연결해야 한다.
# 고친 코드
locked = False
under_review = True
can_archive = (not locked) and (not under_review)
print(can_archive)
# False
수정할 때는 변수마다 붙은 부정뿐 아니라 두 변수를 잇는 연산도 함께 확인한다. 한 행에서 의심이 생기면 네 행 전체를 출력해 보는 편이 빠르다.
함의를 논리곱으로 구현하기
“발급되었다면 승인되었다”를 issued and approved로 쓰면 발급되지 않은 모든 요청을 규칙 위반으로 판정한다. 다음 입력은 아직 발급되지 않았을 뿐인데 잘못된 식은 거짓을 낸다.
# 틀린 코드
issued = False
approved = False
rule_holds = issued and approved
print(rule_holds)
# False
검사하려는 것이 두 사실의 동시 성립인지, 규칙의 위반 여부인지 먼저 구별한다. 함의의 성립 여부를 검사하려면 다음과 같이 쓴다.
# 고친 코드
issued = False
approved = False
rule_holds = (not issued) or approved
print(rule_holds)
# True
이 결과는 해당 요청을 발급해도 좋다는 허가가 아니다. 현재 두 상태가 주어진 함의 규칙을 위반하지 않는다는 뜻이다. 변수 이름도 이 차이를 드러내도록 정한다.
둘 다 참인 경우를 “정확히 하나”에 포함하기
두 알림 경로 가운데 정확히 하나만 선택해야 한다고 하자. 논리합만 쓰면 두 경로를 모두 선택한 입력도 통과한다.
# 틀린 코드
use_message = True
use_mail = True
valid = use_message or use_mail
print(valid)
# True
입력이 불리언 값이라면 두 값이 서로 다를 때 정확히 하나만 참이다. 전공 수업에서는 이 연산을 배타적 논리합(exclusive or)이라고 부른다.
# 고친 코드
use_message = True
use_mail = True
valid = use_message != use_mail
print(valid)
# False
for p in [False, True]:
for q in [False, True]:
print(int(p), int(q), int(p != q))
# 0 0 0
# 0 1 1
# 1 0 1
# 1 1 0
참처럼 취급되는 값과 불리언 결과를 혼동하기
Python의 and와 or는 어떤 값에나 사용할 수 있지만, 결과를 늘 True나 False로 바꾸어 주지는 않는다. 빈 문자열은 조건에서 거짓처럼 취급되고, 비어 있지 않은 문자열은 참처럼 취급된다. 다음 코드는 불리언 결과를 기대했지만 문자열을 얻는다.
# 틀린 코드
name = "reader"
code = "A7"
has_both = name and code
print(has_both)
# A7
불리언 결과가 필요하면 각 조건의 뜻을 분명히 적는다. 다음 예는 두 문자열이 모두 비어 있지 않은지 검사한다.
# 고친 코드
name = "reader"
code = "A7"
has_both = (name != "") and (code != "")
print(has_both)
# True
또한 Python은 결과를 정할 수 있으면 뒤쪽 식을 평가하지 않는다. 이를 단락 평가(short-circuit evaluation)라고 부른다. and는 앞이 거짓이면, or는 앞이 참이면 뒤쪽을 건너뛴다. 함수 호출을 조건에 넣으면 실행 여부까지 고려해야 한다.
def inspect():
print("검사 실행")
return True
print(False and inspect())
print(True or inspect())
# False
# True
검사 함수의 문장이 출력되지 않는다는 점을 확인한다. 완성 코드의 비교 대상은 이미 받은 불리언 값만 계산하고 실행 상태를 바꾸지 않는다. 실제 조건식을 고칠 때도 논리값의 동치와 함께 함수 실행 순서나 외부 상태 변경 여부를 살펴야 한다.
한눈에 보기
| 개념 | Python 표현 | 확인할 점 |
|---|---|---|
| 부정 | not p | 참과 거짓을 뒤집는다 |
| 논리곱 | p and q | 둘 다 참이어야 참이다 |
| 논리합 | p or q | 둘 다 참인 경우도 포함한다 |
| 함의 | (not p) or q | p가 참이고 q가 거짓일 때만 거짓이다 |
| 대우 | implies(not q, not p) | 원래 함의와 동치다 |
| 역 | implies(q, p) | 원래 함의와 동치라고 할 수 없다 |
| 드모르간 법칙 | not (p or q) | (not p) and (not q)와 같다 |
| 동치 검사 | left_value != right_value | 다른 행 하나가 있으면 반례다 |
이 장의 검사기는 불리언 입력 두 개로 표현된 식의 전체 경우를 확인한다. 입력이 세 개라면 각 변수의 두 값을 조합한 여덟 행이 필요하다. 변수의 수가 늘면 표도 커지므로, 작은 규칙을 분리해서 확인하는 습관이 도움이 된다. 다음 장에서는 이런 구체적인 확인과 더 넓은 범위에 대한 주장을 어떻게 연결하는지 살펴본다.
연습 문제
not (p and q)와(not p) or (not q)를 반환하는 함수를 각각 작성하라. 완성 코드의compare로 검사하고, 거짓이 나오는 입력을 적어라.- “전송되었다면 검사를 통과했다”에서
p를 전송 여부,q를 검사 통과 여부로 두라. 대우와 역을 문장으로 적고, 원래 함의와 역의 결과가 다른 입력 하나를 코드로 확인하라. not (p or q)를(not p) or (not q)로 바꾸었다. 완성 코드의 검사기에 이 후보를 연결하라. 처음 출력되는 반례와 그때 두 식의 값을 적어라.- 두 불리언 값 중 정확히 하나만 참인지 검사하는
p != q와(p or q) and not (p and q)를 비교하라. 네 행의 결과를 적고 동치인지 판정하라.
정답과 해설
논리곱 전체의 부정
두 식 모두 두 입력이 함께 참일 때만 거짓이다. 다음 함수들을 완성 코드의 main보다 앞에 추가하고, main 안에 비교 호출을 추가하면 된다.
def not_both(p, q):
return not (p and q)
def either_missing(p, q):
return (not p) or (not q)
# main 안에 추가할 호출
# compare("논리곱의 부정", not_both, either_missing)
진리표의 결과 열은 양쪽 모두 1, 1, 1, 0이다. 따라서 동치이며, 거짓이 나오는 입력은 p=True, q=True다. “둘 다 참은 아니다”는 두 값이 모두 거짓인 경우도 포함한다.
전송 규칙의 방향
대우는 “검사를 통과하지 않았다면 전송되지 않았다”다. 역은 “검사를 통과했다면 전송되었다”다. 검사만 마치고 전송을 기다리는 상태가 원래 함의와 역을 구별한다.
p = False
q = True
print(implies(p, q), implies(q, p))
# True False
원래 규칙은 검사를 통과한 모든 항목을 전송하라고 요구하지 않는다. 전송된 항목이 검사 통과 상태여야 한다는 제약만 담고 있다.
연결 연산을 바꾸지 않은 후보
def wrong_rewrite(p, q):
return (not p) or (not q)
# main 안에 추가할 호출
# compare("잘못 바꾼 조건", original, wrong_rewrite)
검사 순서에서 첫 반례는 p=False, q=True다. 원래 식은 거짓이고 잘못 바꾼 식은 참이다. 거짓·거짓인 첫 행에서는 결과가 같으므로, 그 행만 검사했다면 잘못된 변경을 발견하지 못한다.
정확히 하나가 참인 조건
def exactly_one(p, q):
return p != q
def expanded(p, q):
return (p or q) and not (p and q)
# main 안에 추가할 호출
# compare("정확히 하나", exactly_one, expanded)
두 결과 열은 모두 0, 1, 1, 0이므로 동치다. p or q로 하나 이상이 참임을 요구하고, not (p and q)로 둘 다 참인 경우를 제외했다. 이 해석은 입력이 불리언 값이라는 전제에서 성립한다. 조건을 바꿀 때는 무엇을 입력으로 삼았는지와 어떤 조합을 확인했는지를 함께 기록한다.