Failure to  hash `dataclasses` - results in functions that cannot be hashed or cached in `.map`
Opened by sanchit-gandhi (closed)

Dataclasses cannot be hashed. As a result, they cannot be hashed or cached if used in the `.map` method. Dataclasses are used extensively in Transformers examples scripts: (c.f. [CTC example](https://github.com/huggingface/transformers/blob/main/examples/pytorch/speech-recognition/run_speech_recognition_ctc.py)). Since dataclasses cannot be hashed, one has to define separate variables prior to passing dataclass attributes to the `.map` method:
```python
phoneme_language = data_args.phoneme_language
```
in the example https://github.com/huggingface/transformers/blob/3c7e56fbb11f401de2528c1dcf0e282febc031cd/examples/pytorch/speech-recognition/run_speech_recognition_ctc.py#L603-L630

## Steps to reproduce the bug
```python
from dataclasses import dataclass, field
from datasets.fingerprint import Hasher

@dataclass
class DataTrainingArguments:
    """
    Arguments pertaining to what data we are going to input our model for training and eval.
    """

    phoneme_language: str = field(
        default=None, metadata={"help": "The name of the phoneme language to use."}
    )

data_args = DataTrainingArguments(phoneme_language ="foo")

Hasher.hash(data_args)

phoneme_language = data_args.phoneme_language

Hasher.hash(phoneme_language)
```

## Expected results
A hash.
## Actual results
<details>
<summary> Traceback </summary>

```
---------------------------------------------------------------------------
KeyError                                  Traceback (most recent call last)
Input In [1], in <cell line: 16>()
     10     phoneme_language: str = field(
     11         default=None, metadata={"help": "The name of the phoneme language to use."}
     12     )
     14 data_args = DataTrainingArguments(phoneme_language ="foo")
---> 16 Hasher.hash(data_args)
     18 phoneme_language = data_args. phoneme_language
     20 Hasher.hash(phoneme_language)

File ~/datasets/src/datasets/fingerprint.py:237, in Hasher.hash(cls, value)
    235     return cls.dispatch[type(value)](cls, value)
    236 else:
--> 237     return cls.hash_default(value)

File ~/datasets/src/datasets/fingerprint.py:230, in Hasher.hash_default(cls, value)
    228 @classmethod
    229 def hash_default(cls, value: Any) -> str:
--> 230     return cls.hash_bytes(dumps(value))

File ~/datasets/src/datasets/utils/py_utils.py:564, in dumps(obj)
    562 file = StringIO()
    563 with _no_cache_fields(obj):
--> 564     dump(obj, file)
    565 return file.getvalue()

File ~/datasets/src/datasets/utils/py_utils.py:539, in dump(obj, file)
    537 def dump(obj, file):
    538     """pickle an object to a file"""
--> 539     Pickler(file, recurse=True).dump(obj)
    540     return

File ~/hf/lib/python3.8/site-packages/dill/_dill.py:620, in Pickler.dump(self, obj)
    618     raise PicklingError(msg)
    619 else:
--> 620     StockPickler.dump(self, obj)
    621 return

File /usr/lib/python3.8/pickle.py:487, in _Pickler.dump(self, obj)
    485 if self.proto >= 4:
    486     self.framer.start_framing()
--> 487 self.save(obj)
    488 self.write(STOP)
    489 self.framer.end_framing()

File /usr/lib/python3.8/pickle.py:603, in _Pickler.save(self, obj, save_persistent_id)
    599     raise PicklingError("Tuple returned by %s must have "
    600                         "two to six elements" % reduce)
    602 # Save the reduce() output and finally memoize the object
--> 603 self.save_reduce(obj=obj, *rv)

File /usr/lib/python3.8/pickle.py:687, in _Pickler.save_reduce(self, func, args, state, listitems, dictitems, state_setter, obj)
    684     raise PicklingError(
    685         "args[0] from __newobj__ args has the wrong class")
    686 args = args[1:]
--> 687 save(cls)
    688 save(args)
    689 write(NEWOBJ)

File /usr/lib/python3.8/pickle.py:560, in _Pickler.save(self, obj, save_persistent_id)
    558 f = self.dispatch.get(t)
    559 if f is not None:
--> 560     f(self, obj)  # Call unbound method with explicit self
    561     return
    563 # Check private dispatch table if any, or else
    564 # copyreg.dispatch_table

File ~/hf/lib/python3.8/site-packages/dill/_dill.py:1838, in save_type(pickler, obj, postproc_list)
   1836             postproc_list = []
   1837         postproc_list.append((setattr, (obj, '__qualname__', obj_name)))
-> 1838     _save_with_postproc(pickler, (_create_type, (
   1839         type(obj), obj.__name__, obj.__bases__, _dict
   1840     )), obj=obj, postproc_list=postproc_list)
   1841     log.info("# %s" % _t)
   1842 else:

File ~/hf/lib/python3.8/site-packages/dill/_dill.py:1140, in _save_with_postproc(pickler, reduction, is_pickler_dill, obj, postproc_list)
   1137     pickler._postproc[id(obj)] = postproc_list
   1139 # TODO: Use state_setter in Python 3.8 to allow for faster cPickle implementations
-> 1140 pickler.save_reduce(*reduction, obj=obj)
   1142 if is_pickler_dill:
   1143     # pickler.x -= 1
   1144     # print(pickler.x*' ', 'pop', obj, id(obj))
   1145     postproc = pickler._postproc.pop(id(obj))

File /usr/lib/python3.8/pickle.py:692, in _Pickler.save_reduce(self, func, args, state, listitems, dictitems, state_setter, obj)
    690 else:
    691     save(func)
--> 692     save(args)
    693     write(REDUCE)
    695 if obj is not None:
    696     # If the object is already in the memo, this means it is
    697     # recursive. In this case, throw away everything we put on the
    698     # stack, and fetch the object back from the memo.

File /usr/lib/python3.8/pickle.py:560, in _Pickler.save(self, obj, save_persistent_id)
    558 f = self.dispatch.get(t)
    559 if f is not None:
--> 560     f(self, obj)  # Call unbound method with explicit self
    561     return
    563 # Check private dispatch table if any, or else
    564 # copyreg.dispatch_table

File /usr/lib/python3.8/pickle.py:901, in _Pickler.save_tuple(self, obj)
    899 write(MARK)
    900 for element in obj:
--> 901     save(element)
    903 if id(obj) in memo:
    904     # Subtle.  d was not in memo when we entered save_tuple(), so
    905     # the process of saving the tuple's elements must have saved
   (...)
    909     # could have been done in the "for element" loop instead, but
    910     # recursive tuples are a rare thing.
    911     get = self.get(memo[id(obj)][0])

File /usr/lib/python3.8/pickle.py:560, in _Pickler.save(self, obj, save_persistent_id)
    558 f = self.dispatch.get(t)
    559 if f is not None:
--> 560     f(self, obj)  # Call unbound method with explicit self
    561     return
    563 # Check private dispatch table if any, or else
    564 # copyreg.dispatch_table

File ~/hf/lib/python3.8/site-packages/dill/_dill.py:1251, in save_module_dict(pickler, obj)
   1248     if is_dill(pickler, child=False) and pickler._session:
   1249         # we only care about session the first pass thru
   1250         pickler._first_pass = False
-> 1251     StockPickler.save_dict(pickler, obj)
   1252     log.info("# D2")
   1253 return

File /usr/lib/python3.8/pickle.py:971, in _Pickler.save_dict(self, obj)
    968     self.write(MARK + DICT)
    970 self.memoize(obj)
--> 971 self._batch_setitems(obj.items())

File /usr/lib/python3.8/pickle.py:997, in _Pickler._batch_setitems(self, items)
    995     for k, v in tmp:
    996         save(k)
--> 997         save(v)
    998     write(SETITEMS)
    999 elif n:

File /usr/lib/python3.8/pickle.py:560, in _Pickler.save(self, obj, save_persistent_id)
    558 f = self.dispatch.get(t)
    559 if f is not None:
--> 560     f(self, obj)  # Call unbound method with explicit self
    561     return
    563 # Check private dispatch table if any, or else
    564 # copyreg.dispatch_table

File ~/datasets/src/datasets/utils/py_utils.py:862, in save_function(pickler, obj)
    859     if state_dict:
    860         state = state, state_dict
--> 862     dill._dill._save_with_postproc(
    863         pickler,
    864         (
    865             dill._dill._create_function,
    866             (obj.__code__, globs, obj.__name__, obj.__defaults__, closure),
    867             state,
    868         ),
    869         obj=obj,
    870         postproc_list=postproc_list,
    871     )
    872 else:
    873     closure = obj.func_closure

File ~/hf/lib/python3.8/site-packages/dill/_dill.py:1153, in _save_with_postproc(pickler, reduction, is_pickler_dill, obj, postproc_list)
   1151 dest, source = reduction[1]
   1152 if source:
-> 1153     pickler.write(pickler.get(pickler.memo[id(dest)][0]))
   1154     pickler._batch_setitems(iter(source.items()))
   1155 else:
   1156     # Updating with an empty dictionary. Same as doing nothing.

KeyError: 140434581781568
```

</details>

## Environment info
<!-- You can run the command `datasets-cli env` and copy-and-paste its output below. -->
- `datasets` version: 2.3.3.dev0
- Platform: Linux-5.11.0-1028-gcp-x86_64-with-glibc2.29
- Python version: 3.8.10
- PyArrow version: 8.0.0
- Pandas version: 1.4.2

cc @lhoestq 

Comments:
I think this has been fixed by #4516, let me know if you encounter this again :)

I re-ran your code in 3.7 and 3.9 and it works fine

Thank you!