date: 2024-11-25

真假值的替身可不好吃

在写这文章,作者随便上网调查中文博客的翻译,发现

truthy和falsy一般译为真假值。可是, true和false的值名也应该被翻译为真假值。

故此,我取巧地下了一个标题:真假值的替身。好吧,我这里翻译为虚实值 (falsy and truthy),真假值为true and false。

此外, null为空值。

orand 表达式的简单实现

假设把玩的玩具是蟒蛇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是一门动态语言, 条件判断表达式的式子不一定非得布尔值,可以是任何东西。 orand也是同样的道理。

再来,这也是一个趁手的构造。

意外发现

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

我们可以看到

没有条件运算子的变通

在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的andor表达所展开成以下的表达式

(and e1 e2)
=> (let ((%tmp e1)) (if %tmp e2 %tmp))
(or e1 e2)
=> (let ((%tmp e1)) (if %tmp e2 #f))

如同python, scheme也是动态类型。自然而然,andor的式子非得是布尔值。 可为何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). If test evaluates to a true value, then recipient is evaluated. Its value must be a procedure of one argument; this procedure is then invoked on the value of the test, 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-1record-2没有相关k键。

Lesson learned - only boolean in boolean expression

总结就是仰赖虚实值非常容易出错(我这种菜表示把握不住)。再来,这种写法没有普遍性因为不同编程语言对虚实值定义不一样。

比如

最兼容的写法就是布尔表达式只能是布尔类型,无它。 如果太过严格,弱化版是只定义唯一虚值,然后其他为实值。

pandasnumpy库也弃用虚实值的概念。比如,空数组或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, FalseNone都是单列。

给它们做等同判断(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

参考