126126# FACTORY FUNCTIONS
127127
128128
129- def _resize ( masked , new_size ) :
129+ class _MaskedArrayBuilder :
130130 """
131- Masked arrays can not be resized inplace, and `np.resize` and
132- `ma.resize` are both incompatible with structured arrays.
133- Therefore, we do all this.
131+ Zeroed data and mask arrays for a table being read and grown in place.
132+
133+ Masked arrays can not be resized in place, but these plain arrays own
134+ their memory, so ``ndarray.resize`` can grow and trim them without
135+ holding a second copy of the table. Call `to_masked` once they are
136+ filled.
137+
138+ `resize` can move the arrays to new memory, so they never leave this
139+ class until `to_masked`, and nothing here keeps a view of them.
134140 """
135- new_array = ma .zeros ((new_size ,), dtype = masked .dtype )
136- length = min (len (masked ), new_size )
137- new_array [:length ] = masked [:length ]
138141
139- return new_array
142+ def __init__ (self , dtype , nrows ):
143+ self ._values = np .zeros (nrows , dtype = dtype )
144+ self ._mask = np .zeros (nrows , dtype = ma .make_mask_descr (dtype ))
145+
146+ def __len__ (self ):
147+ return len (self ._values )
148+
149+ def set_row (self , index , values , mask ):
150+ """
151+ Set one row.
152+
153+ Parameters
154+ ----------
155+ index : int
156+ The row to set.
157+ values : tuple
158+ One value per field of the dtype, in field order, as returned
159+ by the column converters
160+ mask : tuple
161+ One mask flag per field, in the same order
162+ """
163+ self ._values [index ] = values
164+ self ._mask [index ] = mask
165+
166+ def set_rows (self , start , values , masks ):
167+ """
168+ Set consecutive rows, starting at row *start*.
169+
170+ Parameters
171+ ----------
172+ start : int
173+ The first row to set.
174+ values : list of tuple
175+ One tuple per row, each as the *values* of `set_row`.
176+ masks : list of tuple
177+ One tuple per row, each as the *mask* of `set_row`, and as
178+ many as there are in *values*.
179+ """
180+ stop = start + len (values )
181+ self ._values [start :stop ] = values
182+ self ._mask [start :stop ] = masks
183+
184+ def resize (self , nrows ):
185+ """Grow or trim both arrays to *nrows* rows, keeping their contents."""
186+ # refcheck=False: from Python 3.14, numpy's reference count check
187+ # rejects a count of 2, which the attribute and this call already
188+ # make. It is safe because no views of these arrays exist.
189+ # Resizing while a view exists would leave the view pointing at
190+ # freed memory.
191+ self ._values .resize (nrows , refcheck = False )
192+ self ._mask .resize (nrows , refcheck = False )
193+
194+ def to_masked (self ):
195+ """
196+ Wrap the arrays in a masked array without copying them.
197+
198+ The builder can not be used afterwards, since resizing would free
199+ the memory the masked array uses.
200+ """
201+ values , mask = self ._values , self ._mask
202+ del self ._values , self ._mask
203+ # keep_mask=False: for a structured dtype, MaskedArray starts from
204+ # its own all-False mask and, with keep_mask=True, ORs this one into
205+ # it, which allocates a second full-size mask. Unneeded here.
206+ return ma .MaskedArray (values , mask = mask , copy = False , keep_mask = False )
140207
141208
142209def _lookup_by_attr_factory (attr , unique , iterator , element_name , doc ):
@@ -2988,17 +3055,15 @@ def _parse_tabledata(self, iterator, colnumbers, config):
29883055 # allocation is by factors of 1.5.
29893056 invalid = config .get ("invalid" , "exception" )
29903057
2991- # Need to have only one reference so that we can resize the
2992- # array
2993- array = self .array
3058+ builder = _MaskedArrayBuilder (self .array .dtype , len (self .array ))
29943059 del self .array
29953060
29963061 fields = self .all_fields
29973062 parsers = [field .converter .parse for field in fields ]
29983063 binparsers = [field .converter .binparse for field in fields ]
29993064
30003065 numrows = 0
3001- alloc_rows = len (array )
3066+ alloc_rows = len (builder )
30023067 colnumbers_bits = [i in colnumbers for i in range (len (fields ))]
30033068 row_default = [field .converter .default for field in self .fields ]
30043069 mask_default = [True ] * len (self .fields )
@@ -3076,10 +3141,9 @@ def _parse_tabledata(self, iterator, colnumbers, config):
30763141 if len (array_chunk ) == chunk_size :
30773142 while numrows + chunk_size > alloc_rows :
30783143 alloc_rows = self ._resize_strategy (alloc_rows )
3079- if alloc_rows != len (array ):
3080- array = _resize (array , alloc_rows )
3081- array [numrows : numrows + chunk_size ] = array_chunk
3082- array .mask [numrows : numrows + chunk_size ] = mask_chunk
3144+ if alloc_rows != len (builder ):
3145+ builder .resize (alloc_rows )
3146+ builder .set_rows (numrows , array_chunk , mask_chunk )
30833147 numrows += chunk_size
30843148 array_chunk = []
30853149 mask_chunk = []
@@ -3091,17 +3155,15 @@ def _parse_tabledata(self, iterator, colnumbers, config):
30913155 # put the last chunk values in there.
30923156 alloc_rows = numrows + len (array_chunk )
30933157
3094- array = _resize (array , alloc_rows )
3095- array [numrows :] = array_chunk
3096- if alloc_rows != 0 :
3097- array .mask [numrows :] = mask_chunk
3158+ builder .resize (alloc_rows )
3159+ builder .set_rows (numrows , array_chunk , mask_chunk )
30983160 numrows += len (array_chunk )
30993161
31003162 if self .nrows is not None and self .nrows >= 0 and self .nrows != numrows :
31013163 warn_or_raise (W18 , W18 , (self .nrows , numrows ), config , pos )
31023164 self ._nrows = numrows
31033165
3104- return array
3166+ return builder . to_masked ()
31053167
31063168 def _get_binary_data_stream (self , iterator , config ):
31073169 have_local_stream = False
@@ -3171,21 +3233,19 @@ def careful_read(length):
31713233 def _parse_binary (self , mode , iterator , colnumbers , config , pos ):
31723234 careful_read = self ._get_binary_data_stream (iterator , config )
31733235
3174- # Need to have only one reference so that we can resize the
3175- # array
3176- array = self .array
3236+ builder = _MaskedArrayBuilder (self .array .dtype , len (self .array ))
31773237 del self .array
31783238
31793239 fields = self .all_fields
31803240 binparsers = [field .converter .binparse for field in fields ]
31813241
31823242 numrows = 0
3183- alloc_rows = len (array )
3243+ alloc_rows = len (builder )
31843244 while True :
31853245 # Resize result arrays if necessary
31863246 if numrows >= alloc_rows :
31873247 alloc_rows = self ._resize_strategy (alloc_rows )
3188- array = _resize ( array , alloc_rows )
3248+ builder . resize ( alloc_rows )
31893249
31903250 row_data = []
31913251 row_mask_data = []
@@ -3230,11 +3290,11 @@ def _parse_binary(self, mode, iterator, colnumbers, config, pos):
32303290 row [j ] = row_data [i ]
32313291 row_mask [j ] = row_mask_data [i ]
32323292
3233- array [numrows ] = tuple (row )
3234- array .mask [numrows ] = tuple (row_mask )
3293+ builder .set_row (numrows , tuple (row ), tuple (row_mask ))
32353294 numrows += 1
32363295
3237- return _resize (array , numrows )
3296+ builder .resize (numrows )
3297+ return builder .to_masked ()
32383298
32393299 def _parse_fits (self , iterator , extnum , config ):
32403300 for start , tag , data , pos in iterator :
0 commit comments