and和or运算符的冷门细节和相关技巧(Python)

目录

一. and和or的运算优先级

二. and的求值特性

三. or的短路求值 

四. 用多行表达复杂的与或条件 

五. 德-摩根公式

六. or的其它登场场所

1. n维向量中的骚操作

2. 创建目录时的骚操作

3. 指定默认值时的操作

七. and对性能的加持


这俩可真是老熟人了,众所周知,它们分别表示“”和“”的关系。本文不再赘述这些,直接从比较冷门的知识和细节说起。

一. and和or的运算优先级

你觉得下面表达式的运算结果是什么呢:

True or False and False

答案是True,其实,and的运算优先级比or高,所以,上面的式子其实相当于:

True or (False and False)

我们没必要去记忆这破东西,只要加上括号,一切问题都不是问题,还能让代码更易读:

 

二. and的求值特性

我们平常将and用于bool值的运算,其实它也可以用在一般的表达式中。

我们想想and的特点,为了尽快判断出下面式子的结果:

带下标的x表示True或False
x1 and x2 and x3 and x4 and……

考虑到and的意思是“所有条件为真,则整体为真,否则整体为假”,我们应该设置这样的逻辑:

 从左到右判断x的真假值,一旦发现假就整体返回假;如果没有发现假,则返回真

依据这个逻辑,一般的二元求值式应当这样判断结果:

n = x and y
如果bool(x)为假,则n=x;否则n=y

由此类推,三元的式子其实可以表示成嵌套and表达式:

n = x and y and z
等价于:
n = x and (y and z)
如果bool(x)为False,则n=x;否则n=(y and z),
此时,当bool(y)为False时n=y,否则n=z。

 再进行一次推广,包含多个and的求值表达式总会返回第一个假值或最后一个真值。或者说,如果“队列”(姑且让我这么叫吧)中有假值,则返回第一个假值。如果全是真,返回最后一个值,这个值确实是“最后一个真值”。

为了验证这种行为,下面我自定义“真字符串”和“假字符串”两个类,只改写它们的__bool__方法,看看带and的三元表达式的求值结果是不是我们想的那样:

from collections import UserString

class TrueStr(UserString):
    """bool值必定是True的字符串"""
    def __bool__(self):
        return True
class FalseStr(UserString):
    """bool值必定是False的字符串"""
    def __bool__(self):
        return False
    
xt = TrueStr('真x')
yt = TrueStr('真y')
zt = TrueStr('真z')

xf = FalseStr('假x')
yf = FalseStr('假y')
zf = FalseStr('假z')

def show_params(func):
    """
    打印调用任意位置参数函数的
    调用参数和调用结果的装饰器
    """
    def wrapper(*args):
        result = func(*args)
        loc_params = ', '.join(str(x) for x in args)
        print(f'参数({loc_params}) -> 结果({result!r})')
        return result
    return wrapper

@show_params
def x_and_y_and_z(x, y, z):
    """包含两个and的求值表达式"""
    return x and y and z

for x in (xt, xf):
    for y in (yt, yf):
        for z in (zt, zf):
            x_and_y_and_z(x, y, z)

这里用了一个装饰器方便查看函数调用的情况,结果如下:

 

大家可以看到,求值的结果确实符合“总会返回第一个假值或最后一个真值”的特性。

三. or的短路求值 

既然and都能用来求值了,那or当然也可以。其实,or的求值特性比and更广为人知。如果第一个表达式是真,则or后面的值会被忽略而不求值,这叫做or的短路求值特性。所以,下面的代码不会报错:

我们直接给出含有一大堆or的表达式的求值规则:总会返回第一个真值或最后一个假值,和and恰好相反,这还挺有趣的。下面是实验:

from collections import UserString

class TrueStr(UserString):
    """bool值必定是True的字符串"""
    def __bool__(self):
        return True
class FalseStr(UserString):
    """bool值必定是False的字符串"""
    def __bool__(self):
        return False
    
xt = TrueStr('真x')
yt = TrueStr('真y')
zt = TrueStr('真z')

xf = FalseStr('假x')
yf = FalseStr('假y')
zf = FalseStr('假z')

def show_params(func):
    """
    打印调用任意位置参数函数的
    调用参数和调用结果的装饰器
    """
    def wrapper(*args):
        result = func(*args)
        loc_params = ', '.join(str(x) for x in args)
        print(f'参数({loc_params}) -> 结果({result!r})')
        return result
    return wrapper

@show_params
def x_or_y_or_z(x, y, z):
    """包含两个or的求值表达式"""
    return x or y or z

for x in (xt, xf):
    for y in (yt, yf):
        for z in (zt, zf):
            x_or_y_or_z(x, y, z)

下面,我们说一说一些类似于“技巧”的东西。

四. 用多行表达复杂的与或条件 

最好不要把带有一大堆and,or的条件表达式压在一行。比如,我曾经用Pygame编写简单的2D游戏,里面有这样一句条件判断,是我Python处于“史山代码时期”的遗留产物:

elif (not self.game_activity) and self.previous_button.rect.collidepoint(mouse_pos) and (self.help.is_showing_help) and (self.help.image_num != 0):
    self.help.image_num -= 1

现在我看到这句代码,想的就是“写成这样是想给鬼看吗”?其实,鬼都不想看。

最好把这种长代码拆成好几行写,用括号把它们包起来,像下面这样:

elif (
    (not self.game_activity) and 
    self.previous_button.rect.collidepoint(mouse_pos) and 
    (self.help.is_showing_help) and 
    (self.help.image_num != 0)
):
    self.help.image_num -= 1

这样虽然还是冗胀,但已经好多了,要彻底让它整洁可能需要进行重构和逻辑的重划分。避免史山代码,从小事做起,从你我做起!

五. 德-摩根公式

我们不是在讨论集合论,但是有时候,利用这个原理确实可以简化一些与或条件。

在集合论中,这个公式也被称为集合运算的“对偶律”,具体来说是:

  • 并的对立是对立的交
  • 交的对立是对立的并

也就是说: 

\overline{\bigcup_{i=1}^{\infty}A_{i}}=\bigcap_{i=1}^{\infty}\overline{A_{i}}\overline{\bigcap_{i=1}^{\infty}A_{i}}=\bigcup_{i=1}^{\infty}\overline{A_{i}}

这个公式的二元形式是:

\overline{A\cup B}=\overline{A}\cap \overline{B}\overline{A\cap B}=\overline{A}\cup \overline{B} 

而对立其实就是“非”,并是“或”,交是“与”,所以写成Python的伪代码就是:

(not A) and (not B) = not (A or B)
(not A) or (not B) = not (A and B)

举个例子,假设我们在写一个网站,对于用户的请求我们想检查对方“是否登录”和“请求头是否是浏览器的”以及“信誉分是否大于60”,如果有一个不是,就拒绝服务:

if not request.is_logged_in or not request.is_from_browser or request.score <= 60:
    return '由于触发网站的安全协议,本次请求被拒绝'

有了德-摩根公式,可以简化一下它的逻辑。请注意,对于score来说,score <= 60和not score > 60是等价的,可以利用这种关系。

if not (request.is_logged_in and request.is_from_browser and request.score > 60):
    return '由于触发网站的安全协议,本次请求被拒绝'

还可以进一步拆成多行:

if not (
    request.is_logged_in and 
    request.is_from_browser and 
    request.score > 60
):
    return '由于触发网站的安全协议,本次请求被拒绝'

这样它的可读性就会大大改善了。

六. or的其它登场场所

在冷门用法中,or比and出场率高太多了。下面挑着说一说我见过的利用“三”中所说性质的操作。

1. n维向量中的骚操作

假设我们在写一个n维向量,要实现它的__bool__方法,在所有元素都是0或者是“空向量”的时候返回假,最简单办法应该是利用绝对值,all或any,或者像下面这样:

from array import array

class Vector:
    def __init__(self, *datas):
        self.elements = array('d', datas)
    def __iter__(self):
        yield from self.elements
    def __bool__(self):
        for ele in self:
            if ele != 0:
                return True
        return False

另外一种办法就是利用“三”中所说的or的求值特性: a or b or c or……在所有变量都是假值的时候才可能返回假值元素,否则一定返回真值元素。像下面这样: 

from array import array
from functools import reduce

class Vector:
    def __init__(self, *datas):
        self.elements = array('d', datas)
    def __iter__(self):
        yield from self.elements
    def __bool__(self):
        return bool(reduce(lambda a, b: a or b, self, 0))

不过我觉得这种办法有点过于不负责任了,前面说的任何一种办法都比这一种易懂,这么写一定是在“炫技”,太自私了!

2. 创建目录时的骚操作

我见过用pathlib创建目录时使用or的,因为用mkdir()方法创建目录时,在目标目录已经存在的情况下会报错,可能想要这么干:

from pathlib import Path

path = Path('某个子目录')

if not path.exists():
    path.mkdir()

某位同志想了个好办法,直接利用or“短路求值”的特性,写成了下面这样:

from pathlib import Path

path = Path('某个子目录')

path.exists() or path.mkdir()

如果第一个表达式是真,则or后面的值会被忽略而不求值,这么做确实可行。那位写出这句代码的同志一定很开心吧——“我真是个天才!用冷门知识优雅地解决了一个问题,快夸夸我!”

但是,文档中明明白白写了,对于这种情况,可以指定exist_ok=True来避免报错:

from pathlib import Path

path = Path('某个子目录')

path.mkdir(exist_ok=True)

所以说,超脱于常人的方案并不一定就是优秀的。

3. 指定默认值时的操作

这倒不是“骚操作”,不过有些需要注意的点。

利用or的短路求值特性,可以方便地为某些可能丢失的值赋默认值。比如,在融合字典时,接收到的参数可能是字典,也可能是None。为了避免拆包None而报错,可以使用or在param为None时使用空字典:

raw_dict = {
    '穿山甲': 68,
    '小明剑魔': 150,
}
param = None

new_dict = {
    **raw_dict,
    **(param or {}),
}

不过,如果指定的默认值不是相应的假值,就可能出现问题。

比如,我要接受一个times参数,可能是int也可能是None,如果是None,则使用“5”作为默认值:

import time

def rest(times: int|None):
    sleep_times = times or 5
    time.sleep(sleep_times)
    print(f'休眠了{sleep_times}秒')

param = None

rest(param)

看上去好像没什么问题是吧?可是,一旦param传入“0”,就会出现问题:

问题就是,在or看来,None和0都是假值对象,所以都使用5这个默认值,导致行为不符合预期。

如果遇到了这种情况,还是不要剑走偏锋了,老老实实地用if吧:

import time

def rest(times: int|None):
    if times is None:
        times = 5
    time.sleep(times)
    print(f'休眠了{times}秒')
# 或者
def rest(times: int|None):
    sleep_times = 5 if times is None else times
    time.sleep(sleep_times)
    print(f'休眠了{sleep_times}秒')

最后,我们来说一说一个“冷知识”。

七. (a<x<b)和(x>a) and (x<b)的性能差别 

容易知道,下面的两种写法在逻辑上等价:

if (1 < x < 3):
if (x > 1) and (x < 3):

那么哪个运行速度更快呢?珍爱赌博、远离生命,第x届Python赌神大赛现在开赌,请在心中默念你支持的写法!下面我们做个实验。

from functools import partial

from my_modules.clock import clock

@clock(report_upon_exit=True)
def run_func_of_n_times(func_name: str, n: int):
    func = eval(func_name)
    for _ in range(n):
        func()

def f(x):
    return (1 < x < 3)
def g(x):
    return (x > 1) and (x < 3)
pf = partial(f, x=2)
pg = partial(g, x=2)

run_func_of_n_times('pf', 100000000)
run_func_of_n_times('pg', 100000000)
  • 首先,f和g两个函数的作用就是执行一次不同写法的判断操作。
  • pf和pg是偏函数,传入了x=2,供run_func_of_n_times()使用。
  • 顾名思义,这个函数运行相应函数n次,用@clock装饰器来计时。最后,分别运行两个偏函数1亿次,查看运行时间。

结果如下:

看来,第二种包含and的写法性能有极其微小的优势!你赌对了吗?

有人可能会怀疑,这么小的差距,不会是随机事件吧?你耍老千诓我,我要揍死你!

先别动手呀,其实这是“命中注定”的。我们用dis模块反汇编一下两个函数,看看字节码就知道是为什么了。

from dis import dis

def f(x):
    return (1 < x < 3)
def g(x):
    return (x > 1) and (x < 3)

dis(f)
print('=' * 50)
dis(g)

结果如下:

两个函数的字节码用===……隔开了,可以看到,第一种写法的字节码明显更长。不熟悉字节码没关系,下面是两者的逐行解释:

所以从理论上说,带and的那种写法在指令数、栈操作、跳转效率上都更胜一筹。执行1亿次这种超大次数,使得两者的差距变得明显了,这是“命中注定”的。

不过,我并不是说“不能用链式判断,必须用and”,这两者的效率差距太小了,而且不是消耗性能的大头,用哪个都无所谓。

就好比做爬虫的时候,10s等待响应,运行Python爬虫代码用时0.01s,运行C++爬虫代码用时0.00001s,效率是Python的1000倍,但是我们根本注意不到10.01s和10.00001s的差距,所以用Python也没事。

请不要被我误导掉进了“完美主义”的陷阱,纠结要不要这一丁点的性能提升其实是精神内耗。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值