constructing an object from another instance of the same class

C

Christoph Groth

Dear all,

sometimes it is handy to have a function which can take as argument
anything which can be converted into something, e.g.

def foo(arg):
arg = float(arg)
# ...

I would like to mimic this behavior of float for a user-defined type,
e.g.

def bar(arg):
arg = My_type(arg)
# ...

Now I wonder what is the most pythonic way to write the __init__ method
of My_type? The following comes to my mind:

class My_type:
def __init__(self, other):
if isinstance(other, type(self)):
self.a = other.a
self.b = other.b
return
# initialize self in some other way

It seems to me that in this way I might get problems when I pass an
instance of Derived_from_my_type to bar, as it will become an instance
of My_type.

What is a good way to express this? In C++ (which I know better than
python) I would make bar accept a const reference to My_type. Then I
could use it directly with instances of My_type, Derived_from_my_type
and other types which can be converted into My_type.

thanks
Christoph
 
B

Bruno Desthuilliers

Christoph Groth a écrit :
Dear all,

sometimes it is handy to have a function which can take as argument
anything which can be converted into something, e.g.

def foo(arg):
arg = float(arg)
# ...

I would like to mimic this behavior of float for a user-defined type,
e.g.

def bar(arg):
arg = My_type(arg)
# ...

Now I wonder what is the most pythonic way to write the __init__ method
of My_type? The following comes to my mind:

class My_type:
def __init__(self, other):
if isinstance(other, type(self)):
self.a = other.a
self.b = other.b
return
# initialize self in some other way

It seems to me that in this way I might get problems when I pass an
instance of Derived_from_my_type to bar, as it will become an instance
of My_type.

The instance you pass to bar won't "become" anything else. You create a
new My_type instance from the Derived_from_my_type one's values, and
rebinding the _local_ name 'arg' only affects the local namespace.

<OT>
BTW, the convention in Python is to use TitleCase for class names
(except - for historical reasons - for most builtin types which are
lowercase).
What is a good way to express this?

Depends on what are the possible initializers / arguments for My_type.
There are a few possible solutions but which one is best depends on the
concrete case and personal tastes.

Forget about C++ - Python is a different beast !-)
(which I know better than
python) I would make bar accept a const reference to My_type. Then I
could use it directly with instances of My_type, Derived_from_my_type
and other types which can be converted into My_type.

If you only worry about being able to use any "My_type like" object -
that is, any object implementing a given subset of My_type's interface,
then just document your expectations in the function's docstring and use
whatever object is passed in as if it was a My_type instance. Period. As
long as you document what your function expects, it's the caller's
responsaibility to make sure it provides something compatible. If he
don't, well he'll get a nice traceback.

I know this might look very freestyle and a bit frightening when coming
from one of these B&D languages, but that's really the Python way, and
from experience, it JustWork(tm).
 
C

Christoph Groth

Bruno Desthuilliers said:
The instance you pass to bar won't "become" anything else. You create
a new My_type instance from the Derived_from_my_type one's values, and
rebinding the _local_ name 'arg' only affects the local namespace.

I understand that it won't become an instance of My_type globally. But
it will become locally and this breaks polymorphism. (See code example
I provide at the end)
Forget about C++ - Python is a different beast !-)

Still, it is useful and interesting to compare languages. Like in the
real world it is also insightful to compare the grammar of quite
different languages.
If you only worry about being able to use any "My_type like" object -
that is, any object implementing a given subset of My_type's
interface, then just document your expectations in the function's
docstring and use whatever object is passed in as if it was a My_type
instance. Period. As long as you document what your function expects,
it's the caller's responsaibility to make sure it provides something
compatible. If he don't, well he'll get a nice traceback.

This is not what I am worrying about. I will try to be more explicit.

I would like to have a class for a "special matrix". This is an
ordinary 2n by 2n matrix which can be thought of as consisting of four n
by n sized blocks.

At this moment, I just use normal numpy arrays (or anything which
behaves like them). But I have very good reasons to actually have a
class for these special matrices. Still, I would like to be able to
call functions which work with "special matrices" with plain numpy
arrays as arguments. In that case, the arguments which are plain
matrices should be converted to "special" ones such that the main part
of the function can assume to always work with "special matrices".

The code attached in the end (which is a complete runnable script)
should illustrate what I mean.

This example works as I want except that when bar is called with a an
argument of type Derived, it behaves as Base. Also, I am not sure
whether there is a nicer way to achieve the following functionality for
Base.__init__:

If other is of type Base already, just "pass it on". Otherwise,
construct an instance of Base from it.

****************************************************************
import numpy as np

class Base:
def __init__(self, other):
if isinstance(other, type(self)):
self = other
return
n = other.shape[0]
assert n == other.shape[1]
assert n % 2 == 0
n //= 2
self.a = other[0 : n, 0 : n]
self.b = other[n : 2*n, 0 : n]
self.c = other[0 : n, n : 2*n]
self.d = other[n : 2*n, n : 2*n]

def hello(self):
print 'hello from Base'

class Derived(Base):
def hello(self):
print 'hello from Derived'

def bar(arg):
arg = Base(arg)
# Do something useful with arg.{a..d}
arg.hello()

# This works.
a = np.identity(4)
bar(a)

# And this also.
a = Base(np.identity(4))
bar(a)

# But this prints "hello from Base"!
a = Derived(np.identity(4))
bar(a)
 
B

Bruno Desthuilliers

Christoph Groth a écrit :
I understand that it won't become an instance of My_type globally. But
it will become locally and this breaks polymorphism.

Then don't do it !-)
(See code example
I provide at the end)


Still, it is useful and interesting to compare languages.

Indeed. But you have to understand enough of a language to compare it
with another one. The old "I can write C in any language" syndrom...

This is not what I am worrying about. I will try to be more explicit.
Ok.

I would like to have a class for a "special matrix". This is an
ordinary 2n by 2n matrix which can be thought of as consisting of four n
by n sized blocks.
Right.

At this moment, I just use normal numpy arrays (or anything which
behaves like them). But I have very good reasons to actually have a
class for these special matrices. Still, I would like to be able to
call functions which work with "special matrices" with plain numpy
arrays as arguments. In that case, the arguments which are plain
matrices should be converted to "special" ones such that the main part
of the function can assume to always work with "special matrices".

Ok. So you want to build a "special matrice" like object from the numpy
array.
The code attached in the end (which is a complete runnable script)
should illustrate what I mean.

This example works as I want except that when bar is called with a an
argument of type Derived, it behaves as Base.
Ok.

Also, I am not sure
whether there is a nicer way to achieve the following functionality for
Base.__init__:

If other is of type Base already, just "pass it on". Otherwise,
construct an instance of Base from it.

You can't do this in the initializer, you have to use either a factory
function or the proper constructor (or an alternate constructor).
****************************************************************
import numpy as np

class Base:

If you're using Python 2.x, make this:

class Base(object):
def __init__(self, other):
if isinstance(other, type(self)):
self = other

'self' is a local name. Rebinding a local name only impact the local
namespace.
return
n = other.shape[0]
assert n == other.shape[1]
assert n % 2 == 0
n //= 2
self.a = other[0 : n, 0 : n]
self.b = other[n : 2*n, 0 : n]
self.c = other[0 : n, n : 2*n]
self.d = other[n : 2*n, n : 2*n]


Question : is there any case where you may want to instanciate this
class with explicit values for a, b, c and d ?


Anyway: the simplest solution here is to replace the call to your Base
class with a call to a factory function. I'd probably go for something
like (Q&D untested code and other usual warnings) :

class Base(object):
def __init__(self, a, b, c, d):
self.a = a
self.b = b
self.c = c
self.d = d

@classmethod
def from_array(cls, arr):
""" alternate constructor from a numpy array """
n = arr.shape[0]
assert n == arr.shape[1]
assert n % 2 == 0
n //= 2
return cls(
arr[0 : n, 0 : n],
arr[n : 2*n, 0 : n],
arr[0 : n, n : 2*n],
arr[n : 2*n, n : 2*n]
)


def hello(self):
print 'hello from Base'


class Derived(Base):
def hello(self):
print 'hello from Derived'

def coerce(obj, cls=Base):
if isinstance(obj, cls):
return obj
else:
return cls.from_array(obj)


def bar(arg):
arg = coerce(arg)
# Do something useful with arg.{a..d}
arg.hello()

# This works.
a = np.identity(4)
bar(a)

# And this also.
a = Base.from_array(np.identity(4))
bar(a)

# And now this should work too
a = Derived.from_array(np.identity(4))
bar(a)



Does it solve your problem ?-)


Note that if using a plain function hurts your feelings - or just isn't
practical for any other reason - you can also make it another
classmethod of Base, ie :


class Base(object):
def __init__(self, a, b, c, d):
self.a = a
self.b = b
self.c = c
self.d = d

@classmethod
def from_array(cls, arr):
""" alternate constructor from a numpy array """
n = arr.shape[0]
assert n == arr.shape[1]
assert n % 2 == 0
n //= 2
return cls(
arr[0 : n, 0 : n],
arr[n : 2*n, 0 : n],
arr[0 : n, n : 2*n],
arr[n : 2*n, n : 2*n]
)

@classmethod
def coerce(cls, obj):
if isinstance(obj, cls):
return obj
else:
return cls.from_array(obj)

def bar(arg):
arg = Base.coerce(arg)
# Do something useful with arg.{a..d}
arg.hello()


HTH
 
B

Bruno Desthuilliers

Bruno Desthuilliers a écrit :
Christoph Groth a écrit :

Indeed. But you have to understand enough of a language to compare it
with another one. The old "I can write C in any language" syndrom...

Re-reading this it might looks a bit patronizing. Sorry, was not my
intention at all - it was just a general consideration (I mean "mostly
useless blah").

(snip)
 
S

Steven D'Aprano

If other is of type Base already, just "pass it on". Otherwise,
construct an instance of Base from it.

**************************************************************** import
numpy as np

class Base:
def __init__(self, other):
if isinstance(other, type(self)):
self = other
return

This does not do what you think it does. I wonder whether you've actually
tried it?
array([[ 1., 0.],
[ 0., 1.]])Traceback (most recent call last):
File "<stdin>", line 1, in <module>
AttributeError: Base instance has no attribute 'a'


In your __init__ method, the line:

self = other

does NOT do what you think. All it does is re-bind the name "self" to
refer to the other object, while leaving the instance untouched.
Consequently, the rest of the initialisation code never gets executed and
your instance is uninitialised.

To do what you are trying to do, you need to do two things:

(1) Use a new-style class, not a classic (old-style) class; and

(2) Use the constructor __new__ and not the initialiser __init__.

And then you should change your mind and not do it, because you will be
opening a huge can of horrible bugs that will be really hard to debug.
The reason being, your class is *mutable*, and you will find yourself
having code where you think you have two different instances but you
actually only have one instance with two different names.

Here's a simple example demonstrating why this is a bad idea:

.... def __new__(cls, other):
.... if isinstance(other, cls):
.... return other
.... else:
.... instance = super(Recycler, cls).__new__(cls, other)
.... return instance
.... def __init__(self, other):
.... # Don't re-initialise when self has been recycled.
.... if self is other:
.... return
.... self.attr = other
....23


a and b are the same object, and whatever you do to one, you do to the
other. Object constructors should construct new instances, not give you
back an existing instance which is already in use elsewhere.

However, if you make the class *immutable*, then it is perfectly safe,
because you can't modify immutable objects and therefore it doesn't
matter whether a and b refer to two different instances or the same
instance. For example, Python caches small integers and reuses them, as a
memory optimization:
True

but doesn't bother for larger integers to avoid filling the cache will
billions of ints that will never be re-used:
False

This is safe, since you can't change the value of the object 42. (You can
rebind the name "a", but not modify the object itself.)

So, to recap:

* you aren't doing what you think you're doing;

* even if you were, you shouldn't;

* unless you make the class immutable.
 
C

Christoph Groth

Bruno Desthuilliers said:
Anyway: the simplest solution here is to replace the call to your Base
class with a call to a factory function. I'd probably go for something
like (Q&D untested code and other usual warnings) :

(...)

Yeah, that will do what I want.

My confusion arose from the expectation that there had to be some
mechanism to do the conversion automatically. But actually a simple

def bar(arg):
if not isinstance(arg, Base):
arg = Base(arg)
# Do something with arg.

is a simple and explicit solution of the problem.

Thanks
Christoph
 
C

Christoph Groth

Steven D'Aprano said:
This does not do what you think it does. I wonder whether you've
actually tried it?

Just quickly. Sorry, I should have written

class Base:
def __init__(self, other):
if isinstance(other, type(self)):
self.a = other.a
self.b = other.b
self.c = other.c
self.d = other.d
return
# ...
 
C

Carl Banks

Yeah, that will do what I want.

My confusion arose from the expectation that there had to be some
mechanism to do the conversion automatically.  But actually a simple

def bar(arg):
    if not isinstance(arg, Base):
        arg = Base(arg)
        # Do something with arg.

is a simple and explicit solution of the problem.

What if someone wants to call bar with an argument that mimics a Base
but isn't a subclass? Your function would try to convert it to an
actual Base.

Something to think about before you make a habit of this.


Carl Banks
 

Ask a Question

Want to reply to this thread or ask your own question?

You'll need to choose a username for the site, which only take a couple of moments. After that, you can post your question and our members will help you out.

Ask a Question

Members online

Forum statistics

Threads
473,764
Messages
2,569,566
Members
45,041
Latest member
RomeoFarnh

Latest Threads

Top