Clean up and document classname_func
diff --git a/README.rst b/README.rst
index f004203..186535f 100644
--- a/README.rst
+++ b/README.rst
@@ -504,41 +504,75 @@
 
 .. code:: python
 
-   from yourapp.models import User
-   from parameterized import parameterized_class
+    from yourapp.models import User
+    from parameterized import parameterized_class
 
-   @parameterized_class(("username", "access_level", "expected_status_code"), [
-      ("user_1", 1, 200),
-      ("user_2", 2, 404)
-   ])
-   class TestUserAccessLevel(TestCase):
-      def setUp(self):
-         self.client.force_login(User.objects.get(username=self.username)[0])
+    @parameterized_class([
+       { "username": "user_1", "access_level": 1 },
+       { "username": "user_2", "access_level": 2, "expected_status_code": 404 },
+    ])
+    class TestUserAccessLevel(TestCase):
+       expected_status_code = 200
 
-      def test_url_a(self):
-         response = self.client.get("/url")
-         self.assertEqual(response.status_code, self.expected_status_code)
+       def setUp(self):
+          self.client.force_login(User.objects.get(username=self.username)[0])
 
-      def tearDown(self):
-         self.client.logout()
+       def test_url_a(self):
+          response = self.client.get('/url')
+          self.assertEqual(response.status_code, self.expected_status_code)
+
+       def tearDown(self):
+          self.client.logout()
 
 
-   @parameterized_class([
-      { "username": "user_1", "access_level": 1 },
-      { "username": "user_2", "access_level": 2, "expected_status_code": 404 },
-   ])
-   class TestUserAccessLevel(TestCase):
-      expected_status_code = 200
+    @parameterized_class(("username", "access_level", "expected_status_code"), [
+       ("user_1", 1, 200),
+       ("user_2", 2, 404)
+    ])
+    class TestUserAccessLevel(TestCase):
+       def setUp(self):
+          self.client.force_login(User.objects.get(username=self.username)[0])
 
-      def setUp(self):
-         self.client.force_login(User.objects.get(username=self.username)[0])
+       def test_url_a(self):
+          response = self.client.get("/url")
+          self.assertEqual(response.status_code, self.expected_status_code)
 
-      def test_url_a(self):
-         response = self.client.get('/url')
-         self.assertEqual(response.status_code, self.expected_status_code)
+       def tearDown(self):
+          self.client.logout()
 
-      def tearDown(self):
-         self.client.logout()
+
+The ``@parameterized_class`` decorator accepts a ``classname_func`` argument,
+which controls the name of the parameterized classes generated by
+``@parameterized_class``:
+
+.. code:: python
+
+    from parameterized import parameterized, parameterized_class
+
+    def get_classname(cls, num, params_dict):
+        # By default the generated class named includes either the "name"
+        # parameter (if present), or the first string value. This example shows
+        # multiple parameters being included in the generated class name:
+        return "%s_%s_%s%s" %(
+            cls.__name__,
+            num,
+            parameterized.to_safe_name(params_dict['a']),
+            parameterized.to_safe_name(params_dict['b']),
+        )
+
+    @parameterized_class([
+       { "a": "hello", "b": " world!", "expected": "hello world!" },
+       { "a": "say ", "b": " cheese :)", "expected": "say cheese :)" },
+    ], classname_func=get_classname)
+    class TestConcatenation(TestCase):
+      def test_concat(self):
+          self.assertEqual(self.a + self.b, self.expected)
+
+::
+
+    $ nosetests -v test_math.py
+    test_concat (test_concat.TestConcatenation_0_hello_world_) ... ok
+    test_concat (test_concat.TestConcatenation_0_say_cheese__) ... ok
 
 
 
diff --git a/parameterized/parameterized.py b/parameterized/parameterized.py
index 090f508..e8e69ab 100644
--- a/parameterized/parameterized.py
+++ b/parameterized/parameterized.py
@@ -62,9 +62,6 @@
     return CompatArgSpec(*args[:4])
 
 
-_param = namedtuple("param", "args kwargs")
-
-
 def skip_on_empty_helper(*a, **kw):
     raise SkipTest("parameterized input is empty")
 
@@ -91,6 +88,8 @@
         func.patchings[:] = []
 
 
+_param = namedtuple("param", "args kwargs")
+
 class param(_param):
     """ Represents a single parameter to a test case.
 
@@ -590,7 +589,7 @@
             test_class_dict = dict(base_class.__dict__)
             test_class_dict.update(input_dict)
 
-            name = classname_func(base_class, idx, input_dicts)
+            name = classname_func(base_class, idx, input_dict)
 
             test_class_module[name] = type(name, (base_class, ), test_class_dict)
 
@@ -608,19 +607,24 @@
     return decorator
 
 
-def default_classname_func(cls, num, p):
-    name_suffix = p and p[num]
-    if isinstance(name_suffix, (list, tuple)) and len(p) > 0:
-        name_suffix = name_suffix[0]
-    name_suffix = (
-        "_%s" %(name_suffix, ) if isinstance(name_suffix, string_types) else
-        ""
-    )
+def get_classname_suffix(params_dict):
+    if "name" in params_dict:
+        return parameterized.to_safe_name(params_dict["name"])
 
-    name = "%s_%s%s" %(
+    params_vals = (
+        params_dict.values() if PY3 else
+        (v for (_, v) in sorted(params_dict.items()))
+    )
+    return parameterized.to_safe_name(next((
+        v for v in params_vals
+        if isinstance(v, string_types)
+    ), ""))
+
+
+def default_classname_func(cls, num, params_dict):
+    suffix = get_classname_suffix(params_dict)
+    return "%s_%s%s" %(
         cls.__name__,
         num,
-        name_suffix,
+        suffix and "_" + suffix,
     )
-
-    return name
diff --git a/parameterized/test.py b/parameterized/test.py
index c263ded..5911892 100644
--- a/parameterized/test.py
+++ b/parameterized/test.py
@@ -447,17 +447,14 @@
 
 @parameterized_class(("a", "b", "c"), [
     ("foo", 1, 2),
-    ("bar", 3, 0),
     (0, 1, 2),
 ])
 class TestParameterizedClass(TestCase):
     expect([
-        "TestParameterizedClass_0:test_method_a('foo', 1, 2)",
-        "TestParameterizedClass_0:test_method_b('foo', 1, 2)",
-        "TestParameterizedClass_1:test_method_a('bar', 3, 0)",
-        "TestParameterizedClass_1:test_method_b('bar', 3, 0)",
-        "TestParameterizedClass_2:test_method_a(0, 1, 2)",
-        "TestParameterizedClass_2:test_method_b(0, 1, 2)",
+        "TestParameterizedClass_0_foo:test_method_a('foo', 1, 2)",
+        "TestParameterizedClass_0_foo:test_method_b('foo', 1, 2)",
+        "TestParameterizedClass_1:test_method_a(0, 1, 2)",
+        "TestParameterizedClass_1:test_method_b(0, 1, 2)",
     ])
 
     def _assertions(self, test_name):
@@ -478,67 +475,43 @@
         self._assertions("test_method_b")
 
 
-def custom_cls_naming_func(clsname, idx, attrs):
-    """
-    Custom class naming function for the form of parameterized_class that
-    takes a tuple of attributes, then a list of tuples of values
-    :param clsname: The original class that the decorator specialises
-    :param idx: the test index (starts at 0)
-    :param attrs: A list of dicts of attribute values
-    :return:
-    """
-    return "%s_%s_%s_%s" % (clsname.__name__, str(attrs[idx]['a']), str(attrs[idx]['b']), str(attrs[idx]['c']))
-
-
-@parameterized_class(("a", "b", "c"), [
-    ("foo", 1, 2),
-    ("bar", 3, 0),
-    (0, 1, 2),
-], classname_func=custom_cls_naming_func)
+@parameterized_class(("a", ), [
+    (1, ),
+    (2, ),
+], classname_func=lambda cls, idx, attrs: "%s_custom_func_%s" %(cls.__name__, attrs["a"]))
 class TestNamedParameterizedClass(TestCase):
     expect([
-        "TestNamedParameterizedClass_foo_1_2:test_method_a('foo', 1, 2)",
-        "TestNamedParameterizedClass_foo_1_2:test_method_b('foo', 1, 2)",
-        "TestNamedParameterizedClass_bar_3_0:test_method_a('bar', 3, 0)",
-        "TestNamedParameterizedClass_bar_3_0:test_method_b('bar', 3, 0)",
-        "TestNamedParameterizedClass_0_1_2:test_method_a(0, 1, 2)",
-        "TestNamedParameterizedClass_0_1_2:test_method_b(0, 1, 2)",
+        "TestNamedParameterizedClass_custom_func_1:test_method(1)",
+        "TestNamedParameterizedClass_custom_func_2:test_method(2)",
     ])
 
-    def _assertions(self, test_name):
-        assert hasattr(self, "a")
-        assert_equal(self.b + self.c, 3)
-        missing_tests.remove("%s:%s(%r, %r, %r)" %(
+    def test_method(self):
+        missing_tests.remove("%s:test_method(%r)" %(
             self.__class__.__name__,
-            test_name,
             self.a,
-            self.b,
-            self.c,
         ))
 
-    def test_method_a(self):
-        self._assertions("test_method_a")
-
-    def test_method_b(self):
-        self._assertions("test_method_b")
-
 
 @parameterized_class([
-    {"foo": 1},
-    {"bar": 1},
+    {"foo": 42},
+    {"bar": "some stuff"},
+    {"bar": "other stuff", "name": "some name", "foo": 12},
 ])
 class TestParameterizedClassDict(TestCase):
     expect([
-        "TestParameterizedClassDict_0:setUp(1, 0)",
-        "TestParameterizedClassDict_0:tearDown(1, 0)",
-        "TestParameterizedClassDict_0:test_method(1, 0)",
-        "TestParameterizedClassDict_1:test_method(0, 1)",
-        "TestParameterizedClassDict_1:setUp(0, 1)",
-        "TestParameterizedClassDict_1:tearDown(0, 1)",
+        "TestParameterizedClassDict_0:setUp(42, 'empty')",
+        "TestParameterizedClassDict_0:test_method(42, 'empty')",
+        "TestParameterizedClassDict_0:tearDown(42, 'empty')",
+        "TestParameterizedClassDict_1_some_stuff:setUp(0, 'some stuff')",
+        "TestParameterizedClassDict_1_some_stuff:test_method(0, 'some stuff')",
+        "TestParameterizedClassDict_1_some_stuff:tearDown(0, 'some stuff')",
+        "TestParameterizedClassDict_2_some_name:setUp(12, 'other stuff')",
+        "TestParameterizedClassDict_2_some_name:test_method(12, 'other stuff')",
+        "TestParameterizedClassDict_2_some_name:tearDown(12, 'other stuff')",
     ])
 
     foo = 0
-    bar = 0
+    bar = 'empty'
 
     def setUp(self):
         # Ensure that super() works (issue #73)
@@ -564,37 +537,3 @@
             self.foo,
             self.bar,
         ))
-
-
-def custom_cls_naming_dict_func(clsname, idx, attrs):
-    """
-    Custom class naming function for the form of parameterized_class that
-    takes a list of dictionaries containing attributes to override.
-    :param clsname: The original class that the decorator specialises
-    :param idx: the test index (starts at 0)
-    :param attrs: A list of dicts of attribute values
-    :return:
-    """
-    return "%s_%s_%s" % (clsname.__name__, str(attrs[idx].get('foo', 0)), str(attrs[idx].get('bar', 0)))
-
-
-@parameterized_class([
-    {"foo": 1},
-    {"bar": 1},
-], classname_func=custom_cls_naming_dict_func
-)
-class TestNamedParameterizedClassDict(TestCase):
-    expect([
-        "TestNamedParameterizedClassDict_1_0:test_method(1, 0)",
-        "TestNamedParameterizedClassDict_0_1:test_method(0, 1)",
-    ])
-
-    foo = 0
-    bar = 0
-
-    def test_method(self):
-        missing_tests.remove("%s:test_method(%r, %r)" %(
-            self.__class__.__name__,
-            self.foo,
-            self.bar,
-        ))