docs: def group_by(self, key_names=[], key=lambda x: x, result_func=lambda x: x):
          """
          Groups an enumerable on given key selector. Index of key name
          corresponds to index of key lambda function.
  
          Usage:
              Enumerable([1,2,3]).group_by(key_names=['id'], key=lambda x: x) _
                  .to_list() -->
                  Enumerable object [
                      Grouping object {
                          key.id: 1,
                          _data: [1]
                      },
                      Grouping object {
                          key.id: 2,
                          _data: [2]
                      },
                      Grouping object {
                          key.id: 3,
                          _data: [3]
                      }
                  ]
              Thus the key names for each grouping object can be referenced
              through the key property. Using the above example:
  
              Enumerable([1,2,3]).group_by(key_names=['id'], key=lambda x: x) _
              .select(lambda g: { 'key': g.key.id, 'count': g.count() }
  
          :param key_names: list of key names
          :param key: key selector as lambda expression
          :param result_func: transformation function as lambda expression
          :return: Enumerable of grouping objects
          """
          result = []
          ordered = sorted(self, key=key)
          grouped = itertools.groupby(ordered, key)
          for k, g in grouped:
              can_enumerate = isinstance(k, list) or isinstance(k, tuple) \
                  and len(k) > 0
              key_prop = {}
              for i, prop in enumerate(key_names):
                  key_prop.setdefault(prop, k[i] if can_enumerate else k)
              key_object = Key(key_prop)
              result.append(Grouping(key_object, list(g)))
          return Enumerable(result).select(result_func)