12

I have something called a Node. Both Definition and Theorem are a type of node, but only Definitions should be allowed to have a plural attribute:

class Definition(Node):


    def __init__(self,dic):
        self.type = "definition"
        super(Definition, self).__init__(dic)
        self.plural = move_attribute(dic, {'plural', 'pl'}, strict=False)


    @property
    def plural(self):
        return self._plural

    @plural.setter
    def plural(self, new_plural):
        if new_plural is None:
            self._plural = None
        else:
            clean_plural = check_type_and_clean(new_plural, str)
            assert dunderscore_count(clean_plural)>=2
            self._plural = clean_plural


class Theorem(Node):


    def __init__(self, dic):
        self.type = "theorem"
        super().__init__(dic)
        self.proofs = move_attribute(dic, {'proofs', 'proof'}, strict=False)
        # theorems CANNOT have plurals:
        # if 'plural' in self:
        #   raise KeyError('Theorems cannot have plurals.')

As you can see, Definitions have a plural.setter, but theorems do not. However, the code

theorem = Theorem(some input)
theorem.plural = "some plural"

runs just fine and raises no errors. But I want it to raise an error. As you can see, I tried to check for plurals manually at the bottom of my code shown, but this would only be a patch. I would like to block the setting of ANY attribute that is not expressly defined. What is the best practice for this sort of thing?


I am looking for an answer that satisfies the "chicken" requirement:

I do not think this solves my issue. In both of your solutions, I can append the code t.chicken = 'hi'; print(t.chicken), and it prints hi without error. I do not want users to be able to make up new attributes like chicken.

Community
  • 1
  • 1
mareoraft
  • 3,474
  • 4
  • 26
  • 62
  • If you implement what you're asking for, you won't be able to set `self._plural`, because it doesn't have a setter. It's important to keep stuff like that in mind in our line of work. – user2357112 Aug 09 '15 at 18:12
  • related: [How to make an immutable object in Python?](http://stackoverflow.com/q/4828080/4279) – jfs Aug 24 '15 at 05:12

4 Answers4

4

The short answer is "Yes, you can."

The follow-up question is "Why?" One of the strengths of Python is the remarkable dynamism, and by restricting that ability you are actually making your class less useful (but see edit at bottom).

However, there are good reasons to be restrictive, and if you do choose to go down that route you will need to modify your __setattr__ method:

def __setattr__(self, name, value):
    if name not in ('my', 'attribute', 'names',):
        raise AttributeError('attribute %s not allowed' % name)
    else:
        super().__setattr__(name, value)

There is no need to mess with __getattr__ nor __getattribute__ since they will not return an attribute that doesn't exist.

Here is your code, slightly modified -- I added the __setattr__ method to Node, and added an _allowed_attributes to Definition and Theorem.

class Node:

    def __setattr__(self, name, value):
        if name not in self._allowed_attributes:
            raise AttributeError('attribute %s does not and cannot exist' % name)
        super().__setattr__(name, value)


class Definition(Node):

    _allowed_attributes = '_plural', 'type'

    def __init__(self,dic):
        self.type = "definition"
        super().__init__(dic)
        self.plural = move_attribute(dic, {'plural', 'pl'}, strict=False)

    @property
    def plural(self):
        return self._plural

    @plural.setter
    def plural(self, new_plural):
        if new_plural is None:
            self._plural = None
        else:
            clean_plural = check_type_and_clean(new_plural, str)
            assert dunderscore_count(clean_plural)>=2
            self._plural = clean_plural


class Theorem(Node):

    _allowed_attributes = 'type', 'proofs'

    def __init__(self, dic):
        self.type = "theorem"
        super().__init__(dic)
        self.proofs = move_attribute(dic, {'proofs', 'proof'}, strict=False)

In use it looks like this:

>>> theorem = Theorem(...)
>>> theorem.plural = 3
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 6, in __setattr__
AttributeError: attribute plural does not and cannot exist

edit

Having thought about this some more, I think a good compromise for what you want, and to actually answer the part of your question about restricting allowed changes to setters only, would be to:

  • use a metaclass to inspect the class at creation time and dynamically build the _allowed_attributes tuple
  • modify the __setattr__ of Node to always allow modification/creation of attributes with at least one leading _

This gives you some protection against both misspellings and creation of attributes you don't want, while still allowing programmers to work around or enhance the classes for their own needs.

Okay, the new meta class looks like:

class NodeMeta(type):

    def __new__(metacls, cls, bases, classdict):
        node_cls = super().__new__(metacls, cls, bases, classdict)
        allowed_attributes = []
        for base in (node_cls, ) + bases:
            for name, obj in base.__dict__.items():
                if isinstance(obj, property) and hasattr(obj, '__fset__'):
                    allowed_attributes.append(name)
        node_cls._allowed_attributes = tuple(allowed_attributes)
        return node_cls

The Node class has two adjustments: include the NodeMeta metaclass and adjust __setattr__ to only block non-underscore leading attributes:

class Node(metaclass=NodeMeta):

    def __init__(self, dic):
        self._dic = dic

    def __setattr__(self, name, value):
        if not name[0] == '_' and name not in self._allowed_attributes:
            raise AttributeError('attribute %s does not and cannot exist' % name)
        super().__setattr__(name, value)

Finally, the Node subclasses Theorem and Definition have the type attribute moved into the class namespace so there is no issue with setting them -- and as a side note, type is a bad name as it is also a built-in function -- maybe node_type instead?

class Definition(Node):

    type = "definition"

    ...

class Theorem(Node):

    type = "theorem"

    ...

As a final note: even this method is not immune to somebody actually adding or changing attributes, as object.__setattr__(theorum_instance, 'an_attr', 99) can still be used -- or (even simpler) the _allowed_attributes can be modified; however, if somebody is going to all that work they hopefully know what they are doing... and if not, they own all the pieces. ;)

Ethan Furman
  • 63,992
  • 20
  • 159
  • 237
  • 1
    Thanks for your time. Unfortunately I've been so busy lately that I can't test it out now. But I'm awarding the bounty because this looks like exactly what I wanted. Thanks! – mareoraft Aug 27 '15 at 01:49
  • @mareoraft: [the accepted answer for the related question](http://stackoverflow.com/a/4828108/4279) says that `object.__setattr__()` can be used to workaround the restricting `__setattr__()` method. – jfs Feb 22 '16 at 22:26
  • @mareoraft: Updated answer: fixed property checking code; added blurb at bottem in response to J.F. Sebastian's comment. – Ethan Furman Feb 22 '16 at 22:45
0

You can check for the attribute everytime you access it.

class Theorem(Node):
    ...
   def __getattribute__(self, name):
       if name not in ["allowed", "attribute", "names"]:
           raise MyException("attribute "+name+" not allowed")
       else:
           return self.__dict__[name]

   def __setattr__(self, name, value):
       if name not in ["allowed", "attribute", "names"]:
           raise MyException("attribute "+name+" not allowed")
       else:
           self.__dict__[name] = value

You can build the allowed method list dynamically as a side effect of a decorator:

 allowed_attrs = []
 def allowed(f):
     allowed_attrs.append(f.__name__)
     return f

You would also need to add non method attributes manually.

deStrangis
  • 1,912
  • 1
  • 18
  • 25
  • Some of my properties (such as `plural`) have their own setters. It seems that the code above bypasses those setters entirely. – mareoraft Aug 19 '15 at 18:37
  • The purpose of the code is to illustrate the general `__getattribute__` & `__setattribute__` mechanism. You can tailor it to include the functionality of the setters. Dirty, but honestly I can think no other way of restricting a basic Python functionality such as adding attributes at runtime. – deStrangis Aug 20 '15 at 09:53
  • 1
    Overriding `__getattribute__` is almost always a bad idea. Did you mean `__getattr__` instead? – Ethan Furman Aug 22 '15 at 02:12
0

If you really want to prevent all other dynamic attributes. I assume there's a well-defined time window that you want to allow adding attributes.

Below I allow it until object initialisation is finished. (you can control it with allow_dynamic_attribute variable.

class A:
    def __init__(self):
        self.allow_dynamic_attribute = True
        self.abc = "hello"
        self._plural = None     # need to give default value
        # A.__setattr__ = types.MethodType(__setattr__, A)
        self.allow_dynamic_attribute = False

    def __setattr__(self, name, value):

        if hasattr(self, 'allow_dynamic_attribute'):
            if not self.allow_dynamic_attribute:

                if not hasattr(self, name):
                    raise Exception

        super().__setattr__(name, value)

    @property
    def plural(self):

        return self._plural

    @plural.setter
    def plural(self, new_plural):

        self._plural = new_plural

a = A()

print(a.abc)                    # fine
a.plural = "yes"                # fine
print(a.plural)                 # fine
a.dkk = "bed"                   # raise exception

Or it can be more compact this way, I couldn't figure out how MethodType + super can get along together.

import types
def __setattr__(self, name, value):
    if not hasattr(self, name):
        raise Exception
    else:
        super().__setattr__(name,value) # this doesn't work for reason I don't know

class A:
    def __init__(self):
        self.foo = "hello"
        # after this point, there's no more setattr for you
        A.__setattr__ = types.MethodType(__setattr__, A) 


a = A()

print(a.foo)                    # fine
a.bar = "bed"                   # raise exception
eugene
  • 39,839
  • 68
  • 255
  • 489
-1

Yes, you can create private members that cannot be modified from outside the class. The variable name should start with two underscores:

class Test(object):

    def __init__(self, t):
        self.__t = t


    def __str__(self):
        return str(self.__t)

t = Test(2)
print(t) # prints 2
t.__t = 3

print(t) # prints 2

That said, trying to access such a variable as we do in t.__t = 3 will not raise an exception.

A different approach which you can take to achieve the wanted behavior is using functions. This approach will require "accessing attributes" using functional notation, but if that doesn't bother you, you can get exactly what you want. The following demo "hardcodes" the values, but obviously you can have Theorem() accept an argument and use it to set values to the attributes dynamically.

Demo:

# -*- coding: utf-8 -*-
def Theorem():
    def f(attrib):

        def proofs():
            return ''

        def plural():
            return '◊◊◊◊◊◊◊◊'

        if attrib == 'proofs':
            return proofs()
        elif attrib == 'plural':
            return plural()
        else:
            raise ValueError("Attribute [{}] doesn't exist".format(attrib))

    return f

t = Theorem()
print(t('proofs'))

print(t('plural'))        

print(t('wait_for_error')) 

OUTPUT


◊◊◊◊◊◊◊◊
Traceback (most recent call last):
  File "/Users/alfasi/Desktop/1.py", line 40, in <module>
    print(t('wait_for_error'))       
  File "/Users/alfasi/Desktop/1.py", line 32, in f
    raise ValueError("Attribute [{}] doesn't exist".format(attrib))
ValueError: Attribute [wait_for_error] doesn't exist
Nir Alfasi
  • 53,191
  • 11
  • 86
  • 129
  • I do not think this solves my issue. In both of your solutions, I can append the code `t.chicken = 'hi'; print(t.chicken)`, and it prints `hi` without error. I do not want users to be able to make up new attributes like `chicken`. – mareoraft Aug 09 '15 at 18:23
  • @mareoraft the second solution requires functional access. having the user do `t.chicken = 'hi'` is meaningless, and if they'll try doing `t(chicken)` an error will be thrown. (and of course you can add setters if you want). – Nir Alfasi Aug 09 '15 at 18:46
  • Two leading underscores does not prevent attribute modification, it just requires some extra typing; and it does nothing to prevent creation of new attributes. – Ethan Furman Aug 28 '15 at 21:18
  • @EthanFurman so how do you explain the output (remaining 2, even after assignment) ? – Nir Alfasi Aug 29 '15 at 01:40
  • 1
    The double leading underscore (with at most one trailing underscore) causes name mangling. To access `__t` from outside the class you have to access it as `_classname__t`. [See here](https://docs.python.org/2/tutorial/classes.html#private-variables-and-class-local-references) for more details. – Ethan Furman Aug 29 '15 at 21:14