Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit 0428e67

Browse files
committed
Access record fields as attributes (#132)
Also accessing a record's "oid" position attribute. Thanks to @philippkraft and #132. Reworked to resolve merge conflicts.
1 parent 9c30664 commit 0428e67

15 files changed

+170
-11
lines changed

README.md

Lines changed: 26 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -349,21 +349,40 @@ To read a single record call the record() method with the record's index:
349349

350350

351351
>>> rec = sf.record(3)
352+
353+
Each record is a list-like Record object containing the values corresponding to each field in
354+
the field list. A record's values can be accessed by positional indexing or slicing.
355+
For example in the blockgroups shapefile the 2nd and 3rd fields are the blockgroup id
356+
and the 1990 population count of that San Francisco blockgroup:
357+
352358

353359
>>> rec[1:3]
354360
['060750601001', 4715]
361+
362+
For simpler access, the fields of a record can also accessed via the name of the field,
363+
either as a key or as an attribute name. The blockgroup id (BKG_KEY) of the blockgroups shapefile
364+
can also be retrieved as:
365+
366+
367+
>>> rec['BKG_KEY']
368+
'060750601001'
369+
370+
>>> rec.BKG_KEY
371+
'060750601001'
355372

356-
Each record is a list containing an attribute corresponding to each field in
357-
the field list.
373+
The record values can be easily integrated with other programs by converting it to a field-value dictionary:
358374

359-
For example in the 4th record of the blockgroups shapefile the 2nd and 3rd
360-
fields are the blockgroup id and the 1990 population count of that San
361-
Francisco blockgroup:
362375

376+
>>> rec.as_dict()
377+
{'MALES': 2620, 'AGE_UNDER5': 597, 'AGE_65_UP': 30, 'MARHH_CHD': 79, 'UNITS10_49': 49, 'POP1990': 4715, 'BLACK': 1007, 'UNITS50_UP': 0, 'AMERI_ES': 6, 'SEPARATED': 49, 'AGE_30_49': 1681, 'DIVORCED': 149, 'AREA': 2.34385, 'UNITS3_9': 672, 'AGE_18_29': 1467, 'BKG_KEY': '060750601001', 'HOUSEHOLDS': 1195, 'HISPANIC': 416, 'HSEHLD_1_M': 22, 'UNITS_1DET': 43, 'POP90_SQMI': 2011.6, 'HSEHLD_1_F': 40, 'HSE_UNITS': 1258, 'MARRIED': 2021, 'MOBILEHOME': 0, 'MARHH_NO_C': 958, 'AGE_5_17': 848, 'OTHER': 288, 'FEMALES': 2095, 'WHITE': 2962, 'NEVERMARRY': 703, 'MEDIAN_VAL': 337500, 'WIDOWED': 37, 'UNITS2': 160, 'FHH_CHILD': 16, 'RENTER_OCC': 3733, 'AGE_50_64': 92, 'VACANT': 93, 'UNITS_1ATT': 302, 'ASIAN_PI': 452, 'MHH_CHILD': 0, 'OWNER_OCC': 66, 'MEDIANRENT': 739}
378+
379+
If at a later point you need to check the record's index position in the original
380+
shapefile, you can do this through the "oid" attribute:
363381

364-
>>> records[3][1:3]
365-
['060750601001', 4715]
366382

383+
>>> rec.oid
384+
3
385+
367386
### Reading Geometry and Records Simultaneously
368387

369388
You may want to examine both the geometry and the attributes for a record at

onlydbf.dbf

0 Bytes
Binary file not shown.

shapefile.dbf

0 Bytes
Binary file not shown.

shapefile.py

Lines changed: 144 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -341,6 +341,141 @@ def _from_geojson(geoj):
341341
@property
342342
def shapeTypeName(self):
343343
return SHAPETYPE_LOOKUP[self.shapeType]
344+
345+
class _Record(list):
346+
"""
347+
A class to hold a record. Subclasses list to ensure compatibility with
348+
former work and allows to use all the optimazations of the builtin list.
349+
In addition to the list interface, the values of the record
350+
can also be retrieved using the fields name. Eg. if the dbf contains
351+
a field ID at position 0, the ID can be retrieved with the position, the field name
352+
as a key or the field name as an attribute.
353+
354+
>>> # Create a Record with one field, normally the record is created by the Reader class
355+
>>> r = _Record({'ID': 0}, [0])
356+
>>> print(r[0])
357+
>>> print(r['ID'])
358+
>>> print(r.ID)
359+
"""
360+
361+
def __init__(self, field_positions, values, oid=None):
362+
"""
363+
A Record should be created by the Reader class
364+
365+
:param field_positions: A dict mapping field names to field positions
366+
:param values: A sequence of values
367+
:param oid: The object id, an int (optional)
368+
"""
369+
self.__field_positions = field_positions
370+
if oid is not None:
371+
self.__oid = oid
372+
else:
373+
self.__oid = -1
374+
list.__init__(self, values)
375+
376+
def __getattr__(self, item):
377+
"""
378+
__getattr__ is called if an attribute is used that does
379+
not exist in the normal sense. Eg. r=Record(...), r.ID
380+
calls r.__getattr__('ID'), but r.index(5) calls list.index(r, 5)
381+
:param item: The field name, used as attribute
382+
:return: Value of the field
383+
:raises: Attribute error, if field does not exist
384+
and IndexError, if field exists but not values in the Record
385+
"""
386+
try:
387+
index = self.__field_positions[item]
388+
return list.__getitem__(self, index)
389+
except KeyError:
390+
raise AttributeError('{} is not a field name'.format(item))
391+
except IndexError:
392+
raise IndexError('{} found as a field but not enough values available.'.format(item))
393+
394+
def __setattr__(self, key, value):
395+
"""
396+
Sets a value of a field attribute
397+
:param key: The field name
398+
:param value: the value of that field
399+
:return: None
400+
:raises: AttributeError, if key is not a field of the shapefile
401+
"""
402+
if key.startswith('_'): # Prevent infinite loop when setting mangled attribute
403+
return list.__setattr__(self, key, value)
404+
try:
405+
index = self.__field_positions[key]
406+
return list.__setitem__(self, index, value)
407+
except KeyError:
408+
raise AttributeError('{} is not a field name'.format(key))
409+
410+
def __getitem__(self, item):
411+
"""
412+
Extends the normal list item access with
413+
access using a fieldname
414+
415+
Eg. r['ID'], r[0]
416+
:param item: Either the position of the value or the name of a field
417+
:return: the value of the field
418+
"""
419+
try:
420+
return list.__getitem__(self, item)
421+
except TypeError:
422+
try:
423+
index = self.__field_positions[item]
424+
except KeyError:
425+
index = None
426+
if index is not None:
427+
return list.__getitem__(self, index)
428+
else:
429+
raise IndexError('"{}" is not a field name and not an int'.format(item))
430+
431+
def __setitem__(self, key, value):
432+
"""
433+
Extends the normal list item access with
434+
access using a fieldname
435+
436+
Eg. r['ID']=2, r[0]=2
437+
:param key: Either the position of the value or the name of a field
438+
:param value: the new value of the field
439+
"""
440+
try:
441+
return list.__setitem__(self, key, value)
442+
except TypeError:
443+
index = self.__field_positions.get(key)
444+
if index is not None:
445+
return list.__setitem__(self, index, value)
446+
else:
447+
raise IndexError('{} is not a field name and not an int'.format(key))
448+
449+
@property
450+
def oid(self):
451+
"""The index position of the record in the original shapefile"""
452+
return self.__oid
453+
454+
@property
455+
def fields(self):
456+
"""The field names of the record"""
457+
srt = sorted(self.__field_positions.items(), key=lambda(f,i): i)
458+
return [f for f,i in srt]
459+
460+
def as_dict(self):
461+
"""
462+
Returns this Record as a dictionary using the field names as keys
463+
:return: dict
464+
"""
465+
return dict((f, self[i]) for f, i in self.__field_positions.items())
466+
467+
def __str__(self):
468+
return 'record #{} '.format(self.__oid)
469+
470+
def __dir__(self):
471+
"""
472+
Helps to show the field names in an interactive environment like IPython.
473+
See: http://ipython.readthedocs.io/en/stable/config/integrating.html
474+
475+
:return: List of method names and fields
476+
"""
477+
attrs = [attr for attr in vars(type(self)) if not attr.startswith('_')]
478+
return attrs + self.fields
344479

345480
class ShapeRecord(object):
346481
"""A ShapeRecord object containing a shape along with its attributes."""
@@ -380,6 +515,7 @@ def __init__(self, *args, **kwargs):
380515
self.numRecords = None
381516
self.fields = []
382517
self.__dbfHdrLength = 0
518+
self.__fieldposition_lookup = {}
383519
self.encoding = kwargs.pop('encoding', 'utf-8')
384520
self.encodingErrors = kwargs.pop('encodingErrors', 'strict')
385521
# See if a shapefile name was passed as an argument
@@ -738,6 +874,9 @@ def __dbfHeader(self):
738874
fmt,fmtSize = self.__recordFmt()
739875
self.__recStruct = Struct(fmt)
740876

877+
# Store the field positions
878+
self.__fieldposition_lookup = dict((f[0], i) for i, f in enumerate(self.fields[1:]))
879+
741880
def __recordFmt(self):
742881
"""Calculates the format and size of a .dbf record."""
743882
if self.numRecords is None:
@@ -751,7 +890,7 @@ def __recordFmt(self):
751890
fmtSize += 1
752891
return (fmt, fmtSize)
753892

754-
def __record(self):
893+
def __record(self, oid=None):
755894
"""Reads and returns a dbf record row as a list of values."""
756895
f = self.__getFileObj(self.dbf)
757896
recordContents = self.__recStruct.unpack(f.read(self.__recStruct.size))
@@ -814,7 +953,8 @@ def __record(self):
814953
value = u(value, self.encoding, self.encodingErrors)
815954
value = value.strip()
816955
record.append(value)
817-
return record
956+
957+
return _Record(self.__fieldposition_lookup, record, oid)
818958

819959
def record(self, i=0):
820960
"""Returns a specific dbf record based on the supplied index."""
@@ -825,7 +965,7 @@ def record(self, i=0):
825965
recSize = self.__recStruct.size
826966
f.seek(0)
827967
f.seek(self.__dbfHdrLength + (i * recSize))
828-
return self.__record()
968+
return self.__record(oid=i)
829969

830970
def records(self):
831971
"""Returns all records in a dbf file."""
@@ -835,7 +975,7 @@ def records(self):
835975
f = self.__getFileObj(self.dbf)
836976
f.seek(self.__dbfHdrLength)
837977
for i in range(self.numRecords):
838-
r = self.__record()
978+
r = self.__record(oid=i)
839979
if r:
840980
records.append(r)
841981
return records

shapefile.pyc

6.29 KB
Binary file not shown.

shapefiles/test/balancing.dbf

0 Bytes
Binary file not shown.

shapefiles/test/contextwriter.dbf

0 Bytes
Binary file not shown.

shapefiles/test/dtype.dbf

0 Bytes
Binary file not shown.

shapefiles/test/line.dbf

0 Bytes
Binary file not shown.

shapefiles/test/linem.dbf

0 Bytes
Binary file not shown.

shapefiles/test/linez.dbf

0 Bytes
Binary file not shown.

shapefiles/test/multipatch.dbf

0 Bytes
Binary file not shown.

shapefiles/test/multipoint.dbf

0 Bytes
Binary file not shown.

shapefiles/test/point.dbf

0 Bytes
Binary file not shown.

shapefiles/test/polygon.dbf

0 Bytes
Binary file not shown.

0 commit comments

Comments
 (0)