MYTHEME_form_element_FORMID?
Using custom properties is not an hack; quite the opposite, it is a perfectly fine way to achieve what you want. It allows you to identify the form elements that need to be altered, which is what you want.

In fact, custom properties are normally used from Drupal, which uses the following code for fields associated to an entity.

  $element = array(
    '#entity' =&gt; $entity,
    '#entity_type' =&gt; $instance['entity_type'],
    '#bundle' =&gt; $instance['bundle'],
    '#field_name' =&gt; $field_name,
    '#language' =&gt; $langcode,
    '#field_parents' =&gt; $parents,
    '#columns' =&gt; array_keys($field['columns']),
    '#title' =&gt; check_plain($instance['label']),
    '#description' =&gt; field_filter_xss($instance['description']),
    // Only the first widget should be required.
    '#required' =&gt; $delta == 0 &amp;&amp; $instance['required'],
    '#delta' =&gt; $delta,
  );


Custom properties are also the Drupal 7 way to pass variables to theme functions. For example, the following code is using theme_table() to render part of a page.

  $build['node_table'] = array(
    '#theme' =&gt; 'table',
    '#header' =&gt; $header,
    '#rows' =&gt; $rows,
    '#empty' =&gt; t('No content types available. &lt;a href="@link"&gt;Add content type&lt;/a&gt;.', array('@link' =&gt; url('admin/structure/types/add'))),
  );


Passing $build to drupal_render() produces the same output produced by the following code.

$html = theme('table', array('header' =&gt; $header, 'rows' =&gt; $rows, 'empty' =&gt; t('No content types available. &lt;a href="@link"&gt;Add content type&lt;/a&gt;.', array('@link' =&gt; url('admin/structure/types/add')))));


The alternative would be adding a #after_build or #process function to the form elements, but those functions are called before the form element is rendered, which means they can just alter the form element properties.

Those functions get access to the content of $form_state, and get $form_state['input']['form_id'] or $form_state['input']['form_build_id']. This is what the documentation for drupal_build_form() says about $form_state['complete form']:


  A reference to the $form variable containing the complete form structure. #process, #after_build, #element_validate, and other handlers being invoked on a form element may use this reference to access other information in the form the element is contained in.


To add a #after_build function (which gets $element and &amp;$form_state as parameters), you should implement hook_element_info_alter() to change the form element definition.

Notice that theme_form_element() is defined as theme wrapper for every form element, similarly to what done with the following code for the textfield form elements.

  $types['textfield'] = array(
    '#input' =&gt; TRUE,
    '#size' =&gt; 60,
    '#maxlength' =&gt; 128,
    '#autocomplete_path' =&gt; FALSE,
    '#process' =&gt; array('ajax_process_form'),
    '#theme' =&gt; 'textfield',
    '#theme_wrappers' =&gt; array('form_element'),
  );


As side note, every theme function has its own preprocess functions; for example, for theme_textfield() those functions are hook_preprocess_textfield(). They can suggest a different theme function to be used by adding values to the $variables['theme_hook_suggestions'] array. ($variables is the parameter passed as reference to the theme preprocess functions.) Unfortunately, the theme and the theme preprocess functions don't get any reference to the form being build.

References


drupal_build_form()
form_builder()
system_element_info()
theme()