Repository navigation
Add funcs to convert table cols to/from unicode/bytestring - #1974
Conversation
|
In general, I agree with the Unicode sandwich concept, however it ignores the reality of really large data structures that need to be shared with C in memory or memmapped on disk. I wonder how this impacts things like FITS, where the I wonder if instead a sort of proxy that converts a single column to unicode when accessing it, and back to bytes when setting it, as I think @embray has threatened to do, wouldn't work better. |
|
I also thought about a proxy but fairly quickly gave up on the idea, guessing that it would be a tricky thing to do in complete and robust way. That would certainly be better, but this is meant as a stopgap solution that I think would work in 90% of real world cases. This is purely an "opt-in if it works for you" utility, but of course if consensus is that it's not the right way forward then it shouldn't be in astropy. I think the best would be a numpy 'ascii' dtype, like 'U' but with ascii encoding. It seems like that would allow a pretty easy virtual remapping from e.g. 'S10' to 'A10' with no need to copy memory. It seems like this should not be technically that difficult since the 'U" machinery is already there for UCS-4, but I don't know. |
|
Indeed, the unicode sandwich you describe here is exactly what PyFITS/astropy.io.fits does for string columns in FITS tables. This actually causes a huge hassle particularly for large tables with either many thousands or millions of rows, or a large number of string columns. Right now the entire column is converted, in memory, to unicode as soon as any row in the column. So even if most of the columns can be read from a mmap you get stuck with this huge column of strings in memory using 4 bytes per character. See this thread for an example of where this can get really ugly: https://groups.google.com/d/msg/astropy-dev/zRIZ6rr4JPg/Qs9IHNTqKPAJ This could be alleviated partially with more sophisticated code that can handle on-demand conversion of values in a column, possibly caching intermediate results. This is something I've been thinking about the best ways to do for a long time, as there are other parts of the FITS format (besides strings) that would benefit from an efficient way to do this generically. But in the meantime, for the string case in particular, I think the "proxy" @mdboom mentioned is the way to go, and so far my experiments suggest that it would work. It would require all the strings in a column to have the same encoding, which I think is a reasonable assumption in the vast majority of cases (in particular for most science data that is going to include mostly ASCII strings). The proxy would have a particular encoding associated with it--the strings would all still be stored in their "native" format of encoded bytes. Whenever displaying a value in the column to the user (like with |
|
That's another option that's sort of in-between. Originally in Python 3 unicode strings were always stored internally as UCS-4. But that was obviously terrible for most strings, so they did a lot of rework and changed things so that internal storage format for unicode text was determined by the character with the highest codepoint in the string. In other words, if all characters in the string are ASCII then they are stored internally as ASCII. It was easy and fast enough when joining strings to upconvert from ASCII to UCS-2 or UCS-4 as needed. It's fast enough since the string object itself has a flag in its type struct specifying what "kind" of string it is. Numpy really ought to do the same for the 'U' dtype. But until and unless it gets fixed we need something else. I suppose we could write our own Numpy dtype that has this sort of behavior. |
|
I should add: One reason that this sort of thing is easier to do for string arrays than for general arrays is that most if not all of the ufuncs in Numpy don't work on strings :) |
|
I started thinking about this harder (and playing a bit in ipython) and came to the same realization that lack of support for all (I think) ufuncs and even operators like But this does bring up the issue of comparisons. (Note that assignment of unicode to a bytestring array and vice versa already seems to work fine). So you need to overload comparisons and have the machinery to convert the other side to the appropriate bytestring array. Maybe that's as simple as |
|
Indeed. I still need to poke at comparisons a bit, but I already have a prototype of this under way that I will show in a bit. The other thing I'm working out is how to handle things like |
|
Another question that comes up is how to handle If we do allow bytes the other question is do we just allow arbitrary bytes, or do we check that they are valid for the array's encoding (by attempting to decode them first)? |
|
If you're interested, here's a rough implementation of this idea that I've worked on incorporating into PyFITS: https://github.com/embray/PyFITS/blob/stringy/lib/pyfits/fitsrec.py#L999 The incorporation is still somewhat partial, but I've gotten all the tests passing at least (in all supported Python 2.x and 3.x versions). I still haven't done any performance testing but I suspect this will be much better (especially for tables containing large string columns). This implementation also has a few FITS-isms built into it, such as the fact that it subclasses I should also explain one of the "trickier" parts of this implementation: Given an existing Numpy string array (specifically an 'S' array) like: there are two ways to make an In general it's much easier to use This actually creates a new subclass of I'm also not 100% sure about comparison. Equal and no-equal are straightforward enough I think. It's greater-than/less-than I'm not sure about. When comparing arrays of unicode strings does it compare their code points? Whereas with encoded strings I assume it's just doing a byte-by-byte comparison. Depending on the encoding this could give very different results for non-ASCII characters. But for PyFITS' case I'm not worried about that difference. Just something to be aware of... |
|
In case people haven't been paying attention to numpy-discussion, it looks like there is movement toward getting a 1-byte unicode type (latin-1 or ascii) implemented in numpy. I have a particular itch to scratch and have been making a case. |
|
Great--I don't follow the Numpy mailing lists, so I'm glad you're following that discussion. I'll have to read up and see if there's anywhere I can jump in too. Because I think Numpy really needs this so that applications using it can get the Unicode sandwiching correct. In the meantime my little hack works pretty well for many uses cases (it mostly falls over on mixed-width encodings a la utf8. It still 'works', but it's not very user-friendly since it makes it difficult to work with such an array on a per-character basis.) |
|
Though I don't like the implicit "latin1" idea at all. That's sloppy, and falls over if you try to mix data of different encodings. A much better approach would be to do what Python itself does, as I described earlier. It still uses fixed-width encodings so strides will still work, but it will use either 1-byte ASCII, UCS-2, or UCS-4 internally depending on the highest codepoint in the string (that's an oversimplification, but the basic idea). I would repurpose the existing 'U' dtype for this, but with some indicator of which internal representation it's using for a given array. Maybe I'll jump on that thread after all... |
|
I think the point of the h5py dev that using 'ascii' encoding ensures byte compatibility for conversion to 'utf-8' is a good one. And with |
|
Yeah, I'm fine with ascii for that purpose, but not latin1 :) |
|
@taldcroft - this looks good, but I'm wondering if we can also expose the general change_dtype method? Sometimes I want to stack tables with columns that are S5 and S10 and it would be nice to easily be able to say convert from S5 to S10. What do you think? |
|
@astrofrog - the existing code won't handle your use case even if exposed since it just works for a particular Also, I'm a little confused about your question because the astropy |
|
@taldcroft - ok, ignore my request for now, I may have been basing it on old behavior. |
|
I'm just worried about the potential for whining that will ensue from users saying "I opened my 1GB table and it used up for 4GB of memory!!!" I get this enough with pyfits as it is. |
|
Seems fine to me. A long term solution might be a custom dtype that implicitly converts between unicode and ascii, but in the meantime, I think this is fine as a stopgap. |
|
I am (or was, until getting distracted by release activities) working on exactly this. It's been slow going learning all the ins and outs of adding a new dtype to Numpy. Parts of it are straightforward--other parts a bit confusing. But it should work in principle. My |
|
I think the desired long term solution is a built-in numpy bytestring type that plays well with Python 3. It's on the numpy dev radar but we probably need to keep pushing it. In the meantime I'm going to merge this. I'm hoping these functions will be deprecated soon enough, but the really good solutions tend to take a little while. |
Add funcs to convert table cols to/from unicode/bytestring
|
Well if I end up working something like this and getting it working I'll definitely submit it to Numpy if nobody else does it first. I need this pretty badly for FITS tables. |
This PR provides a conversion layer to make it easy to implement the "unicode-sandwich" advocated by Ned Batchelder in this talk. The idea is that (for Python 3) files and network data are always bytes and internal manipulation is always unicode.
Before going ahead with tests / docs I'm interested in feedback if this is going in the right direction.
Motivation for this follows from what I wrote on numpy-dev today, which is copied below:
I've been playing around with porting a stack of analysis libraries to Python 3 and this is a very timely thread and comment. What I discovered right away is that all the string data coming from binary HDF5 files show up (as expected) as 'S' type,, but that trying to make everything actually work in Python 3 without converting to 'U' is a big mess of whack-a-mole.
Yes, it's possible to change my libraries to use bytestring literals everywhere, but the Python 3 user experience becomes horrible because to interact with the data all downstream applications need to use bytestring literals everywhere. E.g. doing a simple filter like
string_array == 'foo'doesn't work, and this will break all existing code when trying to run in Python 3. And every time you try to print something it has this horrible "b" in front. Ugly, and it just won't work well in the end.Following the excellent advice at http://nedbatchelder.com/text/unipain.html, I've come to the conclusion that the best way to support Python 3 is to bite the bullet and do the "unicode sandwich". That is to say convert all external bytestring values to 'U' arrays for internal (and user) manipulation, and back to 'S' for delivery to files / network etc. This is a pain and very inefficient, but at least the the Python 3 user experience is natural and pleasant. I figure if you are manipulating anything less than ~Gb of text data then it won't be a disaster.