code: def apply_rules(self, rules, recursive=True):
          
          if recursive:
              new_args = [_apply_rules(arg, rules) for arg in self.args]
              new_kwargs = {
                  key: _apply_rules(val, rules)
                  for (key, val) in self.kwargs.items()}
          else:
              new_args = self.args
              new_kwargs = self.kwargs
          simplified = self.create(*new_args, **new_kwargs)
          return _apply_rules_no_recurse(simplified, rules)
docs: Rebuild the expression while applying a list of rules
  
          The rules are applied against the instantiated expression, and any
          sub-expressions if `recursive` is True. Rule application is best though
          of as a pattern-based substitution. This is different from the
          *automatic* rules that :meth:`create` uses (see :meth:`add_rule`),
          which are applied *before* expressions are instantiated.
  
          Args:
              rules (list or ~collections.OrderedDict): List of rules or
                  dictionary mapping names to rules, where each rule is a tuple
                  (:class:`Pattern`, replacement callable), cf.
                  :meth:`apply_rule`
              recursive (bool): If true (default), apply rules to all arguments
                  and keyword arguments of the expression. Otherwise, only the
                  expression itself will be re-instantiated.
  
          If `rules` is a dictionary, the keys (rules names) are used only for
          debug logging, to allow an analysis of which rules lead to the final
          form of an expression.