Repository: dask/dask

Issue:
dask.dataframe __getitem__ does not work with subclasses of str in python 3.6
I am upgrading my code from python 2.7 to 3.6 and found that that the __getitem__ of a dask dataframe does not work as before: I use a subclass of `str` for the column names and then I want to access the columns with `df[column_name]`. This works fine with pandas and had worked with dask also in python 2.7. Here is a short example for comparing pandas and dask:

```
class MyString(str):
    pass

my_s = MyString('column_1')

import pandas as pd
df = df = pd.DataFrame({'column_1': [1, 1]})
print(df[my_s])

from dask import dataframe as dd
ddf = dd.from_pandas(df, npartitions=1)
print(ddf[my_s])
```

The last line results in following error:
```
---------------------------------------------------------------------------
NotImplementedError                       Traceback (most recent call last)
<ipython-input-15-731527b3e767> in <module>()
      2 ddf = dd.from_pandas(df, npartitions=1)
----> 3 print(ddf[my_s])

/io/venv_debian_8_http/lib/python3.6/site-packages/dask/dataframe/core.py in __getitem__(self, key)
   2310             return new_dd_object(merge(self.dask, key.dask, dsk), name,
   2311                                  self, self.divisions)
-> 2312         raise NotImplementedError(key)
   2313 
   2314     def __setitem__(self, key, value):

NotImplementedError: column_1
```

I would expect pandas and dask dataframe to behave in the same way here. Software Versions are:
`
dask==0.17.1
pandas==0.22.0
Python is 3.6.4
`


Discussion:
This appears to come from a reliance on `np.isscalar`, which checks for a specific set of classes, and so misses subclasses

```
In [1]: import numpy

In [2]: class str2(str):
   ...:     pass
   ...:

In [3]: x1 = 'hi'

In [4]: x2 = str2('hi')

In [5]: x2
Out[5]: 'hi'

In [6]: numpy.isscalar(x1)
Out[6]: True

In [7]: numpy.isscalar(x2)
Out[7]: False
```
> I would expect pandas and dask dataframe to behave in the same way here

@ctrebing brings up a good point. I can add string subclass support for Dask DataFrames.   

Resolving patch:
diff --git a/dask/dataframe/core.py b/dask/dataframe/core.py
--- a/dask/dataframe/core.py
+++ b/dask/dataframe/core.py
@@ -22,7 +22,7 @@
 
 from ..utils import partial_by_order
 from .. import threaded
-from ..compatibility import apply, operator_div, bind_method
+from ..compatibility import apply, operator_div, bind_method, string_types
 from ..context import globalmethod
 from ..utils import (random_state_data, pseudorandom, derived_from, funcname,
                      memory_repr, put_lines, M, key_split, OperatorMethodMixin,
@@ -2338,7 +2338,7 @@ def columns(self, columns):
 
     def __getitem__(self, key):
         name = 'getitem-%s' % tokenize(self, key)
-        if np.isscalar(key) or isinstance(key, tuple):
+        if np.isscalar(key) or isinstance(key, (tuple, string_types)):
 
             if isinstance(self._meta.index, (pd.DatetimeIndex, pd.PeriodIndex)):
                 if key not in self._meta.columns:


Test patch:
diff --git a/dask/dataframe/tests/test_dataframe.py b/dask/dataframe/tests/test_dataframe.py
--- a/dask/dataframe/tests/test_dataframe.py
+++ b/dask/dataframe/tests/test_dataframe.py
@@ -2600,6 +2600,17 @@ def test_getitem_multilevel():
     assert_eq(pdf[[('A', '0'), ('B', '1')]], ddf[[('A', '0'), ('B', '1')]])
 
 
+def test_getitem_string_subclass():
+    df = pd.DataFrame({'column_1': list(range(10))})
+    ddf = dd.from_pandas(df, npartitions=3)
+
+    class string_subclass(str):
+        pass
+    column_1 = string_subclass('column_1')
+
+    assert_eq(df[column_1], ddf[column_1])
+
+
 def test_diff():
     df = pd.DataFrame(np.random.randn(100, 5), columns=list('abcde'))
     ddf = dd.from_pandas(df, 5)