Pythonでのランダムデータの生成(ガイド)

Pythonでのランダムデータの生成(ガイド)

ランダムはどのくらいランダムですか? これは奇妙な質問ですが、情報セキュリティが懸念される場合には非常に重要です。 Pythonでランダムなデータ、文字列、または数値を生成するときはいつでも、少なくともそのデータがどのように生成されたかについて大まかな考えを持っていることをお勧めします。

ここでは、Pythonでランダムデータを生成するためのいくつかの異なるオプションを取り上げ、セキュリティレベル、汎用性、目的、速度の観点からそれぞれを比較する方法を構築します。

このチュートリアルは数学や暗号のレッスンではないことをお約束しますが、そもそも講義の準備は十分ではありません。 必要なだけ数学を学習し、それ以上学習しません。

ランダムはどのようにランダムですか?

まず、顕著な免責事項が必要です。 Pythonで生成されるほとんどのランダムデータは、科学的な意味では完全にランダムではありません。 むしろ、それは pseudorandom です:擬似乱数ジェネレーター(PRNG)で生成されます。これは、基本的には一見ランダムであるが再現可能なデータを生成するための任意のアルゴリズムです。

「真の」乱数は、あなたが推測したとおり、真の乱数ジェネレーター(TRNG)によって生成できます。 1つの例は、床から金型を繰り返し拾い上げ、空中に投げ、それがどのように着地できるようにすることです。

あなたのトスが公平であると仮定すると、あなたはダイスが何番に着くのか本当に全く分かりません。 サイコロを転がすことは、ハードウェアを使用して決定的でない数値を生成する粗い形式です。 (または、https://hackaday.com/2009/05/26/dice-o-matic/[dice-o-matic]にこれを実行させることもできます。)TRNGはこの記事の範囲外ですが、それにもかかわらず、比較のために言及する価値があります。

通常、ハードウェアではなくソフトウェアで実行されるPRNGは、わずかに異なる動作をします。 簡潔な説明は次のとおりです。

_ シードと呼ばれる乱数で始まり、アルゴリズムを使用して、それに基づいてビットの擬似ランダムシーケンスを生成します。 https://bit.ly/2dUfzGb [(ソース)] _

「ドキュメントを読む」ように言われている可能性があります。ある時点で。 まあ、それらの人々は間違っていません。 以下は、見逃したくない `+ random +`モジュールのドキュメントの特に注目すべきスニペットです。

_ 警告:このモジュールの疑似ランダムジェネレーターは、セキュリティ目的で使用しないでください。 https://docs.python.org/3/library/random.html [(ソース)] _

おそらくPythonで + random.seed(999)+、 `+ random.seed(1234)`などを見たことがあるでしょう。 この関数呼び出しは、Pythonの「 random +」モジュールで使用される基になる乱数ジェネレーターをシードしています。 乱数を決定的に生成するための後続の呼び出しは、入力Aが常に出力Bを生成するものです。 この祝福は、悪意を持って使用されると呪いにもなります。

おそらく、「ランダム」と「決定論的」という用語は、互いに隣接して存在することはできないように思われます。 これを明確にするために、 `+ x =(x *3)%19 `を使用して「ランダムな」数値を繰り返し作成する ` random()`の非常に簡潔なバージョンを示します。 ` x +`はもともとシード値として定義されており、そのシードに基づいて確定的な数値シーケンスに変形します。

class NotSoRandom(object):
    def seed(self, a=3):
        """Seed the world's most mysterious random number generator."""
        self.seedval = a
    def random(self):
        """Look, random numbers!"""
        self.seedval = (self.seedval* 3) % 19
        return self.seedval

_inst = NotSoRandom()
seed = _inst.seed
random = _inst.random

この例をあまり文字通りに取り上げないでください。これは主に概念を説明するためのものです。 シード値1234を使用する場合、以降の `+ random()+`の呼び出しシーケンスは常に同一である必要があります。

>>>

>>> seed(1234)
>>> [random() for _ in range(10)]
[16, 10, 11, 14, 4, 12, 17, 13, 1, 3]

>>> seed(1234)
>>> [random() for _ in range(10)]
[16, 10, 11, 14, 4, 12, 17, 13, 1, 3]

これについては、まもなくより深刻な図が表示されます。

「暗号的に安全」とは何ですか

「RNG」の頭字語を十分に理解していない場合は、CSPRNG、または暗号で保護されたPRNGを追加してみましょう。 CSPRNGは、パスワード、オーセンティケーター、トークンなどの機密データの生成に適しています。 ランダムな文字列が与えられると、実際にはMalicious Joeが一連のランダムな文字列でその文字列の前後にどの文字列が来たかを判断する方法はありません。

あなたが見るかもしれないもう一つの用語は*エントロピー*です。 一言で言えば、これは導入または希望されるランダム性の量を指します。 たとえば、ここで説明する1つのPython https://github.com/python/cpython/blob/78392885c9b08021c89649728053d31503d8a509/Lib/secrets.py#L33 [モジュール]は、「+ DEFAULT_ENTROPY = 32+」、バイト数を定義しますデフォルトで戻ります。 開発者は、これを「十分な」バイトで十分な量のノイズとみなします。

注意:このチュートリアルでは、1バイトはデータストレージの他のユニットではなく、1960年代以降のように8ビットを指すと想定しています。 必要に応じて、これをhttps://en.wikipedia.org/wiki/Octet_(computing)[octet]と自由に呼ぶことができます。

CSPRNGの重要な点は、それらがまだ疑似ランダムであることです。 内部的に決定論的な何らかの方法で設計されていますが、他の変数を追加したり、決定論を強制する機能への後退を禁止する「十分にランダム」なプロパティを持っています。

ここでカバーするもの

実際には、これは、統計モデリング、シミュレーション、およびランダムデータの再現性を確保するために、プレーンなPRNGを使用する必要があることを意味します。 また、後で説明するように、それらはCSPRNGよりも大幅に高速です。 データの機密性が不可欠なセキュリティおよび暗号化アプリケーションにCSPRNGを使用します。

このチュートリアルでは、上記のユースケースを拡張することに加えて、PRNGとCSPRNGの両方を使用するためのPythonツールについても掘り下げます。

  • PRNGオプションには、Pythonの標準ライブラリの `+ random `モジュールと、それに対応するNumPyの対応物である ` numpy.random +`が含まれます。

  • Pythonの「+ os 」、「 secrets 」、および「 uuid +」モジュールには、暗号的に安全なオブジェクトを生成するための関数が含まれています。

上記のすべてに触れて、高レベルの比較で締めくくります。

PythonのPRNG

`+ random +`モジュール

おそらく、Pythonでランダムデータを生成するための最も広く知られているツールは、https://github.com/python/cpython/blob/master/Modules/_randommodule.c [Mersenne Twister] PRNGアルゴリズムを使用する `+ random +`モジュールです。コアジェネレーターとして。

前に、 `+ random.seed()`について簡単に触れましたが、今はそれがどのように機能するかを見る良い機会です。 まず、シードなしでランダムデータを作成します。 ` random.random()+`関数は、間隔[0.0、1.0)のランダムな浮動小数点数を返します。 結果は常に右側のエンドポイント(1.0)よりも小さくなります。 これはセミオープン範囲とも呼ばれます:

>>>

>>> # Don't call `random.seed()` yet
>>> import random
>>> random.random()
0.35553263284394376
>>> random.random()
0.6101992345575074

このコードを自分で実行する場合、あなたのマシンに返される数字が異なることは私の命の節約になるに違いありません。 defaultシードしない場合、ジェネレーターは現在のシステム時間または「ランダムなソース」を使用しますOS(使用可能な場合)。

`+ random.seed()`を使用すると、結果を再現可能にすることができ、 ` random.seed()+`の後の一連の呼び出しは同じデータの軌跡を生成します。

>>>

>>> random.seed(444)
>>> random.random()
0.3088946587429545
>>> random.random()
0.01323751590501987

>>> random.seed(444)  # Re-seed
>>> random.random()
0.3088946587429545
>>> random.random()
0.01323751590501987

「乱数」の繰り返しに注意してください。 乱数のシーケンスは決定論的になり、シード値444によって完全に決定されます。

`+ random `の基本的な機能を見てみましょう。 上記では、ランダムなフロートを生成しました。 Pythonでは、 ` random.randint()+`関数を使用して、2つのエンドポイント間にランダムな整数を生成できます。 これは[x、y]間隔全体に及び、両方のエンドポイントが含まれる場合があります。

>>>

>>> random.randint(0, 10)
7
>>> random.randint(500, 50000)
18601

`+ random.randrange()+`を使用すると、間隔の右側を除外できます。つまり、生成される数値は常に[x、y)内にあり、常に正しいエンドポイントよりも小さくなります。

>>>

>>> random.randrange(1, 10)
5

特定の[x、y]間隔内にあるランダムなフロートを生成する必要がある場合は、https://en.wikipedia.org/wiki/continuous_uniform_distribution [連続均一分布]:

>>>

>>> random.uniform(20, 30)
27.42639687016509
>>> random.uniform(30, 40)
36.33865802745107

空でないシーケンス(リストやタプルなど)からランダムな要素を選択するには、 `+ random.choice()`を使用できます。 置換からシーケンスから複数の要素を選択するための ` random.choices()+`もあります(複製が可能です):

>>> items = ['one', 'two', 'three', 'four', 'five']
>>> random.choice(items)
'four'

>>> random.choices(items, k=2)
['three', 'three']
>>> random.choices(items, k=3)
['three', 'five', 'four']

置換せずにサンプリングを模倣するには、 `+ random.sample()+`を使用します。

>>>

>>> random.sample(items, 4)
['one', 'five', 'four', 'three']

`+ random.shuffle()+`を使用して、その場でシーケンスをランダム化できます。 これにより、シーケンスオブジェクトが変更され、要素の順序がランダムになります。

>>>

>>> random.shuffle(items)
>>> items
['four', 'three', 'two', 'one', 'five']

元のリストを変更したくない場合は、https://realpython.com/copying-python-objects/[最初にコピーを作成]してからコピーをシャッフルする必要があります。 Pythonリストのコピーは、https://docs.python.org/library/copy.html [+ copy +]モジュール、または単に + x [:] +`または `+ x.copy()で作成できます+ `、ここで + x + `はリストです。

NumPyを使用してランダムデータを生成する前に、もう少し複雑なアプリケーションを見てみましょう。一定の長さの一意のランダム文字列のシーケンスを生成します。

最初に関数の設計について考えると役立ちます。 文字、数字、句読点などの文字の「プール」から選択し、これらを1つの文字列に結合してから、この文字列がまだ生成されていないことを確認する必要があります。 Pythonの `+ set +`は、このタイプのメンバーシップテストに適しています。

import string

def unique_strings(k: int, ntokens: int,
               pool: str=string.ascii_letters) -> set:
    """Generate a set of unique string tokens.

    k: Length of each token
    ntokens: Number of tokens
    pool: Iterable of characters to choose from

    For a highly optimized version:
    https://stackoverflow.com/a/48421303/7954504
    """

    seen = set()

    # An optimization for tightly-bound loops:
    # Bind these methods outside of a loop
    join = ''.join
    add = seen.add

    while len(seen) < ntokens:
        token = join(random.choices(pool, k=k))
        add(token)
    return seen

`+ ''。join()`は、 ` random.choices()`の文字を、長さ ` k `の単一のPython ` str `に結合します。 このトークンはセットに追加され、重複を含めることはできません。セットに指定した数の要素が含まれるまで、「 while +」ループが実行されます。

リソース:Pythonのhttps://docs.python.org/3/library/string.html [+ string +]モジュールには、多くの便利な定数が含まれています: + ascii_lowercase ++ ascii_uppercase ++ string。句読点+ `、 + ascii_whitespace + `、およびその他の少数。

この機能を試してみましょう。

>>>

>>> unique_strings(k=4, ntokens=5)
{'AsMk', 'Cvmi', 'GIxv', 'HGsZ', 'eurU'}

>>> unique_strings(5, 4, string.printable)
{"'O*1!", '9Ien%', 'W=m7<', 'mUD|z'}

この関数の微調整バージョンについては、https://stackoverflow.com/a/48421303/7954504 [このStack Overflow answer]は、ジェネレーター関数、名前バインディング、およびその他の高度なトリックを使用して、上記の + unique_strings()+

配列のPRNG: + numpy.random +

お気づきかもしれませんが、 + random +`の大部分の関数はスカラー値(単一の `+ int ++ float +、またはその他のオブジェクト)を返します。 乱数のシーケンスを生成したい場合、それを実現する1つの方法は、Pythonリストの内包表記を使用することです。

>>>

>>> [random.random() for _ in range(5)]
[0.021655420657909374,
 0.4031628347066195,
 0.6609991871223335,
 0.5854998250783767,
 0.42886606317322706]

しかし、このために特別に設計された別のオプションがあります。 NumPy独自のhttps://docs.scipy.org/doc/numpy/reference/routines.random.html [+ numpy.random +]パッケージは、標準ライブラリの `+ random +`のようなものですが、https ://realpython.com/numpy-array-programming/[NumPy配列]。 (さらに多くの統計分布から引き出す機能も搭載されています。)

`+ numpy.random `は、単純な古い ` random `とは別の独自のPRNGを使用することに注意してください。 Python独自の ` random.seed()+`を呼び出して、決定論的にランダムなNumPy配列を生成することはありません。

>>>

>>> import numpy as np
>>> np.random.seed(444)
>>> np.set_printoptions(precision=2)  # Output decimal fmt.

苦労せずに、食欲をそそるいくつかの例を次に示します。

>>>

>>> # Return samples from the standard normal distribution
>>> np.random.randn(5)
array([ 0.36,  0.38,  1.38,  1.18, -0.94])

>>> np.random.randn(3, 4)
array([[p` is the probability of choosing each element
>>> np.random.choice([0, 1], p=[0.6, 0.4], size=(5, 4))
array([[In the syntax for `+randn(d0, d1, ..., dn)+`, the parameters `+d0, d1, ..., dn+` are optional and indicate the shape of the final object. Here, `+np.random.randn(3, 4)+` creates a 2d array with 3 rows and 4 columns. The data will be https://en.wikipedia.org/wiki/Independent_and_identically_distributed_random_variables[i.i.d.], meaning that each data point is drawn independent of the others.

Another common operation is to create a sequence of random Boolean values, `+True+` or `+False+`. One way to do this would be with `+np.random.choice([True, False])+`. However, it’s actually about 4x faster to choose from `+(0, 1)+` and then https://docs.scipy.org/doc/numpy/reference/generated/numpy.ndarray.view.html[view-cast] these integers to their corresponding Boolean values:

[.repl-toggle]#>>>#

[source,python,repl]

>>>#NumPyの `randint`は[random、randint()`とは異なり[包括的、排他的] >>> np.random.randint(0、2、size = 25、dtype = np.uint8).view(bool )array([True、False、True、True、False、True、False、False、False、False、False、True、True、False、False、False、True、False、True、False、True、True、True、真偽])

What about generating correlated data? Let’s say you want to simulate two correlated time series. One way of going about this is with NumPy’s https://docs.scipy.org/doc/numpy/reference/generated/numpy.random.multivariate_normal.html#numpy.random.multivariate_normal[`+multivariate_normal()+`] function, which takes a covariance matrix into account. In other words, to draw from a single normally distributed random variable, you need to specify its mean and variance (or standard deviation).

To sample from the https://en.wikipedia.org/wiki/Multivariate_normal[multivariate normal] distribution, you specify the means and covariance matrix, and you end up with multiple, correlated series of data that are each approximately normally distributed.

However, rather than covariance, https://en.wikipedia.org/wiki/correlation[correlation] is a measure that is more familiar and intuitive to most. It’s the covariance normalized by the product of standard deviations, and so you can also define covariance in terms of correlation and standard deviation:

https://files.realpython.com/media/scalar_equation.2ef9746c8834.jpg[image:https://files.realpython.com/media/scalar_equation.2ef9746c8834.jpg[Covariance in Scalar Form,width=620,height=88]]

So, could you draw random samples from a multivariate normal distribution by specifying a correlation matrix and standard deviations? Yes, but you’ll need to get the above https://blogs.sas.com/content/iml/2010/12/10/converting-between-correlation-and-covariance-matrices.html[into matrix form] first. Here, *_S_ *is a vector of the standard deviations,* _P_ *is their correlation matrix, and* _C_* is the resulting (square) covariance matrix:

https://files.realpython.com/media/matrix_equation.d70f9fd73960.jpg[image:https://files.realpython.com/media/matrix_equation.d70f9fd73960.jpg[Covariance in Matrix Form,width=610,height=78]]

This can be expressed in NumPy as follows:

[source,python]

def corr2cov(p:np.ndarray、s:np.ndarray)→ np.ndarray: "" "相関と標準偏差からの共分散行列" "" d = np.diag(s)return d @ p @ d

Now, you can generate two time series that are correlated but still random:

[.repl-toggle]#>>>#

[source,python,repl]

>>>#相関行列と標準偏差から始めます。 >>>#-0.40はAとBの間の相関であり、変数とそれ自身との相関>>>#は1.0です。 >>> corr = np.array([[AとBの標準偏差/平均、それぞれ>>> stdev = np.array([6。、1.])>>> mean = np.array([2。 、0.5])>>> cov = corr2cov(corr、stdev)

>>># `size`は2次元データの時系列の長さ>>>#(500か月、日など)です。 >>> data = np.random.multivariate_normal(mean = mean、cov = cov、size = 500)>>> data [:10] array([[data.shape(500、2)

You can think of `+data+` as 500 pairs of inversely correlated data points. Here’s a sanity check that you can back into the original inputs, which approximate `+corr+`, `+stdev+`, and `+mean+` from above:

[.repl-toggle]#>>>#

[source,python,repl]

>>> np.corrcoef(data、rowvar = False)array([[data.std(axis = 0)array([5.96、1.01])

>>> data.mean(axis = 0)array([2.13、0.49])

Before we move on to CSPRNGs, it might be helpful to summarize some `+random+` functions and their `+numpy.random+` counterparts:

[cols=",,",options="header",]
|===
|Python `+random+` Module |NumPy Counterpart |Use
|`+random()+` |https://docs.scipy.org/doc/numpy/reference/generated/numpy.random.rand.html#numpy.random.rand[`+rand()+`] |Random float in [0.0, 1.0)
|`+randint(a, b)+` |https://docs.scipy.org/doc/numpy/reference/generated/numpy.random.random_integers.html#numpy.random.random_integers[`+random_integers()+`] |Random integer in [a, b]
|`+randrange(a, b[, step])+` |https://docs.scipy.org/doc/numpy/reference/generated/numpy.random.randint.html#numpy.random.randint[`+randint()+`] |Random integer in [a, b)
|`+uniform(a, b)+` |https://docs.scipy.org/doc/numpy/reference/generated/numpy.random.uniform.html#numpy.random.uniform[`+uniform()+`] |Random float in [a, b]
|`+choice(seq)+` |https://docs.scipy.org/doc/numpy/reference/generated/numpy.random.choice.html#numpy.random.choice[`+choice()+`] |Random element from `+seq+`
|`+choices(seq, k=1)+` |https://docs.scipy.org/doc/numpy/reference/generated/numpy.random.choice.html#numpy.random.choice[`+choice()+`] |Random `+k+` elements from `+seq+` with replacement
|`+sample(population, k)+` |https://docs.scipy.org/doc/numpy/reference/generated/numpy.random.choice.html#numpy.random.choice[`+choice()+`] with `+replace=False+` |Random `+k+` elements from `+seq+` without replacement
|`+shuffle(x[, random])+` |https://docs.scipy.org/doc/numpy/reference/generated/numpy.random.shuffle.html#numpy.random.shuffle[`+shuffle()+`] |Shuffle the sequence `+x+` in place
|`+normalvariate(mu, sigma)+` or `+gauss(mu, sigma)+` |https://docs.scipy.org/doc/numpy/reference/generated/numpy.random.normal.html#numpy.random.normal[`+normal()+`] |Sample from a normal distribution with mean `+mu+` and standard deviation `+sigma+`
|===

*Note*: NumPy is specialized for building and manipulating large, multidimensional arrays. If you just need a single value, `+random+` will suffice and will probably be faster as well. For small sequences, `+random+` may even be faster too, because NumPy does come with some overhead.

Now that you’ve covered two fundamental options for PRNGs, let’s move onto a few more secure adaptations.

=== CSPRNGs in Python

[[osurandom-about-as-random-as-it-gets]]
==== `+os.urandom()+`: About as Random as It Gets

Python’s https://docs.python.org/library/os.html#os.urandom[`+os.urandom()+`] function is used by both https://github.com/python/cpython/blob/b225cb770fb17596298f5a05c41a7c90c470c4f8/Lib/secrets.py#L47[`+secrets+`] and https://github.com/python/cpython/blob/b225cb770fb17596298f5a05c41a7c90c470c4f8/Lib/uuid.py#L621[`+uuid+`] (both of which you’ll see here in a moment). Without getting into too much detail, `+os.urandom()+` generates operating-system-dependent random bytes that can safely be called cryptographically secure:

* On Unix operating systems, it reads random bytes from the special file `+/dev/urandom+`, which in turn “allow access to environmental noise collected from device drivers and other sources.” (Thank you, https://en.wikipedia.org/wiki//dev/random[Wikipedia].) This is garbled information that is particular to your hardware and system state at an instance in time but at the same time sufficiently random.
 *On Windows, the C++ function https://msdn.microsoft.com/en-us/library/windows/desktop/aa379942(v=vs.85).aspx[`+CryptGenRandom()+`] is used. This function is still technically pseudorandom, but it works by generating a seed value from variables such as the process ID, memory status, and so on.

With `+os.urandom()+`, there is no concept of manually seeding. While still technically pseudorandom, this function better aligns with how we think of randomness. The only argument is the number of https://docs.python.org/library/stdtypes.html#bytes[bytes] to return:

[.repl-toggle]#>>>#

[source,python,repl]

>>> os.urandom(3)b '\ xa2 \ xe8 \ x02'

>>> x = os.urandom(6)>>> x b '\ xce \ x11 \ xe7 "!\ x84'

>>> type(x)、len(x)(バイト、6)

Before we go any further, this might be a good time to delve into a mini-lesson on https://docs.python.org/howto/unicode.html[character encoding]. Many people, including myself, have some type of allergic reaction when they see `+bytes+` objects and a long line of `+\x+` characters. However, it’s useful to know how sequences such as `+x+` above eventually get turned into strings or numbers.

`+os.urandom()+` returns a sequence of single bytes:

[.repl-toggle]#>>>#

[source,python,repl]

>>> x b '\ xce \ x11 \ xe7 "!\ x84'

But how does this eventually get turned into a Python `+str+` or sequence of numbers?

First, recall one of the fundamental concepts of computing, which is that a byte is made up of 8 bits. You can think of a bit as a single digit that is either 0 or 1. A byte effectively chooses between 0 and 1 eight times, so both `+01101100+` and `+11110000+` could represent bytes. Try this, which makes use of Python https://realpython.com/python-f-strings/[f-strings] introduced in Python 3.6, in your interpreter:

[.repl-toggle]#>>>#

[source,python,repl]

>>> binary = [f '{i:0> 8b}' for i in range(256)] >>> binary [:16] ['00000000'、 '00000001'、 '00000010'、 '00000011'、 ' 00000100」、「00000101」、「00000110」、「00000111」、「00001000」、「00001001」、「00001010」、「00001011」、「00001100」、「00001101」、「00001110」、「00001111」]

This is equivalent to `+[bin(i) for i in range(256)]+`, with some special formatting. https://docs.python.org/3/library/functions.html#bin[`+bin()+`] converts an integer to its binary representation as a string.

Where does that leave us? Using `+range(256)+` above is not a random choice. (No pun intended.) Given that we are allowed 8 bits, each with 2 choices, there are `+2* * 8 == 256+` possible bytes “combinations.”

This means that each byte maps to an integer between 0 and 255. In other words, we would need more than 8 bits to express the integer 256. You can verify this by checking that `+len(f'{256:0>8b}')+` is now 9, not 8.

Okay, now let’s get back to the `+bytes+` data type that you saw above, by constructing a sequence of the bytes that correspond to integers 0 through 255:

[.repl-toggle]#>>>#

[source,python,repl]

>>>バイト=バイト(範囲(256))

If you call `+list(bites)+`, you’ll get back to a Python list that runs from 0 to 255. But if you just print `+bites+`, you get an ugly looking sequence littered with backslashes:

[.repl-toggle]#>>>#

[source,python,repl]

>>>バイトb '\ x00 \ x01 \ x02 \ x03 \ x04 \ x05 \ x06 \ x07 \ x08 \ t \ n \ x0b \ x0c \ r \ x0e \ x0f \ x10 \ x11 \ x12 \ x13 \ x14 \ x15 '' \ x16 \ x17 \ x18 \ x19 \ x1a \ x1b \ x1c \ x1d \ x1e \ x1f! "#$%&\ '()* +、-。/0123456789:; <⇒?@ ABCDEFGHIJK' 'LMNOPQRSTUVWXYZ [\\] ^ _ `abcdefghijklmnopqrstuvwxyz {|}〜\ x7f \ x80 \ x81 \ x82 \ x83 \ x84 \ x85 \ x86 '' \ x87 \ x88 \ x89 \ x8a \ x8b \ x8c \ x8d \ x8e \ x8f \ x90 \ x91 \ x92 \ x93 \ x94 \ x95 \ x96 \ x97 \ x98 \ x99 \ x9a \ x9b '

#…​

These backslashes are escape sequences, and `+\xhh+` https://docs.python.org/3/reference/lexical_analysis.html#string-and-bytes-literals[represents] the character with hex value `+hh+`. Some of the elements of `+bites+` are displayed literally (printable characters such as letters, numbers, and punctuation). Most are expressed with escapes. `+\x08+` represents a keyboard’s backspace, while `+\x13+` is a https://en.wikipedia.org/wiki/Carriage_return[carriage return] (part of a new line, on Windows systems).

If you need a refresher on hexadecimal, Charles Petzold’s https://realpython.com/asins/0735611319/[_Code: The Hidden Language_] is a great place for that. Hex is a base-16 numbering system that, instead of using 0 through 9, uses 0 through 9 and _a_ through _f_ as its basic digits.

Finally, let’s get back to where you started, with the sequence of random bytes `+x+`. Hopefully this makes a little more sense now. Calling `+.hex()+` on a `+bytes+` object gives a `+str+` of hexadecimal numbers, with each corresponding to a decimal number from 0 through 255:

[.repl-toggle]#>>>#

[source,python,repl]

>>> x b '\ xce \ x11 \ xe7 "!\ x84'

>>> list(x)

>>> x.hex() 'ce11e7222184'

>>> len(x.hex())12

One last question: how is `+b.hex()+` 12 characters long above, even though `+x+` is only 6 bytes? This is because two hexadecimal digits correspond precisely to a single byte. The `+str+` version of `+bytes+` will always be twice as long as far as our eyes are concerned.

Even if the byte (such as `+\x01+`) does not need a full 8 bits to be represented, `+b.hex()+` will always use two hex digits per byte, so the number 1 will be represented as `+01+` rather than just `+1+`. Mathematically, though, both of these are the same size.

*Technical Detail*: What you’ve mainly dissected here is how a `+bytes+` object becomes a Python `+str+`. One other technicality is how `+bytes+` produced by `+os.urandom()+` get converted to a `+float+` in the interval [0.0, 1.0), as in the https://github.com/python/cpython/blob/c6040638aa1537709add895d24cdbbb9ee310fde/Lib/random.py#L676[cryptographically secure version] of `+random.random()+`. If you’re interested in exploring this further, https://github.com/realpython/materials/blob/master/random-data/bytes_to_int.py[this code snippet] demonstrates how `+int.from_bytes()+` makes the initial conversion to an integer, using a base-256 numbering system.

With that under your belt, let’s touch on a recently introduced module, `+secrets+`, which makes generating secure tokens much more user-friendly.

==== Python’s Best Kept `+secrets+`

Introduced in Python 3.6 by https://www.python.org/dev/peps/pep-0506/[one of the more colorful PEPs] out there, the `+secrets+` module is intended to be the de facto Python module for generating cryptographically secure random bytes and strings.

You can check out the https://github.com/python/cpython/blob/3.6/Lib/secrets.py[source code] for the module, which is short and sweet at about 25 lines of code. `+secrets+` is basically a wrapper around `+os.urandom()+`. It exports just a handful of functions for generating random numbers, bytes, and strings. Most of these examples should be fairly self-explanatory:

[.repl-toggle]#>>>#

[source,python,repl]

>>> n = 16

>>>#セキュアトークンを生成>>> secrets.token_bytes(n)b’A \ x8cz \ xe1o \ xf9!; \ x8b \ xf2 \ x80pJ \ x8b \ xd4 \ xd3 '>>> secrets.token_hex(n)' 9cb190491e01230ec4239cae643f286f '>>> secrets.token_urlsafe(n)' MJoi7CknFu3YN41m88SEgQ '

>>>#ランダムバージョンの random.choice() >>> secrets.choice( 'rain') 'a'

Now, how about a concrete example? You’ve probably used URL shortener services like https://tinyurl.com[tinyurl.com] or https://bit.ly[bit.ly] that turn an unwieldy URL into something like https://bit.ly/2IcCp9u. Most shorteners don’t do any complicated hashing from input to output; they just generate a random string, make sure that string has not already been generated previously, and then tie that back to the input URL.

Let’s say that after taking a look at the https://www.iana.org/domains/root/db[Root Zone Database], you’ve registered the site *short.ly*. Here’s a function to get you started with your service:

[source,python]

#shortly.py

秘密からインポートtoken_urlsafe

データベース= {}

def短縮(url:str、nbytes:int = 5)→ str:ext = token_urlsafe(nbytes = nbytes)if ext in DATABASE:return short(url、nbytes = nbytes)else:DATABASE.update({ext:url} )f’short.ly/{ext}を返します

Is this a full-fledged real illustration? No. I would wager that bit.ly does things in a slightly more advanced way than storing its gold mine in a global Python dictionary that is not persistent between sessions. However, it’s roughly accurate conceptually:

[.repl-toggle]#>>>#

[source,python,repl]

>>> urls =( . 「https://realpython.com/」、 . 「https://docs.python.org/3/howto/regex.html」 . )

>>> URLのuの場合: . print(shorten(u)) short.ly/p_Z4fLI short.ly/fuxSyNY

>>>データベース{'p_Z4fLI': 'https://realpython.com/'、 'fuxSyNY': 'https://docs.python.org/3/howto/regex.html'}

*Hold On: *One thing you may notice is that both of these results are of length 7 when you requested 5 bytes. _Wait, I thought that you said the result would be twice as long?_ Well, not exactly, in this case. There is one more thing going on here: `+token_urlsafe()+` uses base64 encoding, where each character is 6 bits of data. (It’s 0 through 63, and corresponding characters. The characters are A-Z, a-z, 0-9, and +/.)

If you originally specify a certain number of bytes `+nbytes+`, the resulting length from `+secrets.token_urlsafe(nbytes)+` will be `+math.ceil(nbytes* 8/6)+`, which you can https://github.com/realpython/materials/blob/master/random-data/urlsafe.py[prove] and investigate further if you’re curious.

The bottom line here is that, while `+secrets+` is really just a wrapper around existing Python functions, it can be your go-to when security is your foremost concern.

=== One Last Candidate: `+uuid+`

One last option for generating a random token is the `+uuid4()+` function from Python’s https://docs.python.org/library/uuid.html[`+uuid+`] module. A https://tools.ietf.org/html/rfc4122.html[UUID] is a Universally Unique IDentifier, a 128-bit sequence (`+str+` of length 32) designed to “guarantee uniqueness across space and time.” `+uuid4()+` is one of the module’s most useful functions, and this function https://github.com/python/cpython/blob/78392885c9b08021c89649728053d31503d8a509/Lib/uuid.py#L623[also uses `+os.urandom()+`]:

[.repl-toggle]#>>>#

[source,python,repl]

>>>インポートuuid

>>> uuid.uuid4()UUID( '3e3ef28d-3ff0-4933-9bba-e5ee91ce0e7b')>>> uuid.uuid4()UUID( '2e115fcb-5761-4fa1-8287-19f4ee2877ac')

The nice thing is that all of `+uuid+`’s functions produce an instance of the `+UUID+` class, which encapsulates the ID and has properties like `+.int+`, `+.bytes+`, and `+.hex+`:

[.repl-toggle]#>>>#

[source,python,repl]

>>> tok = uuid.uuid4()>>> tok.bytes b '。\ xb7 \ x80 \ xfd \ xbfIG \ xb3 \ xae \ x1d \ xe3 \ x97 \ xee \ xc5 \ xd5 \ x81'

>>> len(tok.bytes)16 >>> len(tok.bytes)* 8#ビット128

>>> tok.hex '2eb780fdbf4947b3ae1de397eec5d581' >>> tok.int 62097294383572614195530565389543396737

You may also have seen some other variations: `+uuid1()+`, `+uuid3()+`, and `+uuid5()+`. The key difference between these and `+uuid4()+` is that those three functions all take some form of input and therefore don’t meet the definition of “random” to the extent that a Version 4 UUID does:

* `+uuid1()+` uses your machine’s host ID and current time by default. Because of the reliance on current time down to nanosecond resolution, this version is where UUID derives the claim “guaranteed uniqueness across time.”
 *`+uuid3()+` and `+uuid5()+` both take a namespace identifier and a name. The former uses an https://en.wikipedia.org/wiki/MD5[MD5] hash and the latter uses SHA-1.

`+uuid4()+`, conversely, is entirely pseudorandom (or random). It consists of getting 16 bytes via `+os.urandom()+`, converting this to a https://en.wikipedia.org/wiki/Endianness[big-endian] integer, and doing a number of bitwise operations to comply with the https://tools.ietf.org/html/rfc4122.html#section-4.1.1[formal specification].

Hopefully, by now you have a good idea of the distinction between different “types” of random data and how to create them. However, one other issue that might come to mind is that of collisions.

In this case, a collision would simply refer to generating two matching UUIDs. What is the chance of that? Well, it is technically not zero, but perhaps it is close enough: there are `+2* * 128+` or 340 https://en.wikipedia.org/wiki/Names_of_large_numbers[undecillion] possible `+uuid4+` values. So, I’ll leave it up to you to judge whether this is enough of a guarantee to sleep well.

One common use of `+uuid+` is in Django, which has a https://docs.djangoproject.com/en/2.0/ref/models/fields/#uuidfield[`+UUIDField+`] that is often used as a primary key in a model’s underlying relational database.

=== Why Not Just “Default to” `+SystemRandom+`?

In addition to the secure modules discussed here such as `+secrets+`, Python’s `+random+` module actually has a little-used class called https://github.com/python/cpython/blob/b225cb770fb17596298f5a05c41a7c90c470c4f8/Lib/random.py#L666[`+SystemRandom+`] that uses `+os.urandom()+`. (`+SystemRandom+`, in turn, is also used by `+secrets+`. It’s all a bit of a web that traces back to `+urandom()+`.)

At this point, you might be asking yourself why you wouldn’t just “default to” this version? Why not “always be safe” rather than defaulting to the deterministic `+random+` functions that aren’t cryptographically secure ?

I’ve already mentioned one reason: sometimes you want your data to be deterministic and reproducible for others to follow along with.

But the second reason is that CSPRNGs, at least in Python, tend to be meaningfully slower than PRNGs. Let’s test that with a script, https://github.com/realpython/materials/blob/master/random-data/timed.py[`+timed.py+`], that compares the PRNG and CSPRNG versions of `+randint()+` using Python’s `+timeit.repeat()+`:

[source,python]

#timed.py

ランダムインポートtimeitをインポート

#「デフォルト」ランダムは実際には `random.Random()`のインスタンスです。 #CSPRNGバージョンは、 `SystemRandom()`と `os.urandom()`を順番に使用します。 _sysrand = random.SystemRandom()

def prng()→なし:random.randint(0、95)

def csprng()→なし:_sysrand.randint(0、95)

setup = 'ランダムにインポート; main import prng、csprng 'から

if name == 'main':print( 'トライアルごとに1,000,000ループのベストオブ3トライアル:')

for for( 'prng()'、 'csprng()'):best = min(timeit.repeat(f、setup = setup))print( '\ t {:8s} {:0.2f}秒の合計時間。 '.format(f、best))

Now to execute this from the shell:

[source,sh]

$ python3 ./timed.py試行ごとに1,000,000ループの3つの試行のベスト:prng()合計時間1.07秒。 csprng()6.20秒の合計時間。

A 5x timing difference is certainly a valid consideration in addition to cryptographic security when choosing between the two.

=== Odds and Ends: Hashing

One concept that hasn’t received much attention in this tutorial is that of https://en.wikipedia.org/wiki/Cryptographic_hash_function[hashing], which can be done with Python’s https://docs.python.org/3/library/hashlib.html[`+hashlib+`] module.

A hash is designed to be a one-way mapping from an input value to a fixed-size string that is virtually impossible to reverse engineer. As such, while the result of a hash function may “look like” random data, it doesn’t really qualify under the definition here.

=== Recap

You’ve covered a lot of ground in this tutorial. To recap, here is a high-level comparison of the options available to you for engineering randomness in Python:

[cols=",,",options="header",]
|===
|Package/Module |Description |Cryptographically Secure
|https://docs.python.org/library/random.html[`+random+`] |Fasty & easy random data using Mersenne Twister |No
|https://docs.scipy.org/doc/numpy/reference/routines.random.html[`+numpy.random+`] |Like `+random+` but for (possibly multidimensional) arrays |No
|https://docs.python.org/library/os.html[`+os+`] |Contains `+urandom()+`, the base of other functions covered here |Yes
|https://docs.python.org/library/secrets.html[`+secrets+`] |Designed to be Python’s de facto module for generating secure random numbers, bytes, and strings |Yes
|https://docs.python.org/library/uuid.html[`+uuid+`] |Home to a handful of functions for building 128-bit identifiers |Yes, `+uuid4()+`
|===

Feel free to leave some totally random comments below, and thanks for reading.

=== Additional Links

* https://www.random.org/[Random.org] offers “true random numbers to anyone on the Internet” derived from atmospheric noise.
* The https://docs.python.org/3.6/library/random.html#examples-and-recipes[Recipes] section from the `+random+` module has some additional tricks.
* The seminal paper on the http://www.math.sci.hiroshima-u.ac.jp/%7Em-mat/MT/ARTICLES/mt.pdf[Mersienne Twister] appeared in 1997, if you’re into that kind of thing.
* The https://docs.python.org/3.6/library/itertools.html#itertools-recipes[Itertools Recipes] define functions for choosing randomly from a combinatoric set, such as from combinations or permutations.
* http://scikit-learn.org/stable/datasets/index.html#sample-generators[Scikit-Learn] includes various random sample generators that can be used to build artificial datasets of controlled size and complexity.
* Eli Bendersky digs into `+random.randint()+` in his article https://eli.thegreenplace.net/2018/slow-and-fast-methods-for-generating-random-integers-in-python/#id2[Slow and Fast Methods for Generating Random Integers in Python].
* Peter Norvig’s a http://nbviewer.jupyter.org/url/norvig.com/ipython/Probability.ipynb[Concrete Introduction to Probability using Python] is a comprehensive resource as well.
* The Pandas library includes a https://github.com/pandas-dev/pandas/blob/f9cc39fb1391cb05f55232367f6547ff9ea615b8/pandas/util/testing.py#L2513[context manager] that can be used to set a temporary random state.
* From Stack Overflow:
** https://stackoverflow.com/q/50559078/7954504[Generating Random Dates In a Given Range]
** https://stackoverflow.com/q/48421142/7954504[Fastest Way to Generate a Random-like Unique String with Random Length]
** https://stackoverflow.com/q/21187131/7954504[How to Use `+random.shuffle()+` on a Generator]
** https://stackoverflow.com/q/31389481/7954504[Replace Random Elements in a NumPy Array]
** https://stackoverflow.com/q/14720799/7954504[Getting Numbers from/dev/random in Python]