8

I need to assign a default random value in __init__(). For example:

import math
import random
class Test:
    def __init__(self, r = random.randrange(0, math.pow(2,128)-1)):
        self.r = r
        print self.r

If I create 10 instances of Test, they all get the exact same random value. I don't understand why this is happening. I know I can assign the random value inside the __init__(), but I am curious why this is happening. My first guess was that the seed is the current time and objects are being created too close together, and, as a result, get the same random value. I created objects 1 second apart, but still same result.

martineau
  • 119,623
  • 25
  • 170
  • 301
gmemon
  • 2,573
  • 5
  • 32
  • 37

4 Answers4

19

The value of the default parameter is being set at the time the function is created, not when it is called - that's why it's the same every time.

The typical way to deal with this is to set the default parameter to None and test it with an if statement.

import math
import random
class Test:
     def __init__(self, r = None):
             if r is None:
                 r = random.randrange(0, math.pow(2,128)-1)
             self.r = r
             print self.r
Mark Ransom
  • 299,747
  • 42
  • 398
  • 622
5

The random.randrange(0, math.pow(2,128)-1) is evaluated when the function is defined, not when it is called.

Use

class Test:
    def __init__(self, r = None):
        if r is None:
            r = random.randrange(0, math.pow(2,128)-1)
        self.r = r
        print self.r

instead.

RoundTower
  • 899
  • 5
  • 9
1

When you put the expression inside the constructor header (instead of the constructor body) it only gets evaluated once.

Try this in the body:

self.r = random.randrange(0,math.pow(0,128)-1)
Ethan Furman
  • 63,992
  • 20
  • 159
  • 237
bjarneh
  • 608
  • 5
  • 14
  • True when the expression is used to define the default value of the constructor argument, however, generally speaking, other expressions in a constructor will be evaluated every time it's called to initialize an instance. – martineau Jun 24 '11 at 11:41
  • @martineau: What other expressions are you referring to? – Ethan Furman Nov 14 '11 at 23:09
  • @Ethan Furman: Any expressions not used to define the default value of one of the constructor's arguments will be evaluated every time it's called, such as those appearing within the body of the method itself -- such as shown in this answer. – martineau Nov 15 '11 at 19:34
  • @Ethan Furman: A function definition is a compound executable statement and default parameter values are evaluated when the function definition is executed. This is explained in the [Function Definitions](http://docs.python.org/reference/compound_stmts.html#function-definitions) section of the Python documentation. – martineau Nov 16 '11 at 01:19
1

The reason all your class instances have the same value for the argument is because its default value is determined only once, when the method definition it is part of is compiled as part of the class statement's execution.

Although this is not the use it was designed for, you could use Recipe 20.14 titled Automatic Initialization of Instance Attributes from the somewhat dated Python Cookbook, 2nd Edition to achieve what you want to do.

This is it applied to the sample code in your question:

class AutoAttr(object):
    def __init__(self, name, factory, *args, **kwds):
        self.data = name, factory, args, kwds
    def __get__(self, obj, cls=None):
        name, factory, args, kwds = self.data
        setattr(obj, name, factory(*args, **kwds))
        return getattr(obj, name)

import math
import random

class Test(object):
    r = AutoAttr('r', random.randrange, 0, math.pow(2,128)-1)  # default value
    def __init__(self, r=None):
        if r is not None:  # argument value supplied to override default?
            self.r = r
        print format(self.r, ',d')

for i in xrange(5):
    Test()
Test(42)  # override default random initial value

Sample output:

282,608,676,427,101,189,083,121,399,193,871,110,434
211,475,719,281,604,076,410,306,973,803,289,140,631
86,842,148,927,120,143,765,936,219,265,140,532,918
41,767,122,731,332,110,507,836,985,804,081,250,336
97,993,619,669,833,151,963,441,072,354,430,500,011
42

Here's how it works:

The auto_attr class from the recipe is a called a descriptor. One of them is assigned to a Test class attribute named r. The first time you access this attribute in an instance of Test using self.r, Python notices it's bound to a descriptor and calls its __get__() method with the Test instance as an obj argument.

The descriptor's __get__() method calls the associated factory function and assigns the result to an instance attribute with the same name, so that all further references to that attribute through the instance will get its actual value instead of the Test class's auto_attr descriptor.

martineau
  • 119,623
  • 25
  • 170
  • 301