为什么空格会影响相等字符串的标识符比较?

51 阅读3分钟

在 Python 中,使用 is 操作符比较两个字符串时,如果两个字符串相等,则 is 操作符会返回 True;如果两个字符串不相等,则 is 操作符会返回 False。但是,如果在两个相等字符串之间添加一个空格,则 is 操作符会返回 False

a = 'abc'
b = 'abc'
a is b
#outputs: True

a = 'abc abc'
b = 'abc abc'
a is b
#outputs: False

解决方案

Python 解释器会根据某些标准缓存一些字符串,第一个 abc 字符串被缓存并用于这两个变量,但第二个 abc abc 字符串没有被缓存。这是因为字符串被内部化/缓存,将 ab 赋值为 "abc" 会使 ab 指向内存中的同一个对象,因此使用 is 来检查两个对象是否实际上是同一个对象时,会返回 True

第二个字符串 abc abc 没有被缓存,因此它们是内存中完全不同的两个对象,所以使用 is 进行标识符检查时会返回 False。此时 ab 并不相同,它们指向内存中的不同对象。

以下是代码示例:

a = "abc"  # python caches abc
b = "abc"  # it reuses the object when assigning to b
id(a)
#outputs: same id's, same object in memory
id(b)


a = 'abc abc'  # not cached
id(a)
b = 'abc abc'
id(b)  # different id's different objects
a is b
#outputs: False

字符串是否被缓存的标准是,如果字符串只包含字母、下划线和数字,那么在你的情况下,空格不满足这个标准。使用解释器,即使字符串不满足上述标准,也能指向同一个对象,即多重赋值。

a, b = 'abc abc', 'abc abc'
id(a)
id(b)
a is b
#outputs: True

通过查看 codeobject.c 源代码可以确认字符串被内部化的标准:

#define NAME_CHARS \
    "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ_abcdefghijklmnopqrstuvwxyz"

/* all_name_chars(s): true iff all chars in s are valid NAME_CHARS */

static int
all_name_chars(unsigned char *s)
{
    static char ok_name_char[256];
    static unsigned char *name_chars = (unsigned char *)NAME_CHARS;

    if (ok_name_char[*name_chars] == 0) {
        unsigned char *p;
        for (p = name_chars; *p; p++)
            ok_name_char[*p] = 1;
    }
    while (*s) {
        if (ok_name_char[*s++] == 0)
            return 0;
    }
    return 1;
}

当字符串的长度为 0 或 1 时,它们总是会被共享,这可以在 stringobject.c 源代码中的 PyString_FromStringAndSize 函数中看到:

/* share short strings */
    if (size == 0) {
        PyObject *t = (PyObject *)op;
        PyString_InternInPlace(&t);
        op = (PyStringObject *)t;
        nullstring = op;
        Py_INCREF(op);
    } else if (size == 1 && str != NULL) {
        PyObject *t = (PyObject *)op;
        PyString_InternInPlace(&t);
        op = (PyStringObject *)t;
        characters[*str & UCHAR_MAX] = op;
        Py_INCREF(op);
    }
    return (PyObject *) op;
}

虽然这与问题没有直接关系,但对于那些感兴趣的人来说,codeobject.c 源代码中的 PyCode_New 函数展示了在满足 all_name_chars 中的标准后,在构建代码对象时,还有更多的字符串会被内部化:

PyCodeObject *
PyCode_New(int argcount, int nlocals, int stacksize, int flags,
       PyObject *code, PyObject *consts, PyObject *names,
       PyObject *varnames, PyObject *freevars, PyObject *cellvars,
       PyObject *filename, PyObject *name, int firstlineno,
       PyObject *lnotab)
{
    PyCodeObject *co;
    Py_ssize_t i;
    /* Check argument types */
    if (argcount < 0 || nlocals < 0 ||
        code == NULL ||
        consts == NULL || !PyTuple_Check(consts) ||
        names == NULL || !PyTuple_Check(names) ||
        varnames == NULL || !PyTuple_Check(varnames) ||
        freevars == NULL || !PyTuple_Check(freevars) ||
        cellvars == NULL || !PyTuple_Check(cellvars) ||
        name == NULL || !PyString_Check(name) ||
        filename == NULL || !PyString_Check(filename) ||
        lnotab == NULL || !PyString_Check(lnotab) ||
        !PyObject_CheckReadBuffer(code)) {
        PyErr_BadInternalCall();
        return NULL;
    }
    intern_strings(names);
    intern_strings(varnames);
    intern_strings(freevars);
    intern_strings(cellvars);
    /* Intern selected string constants */
    for (i = PyTuple_Size(consts); --i >= 0; ) {
        PyObject *v = PyTuple_GetItem(consts, i);
        if (!PyString_Check(v))
            continue;
        if (!all_name_chars((unsigned char *)PyString_AS_STRING(v)))
            continue;
        PyString_InternInPlace(&PyTuple_GET_ITEM(consts, i));
    }

这个答案基于使用 CPython 解释器进行的简单赋值,至于在简单赋值之外的函数或任何其他功能中与内部化相关的内容,这些内容并未被问到或回答。
如果有人对 C 代码有更深入的了解,可以随时编辑来添加内容。
这里有一个非常彻底的解释,可以详细了解整个字符串内部化过程。