7

Is there a way to make read-only class properties in Python? Ex. in Unity3d you can do this:

transform.position = Vector3.zero

Vector3.zero returns an instance of the Vector3 class where x, y, and z are 0. This is basically the same as:

transform.position = Vector3(0, 0, 0)

I've tried doing something like this:

class Vector3(object):
    zero = Vector3(0, 0, 0)
    ...

But I get an undefined variable error because the class hasn't been defined yet. So how do you make read-only class properties that don't require an instance of the class?

Mike Graham
  • 73,987
  • 14
  • 101
  • 130
topher
  • 83
  • 1
  • 5

7 Answers7

10

The most obvious way might be to alter the class object after the fact:

class Vector3(object):
    # ...
Vector3.zero = Vector3(0, 0, 0)

The main problem with this is that there's then only one zero object, and if it's mutable you can cause accidental damage all over the place. It may be easier (and feel less hacky) to use a dynamic descriptor that creates a zero vector every time it's accessed (this is done by creating a ClassProperty class):

class ClassProperty(property):
    def __get__(self, cls, owner):
        return self.fget.__get__(None, owner)()

class Vector3(object):
    @ClassProperty
    @classmethod
    def zero(cls):
        return cls(0, 0, 0)

I consider none of these really "pythonic", though. Consider the other mathematical types in Python: ints, floats, and complex numbers. None of these have a "zero" class attribute, or a zero constructor, instead they return zero when called with no arguments. So perhaps it might be best to do like so:

class Vector3(object):
    def __init__(self, x=0, y=0, z=0):
        self.x = x
        self.y = y
        self.z = z 

This is less like Unity3D and more like Python, if you know what I mean.

Community
  • 1
  • 1
Devin Jeanpierre
  • 92,913
  • 4
  • 55
  • 79
  • The first answer is the obvious one, but then it's not read-only. The second one requires an instance of the class in order to use the property. The third one doesn't really do what I want it to do. I'm more looking for the way Unity made it work. – topher Sep 01 '11 at 16:36
  • 1
    @topher fixed, but it's strange to code things in a way that are alien to the environment. Python classes don't usually work this way. – Devin Jeanpierre Sep 01 '11 at 16:47
  • @topher, In Python, we don't habitually enforce things like read-only at a programatic level. If you don't give someone any reason to reassign this value and they don't have malicious intent, they won't do so. If they are out to do bad things, you can't help them by doing the special-descriptor/metaclass thing. – Mike Graham Sep 01 '11 at 17:00
  • 1
    *"Consider the other mathematical types in Python: ints, floats, and complex numbers. None of these have a "zero" class attribute, or a zero constructor, instead they return zero when called with no arguments."* This is a fantastic insight. – Mike Graham Sep 01 '11 at 17:01
  • @Mike, I also want to have class properties for the unit vectors i, j, and k. So adding defaults in the init function isn't what I'm looking for. But the class property is really close, it's just not read-only. – topher Sep 01 '11 at 17:50
  • 2
    @topher, Your insistence on read-only-ness is misplaced. This *is* nominally read-only and additionally I could quite easily change it with the metaclass solution. *In Python, trying to enforce things like "read only" at a language level is a waste of effort.* – Mike Graham Sep 01 '11 at 18:23
  • @topher, The fact that there might be other constants of interest does not really impact the implementation of your zero, a thing for which Devin recommends a way with *tons* of precedent. As a remark in the same vein, `Vector3(y=1)` might prove nicer than `Vector3.j`. – Mike Graham Sep 01 '11 at 18:30
8

Use a metaclass

class MetaVector3(type):

    @property
    def zero(cls):
        return cls(0,0,0)

class Vector3(object):
    __metaclass__ = MetaVector3

    def __init__(self, x, y, z):
        self.x = x
        self.y = y
        self.z = z

>>> v = Vector3.zero
>>> v.x, v.y, v.z
(0, 0, 0)
Eryk Sun
  • 33,190
  • 5
  • 92
  • 111
  • 5
    Metaclasses can't be multiply inherited or composed, so if you want another metaclass ever, or another superclass, you're SOL. I generally avoid them. – Devin Jeanpierre Sep 01 '11 at 17:00
  • Metaclasses can be very useful, but one certainly doesn't seem like the appropriate tool for this job. – Mike Graham Sep 01 '11 at 17:03
  • 1
    @Devin -- that's a self defeating argument. You get no value from metaclasses if you never use them. Even if you could only ever use one metaclass, you'd get some value -- more than if you don't use them at all just because you can't use multiple ones in some situations. eryksun, beautiful answer as always. – agf Sep 01 '11 at 17:11
  • 1
    @agf Implicit in "I avoid them" was "unless they are necessary". This is not such a case, as there are other things that produce identical behavior but don't involve metaclasses (in particular, descriptors). – Devin Jeanpierre Sep 01 '11 at 17:15
  • An additional benefit of this method is that the `zero` property won't appear on instances. – Erik Youngren Sep 02 '11 at 07:08
  • This works well enough for my purposes: creating a `Constants` class. – 2rs2ts Oct 12 '15 at 17:54
6

Use a descriptor:

class Zero(object):
    def __get__(self, instance, owner):
        return owner(0, 0, 0)

    def __set__(self, instance, value):
        #could raise an exception here or somethiing
        #this gets called if the user attempts to overwrite the property
        pass  

class Vector3(object):
    zero = Zero()

    def __init__(self, x, y, z):
        self.x = x
        self.y = y
        self.z = z

    def __repr__(self):
        return str(self.__dict__)

Should do what you want:

>>> v = Vector3(1, 2, 3)
>>> v
{'y': 2, 'x': 1, 'z': 3}
>>> v.zero
{'y': 0, 'x': 0, 'z': 0}
>>> v.zero = 'foo'
>>> v.zero
{'y': 0, 'x': 0, 'z': 0}
cnelson
  • 1,355
  • 11
  • 14
  • I believe that the `owner` parameter in descriptor's `__get__` method is the owning class, so that can be rewritten as `return owner(0, 0, 0)`. – Erik Youngren Sep 01 '11 at 20:58
2

What you're imagining is possible, but not necessary in this case. Just wait until after your class is defined to assign the attribute

class Vector3(object):
    ...
Vector3.zero = Vector3(0, 0, 0)

or make it a module level constant.


There is a good chance you want simply to use a shape (3,) numpy array instead of writing this class, for any practical purposes.

Mike Graham
  • 73,987
  • 14
  • 101
  • 130
1

That's a really interesting question, the workaround I would go with is to do a classmethod as a "getter" for the zero object:

class Vector3(object):
    __zero = None
    def __init__(self, x,y,z):
        self.x = x
        self.y = y
        self.z = z

    @classmethod
    def zero(cls):
        if not cls.__zero:
            cls.__zero = Vector3(0,0,0) 
        return cls.__zero

myzerovec = Vector3.zero()
Adam Parkin
  • 17,891
  • 17
  • 66
  • 87
  • This is almost what I'm looking for, but this is a method. Is there a way to make this a class property? – topher Sep 01 '11 at 16:43
  • Not to my knowledge, aside from the metaclass answer above, but that causes zero to actually return a different Vector3 instance each time you refer to it, which I don't think is what you wanted (a single statically defined object). Honestly though is it really that big of a difference doing "Vector3.zero()" as opposed to "Vector3.zero"? – Adam Parkin Sep 01 '11 at 17:21
0
class ClassProperty(property):
    def __get__(self, cls, owner):
        return self.fget.__get__(None, owner)()

class Vector3(object):
    _zero = None

    def __init__(self, x, y, z):
        self.x = x
        self.y = y
        self.z = z

    @ClassProperty
    @classmethod
    def zero(cls):
        if cls._zero is None:
            cls._zero = cls(0,0,0) 
        return cls._zero

Shamelessly stolen from here

Community
  • 1
  • 1
neurino
  • 11,500
  • 2
  • 40
  • 63
  • Take a closer look at Devin's answer, he already posted this before you. – agf Sep 01 '11 at 17:14
  • @agf: I saw it, but you can notice he edited his answer (see [revisions](http://stackoverflow.com/posts/7273347/revisions)) and it wasn't like that when I started writing my answer. Thanks for pointing it out anyway... – neurino Sep 02 '11 at 08:33
-1

As for the read-only part, this is a good resource.

template = property(lambda self: self.__template)
Nick ODell
  • 15,465
  • 3
  • 32
  • 66