x不在集合中与x!=集合中的每个元素有何区别?

4
假设我们有x = ['a','b']
对于语句下面的操作,背后发生了什么?
x not in {None, False}

怎么解决出现“不可哈希类型:'list'”错误的问题?

我发现的解决方法是改为写成这样:

x != None and x!= False

但我感到困惑的是,从数学上讲,这两个布尔表达式是等效的。


1
出于好奇,你能用言语描述一下你实际想要实现什么吗?我怀疑两个选项都不是你真正想要的。 - Mad Physicist
2
x is not hashable, so it cannot be in your set - Jean-François Fabre
2
你正在尝试查找一个列表对象是否在两个值的集合中(即其中一个值在集合中是一个列表)。列表无法进行哈希运算,因此不能成为集合的一部分,因此也不可比较。你希望该操作执行什么操作? - deceze
2
set() 中的 [] 引发错误而不是评估为 False,这是否有一些合理性? - Patrick Haugh
2
@Patrick Python会警告你进行无意义的操作,而不是悄悄地失败。 - deceze
显示剩余3条评论
4个回答

5

背景

下面是官方文档的陈述:

  1. [Python 3]: class set([iterable])

    返回一个新的集合或不可变集合对象,其元素来自于iterable。 集合的元素必须可哈希的。

  2. [Python 3]: 可哈希:

    如果对象拥有永远不会在其生命周期内更改的哈希值(需要一个__hash__()方法),并且可以与其他对象进行比较(需要一个__eq__()方法),则该对象是可哈希的。相等的可哈希对象必须具有相同的哈希值。
    ...
    Python所有的不可变内置对象都是可哈希的; 可变容器(如列表或字典)则不是

  3. [Python 3]: object.__contains__(self, item) (在锚点上方):

    成员资格测试运算符(innot in)通常是通过迭代序列实现的。但是,容器对象可以提供以下特殊方法来实现更高效的实现,而且也不要求对象是一个序列。

深入参考[GitHub]: python/cpython - (v3.5.4) cpython/Objects/setobject.c:

  • Line #1991:

    static PyMethodDef set_methods[] = {
        {"add",             (PyCFunction)set_add,           METH_O,
         add_doc},
        {"clear",           (PyCFunction)set_clear,         METH_NOARGS,
         clear_doc},
        {"__contains__",(PyCFunction)set_direct_contains,           METH_O | METH_COEXIST,  // @TODO - cfati: MARK THIS LINE
    
  • Line #1843:

    static PyObject *
    set_direct_contains(PySetObject *so, PyObject *key)
    {
        long result;
    
        result = set_contains(so, key);  // @TODO - cfati: MARK THIS LINE
        if (result == -1)
            return NULL;
        return PyBool_FromLong(result);
    }
    
  • Line #1823:

    static int
    set_contains(PySetObject *so, PyObject *key)
    {
        PyObject *tmpkey;
        int rv;
    
        rv = set_contains_key(so, key);    // @TODO - cfati: MARK THIS LINE
        if (rv == -1) {
            if (!PySet_Check(key) || !PyErr_ExceptionMatches(PyExc_TypeError))
                return -1;
            PyErr_Clear();
            tmpkey = make_new_set(&PyFrozenSet_Type, key);
            if (tmpkey == NULL)
                return -1;
            rv = set_contains_key(so, tmpkey);  // @TODO - cfati: MARK THIS LINE
            Py_DECREF(tmpkey);
        }
        return rv;
    }
    
  • Line #627:

    static int
    set_contains_key(PySetObject *so, PyObject *key)
    {
        setentry entry;
        Py_hash_t hash;
    
        if (!PyUnicode_CheckExact(key) ||
            (hash = ((PyASCIIObject *) key)->hash) == -1) {  // @TODO - cfati: MARK THIS LINE
            hash = PyObject_Hash(key);
            if (hash == -1)
                return -1;
        }
        entry.key = key;
        entry.hash = hash;
        return set_contains_entry(so, &entry);  // @TODO - cfati: MARK THIS LINE
    }
    
  • Line #614:

    static int
    set_contains_entry(PySetObject *so, setentry *entry)
    {
        PyObject *key;
        setentry *lu_entry;
    
        lu_entry = set_lookkey(so, entry->key, entry->hash);  // @TODO - cfati: MARK THIS LINE
        if (lu_entry == NULL)
            return -1;
        key = lu_entry->key;
        return key != NULL && key != dummy;
    }
    
从“调用栈”可以看出(以相反的顺序呈现),为了测试成员资格(in / not in),在所有代码路径上都要对候选成员(“包含者”)执行哈希操作,由于列表实例没有哈希功能,解释器会抛出TypeError
解决方法:
  • 使用不需要其元素可哈希的容器(listtuple
  • 测试__hash__成员
  • 将成员资格测试包装在try/except块中
  • 为元素使用可哈希容器(tuple):x = ('a', 'b')
但(一般来说)这些只是解决问题的方法(这是我的个人意见),因为如果最终将列表与NoneFalse进行比较,则生成该列表的代码可能需要进行重构。

3
如果您可以将所有要测试的元素放入一个set中,这意味着所有不可哈希的元素不会属于您的set(因为您不能将它们放入其中)。
你可以这样做:
if x.__hash__ and x in {None,False}:

当对象不可哈希时,x.__hash__None(其他替代方法:询问 Python 值的“可哈希性”),第二部分不会被评估。
或者(宁愿请求宽恕而不是允许):
def belongs(x):
    try:
        return x in {None,False}
    except TypeError:   # unhashable type
        return False

这两种方案都比使用listtuple(None,False))更快,因为它们没有涉及线性搜索(如果测试列表中有很多元素,则是如此,而对于只有2个元素的情况不是这样)


我认为 try: [] in {} except TypeError: ... 更符合 Python 风格。 - deceze
@deceze 是的,我想使用短路。异常会强制创建一个函数。我提供的链接中还有其他选择(比如测试类型是否为 collections.Hashable,但我怀疑这会非常慢)。 - Jean-François Fabre
1
异常并不强制要求创建一个函数;它取决于代码使用的上下文以及您希望它有多么冗长/简洁。也许当出现 TypeError 或者显然 x 的类型错误时,您可以中止整个操作或者不中止。 - deceze

1

{None, False} 是一个集合。集合只能包含可哈希对象,因此只能测试可哈希对象的成员资格。列表不可哈希。

相反,您可以使用元组执行相同类型的成员资格比较。元组元素不需要是可哈希的。

x not in (None, False)

6
如果 x 是不可哈希的,那么它就不会在集合里 :) - Jean-François Fabre
@Jean-FrançoisFabre 是的,没错。 - glibdud

0
我想简要比较一下在setlist上进行成员测试的区别。
成员测试会调用__contains__ dunder(如果类实现了这个方法)。因此,如果我们编写
>>> 1 in [1,2] 

它将等同于

>>> list.__contains__([1,2],1)
>>> True

如果我们这样做:
>>> [1,2] in [1,2,3]
>>> False #We get False instead of TypeError here

但是为什么上述情况不适用于集合呢?成员测试在列表和集合中的工作方式不同。实际上,列表和集合的实现方式也不同。说到集合,它们是使用{{link1:哈希表}}实现的。这使得sets能够执行成员测试,即在O(1)中查找,而在list中查找则需要O(n)。因此,当在集合上执行in时,__contains__会尝试使用{{link2:__hash__}}计算需要查找的对象的hash。由于列表在Python中是不可哈希的,因此您会收到错误消息:TypeError: unhashable type: 'list'。如果您对列表执行相同的操作,则不会收到任何错误消息,因为列表不会为成员测试计算哈希值。

简而言之,无法对不可哈希的对象执行集合的成员测试。一般来说,所有可变对象(list、set、dict)都是不可哈希的。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接