将整数转换为随机但可确定重复选择的选项。

7
如何将无符号整数(表示用户ID)转换为看起来随机但实际上是可重复的选择? 选择必须以相等的概率被选中(不考虑输入整数的分布)。例如,如果我有3个选择,即[0,1,2],则用户ID 123可能始终随机分配选择2,而用户ID 234可能始终分配选择1。
跨语言和跨平台算法的可重现性是可取的。我倾向于使用哈希函数和取模,除非有更好的方法。这是我的代码:
>>> num_choices = 3
>>> id_num = 123
>>> int(hashlib.sha256(str(id_num).encode()).hexdigest(), 16) % num_choices
2

我正在使用最新的稳定版Python 3。请注意,这个问题与相关问题转换字符串为随机但可重复确定性均匀概率类似但不完全相同。


1
在您的实际应用中,用户ID整数的域是什么,选择集有多大?您希望这种随机化有多安全?它只需要看起来随机,还是您想要一些具有密码学强度的东西? - PM 2Ring
@PM2Ring 我不知道用户ID整数的域,但它们可能是从数据库获取的32位或64位无符号整数。选择集长度为2到10。加密随机性并不是必需的,但重复性和等概率性是必须的。 - Asclepius
我认为“选择集长度为2到10”意味着最多只有10个选项,而不是选项数量可以是10位数字。如果前者是真的,那么要使它具有加密强度将会非常困难。但您可能仍然对格式保留加密这个主题感兴趣。 - PM 2Ring
将ID转换为可重复的确定性选择意味着从所有可能ID的集合A到所有选择的集合C定义一个固定函数。选择的概率是与该选择对应的用户ID概率之和。这意味着您必须对用户ID分布做出某些说明。问题中有一些缺失。我认为你真正想要的是这个函数看起来像随机的,尽管它并不是随机的。 - Gribouillis
@Gribouillis 是的,确实如此。我想要一个看起来随机但却是确定性的函数。我已经编辑了问题来提及这一点。 - Asclepius
显示剩余3条评论
4个回答

7

使用哈希和取模运算

import hashlib

def id_to_choice(id_num, num_choices):
    id_bytes = id_num.to_bytes((id_num.bit_length() + 7) // 8, 'big')
    id_hash = hashlib.sha512(id_bytes)
    id_hash_int = int.from_bytes(id_hash.digest(), 'big')  # Uses explicit byteorder for system-agnostic reproducibility
    choice = id_hash_int % num_choices  # Use with small num_choices only
    return choice

>>> id_to_choice(123, 3)
0
>>> id_to_choice(456, 3)
1

注意:

  • 不应使用内置的hash方法,因为它可能会保留输入的分布,例如使用hash(123)。或者,它可能会在 Python 重新启动时返回不同的值,例如使用hash('123')

  • 要将 int 转换为字节,bytes(id_num) 虽然可行,但效率极低,因为它返回一个由 null 字节组成的数组,因此不应使用。使用int.to_bytes更好。使用 str(id_num).encode() 可行,但会浪费一些字节。

  • 诚然,使用取模并不能提供完全均匀的概率,[1][2]但是,对于这个应用程序,这不应该有太大的偏差,因为预计 id_hash_int 很大,而 num_choices 被认为很小。

使用随机数

random 模块可以使用 id_num 作为种子,同时解决了线程安全性和连续性的问题。在此方式中使用 randrange 与哈希种子并取模相比是可比且更简单的方法。

使用这种方法不仅考虑到跨语言的可重复性,还考虑到在未来多个 Python 版本之间的可重复性。因此不建议使用。

import random

def id_to_choice(id_num, num_choices):
    localrandom = random.Random(id_num)
    choice = localrandom.randrange(num_choices)
    return choice

>>> id_to_choice(123, 3)
0
>>> id_to_choice(456, 3)
2

0

另一种选择是加密用户ID。如果保持加密密钥不变,则每个输入数字将加密为不同的输出数字,最多达到您使用的密码块大小。DES使用64位块,可覆盖ID 000000至18446744073709551615。这将为用户ID提供看似随机的替代,保证不会给两个不同的用户ID相同的“随机”数字,因为加密是块值的一对一置换。


0

非常抱歉,我没有Python实现,但是我有一个非常清晰、易读和自证不疑的Java实现,应该很容易转换成Python,只需要最小的努力。以下产生长、可预测、均匀分布的序列,覆盖除零以外的所有范围

XorShift ( http://www.arklyffe.com/main/2010/08/29/xorshift-pseudorandom-number-generator )

public int nextQuickInt(int number) {
    number ^= number << 11;
    number ^= number >>> 7;
    number ^= number << 16;
    return number;
}

public short nextQuickShort(short number) {
    number ^= number << 11;
    number ^= number >>> 5;
    number ^= number << 3;
    return number;
}

public long nextQuickLong(long number) {
    number ^= number << 21;
    number ^= number >>> 35;
    number ^= number << 4;
    return number;
}

或者使用XorShift128Plus(在使用之前需要将state0和state1重新设置为非零值,http://xoroshiro.di.unimi.it/xorshift128plus.c

public class XorShift128Plus {

private long state0, state1; // One of these shouldn't be zero

public long nextLong() {
    long state1 = this.state0;
    long state0 = this.state0 = this.state1;
    state1 ^= state1 << 23;
    return (this.state1 = state1 ^ state0 ^ (state1 >> 18) ^ (state0 >> 5)) + state0;
}

public void reseed(...) {
    this.state0 = ...;
    this.state1 = ...;
}

}

或者 XorOshiro128Plushttp://xoroshiro.di.unimi.it/

public class XorOshiro128Plus {

private long state0, state1; // One of these shouldn't be zero

public long nextLong() {
    long state0 = this.state0;
    long state1 = this.state1;
    long result = state0 + state1;
    state1 ^= state0;
    this.state0 = Long.rotateLeft(state0, 55) ^ state1 ^ (state1 << 14);
    this.state1 = Long.rotateLeft(state1, 36);
    return result;
}

public void reseed() {

}

}

或者 SplitMix64http://xoroshiro.di.unimi.it/splitmix64.c

public class SplitMix64 {

private long state;

public long nextLong() {
    long result = (state += 0x9E3779B97F4A7C15L);
    result = (result ^ (result >> 30)) * 0xBF58476D1CE4E5B9L;
    result = (result ^ (result >> 27)) * 0x94D049BB133111EBL;
    return result ^ (result >> 31);
}

public void reseed() {
    this.state = ...;
}
}

或者使用 XorShift1024Multhttp://xoroshiro.di.unimi.it/xorshift1024star.c)或 Pcg64_32http://www.pcg-random.org/, http://www.pcg-random.org/download.html)。


那么这四个(非Python)PRNG提供了什么比A-B-B的答案更好的东西,以至于OP想要将它们移植过来? - pjs
1
选项先生。这个答案并不比第一个更好,而是提供了一些替代方案。重点甚至不在于这6个具体的选择,而是指向探索和寻找的方向。 - oᴉɹǝɥɔ

-1

最简单的方法是将user_id取模,除以选项数:

choice = user_id % number_of_options

这很容易且快速。但是如果你知道用户ID,你可能会猜测算法。

此外,可以从使用用户常量(例如user_id)作为种子的random中获得伪随机序列:

>>> import random
>>> def generate_random_value(user_id):
...     random.seed(user_id)
...     return random.randint(1, 10000)
...
>>> [generate_random_value(x) for x in range(20)]
[6312, 2202, 927, 3899, 3868, 4186, 9402, 5306, 3715, 7586, 9362, 7412, 7776, 4244, 1751, 3424, 5924, 8553, 2970, 709]
>>> [generate_random_value(x) for x in range(20)]
[6312, 2202, 927, 3899, 3868, 4186, 9402, 5306, 3715, 7586, 9362, 7412, 7776, 4244, 1751, 3424, 5924, 8553, 2970, 709]
>>>

这一点都不随机。 - G. Sliepen

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接