<SYSTEM_TASK:>
Solve the following problem using Python, implementing the functions described below, one line at a time
<END_TASK>
<USER_TASK:>
Description:
 def _step(self, model: TrainingModel, batch: mx.io.DataBatch, checkpoint_interval: int, metric_train: mx.metric.EvalMetric, metric_loss: Optional[mx.metric.EvalMetric] = None):
 """ Performs an update to model given a batch and updates metrics. """


        if model.monitor is not None:
            model.monitor.tic()

        ####################
        # Forward & Backward
        ####################
        model.run_forward_backward(batch, metric_train)

        # If using an extended optimizer, provide extra state information about the current batch
        optimizer = model.optimizer
        if metric_loss is not None and isinstance(optimizer, SockeyeOptimizer):
            # Loss for this batch
            metric_loss.reset()
            metric_loss.update(batch.label, model.module.get_outputs())
            [(_, m_val)] = metric_loss.get_name_value()
            batch_state = BatchState(metric_val=m_val)
            optimizer.pre_update_batch(batch_state)

        ########
        # UPDATE
        ########
        if self.update_interval == 1 or self.state.batches % self.update_interval == 0:

            # Gradient rescaling
            gradient_norm = None
            if self.state.updates > 0 and (self.state.updates + 1) % checkpoint_interval == 0:
                # compute values for logging to metrics (before rescaling...)
                gradient_norm = self.state.gradient_norm = model.get_global_gradient_norm()
                self.state.gradients = model.get_gradients()

            # note: C.GRADIENT_CLIPPING_TYPE_ABS is handled by the mxnet optimizer directly
            if self.optimizer_config.gradient_clipping_type == C.GRADIENT_CLIPPING_TYPE_NORM:
                if gradient_norm is None:
                    gradient_norm = model.get_global_gradient_norm()
                # clip gradients
                if gradient_norm > self.optimizer_config.gradient_clipping_threshold:
                    ratio = self.optimizer_config.gradient_clipping_threshold / gradient_norm
                    model.rescale_gradients(ratio)

            model.update()

            if self.update_interval > 1:
                model.zero_gradients()

            self.state.updates += 1

        if model.monitor is not None:
            results = model.monitor.toc()
            if results:
                for _, k, v in results:
                    logger.info('Monitor: Batch [{:d}] {:s} {:s}'.format(self.state.updates, k, v))