code: def _calc_hash(op, args, keywords):
          
          args_tup = tuple(a if type(a) in (int, float) else hash(a) for a in args)
          # HASHCONS: these attributes key the cache
          # BEFORE CHANGING THIS, SEE ALL OTHER INSTANCES OF "HASHCONS" IN THIS FILE
          to_hash = (
              op, args_tup,
              str(keywords.get('length', None)),
              hash(keywords['variables']),
              keywords['symbolic'],
              hash(keywords.get('annotations', None)),
          )
  
          # Why do we use md5 when it's broken? Because speed is more important
          # than cryptographic integrity here. Then again, look at all those
          # allocations we're doing here... fast python is painful.
          hd = hashlib.md5(pickle.dumps(to_hash, -1)).digest()
          return md5_unpacker.unpack(hd)[0]
docs: Calculates the hash of an AST, given the operation, args, and kwargs.
  
          :param op:          The operation.
          :param args:        The arguments to the operation.
          :param keywords:    A dict including the 'symbolic', 'variables', and 'length' items.
          :returns:           a hash.
  
          We do it using md5 to avoid hash collisions.
          (hash(-1) == hash(-2), for example)