【问题标题】:x not in set versus x != each element in the set?x 不在集合中与 x != 集合中的每个元素?
【发布时间】:2018-11-29 16:11:15
【问题描述】:

假设我们有x = ['a', 'b']

语句的幕后发生了什么:

x not in {None, False}

这会引发unhashable type: 'list' 错误?

我发现的解决方法是改写这个:

x != None and x!= False

但我很困惑,因为在数学上,两个布尔表达式是等价的。

【问题讨论】:

  • 出于好奇,您能用文字描述一下您真正想要实现的目标吗?我怀疑这两种选择都不是您真正想要的。
  • x 不可散列,因此它不能在您的set
  • 您正试图找出一个列表对象是否在一组两个值中(意味着集合中的两个值中的一个是否是一个列表)。列表不能被散列,因此不能成为集合的一部分,因此不可比较。您希望该操作做什么?
  • [] in set() 引发错误而不是评估 False 是否有一些理由?
  • @Patrick Python 警告你无意义的操作,而不是悄悄地失败。

标签: python python-3.x


【解决方案1】:

基本原理

以下是官方文档的说明:

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

    返回一个新的 set 或 freezeset 对象,其元素取自 iterable。集合的元素必须是 hashable

  2. [Python 3]: hashable:

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

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

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

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

  • #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
    
  • #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);
    }
    
  • #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;
    }
    
  • #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
    }
    
  • #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),正在执行 hash(在所有代码路径上)在候选成员(“includee”)上,并且由于 list 实例没有 hash 功能,解释器吐出 类型错误

分辨率

有很多方法可以解决这个问题(正如许多其他人已经指出的那样):

  • 使用不需要其元素可散列的容器(listtuple
  • 测试 __hash__ 成员
  • 将成员资格测试包装在 try / except 块中
  • 为元素使用可散列容器(元组):x = ('a', 'b')

但是(通常)这些只是解决问题的方法(这是我个人的看法),因为如果您最终将列表与 None 和 False,代码(产生该列表)可以使用一些重构。

【讨论】:

    【解决方案2】:

    如果您可以在set 中输入您要测试的所有元素,则意味着所有不可散列的元素属于您的集合(因为您无法将它们放入)

    你可以这样做:

    if x.__hash__ and x in {None,False}:
    

    当对象不可散列时,x.__hash__None(其他替代方案:Asking "is hashable" about a Python value)并且不评估第二部分。

    或者(最好是请求原谅而不是允许):

    def belongs(x):
        try:
            return x in {None,False}
        except TypeError:   # unhashable type
            return False
    

    这两种解决方案都比使用 listtuple ((None,False)) 更快,因为不涉及线性搜索(也就是说,如果测试列表中有很多元素,则不是只有 2 个元素)

    【讨论】:

    • 我认为try: [] in {} except TypeError: ... 会更pythonic。
    • @deceze 是的,我想使用短路。异常强制创建函数。我提供的链接中还有其他选择(例如测试类型 aganst collections.Hashable,但我怀疑这将是 slooooow)
    • 异常不会强制创建函数;这取决于使用此代码的上下文以及您想要它的详细程度/简洁程度。当TypeError 发生或不发生时,也许您可​​以中止整个操作,因为显然x 是错误的类型……
    【解决方案3】:

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

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

    x not in (None, False)
    

    【讨论】:

    • 所以如果x 是不可散列的,那么它不在集合中:)
    • @Jean-FrançoisFabre 是的,没错。
    【解决方案4】:

    我想对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
    

    但为什么上述情况不适用于集合?成员资格测试在列表和集合中以不同的方式工作。事实列表和集合的实现方式不同。说到集合,它们是使用Hash-Table 实现的。这允许sets 执行成员资格测试,即在O(1) 中查找,而在list 中查找是O(n)。所以当in 对一个集合执行时,__contains__ 尝试使用__hash__ 计算需要查看的对象的hash。自从 列表在 python 中是不可散列的,你会得到错误:TypeError: unhashable type: 'list'。如果你对 list 做同样的事情,你不会得到任何错误,因为 list 不会为成员资格测试计算哈希。

    简而言之,成员资格测试不能在具有不可散列对象的集合上执行。一般来说,所有可变对象(list, sets, dict) 都是不可散列的。

    【讨论】:

      猜你喜欢
      • 2020-05-06
      • 2011-10-19
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2016-06-19
      • 2021-03-08
      • 2011-10-28
      • 2011-06-11
      相关资源
      最近更新 更多