2
\$\begingroup\$

I really appreciate the way filtering is applied to queries in Django. I have a list of items (each item is an Item instance which contains a few attributes) that I would like to provide a similar API for filtering.

For reference, the Item class contains the following attributes:

  • Name
  • Description
  • Price
  • Stock

Currently, Item instances are stored in a list contained inside an Inventory instance. The end-goal is to have code like the following:

>>> inv = Inventory('some id')
>>> inv.load()
>>> inv.find(name='Tomato Paste')
[Item <Tomato Paste>]
>>> inv.find(name__startswith = 'T', name__contains='paste')
[Item <Tomato Paste>]
>>> inv.find(price__gt=200)
[Item <Some Expensive Item>, Item <Some Other Expensive Item>]

Here is an excerpt from my Inventory class with my attempt at accomplishing this:

def find(self, **kwargs):
    matches = []

    for item in self.data:
        match = 0
        for key in kwargs.keys():
            try:
                # Determine if this query has extra arguments
                if '__' in key:
                    arg = key.split('__')[1]
                    atrib = key.split('__')[0]
                    value = getattr(item, atrib)

                    if arg not in self.query_types:
                        continue

                    if arg == 'contains' and type(value) is str:
                        if kwargs[key].lower() in value.lower():
                            match += 1
                    elif arg == 'startswith' and type(value) is str:
                        if value.lower().startswith(kwargs[key].lower()):
                            match += 1
                    elif arg == 'endswith' and type(value) is str:
                        if value.lower().endswith(kwargs[key].lower()):
                            match += 1
                    elif arg == 'gt' and self._is_int(value):
                        if int(value) > int(kwargs[key]):
                            match += 1
                    elif arg == 'lt' and self._is_init(value):
                        if int(value) < int(kwargs[key]):
                            match += 1
                else:
                    value = getattr(item, key)
                    if type(kwargs[key]) is str:
                        if kwargs[key].lower() == value.lower():
                            match += 1
                    else:
                        if kwargs[key] == value:
                            match += 1
            except Exception:
                continue

        if match == len(kwargs.keys()):
            matches.append(item)
    return matches

A few notes:

  • self._is_init does what it implies
  • self.data is the list of Item instances

The above code does what I need it to, but it seems to have a serious boilerplate issue. I feel like there's a much more efficient and easier to understand solution. Optimization and feedback is welcomed.

\$\endgroup\$
2
\$\begingroup\$

Here's some improvements you can make and some (untested) code with those improvements:

  1. Instead of checking if the argument key contains '__' then splitting it, just split it. If it didn't contain '__' the second part will be an empty string.
  2. Instead of through all the items and each keyword argument each time, loop through the arguments and use list comprehensions to find the matches. This way the matches list is shrinking and you don't need to look at values that matched the previous arguments.
  3. Instead of checking if the type of a value is string, use isinstance(obj, basestring). This will allow unicode strings also.
  4. Instead of catching all exceptions, catch only the exceptions you expect. (others are actually errors and should be propagated to the caller).

...

def find(self, **kwargs):
    matches = self.data

    for key, key_value in kwargs.iteritems():
        # Determine if this query has extra arguments
        key_parts = key.split('__')
        attrib = key_parts[0]

        if (len(parts) > 1):
            arg = key_parts[1]
            if arg not in self.query_types:
                continue

            if arg == 'contains':
                matches = [ item for item in matches if isinstance(getattr(item,arg, None), basestring) and 
                            ( key_value.lower() in getattr(item,arg, "").lower() ) ]
            elif arg == 'startswith':
                matches = [ item for item in matches if isinstance(getattr(item, arg, None), basestring) and 
                            ( getattr(item,arg, "").lower().startswith(key_value.lower()) ) ]
            elif arg == 'endswith':
                matches = [ item for item in matches if isinstance(getattr(item, arg, None), basestring) and 
                            ( getattr(item,arg, "").lower().endswith(key_value.lower()) ) ]
            elif arg == 'gt':
                matches = [ item for item in matches if self._is_int((getattr(item, arg, None)) and 
                            ( int(getattr(item, arg, 0)) > int(key_value) ) ]
            elif arg == 'lt':
                matches = [ item for item in matches if self._is_int((getattr(item, arg, None)) and 
                            ( int(getattr(item, arg, 0)) < int(key_value) ) ]
        else:
            attr_value = getattr(item, key)
            if isinstance(value, str):
                if key_value.lower() == attr_value.lower():
                    matches = [ item for item in matches if 
                                ( isinstance(getattr(item,arg, None), str) and 
                                    key_value.lower() == isinstance(getattr(item,arg,"")).lower() )
                                or ( key_value == isinstance(getattr(item,arg,None)) ) ]

    return matches
\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.