1

I am receiving Points in large number from a sensor in real-time. However, I just need 4 categories of points, i.e., top_left, top_right, bottom_left, and bottom_right. I have an if-elif statement in Python 2 as follows:

from random import random, randint

# points below are received from sensor. however, 
# here in this post I am creating it randomly.
points = [Point(randint(0, i), random(), random(), random()) for i in range(100)] 

# 4 categories
top_left, top_right, bottom_left, bottom_right = None, None, None, None
for p in points:
    if p.id == 5:
        top_left = p
    elif p.id == 7:
        top_right = p
    elif p.id == 13:
        bottom_left = p
    elif p.id == 15:
        bottom_right = p

print top_left.id, top_left.x, top_left.y, top_left.z # check variable

Each Point has an id and x, y, z parameters. This is an inbuilt class. I am just showing a sample class here.

class Point():
    def __init__(self, id, x, y, z):
        self.id = id
        self.x = x
        self.y = y
        self.z = z

Is there any efficient way considering runtime of achieving the same.

Answer: I am adding the results which I got from the answers. It seems that the answer by Elis Byberi is fastest among all. Below is my test code:

class Point():
    def __init__(self, id, x, y, z):
        self.id = id
        self.x = x
        self.y = y
        self.z = z

from random import random, randint
n = 1000
points = [Point(randint(0, i), random(), random(), random()) for i in range(n)]

def method1():
    top_left, top_right, bottom_left, bottom_right = None, None, None, None
    for p in points:
        if p.id == 5:
            top_left = p
        elif p.id == 7:
            top_right = p
        elif p.id == 13:
            bottom_left = p
        elif p.id == 15:
            bottom_right = p
    #print top_left.id, top_left.x, top_left.y, top_left.z

def method2():
    categories = {
        5: None,  # top_left
        7: None,  # top_right
        13: None,  # bottom_left
        15: None  # bottom_right
    }

    for p in points:
        categories[p.id] = p

    top_left = categories[5]
    #print top_left.id, top_left.x, top_left.y, top_left.z

def method3():
    name_to_id = {'top_left': 5, 'top_right': 7, 'bottom_left': 13, 'bottom_right': 15}
    ids = [value for value in name_to_id.values()]
    bbox = {id: None for id in ids}

    for point in points:
        try:
            bbox[point.id] = Point(point.id, point.x, point.y, point.z)
        except KeyError:  # Not an id of interest.
            pass

    top_left = bbox[name_to_id['top_left']]
    #print top_left.id, top_left.x, top_left.y, top_left.z

from timeit import Timer
print 'method 1:', Timer(lambda: method1()).timeit(number=n)
print 'method 2:', Timer(lambda: method2()).timeit(number=n)
print 'method 3:', Timer(lambda: method3()).timeit(number=n)

See Below the returned output:

ravi@home:~/Desktop$ python test.py 
method 1: 0.174991846085
method 2: 0.0743980407715
method 3: 0.582262039185
ravi
  • 6,140
  • 18
  • 77
  • 154
  • 3
    Efficient in which way? Run time, readability, or time it takes to type? – Nick Oct 29 '17 at 16:37
  • @NickPredey: Runtime – ravi Oct 29 '17 at 16:39
  • 1
    That's as efficient as a control structure can get, I think – arielnmz Oct 29 '17 at 16:44
  • I'd use dictionaries for the `buttons` and the `id` -> `buttons` mapping. – Klaus D. Oct 29 '17 at 16:44
  • 1
    Why are you making 100 random points but only keeping 4 of them? – PM 2Ring Oct 29 '17 at 16:47
  • @KlausD. More explanation, please. The `Point` class is inbuild and I can't change it. I just posted a sample code here. – ravi Oct 29 '17 at 16:49
  • @RaviJoshi not only you changed the question, not there is nothing that can guarantee that you'll get points for top_left, top_right and etc! – Nir Alfasi Oct 29 '17 at 16:50
  • alfasin and @PM2Ring: I don't need all the points. I just need 4 points out of them. These points are received from a sensor in real-time. Thanks! – ravi Oct 29 '17 at 16:53
  • @RaviJoshi if you're using the IDs as a criteria for choosing and the IDs are chosen randomly, you can't guarantee that you'll have a point with id 5 or 7 and etc. – Nir Alfasi Oct 29 '17 at 16:55
  • @alfasin: I agree with you. However, these points are received from a sensor. It may be true that some categories are none at the end. – ravi Oct 29 '17 at 17:02
  • Does the Point class have a `__lt__` method? – wwii Oct 29 '17 at 17:20
  • Why do you think your solution is inefficient? – wwii Oct 29 '17 at 17:24
  • Your series of `if` statements in the `for` loop could be handled with what in some languages, like C/C++, is called a `switch` statement. Python doesn't have one, but they can be emulated fairly efficiently. See [**Replacements for switch statement in Python?**](https://stackoverflow.com/questions/60208/replacements-for-switch-statement-in-python) However in this case one is not really needed. See [my answer](https://stackoverflow.com/a/47004474/355230) below to see how. – martineau Oct 29 '17 at 19:56

3 Answers3

3

You can use a dict to save objects. Dict is very efficient in key lookup.

Using dict is twice as fast as using if else block.

This is the most efficient way in python:

from random import random, randint

class Point():
    def __init__(self, id, x, y, z):
        self.id = id
        self.x = x
        self.y = y
        self.z = z

# points below are received from sensor. however,
# here in this post I am creating it randomly.
points = [Point(randint(0, i), random(), random(), random()) for i in
          range(100)]

# 4 categories
categories = {
    5: None,  # top_left
    7: None,  # top_right
    13: None,  # bottom_left
    15: None  # bottom_right
}

for p in points:
    categories[p.id] = p

>>> print categories[5].id, categories[5].x, categories[5].y, categories[5].z  # check variable
5 0.516239541892 0.935096344266 0.0859987803457
Elis Byberi
  • 1,422
  • 1
  • 11
  • 20
  • 1
    Why limit the dictionary to those keys and have to handle exceptions for all other Point.id's? Just add every Point.id to the dict and only use the keys you are interested in? – wwii Oct 29 '17 at 17:43
  • Do you think that if else block is faster than exception handling? I did a benchmark and it is twice as fast as if else block. – Elis Byberi Oct 29 '17 at 17:45
  • No, just make a dictionary with *every* Point.id - don't restrict it and manage the restriction with exception handling. Subsequent code can just use the relevant keys and ignore the other. – wwii Oct 29 '17 at 19:19
  • We do not know how many points are there and try/except block does not have any overhead. – Elis Byberi Oct 29 '17 at 19:23
  • try/except block was useless. Key will be added in dict anyway. Your statement about managing the restriction was strange because it was not restricting it anyway. It made me think about it. – Elis Byberi Oct 29 '17 at 19:33
1

Instead of using list-comprehension:

points = [Point(randint(0, i), random(), random(), random()) for i in range(100)]

use a loop and assign the points during creation:

points = []
for i in range(100):
    p = Point(randint(0, i), random(), random(), random())
    points.append(p)
    if p.id == 5:
        top_left = p
    elif p.id == 7:
       top_right = p
    elif p.id == 13:
        bottom_left = p
    elif p.id == 15:
        bottom_right = p

This way you get all done in one iteration instead of two.

Nir Alfasi
  • 53,191
  • 11
  • 86
  • 129
  • Unfortunately, I have no control over the index of this array. It can be any integer number. Please check the question again. Thanks a lot. – ravi Oct 29 '17 at 16:46
1

Here's a way that should be faster because it uses a single if to determine whether a Point is one of the ones representing the extremes based on their id attribute, plus the if uses the very fast dictionary in operation for membership testing. Essentially what is done it the bbox dictionary is preloaded with keys that correspond to the four ids sought, which make checking for any of them a single relatively efficient operation.

Note that if there are points with duplicate ids in the Point list, the last one seen will be the one selected. Also note that if no point with a matching id is found, some of the final variables will have a value of None instead of a Point instance.

from random import randint, random
from pprint import pprint
from operator import attrgetter


class Point():
    def __init__(self, id, x, y, z):
        self.id = id
        self.x = x
        self.y = y
        self.z = z


points = [Point(randint(0, 20), random(), random(), random()) for i in range(100)]
name_to_id = {'top_left': 5, 'top_right': 7, 'bottom_left': 13, 'bottom_right': 15}
bbox = {id: None for id in name_to_id.values()}  # Preload with ids of interest.

for point in points:
    if point.id in bbox:  # id of interest?
       bbox[point.id] = point

# Assign bbox's values to variables with meaningful names.
top_left = bbox[name_to_id['top_left']]
top_right = bbox[name_to_id['top_right']]
bottom_left = bbox[name_to_id['bottom_left']]
bottom_right = bbox[name_to_id['bottom_right']]

for point in [top_left, top_right, bottom_left, bottom_right]:
    print('Point({}, {}, {}, {})'.format(point.id, point.x, point.y, point.z))
martineau
  • 119,623
  • 25
  • 170
  • 301
  • Thank you very much. I like the coding style especially `Assign bbox's values to variables with meaningful names.` – ravi Oct 30 '17 at 12:02