7

Currently I override the class' __setattr__() towards the end of the class' __init__() method to prevent new attribute creation -

class Point(object):
    def __init__(self):
        self.x = 0
        self.y = 0
        Point.__setattr__ = self._setattr

    def _setattr(self, name, value):
        if not hasattr(self, name):
            raise AttributeError("'" + name + "' not an attribute of Point object.")
        else:
            super(Point, self).__setattr__(name, value)

Is there a way to avoid manually overriding __setattr__() and do this automatically with the help of metaclasses?

The closest I came was -

class attr_block_meta(type):
    def __new__(meta, cname, bases, dctry):
        def _setattr(self, name, value):
            if not hasattr(self, name):
                raise AttributeError("'" + name + "' not an attribute of " + cname + " object.")
            object.__setattr__(self, name, value)

        dctry.update({'x': 0, 'y': 0})
        cls = type.__new__(meta, cname, bases, dctry)
        cls.__setattr__ = _setattr
        return cls

class ImPoint(object):
    __metaclass__ = attr_block_meta

Is there a more generic way of doing this such that apriori knowledge of the subclass attributes is not required?
Basically, how to avoid the line dctry.update({'x': 0, 'y': 0}) and make this work irrespective of what the names of class attributes are?

P.S. - FWIW I have already evaluated the __slots__ and namedtuple options and found them lacking for my needs. Please don't narrow your focus to the pared down Points() example that I have used to illustrate the question; the actual use case involves a far more complex class.

BioGeek
  • 21,897
  • 23
  • 83
  • 145
work.bin
  • 1,078
  • 7
  • 29
  • 3
    Why? Under what circumstances would someone add an attribute to your class? As the class author, how would that affect you? As the maxim says, "we're all consenting adults" in Python land; in this case it means if I violate the class API by adding an attribute, I'm responsible for any consequences. Python is not Java nor C++. – msw Sep 24 '15 at 15:31
  • 1
    @msw - Mainly to catch mistakes involving typos ASAP. If I could avoid programmers accidentally typing _obj.staet_ and then wondering why is the state machine working wonkily, then I would. Basically, I would like to reduce the time that goes in between typo to eventual realization. – work.bin Sep 24 '15 at 15:41
  • In Python you really can't use the language to coerce behavior. That's one of the reasons that tests and code reviews are even more critical in Python. So let's say you catch the `obj.staet` case you describe, how about `ojb.state` and when do you stop. People can (and will) write broken code in any language and it is hard to protect them from themselves. Finally, you are protected from half of the `staet` problems already: referencing an object `if obj.staet == 5:` will throw a NameError; assignment `obj.staet = 6` will not be caught by Python. – msw Sep 24 '15 at 16:05
  • `obj.staet = 6` is the error I am trying to catch. Also, subsequent `obj.staet == 5` will not throw an error. `ojb.state` will raise a name error. Testing/reviewing are extremely costly means (relatively speaking) to catch trivial errors, especially when these can be easily caught by the interpreter. – work.bin Sep 24 '15 at 16:16
  • 2
    *"I have already evaluated the `__slots__` and `namedtuple` options and found them lacking for my needs"* - in what way? Provide that information **in the question**, don't just vaguely refer to it. *"Please don't narrow your focus to the pared down Points() example that I have used to illustrate the question; the actual use case involves a far more complex class"* - then it's not a useful example, provide something more representative. Do you want to fix the *values* of the attributes at instance creation time, or just which attributes are defined? – jonrsharpe Sep 30 '15 at 15:00
  • 1
    I don't really see why you link that to metaclasses? Your example show it: what you did with a metaclass could as easily be done by overriding methods. You could put that stuff in a base class and inherit it just as well. – spectras Oct 02 '15 at 01:50
  • 2
    @msw While it is true that "we're all consenting adults" is generally considered a maximum in the Python land, there are scenarios where this kind of constraint does add real value. The good thing about Python is not that there are no constraints, but that you are free to design the precise level of constraint that suites your needs. – ArnauOrriols Oct 02 '15 at 15:30
  • 1
    @msw Also, it is not about being protective, it is about design, intention, and communication. – ArnauOrriols Oct 02 '15 at 21:30

4 Answers4

11

Don't reinvent the wheel.

Two simple ways to achieve that (without directly using a metaclass) are using:

  1. namedtuples
  2. __slots__

For example, using namedtuple (based on the example in the docs):

Point = namedtuple('Point', ['x', 'y'])
p = Point(11, 22)
p.z = 33  # ERROR

For example, using __slots__:

class Point(object):
    __slots__ = ['x', 'y']
    def __init__(self, x=0, y=0):
        self.x = x
        self.y = y

p = Point(11,22)
p.z = 33  # ERROR
approxiblue
  • 6,982
  • 16
  • 51
  • 59
shx2
  • 61,779
  • 13
  • 130
  • 153
  • 5
    FWIW, `__slots__` is intended for space optimization, not for arbitrary restriction of the possible attributes of an instance. It also impose quite a few other restrictions, cf https://docs.python.org/2/reference/datamodel.html#slots. Now that it wouldn't make sense here - a point is a perfect use case for slots since it has few attributes, usually quite a few instances, and as mostly a "data" object adding attributes wouldn't make much sense... – bruno desthuilliers Sep 24 '15 at 12:16
  • FYI, the two are essentially equivalent. `namedtuple` dynamically creates a new class that uses `__slots__` to restrict the allowable attributes to the names in the second argument. – chepner Sep 24 '15 at 12:16
  • 4
    @shx2 as I mentioned it does makes sense to use slots for a simple "data" object - what I wanted to point out is that slots are _not_ intended as a way to restrict dynamism, but as a space optimization trade-off. Useless arbitrary restrictions are highly unpythonic. – bruno desthuilliers Sep 24 '15 at 12:22
  • For more on the intended use and drawbacks of slots : http://stackoverflow.com/questions/472000/python-slots – bruno desthuilliers Sep 24 '15 at 12:24
  • FWIW I have already evaluated the `__slots__` and namedtuple options and found them lacking for my needs. Please don't get distracted by the Points() example I have used to illustrate the question, the actual use case involves a far more complex class. – work.bin Sep 24 '15 at 13:12
  • @chepner: Not quite. `namedtuple`s are immutable (can't reassign the attributes) where `__slots__` only limits what names may exist; it's still possible to change the value of any given name. – ShadowRanger Sep 29 '15 at 01:10
  • @ShadowRanger True; my point was mostly that a `namedtuple` isn't something built-in to Python, but just a wrapper around a regular class that uses `__slots__`. The generated class has some other features as well (such as immutability). – chepner Sep 29 '15 at 11:30
  • 7
    @chepner: If you want to get really technical, it doesn't actually use `__slots__` the way a normal user-defined class does. It has `__slots__ = ()` (no defined attributes at all) to prevent creation of `__weakref__` and `__dict__`; the actual storage is done with `@property`s that delegate to `tuple` indexing. – ShadowRanger Sep 29 '15 at 13:59
7

Would this make sense for your case?

from functools import wraps

class attr_block_meta(type):
    def __new__(meta, cname, bases, dctry):
        def _setattr(self, name, value):
            if not hasattr(self, name):
                raise AttributeError("'" + name + "' not an attibute of " + cname + " object.")
            object.__setattr__(self, name, value)

        def override_setattr_after(fn):
            @wraps(fn)
            def _wrapper(*args, **kwargs):
                cls.__setattr__ = object.__setattr__
                fn(*args, **kwargs)
                cls.__setattr__ = _setattr
            return _wrapper

        cls = type.__new__(meta, cname, bases, dctry)
        cls.__init__ = override_setattr_after(cls.__init__)
        return cls


class ImPoint(object):
    __metaclass__ = attr_block_meta
    def __init__(self, q, z):
        self.q = q
        self.z = z

point = ImPoint(1, 2)
print point.q, point.z
point.w = 3  # Raises AttributeError

See this for more details on 'wraps'.

You probably need to fiddle a little bit more with it to get it more elegant, but the general idea is to override __setattr__ only after init is called.

Having said that, a common approach to this is just to use object.__setattr__(self, field, value) internally to bypass the AttributeError:

class attr_block_meta(type):
    def __new__(meta, cname, bases, dctry):
        def _setattr(self, name, value):
            if not hasattr(self, name):
                raise AttributeError("'" + name + "' not an attibute of " + cname + " object.")
            object.__setattr__(self, name, value)

        cls = type.__new__(meta, cname, bases, dctry)
        cls.__setattr__ = _setattr
        return cls


class ImPoint(object):
    __metaclass__ = attr_block_meta
    def __init__(self, q, z):
        object.__setattr__(self, 'q', q)
        object.__setattr__(self, 'z', z)

point = ImPoint(1, 2)
print point.q, point.z
point.w = 3  # Raises AttributeError
Community
  • 1
  • 1
ArnauOrriols
  • 584
  • 1
  • 4
  • 15
  • 1
    Thanks for the answer; this is (almost) exactly what I was looking for - a post __init__() hook. – work.bin Oct 06 '15 at 09:37
  • The second approach wouldn't work for me since the requirement is to delegate concrete class development to others whose knowledge of Python is feebler than mine. I am just trying to provide a safe sandbox. – work.bin Oct 06 '15 at 09:47
  • @work.bin In this case what I usually do is to provide a helper method that abstracts the use of ``object.__setattr__`` – ArnauOrriols Oct 06 '15 at 14:35
  • That would work but why would you prefer that over the first solution? I am unable to see a good reason to prefer this over that. – work.bin Oct 06 '15 at 14:41
  • 1
    @work.bin no particular reason, whichever works for you best. It is fair to mention that I've seen the latter few more times in the wild, but then for your use case I can imagine myself going for the first option as well. – ArnauOrriols Oct 06 '15 at 14:55
6

You don't need metaclasses to solve this kind of problem.

If you want to create the data once up front and then have it be immutable, I would definitely use a namedtuple as shx2 suggests.

Otherwise, just define a collection of allowed fields on the class, and have __setattr__ check to see if the name that you're attempting to set is in the allowed fields collection. You don't need to change the implementation of __setattr__ part way through __init__ -- it will work during __init__ the just the same as it will work later. Use a tuple or a frozenset as the data structure for the allowed fields, if you want to discourage mutating/changing them on a given class.

class Point(object):
    _allowed_attrs = ("x", "y")

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

    def __setattr__(self, name, value):
        if name not in self._allowed_attrs:
            raise AttributeError(
                "Cannot set attribute {!r} on type {}".format(
                    name, self.__class__.__name__))
        super(Point, self).__setattr__(name, value)

p = Point(5, 10)
p.x = 9
p.y = "some string"
p.z = 11  # raises AttributeError

This can easily be factored out into a base-class for re-use:

class RestrictedAttributesObject(object):
    _allowed_attrs = ()

    def __setattr__(self, name, value):
        if name not in self._allowed_attrs:
            raise AttributeError(
                "Cannot set attribute {!r} on type {}".format(
                    name, self.__class__.__name__))
        super(RestrictedAttributesObject, self).__setattr__(name, value)

class Point(RestrictedAttributesObject):
    _allowed_attrs = ("x", "y")

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

I don't think it would be considered pythonic to lock down the allowed attributes of an object in this way, and it will cause some complication for subclasses that need additional attributes (a subclass will have to ensure that the _allowed_attrs field has contents appropriate for it).

Matt Anderson
  • 19,311
  • 11
  • 41
  • 57
1

I have this same need (for a development quick-hack API). I don't use metaclasses for this, just inheritance:

class LockedObject(object):
    def __setattr__(self, name, value):
        if name == "_locked":
            object.__setattr__(self, name, value)
            return

        if hasattr(self, "_locked"):
            if not self._locked or hasattr(self, name):
                object.__setattr__(self, name, value)
            else:
                raise NameError("Not allowed to create new attribute {} in locked object".format(name))
        else:  # never called _lock(), so go on
            object.__setattr__(self, name, value)

    def _lock(self):
        self._locked = True

    def _unlock(self):
        self._locked = False

Then:

class Base(LockedObject):
    def __init__(self):
        self.a = 0
        self.b = 1
        self._lock()

If I need to subclass Base and add extra attributes I use unlock:

class Child(Base):
    def __init__(self):
        Base.__init__(self)
        self._unlock()
        self.c = 2
        self._lock()

If Base is abstract you can skip its locking and just lock the childs. I have then some unittests that check that every public class is locked after init to catch me if I forget the locking.

agomcas
  • 695
  • 5
  • 12
  • Thanks for the answer. I had already resorted to this (since raising this question). However I was looking for something similar to the answer provided by @ArnauOrriols. – work.bin Oct 06 '15 at 09:35