Repository: dask/dask

Issue:
`dask.array.nanmean()` generates unstable name (hash)
#### Code Sample

```python
import dask.array as da
import numpy as np

x = da.ones((5, 5), chunks=(2, 2))
x = da.nanmean(x, axis=0)
print(x.name)
```

#### Problem Description
Running the above sample three times outputs:
```
mean_agg-aggregate-8157871966f12f849b40951c495fef6f
mean_agg-aggregate-c6939e3b5eb3290f17252b895e10c7c3
mean_agg-aggregate-17e2ac663f1cdddf5b6fc02b1ad0b889
```
which shows the dask name (hash) is unstable.

#### Expected Output
For processing large datasets, it's convenient to have a stable hash name in order to save intermediate results or compare them among developers. 
> Workaround: use `dask.array.mean()` instead of `dask.array.nanmean()`


Discussion:
Please included how you created your test environment (e.g. `conda env export` if using `conda`).
In order to get those results, I created a fresh virtual environment based on [virtualenvwrapper](https://virtualenvwrapper.readthedocs.io/) and installed dask using pip, as follows:
```bash
$ mkvirtualenv dask_test
$ pip install dask[complete]
```
So could you please include the results from `pip freeze`? Thanks.
Here (Python 3.5.2):
```
click==6.7
cloudpickle==0.5.2
dask==0.16.0
distributed==1.20.2
HeapDict==1.0.0
locket==0.2.0
msgpack-python==0.4.8
numpy==1.13.3
pandas==0.21.0
partd==0.3.8
psutil==5.4.2
python-dateutil==2.6.1
pytz==2017.3
six==1.11.0
sortedcontainers==1.5.9
tblib==1.3.2
toolz==0.8.2
tornado==4.5.2
zict==0.1.3
```

Resolving patch:
diff --git a/dask/base.py b/dask/base.py
--- a/dask/base.py
+++ b/dask/base.py
@@ -584,8 +584,10 @@ def _normalize_function(func):
         funcs = reversed((first,) + func.funcs) if first else func.funcs
         return tuple(normalize_function(f) for f in funcs)
     elif isinstance(func, partial):
-        kws = tuple(sorted(func.keywords.items())) if func.keywords else ()
-        return (normalize_function(func.func), func.args, kws)
+        args = tuple(normalize_token(i) for i in func.args)
+        kws = tuple((k, normalize_token(v))
+                    for k, v in sorted(func.keywords.items()))
+        return (normalize_function(func.func), args, kws)
     else:
         try:
             result = pickle.dumps(func, protocol=0)


Test patch:
diff --git a/dask/tests/test_base.py b/dask/tests/test_base.py
--- a/dask/tests/test_base.py
+++ b/dask/tests/test_base.py
@@ -32,16 +32,19 @@ def import_or_none(path):
 pd = import_or_none('pandas')
 
 
-def test_normalize_function():
+def f1(a, b, c=1):
+    pass
+
 
-    def f1(a, b, c=1):
-        pass
+def f2(a, b=1, c=2):
+    pass
 
-    def f2(a, b=1, c=2):
-        pass
 
-    def f3(a):
-        pass
+def f3(a):
+    pass
+
+
+def test_normalize_function():
 
     assert normalize_function(f2)
 
@@ -168,6 +171,15 @@ def test_tokenize_numpy_ufunc_consistent():
     assert tokenize(inc) == tokenize(inc)
 
 
+def test_tokenize_partial_func_args_kwargs_consistent():
+    f = tz.partial(f3, f2, c=f1)
+    res = normalize_token(f)
+    sol = (b'cdask.tests.test_base\nf3\np0\n.',
+           (b'cdask.tests.test_base\nf2\np0\n.',),
+           (('c', b'cdask.tests.test_base\nf1\np0\n.'),))
+    assert res == sol
+
+
 def test_normalize_base():
     for i in [1, long(1), 1.1, '1', slice(1, 2, 3)]:
         assert normalize_token(i) is i