text: import collections
  import hashlib
  
  import numpy as np
  import pandas as pd
  from sklearn.base import TransformerMixin
  from sklearn.utils.validation import check_is_fitted, check_array, check_X_y
  
  
  class TrainOnlyTransformerMixin(TransformerMixin):
      """
      Allows using a separate function for transforming train and test data
  
      Usage:
          >>> from sklearn.base import BaseEstimator
          >>> class TrainOnlyTransformer(TrainOnlyTransformerMixin, BaseEstimator):
          ...     def fit(self, X, y):
          ...         super().fit(X, y)
          ...
          ...     def transform_train(self, X, y=None):
          ...          return X + np.random.normal(0, 1, size=X.shape)
          ...
          >>> X_train, X_test = np.random.randn(100, 4), np.random.randn(100, 4)
          >>> y_train, y_test = np.random.randn(100), np.random.randn(100)
          >>>
          >>> trf = TrainOnlyTransformer()
          >>> trf.fit(X_train, y_train)
          >>>
          >>> assert np.all(trf.transform(X_train) != X_train)
          >>> assert np.all(trf.transform(X_test) == X_test)
  
      .. warning:: Transformers using this class as a mixin should at a minimum:
  
          - call `super().fit` in their fit method
          - implement `transform_train()`
  
          They may also implement `transform_test()`. If it is not implemented,
          `transform_test` will simply return the untransformed dataframe
      """
  
      _HASHERS = {
          pd.DataFrame: lambda X: hashlib.sha256(pd.util.hash_pandas_object(X, index=True).values).hexdigest(),
          np.ndarray: lambda X: hash(X.data.tobytes()),
          np.memmap: lambda X: hash(X.data.tobytes()),
  
      }
  
      def fit(self, X, y=None):
          """Calculates the hash of X_train"""
          if y is None:
              check_array(X, estimator=self)
          else:
              check_X_y(X, y, estimator=self)
          self.X_hash_ = self._hash(X)
          self.dim_ = X.shape[1]
          return self
  
      @staticmethod
      def _hash(X):
          """Returns a hash of X based on the type of X. Hashers are defined in TrainOnlyMixin.HASHERS"""
          try:
              hasher = TrainOnlyTransformerMixin._HASHERS[type(X)]
          except KeyError:
              raise ValueError(f'Unknown datatype {type(X)}, '
                               f'TransformerSelector only supports {TrainOnlyTransformerMixin.HASHERS.keys()}')
          else:
              return hasher(X)
  
      def transform(self, X, y=None):
          """
          Dispatcher for transform method.
  
          It will dispatch to `self.transform_train` if X is the same as X passed to `fit`, otherwise, it will dispatch
          to `self.trainsform_test`
          """
          check_is_fitted(self, ['X_hash_', 'dim_'])
          check_array(X, estimator=self)
  
          if X.shape[1] != self.dim_:
              raise ValueError(f'Unexpected input dimension {X.shape[1]}, expected {self.dim_}')
  
          if self._hash(X) == self.X_hash_:
              return self.transform_train(X)
          else:
              return self.transform_test(X)
  
      def transform_train(self, X, y=None):
          raise NotImplementedError('Subclasses of TrainOnlyMixin should implement `transform_train`')
  
      def transform_test(self, X, y=None):
          return X
  
  
  def as_list(val):
      """
      Helper function, always returns a list of the input value.
  
      :param val: the input value.
      :returns: the input value as a list.
  
      :Example:
  
      >>> as_list('test')
      ['test']
  
      >>> as_list(['test1', 'test2'])
      ['test1', 'test2']
      """
      treat_single_value = (str)
  
      if isinstance(val, treat_single_value):
          return [val]
  
      if hasattr(val, '__iter__'):
          return list(val)
  
      return [val]
  
  
  def flatten(nested_iterable):
      """
      Helper function, returns an iterator of flattened values from an arbitrarily nested iterable
  
      >>> list(flatten([['test1', 'test2'], ['a', 'b', ['c', 'd']]]))
      ['test1', 'test2', 'a', 'b', 'c', 'd']
  
      >>> list(flatten(['test1', ['test2']]))
      ['test1', 'test2']
      """
      for el in nested_iterable:
          if isinstance(el, collections.abc.Iterable) and not isinstance(el, (str, bytes)):
              yield from flatten(el)
          else:
              yield el
  
  
  def expanding_list(list_to_extent, return_type=list):
      """
      Make a expanding list of lists by making tuples of the first element, the first 2 elements etc.
  
      :param list_to_extent:
      :param return_type: type of the elements of the list (tuple or list)
  
      :Example:
  
      >>> expanding_list('test')
      [['test']]
  
      >>> expanding_list(['test1', 'test2', 'test3'])
      [['test1'], ['test1', 'test2'], ['test1', 'test2', 'test3']]
  
      >>> expanding_list(['test1', 'test2', 'test3'], tuple)
      [('test1',), ('test1', 'test2'), ('test1', 'test2', 'test3')]
      """
      listed = as_list(list_to_extent)
      if len(listed) <= 1:
          return [listed]
  
      return [return_type(listed[:n+1]) for n in range(len(listed))]
  