date: 2024-11-25
真假值的替身可不好吃
在写这文章,作者随便上网调查中文博客的翻译,发现
truthy和falsy一般译为真假值。可是, true和false的值名也应该被翻译为真假值。
故此,我取巧地下了一个标题:真假值的替身。好吧,我这里翻译为虚实值 (falsy and truthy),真假值为true and false。
此外, null为空值。
or
和and
表达式的简单实现
假设把玩的玩具是蟒蛇python,
不过我们的python会遵循以下规则
<conseq> if <pred> else <altern> => <conseq> if <boolean> else <altern>
<conseq> if True else <altern> => <conseq>
<conseq> if False else <atlern> => <altern>
应用于几个式子
print(None if True else "no") # None
print("yes" if False else "no") # "no"
print("safe" if (True if True else False) else "doom") # "safe"
以下是一个or
表达式
x = <sub-expression-1> or <sub-expression-2>
print(x)
数学一般定义or
为
True or True == True
True or False == False
False or True == True
False or False == True
可以转换成下面符合or
定义的式子
# x = <sub-expression-1> or <sub-expression-2>
x = True if <sub-expression-1> else <sub-expression-2>
print(x)
类似的办法也可以实现and
表达式
# x = <sub-expression-1> and <sub-expression-2>
x = <sub-expression-2> if <sub-expression-1> else False
print(x)
为什么python会有虚实值?
可惜,以上模型不符合原生python的行为。 以下才是更精确的实现
# x = <sub-expression-1> or <sub-expression-2>
tmp = <sub-expression-1>
x = tmp if bool(tmp) else <sub-expression-2>
print(x)
# x = <sub-expression-1> and <sub-expression-2>
tmp = <sub-expression-1>
x = <sub-expression-2> if bool(tmp) else tmp
print(x)
虽然它们结果都是类似的,但为什么?多出来的构造是有什么用?
首先,python是一门动态语言, 条件判断表达式的式子不一定非得布尔值,可以是任何东西。
or
和and
也是同样的道理。
再来,这也是一个趁手的构造。
意外发现
def query_doc(title: str, authors: list[str] = None):
if authors is None:
authors = []
...
假如以上程序会向数据库查询文件的作者列表。有些文件是可以完全没有作者。
authors
形参设默认值是有意义的。
不过,使用mutable values来设默认参数是有一些坑
(详见: https://docs.python-guide.org/writing/gotchas/#mutable-default-arguments).
常见的workaround是把None
当作authors
默认参数, 然后如果是None
, 函数才会初始化authors
变量。
很久以前 (2005之前), 有些pythonistas意外发现这样做也可以达到同样的效果.
authors = authors or []
也是一样的结果因为如果根据我们的前面模型来转换
# x = <sub-expression-1> or <sub-expression-2>
# authors = authors or []
tmp = authors
authors = tmp if bool(tmp) else []
为了达到“如果是空值,就初始化”的效果,那么bool(None)
必须是假值。
这里的bool(None)
也可以解读为虚值因为不会被后续程序使用。(灵感:兵法中的虚实,实为有,虚为无,以实击虚)。
这也是python <var> or <default-val>
语法糖的由来。
如何在python里面一探虚实?
使用bool
函数就可以
vals = [
True,
False,
None,
-1,
0,
1,
'',
'delay no more',
[],
['',False],
{},
{'':''},
set(),
object(),
bool,
]
for v in vals:
print(f'bool({repr(v)}) = {bool(v)}')
在python运行一遍后
bool(True) = True
bool(False) = False
bool(None) = False
bool(-1) = True
bool(0) = False
bool(1) = True
bool('') = False
bool('delay no more') = True
bool([]) = False
bool(['', False]) = True
bool({}) = False
bool({'': ''}) = True
bool(set()) = False
bool(<object object at 0x7f2e31d786f0>) = True
bool(<class 'bool'>) = True
我们可以看到
None
是虚值,因为bool(None)
返回False
0
是虚值''
是虚值
没有条件运算子的变通
在2005前, python没有条件运算子<conseq> if <pred> else <altern>
。
当时的workaround是自己人肉翻译手写成以下
# x = <conseq> if <pred> else <altern>
x = (<pred> and <conseq>) or <altern>
根据我们的模型,转换上面or
表达式
tmp1 = (<pred> and <conseq>)
x = tmp1 if bool(tmp1) else <altern>
再转换and
表达式
tmp2 = <pred>
tmp1 = <conseq> if bool(tmp2) else False
x = tmp1 if bool(tmp1) else <altern>
n = -1
res = ((n < 0) and -n) or n
n = 3
res = ((n < 0) and -n) or n
如果应用于-n if n < 0 else n
,将会得到
n = -1
tmp2 = n < 0
tmp1 = -n if bool(tmp2) else False
res = tmp1 if bool(tmp1) else n
print(res)
n = 3
tmp2 = n < 0
tmp1 = -n if bool(tmp2) else False
res = tmp1 if bool(tmp1) else n
print(res)
python运行的结果也符合预期
1
3
虚值坏了好事
实例出处: https://mail.python.org/pipermail/python-dev/2005-September/056510.html
from dataclasses import dataclass
@dataclass
class ComplexType:
real: int|float = 0
imag: int|float = 0
def real(zs: list):
'Return a list with the real part of each input element'
# do not convert integer inputs to floats
return [(type(z)==ComplexType and z.real) or z
for z in zs]
The code fails silently when z is (0+4i) (i.e.:
ComplexType(0, 4)
)
如果我们颅内计算这个(type(z)==ComplexType and z.real) or z
表达式
z = Complex(0,4)
(type(z)==ComplexType and z.real) or z
| (type(z)==ComplexType and z.real)
| | type(z)==ComplexType
| | True
| True and z.real
| | z.real
| | 0
| | bool(0)
| | False
| True and False
| False
False or z
| z
结果是复数,不符合预想的结果。
这也为什么 PEP 308 提出要增加条件运算子<conseq> if <pred> else <altern>
.
初始化bug
某天,我spark job本地预览运行不能。spark job从.env
文件初始化configuration。
这是因为pydantic model抛出validation error 抱怨说port
不是整数.
# https://airflow.apache.org/docs/apache-airflow/stable/_api/airflow/models/connection/index.html
# AirflowConnection is a custom pydantic model
# which assert port must be integer
from warehouse import AirflowConnection
def connection_factory(config_prefix: str, spark = None):
if spark is None:
spark: SparkSession = SparkSession.getActiveSession()
conf = spark.sparkContext.getConf()
# https://spark.apache.org/docs/latest/api/python/reference/api/pyspark.SparkConf.get.html#pyspark.SparkConf.get
# suppose that conf.get always return either None or str
# conf.get(key: str, defaultValue: Optional[str] = None) -> Optional[str]
air_conf = {}
options = ['type', 'host', 'port', 'schema', 'login', 'password', 'extra']
for k in options:
val = conf.get(f'{config_prefix}.{k}')
if val == 'None':
val = None
air_conf[k] = val
...
if air_conf['port']:
air_conf['port'] = int(air_conf['port'])
return AirflowConnection(**cfg)
然后,.env
文件有一行port
如下
MYDB.PORT=""
我们都知道空串""
是虚值,当port
是空串,python会忽略并不会初始化,然后后面的程序会抛出validation error。
if air_conf['port']:
air_conf['port'] = int(air_conf['port'])
Scheme - false是唯一虚值
shceme的and
和or
表达所展开成以下的表达式
(and e1 e2)
=> (let ((%tmp e1)) (if %tmp e2 %tmp))
(or e1 e2)
=> (let ((%tmp e1)) (if %tmp e2 #f))
如同python, scheme也是动态类型。自然而然,and
和or
的式子非得是布尔值。
可为何schemers不像pythonista一样混淆?
因为RnRs标准定义#f
是唯一虚值,无它,不然全都是实值。
if
的规则
(if #f <conseq> <altern>)
=> <altern>
(if <any> <conseq> <altern>)
=> <conseq>
A motivating use case of =>
in cond
出处: Exercise 4.5 of SICP chapter 4
(cond <clause> ...)
<clause> := (<test> => <proc>)
| (<test> <e> ...)
(懒惰翻译原文)
Scheme allows an additional syntax for cond clauses, (
test
=>recipient
). Iftest
evaluates to a true value, thenrecipient
is evaluated. Its value must be a procedure of one argument; this procedure is then invoked on the value of thetest
, and the result is returned as the value of the cond expression.
与其
(define (apply-env x env)
(let ((res (assoc x env)))
(if res
(cadr res)
(error "apply-env" "unbound" x))))
我们可以使用=>
来接收assoc
的返还值
(define (apply-env x env)
(cond ((assoc x env) => cadr)
(else (error "apply-env" "unbound" x))))
这是可行因为如果assoc
找不到,就会返还#f
,然后跳到else的部分。
如果能找到,assoc
就会返回(键 值)
列表,因为不是#f
,然后会执行cadr
的部分。
假如有一个scheme编译器教学实现,其中uniquify
pass会转换sexp
程序成中间表示(intermediate representation)。
例子
(<form> <subform> ...)
(if <pred> <conseq> <altern>) ; `if` is a keyword
=> (if <pred> <conseq> <altern>)
(+ <left> <right>) ; `+` is primitive operator
=> (prim-call + <left> <right>)
(sqr 2) ; depends on current environment
=> (call sqr 2)
要么是keywords,要么是primitives,要么是其他。
以下是uniquify
pass的部分实现
(define (apply-env x env)
(cond ((assoc x env) => cadr)
(else (error "apply-env" "unbound" x))))
(define (keyword? kw env)
(and (symbol? kw)
(let ((maybe (assoc kw env)))
(if maybe
(eq? (cadr maybe) 'keyword)
maybe))))
(define (prim? x)
(and (pair? x) (eq? (car x) 'prim)))
(define (prim->prim-call prim es)
(list* 'prim-call (car es) es))
(define (init-env)
'((if keyword)
(+ (prim +))))
(define (uniquify e env)
(cond
((symbol? e) (apply-env e env))
((not (pair? e)) e)
((not (keyword? (car e)))
(let ((e* (uniquify e env)))
(if (prim? e*)
(prim->prim-call op (uniquify-each (cdr e) env))
(make-apply (uniquify (car e)) (uniquify-each (cdr e) env)))))
((if? e) ...))
...)
当运行(uniquify '(+ 1 2) (init-env))
的keyword?
部分,程序已经知道+
是primitive。
为了避免多次遍历列表,与其keyword?
返还布尔值,可以改写成返还匹配结果,如果有对应键;如果没有,还是返还#f
。
然后利用=>
来接收匹配结果。虽然个人甚少使用这个构造。
(define (keyword? kw env)
(and (symbol? kw)
(let ((maybe (assoc kw env)))
(if maybe
(if (eq? (cadr maybe) 'keyword)
#t
(cadr maybe))
maybe))))
(define (uniquify e env)
(cond
((symbol? e) (apply-env e env))
((not (pair? e)) e)
((not (keyword? (car e)))
=> (lambda (op)
(if (prim? op)
(prim->prim-call op (uniquify-each (cdr e) env))
(make-apply (uniquify (car e)) (uniquify-each (cdr e) env)))))
((if? e) ...)
...))
约定俗成的用法使用#f
来表示缺失。这也是schemers来实现option type/maybe type。
比如,assoc
函数,如果找不到对应键,返还#f
,如果有,返还一个包含键和值的列表。
(define (assoc x alist)
(cond
((null? alist) #f)
((not (pair? alist))
(error "assoc" "improperly formed alist"))
(((not (pair? (car alist))))
(error "assoc" "improperly formed alist"))
((equal? (caar alist) x)
(car alist))
(else (assoc x (cdr alist)))))
当然,同样的坑还是有的。如例子下
(define x (cadr (or (assoc k record-1) (assoc k record-2) '(whatever default-value))))
(define y (cadr (or (assoc k record-1) (assoc k record-2))))
第一行大多数时候是可以运行,第二行会抛出异常,如果record-1
和record-2
没有相关k
键。
Lesson learned - only boolean in boolean expression
总结就是仰赖虚实值非常容易出错(我这种菜表示把握不住)。再来,这种写法没有普遍性因为不同编程语言对虚实值定义不一样。
比如
- Javascript的空对象
{}
并不虚但python的空字典{}
表示很虚 - Scheme
#f
是唯一虚值 - Lua
nil
和false
唯二虚值 - Common Lisp空列表
nil
(i.e.:'()
)是唯一虚值 - C 把
0
当作虚值,其他都是实值 - 静态语言如ocaml和haskell布尔表达式只允许布尔值
最兼容的写法就是布尔表达式只能是布尔类型,无它。 如果太过严格,弱化版是只定义唯一虚值,然后其他为实值。
pandas
和numpy
库也弃用虚实值的概念。比如,空数组或empty dataframe没有虚实值。
与其
if not authors:
authors = []
不如
if authors is None:
authors = []
与其
if not author:
author = 'reimu'
不如
if author == '':
author = 'reimu'
与其
if not rate:
rate = 0.06
不如
if rate is None:
rate = 0.06
PEP 8 programming recommendations 部分也建议类似的东西。
间幕: 隐式转换以及Identity(等同)和Equality(等值)的概念
# python
print(0 == False)
Quick quiz: 以上会打印什么?会打印true
,想不到吧!
论为何,并不重要,因为鲁路修曾说过
如果不符合期望,肯定是编程语言的错!- by 鲁路修 probably
Python pep 285 (see https://peps.python.org/pep-0285/) 保证 True
, False
和None
都是单列。
给它们做等同判断(identity)必会是它们自己本身,如果是真值
x is True
x is False
x is None
然而, python不保证如果x是0,x is 0
返还真值因为integer是堆分配对象。
from math import factorial
print(factorial(10) == factorial(10)) # True
print(factorial(10) is factorial(10)) # False
这是因为有两种“相同”的概念:identity和equality。 前者问你是不是这个人,后者问你和你双胞胎一样么。
打个比方,虽然灵梦账户一分钱都没有,魔理沙账户一分钱也没有,并不代表她们在用同一个账户(identity),虽然一样穷(equality)。
python的is
操作符判断是不是同一个对象,表现为内存地址为一样。python的==
判断是不是等值,可以是两个不同的对象,仍然是等值。
一般都是用is
来判断是不是单列。这也是为什么这里程序使用is
来判断是不是None
。
参考
- PEP 308 - Conditional Expressions, https://peps.python.org/pep-0308/
- PEP 285 - Adding a bool type, https://peps.python.org/pep-0285/
- [Python-Dev] "and" and "or" operators in Py3.0, https://mail.python.org/pipermail/python-dev/2005-September/056846.html
- [Python-Dev] Conditional Expression Resolution, https://mail.python.org/pipermail/python-dev/2005-September/056510.html
- SICP, https://web.mit.edu/6.001/6.037/sicp.pdf
- R5RS scheme standard, https://conservatory.scheme.org/schemers/Documents/Standards/R5RS/r5rs.pdf
- Caveat of mutable default argument, https://docs.python-guide.org/writing/gotchas/#mutable-default-arguments
- Maybe type in racket, https://docs.racket-lang.org/functional/maybe.html
- 【python】python中什么会被当真?你知道if判断背后的规则吗?https://www.bilibili.com/video/BV1dZ4y127fR/?share_source=copy_web&vd_source=28010969e8f47cd5d07fff1fbe2a90fe