<SYSTEM_TASK:>
Solve the following problem using Python, implementing the functions described below, one line at a time
<END_TASK>
<USER_TASK:>
Description:
 def add_sample(self, sample_id, features, label, class_id=None, overwrite=False, feature_names=None):
 """Adds a new sample to the dataset with its features, label and class ID. This is the preferred way to construct the dataset. Parameters sample_id : str, int The identifier that uniquely identifies this sample. features : list, ndarray The features for this sample label : int, str The label for this sample class_id : int, str The class for this sample. If not provided, label converted to a string becomes its ID. overwrite : bool If True, allows the overwite of features for an existing subject ID. Default : False. feature_names : list The names for each feature. Assumed to be in the same order as `features` Raises ------ ValueError If `sample_id` is already in the MLDataset (and overwrite=False), or If dimensionality of the current sample does not match the current, or If `feature_names` do not match existing names TypeError If sample to be added is of different data type compared to existing samples. """


        if sample_id in self.__data and not overwrite:
            raise ValueError('{} already exists in this dataset!'.format(sample_id))

        # ensuring there is always a class name, even when not provided by the user.
        # this is needed, in order for __str__ method to work.
        # TODO consider enforcing label to be numeric and class_id to be string
        #  so portability with other packages is more uniform e.g. for use in scikit-learn
        if class_id is None:
            class_id = str(label)

        features = self.check_features(features)
        if self.num_samples <= 0:
            self.__data[sample_id] = features
            self.__labels[sample_id] = label
            self.__classes[sample_id] = class_id
            self.__dtype = type(features)
            self.__num_features = features.size if isinstance(features,
                                                              np.ndarray) else len(
                features)
            if feature_names is None:
                self.__feature_names = self.__str_names(self.num_features)
        else:
            if self.__num_features != features.size:
                raise ValueError('dimensionality of this sample ({}) '
                                 'does not match existing samples ({})'
                                 ''.format(features.size, self.__num_features))
            if not isinstance(features, self.__dtype):
                raise TypeError("Mismatched dtype. Provide {}".format(self.__dtype))

            self.__data[sample_id] = features
            self.__labels[sample_id] = label
            self.__classes[sample_id] = class_id
            if feature_names is not None:
                # if it was never set, allow it
                # class gets here when adding the first sample,
                #   after dataset was initialized with empty constructor
                if self.__feature_names is None:
                    self.__feature_names = np.array(feature_names)
                else:  # if set already, ensure a match
                    if not np.array_equal(self.feature_names, np.array(feature_names)):
                        raise ValueError(
                            "supplied feature names do not match the existing names!")