master
/ 2.6.4 布尔运算.ipynb

2.6.4 布尔运算.ipynb @masterview markup · raw · history · blame

Notebook

布尔运算

Python 语言支持布尔运算符,__布尔运算__也称为__逻辑运算__,包括:
and (与)
or (或)
not (非)
在执行布尔运算或当表达式被用于流程控制语句时,以下值会被解析为False:
False、None、所有类型的数字零、以及空字符串和空容器(包括字符串、元组、列表、字典、集合与冻结集合)。
所有其他值都会被解析为True。
三种运算的表达式与功能描述见表 3.3,按优先级升序排序。

运算符 表达式 功能描述
or x or y 首先对表达式 x 求值,如果值为True则返回 x的值,否则对表达式y求值并返回其结果值。
and x and y 首先对表达式 x 求值,如果值为False则返回 x的值,否则对表达式y求值并返回其结果值。
not not x 表达式 x 值为 False时返回 True,否则返回 False。

注释:

or与and都是__短路运算符__,因此只有在第一个参数为假值时才会对第二个参数求值。</font>
not 的优先级比非布尔运算符低,因此 not a == b 会被解读为 not (a == b) 而 a == not b 会引发语法错误。

and 和 or 两边的 x 和 y 可以是数字、变量或表达式,布尔运算返回的并不是 False 和 True,而是返回参与布尔运算的操作数或表达式的值。

运算符为or时,解释器首先对 or 左边的表达式进行运算,当其值为 True 时直接返回左侧表达式的运算结果。此时不会对右侧表达式进行运算,这种特性也称为短路特性。

In [1]:
print(10 or 'hello')  # 先对左侧求值,值为True则返回左侧的值,返回整数 10
print('hello' or 10)  # 先对左侧求值,值为True则返回左侧的值,返回'hello'
print(10 or '')       # 先对左侧求值,值为True则返回左侧的值,返回整数 10
10
hello
10

运算符为or时,解释器仅当 or 左边的表达式运算结果为False时才对右侧的表达式进行运算,此时右侧表达式的值为逻辑运算表达式的值。

In [3]:
print(0 or 'hello')   # 先对左侧求值,值为False则返回右侧的值,返回'hello'
print(0 or False)     # 先对左侧求值,值为False则返回右侧的值,返回False
print('' or [])       # 先对左侧求值,值为False则返回右侧的值,返回[]

运算符为and时,解释器首先对 and 左边的表达式进行运算,当左侧表达式的值为 False 时,直接返回左侧表达式的值,而右侧的表达式不会被运算。

In [3]:
print(0 and 'hello')   # 先对左侧求值,值为False则返回左侧的值,返回0
print(0 and False)     # 先对左侧求值,值为False则返回左侧的值,返回0
print('' and [])       # 先对左侧求值,值为False则返回左侧的值,返回''
0
0

运算符为and时,当左侧的表达式值为 True 时才对右侧的表达式进行运算并返回右侧表达式的值。

In [4]:
print(10 and 'hello')  # 先对左侧求值,值为True则返回右侧的值,返回'hello'
print('hello' and 10)  # 先对左侧求值,值为True则返回右侧的值,返回整数 10
print(10 and '')       # 先对左侧求值,值为True则返回右侧的值,返回''
hello
10

逻辑运算 or 和 and 的值是参与运算的表达式的值之一 not 运算结果一定是布尔值,值为True或False

In [6]:
print(not 10)       # 10是非0整数,布尔值为True,取非结果为False
print(not '')       # '',空字符串,布尔值为False,取非结果为True

布尔运算可放到 __if____while__ 后面的__条件表达式__中,先得到布尔运算的值,再对返回值做逻辑值检测,根据真值测试的结果决定是否执行分支下的语句。

In [2]:
if 10 or ' ':     # 10 and ' '布尔运算的值是10,条件运算结果为True
    print(True)   # 当10 and ''布尔运算值是True时,输出True
else:             # 否则
    print(False)  # 输出 False,执行此分支

上述分支语句的程序可以用下面一行代码实现:

In [2]:
print(bool(10 or ''))  # 先得到10 or ' '的结果为10,再用bool(10)返回True输出

布尔运算可放到__赋值语句__中,将布尔运算的结果赋值给前面的变量。
下述语句中,若input()函数接收到非空输入时,值为True,将接收到的字符串赋值给变量;若直接回车,接收到空字符串,此时布尔运算的值为or右侧的“保密”,将“保密”赋值给左侧的变量。

In [4]:
birthdate = input('请输入出生日期: ') or '保密'
print(birthdate)                    # 无输入,直接回车时,输出'保密'

解释器先对布尔运算符左侧的操作数进行运算这种特性称为__短路特性__
__布尔运算符左侧__的表达式已经可以__决定布尔运算的结果__时,该语句__之后__的所有代码都__不会被执行__
短路特性可以有效的__提高效率__
把容易判断、__计算量较小的表达式__放在布尔运算符的__左侧__,可以减少不必要的运算,提高算法效率。

实例2.3 输入用户信息

编程接收用户输入的出生日期,当用户不想公开自己的出生日期时,可以直接输入回车,此时记录“保密”。 实现这个功能,可以先接收用户的输入,再用一个分支语句根据用户不同的输入给出不同的输出信息。

In [ ]:
birthdate = input('请输入出生日期: ')  # 接收用户输入的出生日期或回车符
if birthdate:          # 当birthdate不为空字符串时,逻辑值检测结果为True
    print('你的生日是:', birthdate)   # 有输入时,输出用户输入的出生日期
else:                  # 无输入时(直接回车时),输出'保密'
    print('你的生日是:', '保密')

利用布尔运算,这个代码可以用以下方法实现,5行代码精简为2行,且代码的逻辑变得更加简洁:

In [ ]:
birthdate = input('请输入出生日期: ') or '保密'  # 布尔表达式的短路求值结果赋值给birthdate
print('你的生日是:', birthdate)  

input()函数的返回值为接收到用户输入的字符串,在上面的代码段中,当用户输入非空时,input() 函数返回值的布尔值为“True”,那么他的值就会赋给 birthdate,且不再对or右侧进行处理;当用户不输入任何字符直接回车时,input()函数获得的是空字符串,其布尔值为False,此时布尔运算的值为or 右侧的字符串'保密',即将字符串“保密”赋值给变量 birthdate。这种表达方法与使用 if 语句效果相同,但更简洁。

解释器先对逻辑运算符左侧的操作数进行运算这种特性称为短路特性。当发生短路之后,该语句短路处之后的所有代码都不会被执行。短路特性可以有效的提高效率。把容易判断、计算量较小的表达式放在逻辑运算符的左侧,可以减少不必要的运算,提高算法效率。 例如判断一个数是否是回文素数时,将判断回文表达式str(i) == str(i)[::-1]放在运算符左侧,当判断不是回文时,不再执行右侧判定素数函数prime(i)。因判定素数的计算量较大,这样设计可以极大的降低运算量,提高效率。

实例:输出20000以内的所有回文素数

判断一个数是否是回文素数时,将判断回文表达式str(i) == str(i)[::-1]放在运算符左侧,当判断不是回文时,不再执行右侧判定素数函数prime(i)
因为判定素数的计算量较大,这样设计可以极大的降低运算量,提高效率。
断回文素数的完整程序如下(后续章节将详细讲解,这里不讲函数,只让大家了解__计算量大的__放到__逻辑运算符右侧__会极大提高效率)

In [1]:
import datetime # 导入datetime,用于计算程序运行时间


def prime(n):
    """接收正整数n,判断是否为素数,返回布尔值"""
    if n < 2:
        return False       # 0和1不是素数
    for i in range(2, n):  # 遍历(2, n-1)中的数
        if n % i == 0:     # 若在(2, n-1)中存在因子则不是素数
            return False   # 不是素数时返回False
    else:   # for语句遍历(2, n-1)中所有数,未发现因子存在时,才是素数
        return True        # 素数时返回True

    
start = datetime.datetime.now()  # 记录程序开始时间

for i in range(20000):
    # 利用短路提高效率
    if prime(i) and str(i) == str(i)[::-1]: # 先判断素数,再判断回文,效率低
        print(i,end=' ')
        
end = datetime.datetime.now()  # 记录程序结束时间
during = (end - start).seconds * 1000 + (end - start).microseconds / 1000  # 计算耗时,精确到毫秒 
print(f'\n耗时:{during}毫秒')
In [8]:
import datetime # 导入datetime,用于计算程序运行时间


def prime(n):
    """接收正整数n,判断是否为素数,返回布尔值"""
    if n < 2:
        return False       # 0和1不是素数
    for i in range(2, n):  # 遍历(2, n-1)中的数
        if n % i == 0:     # 若在(2, n-1)中存在因子则不是素数
            return False   # 不是素数时返回False
    else:   # for语句遍历(2, n-1)中所有数,未发现因子存在时,才是素数
        return True        # 素数时返回True

    
start = datetime.datetime.now()  # 记录程序开始时间

for i in range(20000):
    # 利用短路提高效率
    if str(i) == str(i)[::-1] and prime(i): # 先判断回文,再判断素数,效率高
        print(i,end=' ')
        
end = datetime.datetime.now()  # 记录程序结束时间
during = (end - start).seconds * 1000 + (end - start).microseconds / 1000   # 计算耗时,精确到毫秒 
print(f'\n耗时:{during}毫秒')

若将回文判定改写为通过取余和整除操作来反转整数再比较的方法,性能还可以进一步提升,表明纯粹的数值运算的优能是优于转字符串再处理的。

In [3]:
import datetime # 导入datetime,用于计算程序运行时间


def prime(n):
    """接收正整数n,判断是否为素数,返回布尔值"""
    if n < 2:
        return False       # 0和1不是素数
    for i in range(2, n):  # 遍历(2, n-1)中的数
        if n % i == 0:     # 若在(2, n-1)中存在因子则不是素数
            return False   # 不是素数时返回False
    else:   # for语句遍历(2, n-1)中所有数,未发现因子存在时,才是素数
        return True        # 素数时返回True


def reverse_number(num):
    """接收正整数n,判断是否为回文数,返回布尔值"""
    temp = num
    reverse_n = 0
    while temp > 0:
        reverse_n = reverse_n * 10 + temp % 10
        temp //= 10
    return num == reverse_n


start = datetime.datetime.now()  # 记录程序开始时间

for i in range(20000):
    # 利用短路提高效率
    if reverse_number(i) and prime(i): # 先判断回文,再判断素数,效率高
        print(i,end=' ')
        
end = datetime.datetime.now()  # 记录程序结束时间
during = (end - start).seconds * 1000 + (end - start).microseconds / 1000   # 计算耗时,精确到毫秒 
print(f'\n耗时:{during}毫秒')

练一练

补充完成下面的程序,判断输入的年份是否为闰年。

用户输入一个年份,若该年份为闰年,则输出'XXXX年是闰年';否则输出'XXXX年不是闰年'。

公历闰年判定遵循的规律为:“四年一闰,百年不闰,四百年再闰。”,符合以下条件之一的年份即为闰年

  1. 能被4整除而不能被100整除。
  2. 能被400整除。

输入一个正整数表示的年份,判定是否是闰年。 闰年的判定标准是能被4整除但不能被100整除的年份,或者能被400整除的年份。这个判定可以用布尔运算来实现:

输入输出样例:

输入:2000
输出:2000年是闰年

输入:2022
输出:2022年不是闰年
In [ ]:
year = int(input())  # 输入整数年份
# 参考前例,根据条件使用if...else...判断是否为闰年,并输出对应信息
In [ ]:
# 参考答案:
year = input()  # 输入要检查的年份

is_leap_year = year % 4 == 0 and year % 100 != 0 or year % 400 == 0

if is_leap_year:
    print(year, '年是闰年')
else:
    print(year, '年不是闰年')

and优先级高,先计算year % 4 == 0 and year % 100 != 0的值,and两侧的比较运算的结果都是布尔值,再参与到and运算得到的结果也只能是布尔值True或False,再参与优先级较低的 or 运算得到最终结果仍是布尔值 True或False,表示是闰年或非闰年。程序中的布尔运算相当于(year % 4 == 0 and year % 100 != 0) or year % 400 == 0。为了避免引起误读,在同一个表达式中同时出现 and 和 or 时,建议用加小括号的方法明确顺序,这样可以更准确的表达逻辑顺序,同时提高程序的可读性和易维护性。

练一练

改变逻辑运算的顺序,参考回文素数的例子,计算运算时间,考察不同运算顺序是否影响效率?

In [ ]:
# 补充你的代码

逻辑运算符 or、and 和 not 中,not 优先级最高,or 最低,按优先级升序排序为 __or < and < not__
在同一个表达式中同时出现 and 和 or 时,建议用加小括号的方法明确顺序,这样可以更准确的表达逻辑顺序,同时提高程序的可读性和易维护性。

In [16]:
print(1 or 0 and 2 )     # 输出 1
# 由于 or 优先级最低,最后参与运算,先计算0 and 2的值,整个表达式等价于在0 and 2上加括号
print(1 or (0 and 2))    # 输出 1

实例: 登录验证

用户名为admin和root的用户的密码均为“123456”。用户输入用户名与密码,当输入的用户名为admin或root,且密码为“123456”时,输出“登录成功”;否则输出“登录失败”。

In [17]:
user_name = input()
password = input()
if user_name == 'root' or 'admin' and password == '123456': # 等价于(user_name == 'root') or ('admin' and password == '123456')
    print('登录成功')
else:
    print('登录失败')

上面的代码中if后面的条件表达式等价于:

(user_name == 'root') or ('admin' and password == '123456')

or两边有一边为真时,就可以通过验证。
当用户输入的用户名为root时,左边为True,or运算发生__短路__,略过右边表达式,所以不论密码输入什么也可登陆成功。
当用户输入任意用户名时,or左边为False,这时表达式结果由右边and运算结果确定。因为and左边是a非空字符串“admin”,为True,那么只要右侧的比较运算结果为True,即只要密码输入正确,无论用户名是什么,都可以登陆成功;
显然与题目要求不符。

In [18]:
user_name = input()
password = input()
if (user_name == 'root' or 'admin') and password == '123456': 
    print('登录成功')
else:
    print('登录失败')

上面修改后的代码,表达式user_name == 'root' or 'admin'中字符串'admin'的真值测试结果为True,因此该表达式值恒为True,这时or左边的用户名验证会失效。
无论用户输入任何用户名,只要密码输入正确即可登陆成功,显然与题目要求仍然不符。

正确的代码如下。

In [19]:
user_name = input()
password = input()
if (user_name == 'root' or user_name == 'admin') and password == '123456': 
    print('登录成功')
else:
    print('登录失败')
In [12]:
user_name = input()
password = input()
if (user_name == 'admin' and password == '123456') or (user_name == 'root' and password == '123456'): 
    print('登录成功')
else:
    print('登录失败')
In [ ]:
user_name = input()
password = input()
if user_name == 'root' or user_name == 'admin':
    if password == '123456': 
        print('登录成功')
else:
    print('登录失败')

运算优先级

Python支持多种运算符的混合运算,所有运算符的优先级(由高到低排列)的描述如下表所示。

序号 运算符 描述
1 ()、[]、{} __括号表达式,元组、列表、字典、集合__
2 x[i],x[m:n] 索引、切片
3 ** __幂运算__
4 +x、 -x、~x 正、负,按位非 NOT
5 *、 / 、//、% __乘法、除法与取模__
6 + 、- __加法与减__
7 << 、>> 移位
8 & 按位与
9 ^ 按位异或
10 | 按位或
11 <、<=、>、>=、!=、== 、is、is not、in、not in __比较运算、成员检测和标识号检测__
12 not x 逻辑非
13 and __逻辑与运算符__
14 or __逻辑或运算符__
15 if…else 条件表达式
16 lambda lambda表达式
17 := __赋值表达式、海象运算符__
In [20]:
print(4 * 2 ** 3)      # 先幂运算,再计算乘法,输出:32
print(3 + 4 * -2)      # 2先取反、与4相乘,再做加法,输出:-5
print(3 + 4 * 2 / 2)   # 先计算乘除法,除法运算结果为浮点数,输出:7.0
print(3 << 2 + 1)      # 加法优先级高,先2+1,3(11)左移3位变成24 (11000)

括号的优先级最高,可以强制表达式按照需要的顺序求值。可以通过加入小括号“()”的方法来提供弱优先级的优先执行。加了括号,无需比较哪个优先级更高,使程序和表达式更加易于阅读和维护。

实例:判断是否直角三角形

输入三个数a,b,c, 判断能否以它们为三个边长构成直角三角形。若能,输出YES,否则输出NO。

In [21]:
a = float(input())
b = float(input())
c = float(input())

# 三角形判定条件:边都为正数,任意两边之和大于第三边
if a <= 0 or b <= 0 or c <= 0 or (a + b) <= c or (a + c) <= b or (b + c) <= a:
    print('No')
# 直角三角形条件,两边平方和等于第三边
elif a * a + b * b == c * c or a * a + c * c == b * b or c * c + b * b == a * a:
    print('Yes')
else:
    print('No')

上面的代码中条件都比较长,可通过改进算法简化,代码如下。

In [22]:
a = float(input())
b = float(input())
c = float(input())

shortest = min(a, b, c)  # 获得最短边长度
longest = max(a, b, c)   # 获得最长边长度
middle = sum([a, b, c]) - shortest - longest

# 若最小边为负值或长边小于等于其他两边之和时,构不成三角形
if shortest <= 0 or shortest + middle <= longest:
    print('NO')
    
# 两边短边长度平方和等于第三边长度平方为直角三角形
elif shortest ** 2 + middle ** 2 == longest ** 2:
    print('YES')
    
else:
    print('NO')