PERF: Native version of `is_hashable`
State: closed

- [ ] closes #xxxx (Replace xxxx with the GitHub issue number)
- [ ] [Tests added and passed](https://pandas.pydata.org/pandas-docs/dev/development/contributing_codebase.html#writing-tests) if fixing a bug or adding a new feature
- [x] All [code checks passed](https://pandas.pydata.org/pandas-docs/dev/development/contributing_codebase.html#pre-commit).
- [ ] Added [type annotations](https://pandas.pydata.org/pandas-docs/dev/development/contributing_codebase.html#type-hints) to new arguments/methods/functions.
- [ ] Added an entry in the latest `doc/source/whatsnew/vX.X.X.rst` file if fixing a bug or adding a new feature.

Add native version of `is_hashable(...)` for some marginal performance improvements.

Labels:
name: Performance
description: Memory or execution speed performance
name: hashing
description: hash_pandas_object
name: Stale

Comments:
Can you provide some measurements on the perf impact?
> Can you provide some measurements on the perf impact?

I've benchmarked the current pure Python implementation against two different Cython versions with the following results. Overall the proposed cython version is twice a fast as the pure python version in the happy path and 10x faster in the exceptional path. The proposed cython version is ~33 % faster than the simple Cython version in the happy and 10x faster in the exceptional path

```python

def is_hashable(obj: object) -> bool:
    try:
        hash(obj)
    except TypeError:
        return False
    else:
        return True


cpdef bint c_is_hashable(object obj) noexcept:
    if PyLong_CheckExact(obj) or PyFloat_CheckExact(obj) or PyUnicode_CheckExact(obj):
        return True

    if PyTuple_CheckExact(obj) or PyFrozenSet_CheckExact(obj):
        for o in obj:
            if not c_is_hashable(o):
                return False
        return True

    if PyAnySet_CheckExact(obj) or PyDict_CheckExact(obj) or PyList_CheckExact(obj):
        return False

    try:
        hash(obj)
    except TypeError:
        return False
    else:
        return True


cpdef bint c_is_hashable2(object obj) noexcept:
    try:
        hash(obj)
    except TypeError:
        return False
    else:
        return True


for obj in (2, "hello world", ("hello", "world", 1, 2.0), ([],), np.int64(5), []):
    print(repr(obj))
    %timeit is_hashable(obj)
    %timeit c_is_hashable(obj)
    %timeit c_is_hashable2(obj)
    print("")
 
>>> 2
>>> 58.8 ns ± 0.344 ns per loop (mean ± std. dev. of 7 runs, 10,000,000 loops each)
>>> 33.3 ns ± 0.333 ns per loop (mean ± std. dev. of 7 runs, 10,000,000 loops each)
>>> 47.9 ns ± 0.284 ns per loop (mean ± std. dev. of 7 runs, 10,000,000 loops each)

>>> 'hello world'
>>> 75.4 ns ± 0.703 ns per loop (mean ± std. dev. of 7 runs, 10,000,000 loops each)
>>> 33.7 ns ± 0.561 ns per loop (mean ± std. dev. of 7 runs, 10,000,000 loops each)
>>> 42.1 ns ± 0.508 ns per loop (mean ± std. dev. of 7 runs, 10,000,000 loops each)

>>> ('hello', 'world', 1, 2.0)
>>> 101 ns ± 0.421 ns per loop (mean ± std. dev. of 7 runs, 10,000,000 loops each)
>>> 46.8 ns ± 0.493 ns per loop (mean ± std. dev. of 7 runs, 10,000,000 loops each)
>>> 71.9 ns ± 0.244 ns per loop (mean ± std. dev. of 7 runs, 10,000,000 loops each)

>>> ([],)
>>> 392 ns ± 2.84 ns per loop (mean ± std. dev. of 7 runs, 1,000,000 loops each)
>>> 39.4 ns ± 0.87 ns per loop (mean ± std. dev. of 7 runs, 10,000,000 loops each)
>>> 464 ns ± 1.95 ns per loop (mean ± std. dev. of 7 runs, 1,000,000 loops each)

>>> np.int64(5)
>>> 66 ns ± 1.82 ns per loop (mean ± std. dev. of 7 runs, 10,000,000 loops each)
>>> 47.8 ns ± 1.08 ns per loop (mean ± std. dev. of 7 runs, 10,000,000 loops each)
>>> 46.6 ns ± 0.377 ns per loop (mean ± std. dev. of 7 runs, 10,000,000 loops each)

>>> []
>>> 396 ns ± 6.3 ns per loop (mean ± std. dev. of 7 runs, 1,000,000 loops each)
>>> 36 ns ± 0.578 ns per loop (mean ± std. dev. of 7 runs, 10,000,000 loops each)
>>> 477 ns ± 4.88 ns per loop (mean ± std. dev. of 7 runs, 1,000,000 loops each)

```
> I've benchmarked the current pure Python implementation against two different Cython versions with the following results.

Is there anything user-facing that would be improved by this? I see the performance improvements in the benchmarks you posted, but being down in the range of nanoseconds I'm unsure if its worth the added complexity
> > I've benchmarked the current pure Python implementation against two different Cython versions with the following results.
> 
> Is there anything user-facing that would be improved by this? I see the performance improvements in the benchmarks you posted, but being down in the range of nanoseconds I'm unsure if its worth the added complexity

Fair point! See below user-facing benchmark. I cannot judge if it is worth it or not

```python
>>> idx = pd.MultiIndex([[] for _ in range(20)], [[] for _ in range(20)])
>>> new_names = [f"new_name_{i}" for i in range(20)]
>>> %timeit idx.set_names(new_names, inplace=True)
8.68 μs ± 1.71 μs per loop (mean ± std. dev. of 7 runs, 100,000 loops each)  <-- before
7.79 μs ± 3.86 μs per loop (mean ± std. dev. of 7 runs, 100,000 loops each)  <-- after
```
This pull request is stale because it has been open for thirty days with no activity. Please [update](https://pandas.pydata.org/pandas-docs/stable/development/contributing.html#updating-your-pull-request) and respond to this comment if you're still interested in working on this.
Thanks for the PR, but agreed that I'm not sure this maintenance burden of this new implementation is worth the minimal speedup. Thanks for the effort here but closing