3
\$\begingroup\$

I have this class to generate unique ids and random ids from those unique ids.

For the unique ids, it's a sequence of numbers between (start number = given number [start_seed]/or random number (0:1000000), and end number = start number + number of desired unique id -1) converted to string and hashed using md5 and converted to hex digest.

For the random id, get a random number between the start and the end and converted to string and hashed using md5 and converted to hex digest.

Is there any better way to generate random ids, and if there are feedbacks in the code structure, efficiency, and performance

import hashlib
import random


class RandomIdsGenerator:
    """
    Generate random ids from specific numbers of auto-generated unique ids.
    For instance: you maybe want to generate 1000 random user ids from 10 unique ids.
    How it works: it generate random number in specific range and hash this number using md5
     and convert it to hexdigest
    """
    __slots__ = ['__n_unique_id', '__start_num', '__end_num']

    def __init__(self, n_unique_id: int, start_seed: int = None):
        """
        Initialize RandomIdsGenerator
        :param n_unique_id: number of unique ids you desire
        :param start_seed: start number of the unique ids,
                Default is None that will pick up a number between (0:1000000)
                you can can use if you want generate ids in specific ranges.
        """
        self.__n_unique_id = n_unique_id
        self.__start_num = start_seed
        if not self.__start_num:
            self.__start_num = random.randrange(1000000)
        self.__end_num = self.__start_num + n_unique_id - 1

    def random(self):
        """
        Generate single random id
        :return: random id
        """
        random_num = random.randrange(self.__start_num, self.__end_num)
        hashed_num = hashlib.md5(str(random_num).encode())
        return hashed_num.hexdigest()

    def randoms(self, n_ids: int):
        """
        Generate list of random ids
        :param n_ids: number of id you need to generate
        :return: list of random ids it might contains duplications
        """
        random_ids = []
        for i in range(0, n_ids):
            random_ids.append(self.random())
        return random_ids

    def get_unique_ids(self):
        """
        :return: list of unique ids it randomize from
        """
        unique_ids = []
        for i in range(self.__start_num, self.__end_num + 1):
            hashed_num = hashlib.md5(str(i).encode())
            unique_ids.append(hashed_num.hexdigest())
        return unique_ids

The benchmark of get_unique_ids()

number of unique ids:10 - avg time of 1k iteration: 2.090144157409668e-05
number of unique ids:100 - avg time of 1k iteration: 7.855653762817383e-05
number of unique ids:1000 - avg time of 1k iteration: 0.0006839170455932617
number of unique ids:10000 - avg time of 1k iteration: 0.006684443712234497
number of unique ids:100000 - avg time of 1k iteration: 0.07844765543937683
number of unique ids:1000000 - avg time of 1k iteration: 0.7951802101135254

The benchmark of getting random id random() from 1M unique id. Time calculated from creating the object and call the function n times

get random id:10 - avg time of 1k iteration: 2.903556823730469e-05
get random id:100 - avg time of 1k iteration: 0.00015737485885620117
get random id:1000 - avg time of 1k iteration: 0.0019962642192840577
get random id:10000 - avg time of 1k iteration: 0.01631594944000244
get random id:100000 - avg time of 1k iteration: 0.17304418659210205

Measure performance code

from time import time

def average(lst): 
    return sum(lst) / len(lst)

def measure_performance_generate_unique_ids(n_unique_id, n_iterations=1000):
    time_performance = []
    for i in range(0, n_iterations):
        start = time()
        random_id_generator = RandomIdsGenerator(n_unique_id)
        random_id_generator.get_unique_ids()
        end = time()
        total_time = end - start
        time_performance.append(total_time)
    perf_str = 'number of unique ids:{} - avg time per iteration: {}'.format(n_unique_id, average(time_performance))
    print(perf_str)

def measure_performance_pick_random_id(n_random_ids,n_unique_ids=1000000, n_iteration=1000):
    time_performance = []
    for i in range(0, n_iteration):
        start = time()
        random_id_generator = RandomIdsGenerator(n_unique_ids)
        for n in range(0, n_random_ids):
            random_id_generator.random()
        end = time()
        total_time = end - start
        time_performance.append(total_time)
    perf_str = 'get random id:{} - avg time per iteration: {}'.format(n_random_ids ,average(time_performance))
    print(perf_str)

Call the measurement methods:

measure_performance_generate_unique_ids(10)
measure_performance_generate_unique_ids(100)
measure_performance_generate_unique_ids(1000)
measure_performance_generate_unique_ids(10000)
measure_performance_generate_unique_ids(100000)
measure_performance_generate_unique_ids(1000000)

measure_performance_pick_random_id(10)
measure_performance_pick_random_id(100)
measure_performance_pick_random_id(1000)
measure_performance_pick_random_id(10000)
measure_performance_pick_random_id(100000)
measure_performance_pick_random_id(1000000)
\$\endgroup\$
2
  • 1
    \$\begingroup\$ Why not use a GUID? There are lots of libraries that handle this. How to create a GUID/UUID in Python \$\endgroup\$ Commented Jan 27, 2020 at 16:26
  • \$\begingroup\$ Because I need a specific number of unique ids and select randomly from those ids. For instance, want to have 100k unique id and generate 1M randomly from the unique 100K ids. And to do that using GUID/UUID I have to save those 100K unique ids. so this class makes you able to generate random id from unique ids on the fly without saving it. @MartinYork \$\endgroup\$
    – Eslam Ali
    Commented Jan 27, 2020 at 16:42

1 Answer 1

2
\$\begingroup\$

This is some nice-looking Python code. Good work. Though, if you were to run pylint over this, you'd still find:

************* Module cr
cr.py:1:0: C0114: Missing module docstring (missing-module-docstring)
cr.py:44:12: W0612: Unused variable 'i' (unused-variable)

------------------------------------------------------------------
Your code has been rated at 9.20/10

So, it's usually good practice to add a module docstring at the beginning of the module. I've seen many people adding the same docstring as the one for the class within that module (if it's just one).

From PEP8:

All modules should normally have docstrings, and all functions and classes exported by a module should also have docstrings. Public methods (including the __init__ constructor) should also have docstrings. A package may be documented in the module docstring of the __init__.py file in the package directory.

So, in the end, it is just a matter of preference.

You also have a magic number: 1000000. I'd just take it out of your __init__ and define it as a constant. Something like:

MAX_RANGE = 1000000

Now, the second pylint warning tells you that here:

for i in range(0, n_ids):  # you're not using i at all
    random_ids.append(self.random())
return random_id

So you could just replace it with _:

for _ in range(0, n_ids):
    random_ids.append(self.random())
return random_id

Even better, you could entirely rewrite the above and use a list comprehension instead:

def randoms(self, n_ids: int):
    """
    Generate list of random ids
    :param n_ids: number of id you need to generate
    :return: list of random ids it might contains duplications
    """

    return [self.random() for _ in range(0, n_ids)]

The same applies for get_unique_ids() method (although some might argue that there's a small benefit in favour of readability):

def get_unique_ids(self):
    """
    :return: list of unique ids it randomize from
    """

    return [
        hashlib.md5(str(i).encode()).hexdigest()
        for i in range(self.__start_num, self.__end_num + 1)
    ]

From this SO answer:

List comprehension is basically just a "syntactic sugar" for the regular for loop. In this case the reason that it performs better is because it doesn't need to load the append attribute of the list and call it as a function at each iteration. In other words and in general, list comprehensions perform faster because suspending and resuming a function's frame, or multiple functions in other cases, is slower than creating a list on demand.

This won't have such a big impact on the actual speed, but it's definitely giving you a nice start :)

Another advice would be to use Numpy if you want to generate large numbers of random ints; if you're just generating one-at-a-time, it may not be as useful (but then how much do you care about performance, really?).

Libraries like Numpy carefully move as much compute as possible to underlying C code.

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

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