I have a few classes each of which has a number of attributes. What all of the attributes have in common is that they should be numeric properties. This seems to be an ideal place to use python's decorators, but I can't seem to wrap my mind around what the correct implementation would be. Here is a simple example:
class Junk(object):
def __init__(self, var):
self._var = var
#property
def var(self):
"""A numeric variable"""
return self._var
#var.setter
def size(self, value):
# need to make sure var is an integer
if not isinstance(value, int):
raise ValueError("var must be an integer, var = {}".format(value))
self._var = value
#var.deleter
def size(self):
raise RuntimeError("You can't delete var")
It seems to me that it should be possible to write a decorator that does everything so that the above can be transformed into:
def numeric_property(*args, **kwargs):
...
class Junk(object):
def __init__(self, var):
self._var = var
#numeric_property
def var(self):
"""A numeric variable"""
return self._var
That way the new numeric_property decorator can be used in many classes.
A #property is just a special case of Python's descriptor protocol, so you can certainly build your own custom versions. For your case:
class NumericProperty:
"""A property that must be numeric.
Args:
attr (str): The name of the backing attribute.
"""
def __init__(self, attr):
self.attr = attr
def __get__(self, obj, type=None):
return getattr(obj, self.attr)
def __set__(self, obj, value):
if not isinstance(value, int):
raise ValueError("{} must be an integer, var = {!r}".format(self.attr, value))
setattr(obj, self.attr, value)
def __delete__(self, obj):
raise RuntimeError("You can't delete {}".format(self.attr))
class Junk:
var = NumericProperty('_var')
def __init__(self, var):
self.var = var
In use:
>>> j = Junk('hi')
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "/Users/jonrsharpe/test.py", line 29, in __init__
self.var = var
File "/Users/jonrsharpe/test.py", line 17, in __set__
raise ValueError("{} must be an integer, var = {!r}".format(self.attr, value))
ValueError: _var must be an integer, var = 'hi'
>>> j = Junk(1)
>>> del j.var
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "/Users/jonrsharpe/test.py", line 21, in __delete__
raise RuntimeError("You can't delete {}".format(self.attr))
RuntimeError: You can't delete _var
>>> j.var = 'hello'
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "/Users/jonrsharpe/test.py", line 17, in __set__
raise ValueError("{} must be an integer, var = {!r}".format(self.attr, value))
ValueError: _var must be an integer, var = 'hello'
>>> j.var = 2
>>> j.var
2
Option 1: inherit from property
property is a descriptor. See Descriptor HowTo on python.org.
So, can inherit from property and override the relevant methods.
For example, to enforce int on setter:
class numeric_property(property):
def __set__(self, obj, value):
assert isinstance(value, int), "numeric_property requires an int"
super(numeric_property, self).__set__(obj, value)
class A(object):
#numeric_property
def x(self):
return self._x
#x.setter
def x(self, value):
self._x = value
And now you have integers enforced:
>>> a = A()
>>> a.x = 'aaa'
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "<stdin>", line 3, in __set__
AssertionError: numeric_property requires an int
Option 2: Create a better descriptor
On the other hand, it may be even better to implement a brand new descriptor which does not inherit from property, which would enable you to define the property in one go.
It would be nicer to have this kind of interface:
class A(object):
x = numeric_property('_x')
For that you would implement a descriptor which takes the attribute name:
class numeric_property(object):
def __init__(self, private_attribute_name, default=0):
self.private_attribute_name = private_attribute_name
self.default = default
def __get__(self, obj, typ):
if not obj: return self
return getattr(obj, self.private_attribute_name, self.default)
def __set__(self, obj, value):
assert isinstance(value, int), "numeric_property requires an int"
setattr(obj, self.private_attribute_name, value)
Disclaimer :)
I would rather not enforce strict typing in Pyhon, because Python is much more powerful without it.
You may just create a function that does it for you . As simple as it can get, no need to create a custom descriptor:
def numprop(name, privname):
#property
def _numprop(self):
return getattr(self, privname)
#_numprop.setter
def _numprop(self, value):
if not isinstance(value, int):
raise ValueError("{name} must be an integer, {name} = {}".format(value, name=name))
setattr(self, privname, value)
#_numprop.deleter
def _numprop(self):
raise RuntimeError("You can't delete var")
return _numprop
class Junk(object):
def __init__(self, var):
self._var = var
var = numprop("var", "_var")
Related
I have the following base class.
class BaseWithMethod:
def __init__(self, prop=None):
self.prop = prop
def evil_method(self):
print(f"%##&ç? {self.prop}")
I want to create a wrapper class ReadonlyWrapperSelectedMethods that shows the same functionality as the base class but does not allow certain methods (evil_method in this example) to be called. Further, wrapped instances should be read-only, as discussed in my other SO question here. That means that calls to __setattr__ should raise an error once the instance is initialized. The behavior is demonstrated in the following code:
# Instantiate the wrapper class
readonly_instance = ReadonlyWrapperSelectedMethods()
# I can access properties
prop = readonly_instance.prop
# This should raise a PermissionError
readonly_instance.prop = 23
# This should also raise a PermissionError
readonly_instance.evil_method()
Is there a way to implement this behavior without modifying the base class? See below how it can be done when the base class may be changed.
Attempt 1: Modifying the base class
So far I have tried the following. I added an attribute _initialized to the base class and set it to True at the end of __init__:
class BaseWithMethodModified:
_initialized = False
def __init__(self, prop=None):
self.prop = prop
self._initialized = True
def evil_method(self):
print(f"%##&ç? {self.prop}")
In this case the following wrapper class should do the job. It overrides the __getattribute__ method and delegates calls to methods that are allowed to the super class.
class ReadonlyWrapperSelectedMethods(BaseWithMethodModified):
"""Read-only wrapper class."""
def __getattribute__(self, name: str):
if "evil" in name:
raise PermissionError()
else:
return super().__getattribute__(name)
def __setattr__(self, key, value) -> None:
if self.__getattribute__("_initialized"):
raise PermissionError()
else:
super().__setattr__(key, value)
The issue with this attempt is that I do not want to modify the base class and if the attribute _initialized is defined in the wrapper class, it cannot be accessed since all attribute accesses are delegated to the base class through __getattribute__. Maybe this can be circumvented in some way?
You could simply override the __init__ method:
class ReadonlyWrapperSelectedMethods(BaseWithMethod):
"""Read-only wrapper class."""
def __init__(self, prop=None):
super().__init__(prop)
self._initialized = True
def __getattribute__(self, name: str):
if "evil" in name:
raise PermissionError()
else:
return super().__getattribute__(name)
def __setattr__(self, key, value) -> None:
if hasattr(self, "_initialized"):
raise PermissionError()
else:
super().__setattr__(key, value)
After __init__ returns, the object is readonly:
>>> readonly_instance = ReadonlyWrapperSelectedMethods()
>>> vars(readonly_instance)
{'prop': None, '_initialized': True}
>>> prop = readonly_instance.prop
>>> readonly_instance.prop = 23
Traceback (most recent call last):
File "<pyshell#126>", line 1, in <module>
readonly_instance.prop = 23
File "<pyshell#121>", line 16, in __setattr__
raise PermissionError()
PermissionError
>>> readonly_instance.evil_method()
Traceback (most recent call last):
File "<pyshell#127>", line 1, in <module>
readonly_instance.evil_method()
File "<pyshell#121>", line 10, in __getattribute__
raise PermissionError()
PermissionError
Don't use inheritance, use composition. Take advantage of __slots__:
class Foo:
def __init__(self, prop=None):
self.prop = prop
def evil_method(self):
print(f"%##&ç? {self.prop}")
class ReadOnlyWrapper:
__slots__ = ('_foo',)
def __init__(self, foo: Foo):
self._foo = foo
def __getattr__(self, name: str):
if "evil" in name:
raise PermissionError()
else:
return getattr(self._foo, name)
wrapper = ReadOnlyWrapper(Foo())
While trying to create descriptors a few different ways I noticed some strange behavior that I'm trying to understand. Below are the three different ways I have gone about creating descriptors:
>>> class NumericValueOne():
... def __init__(self, name):
... self.name = name
... def __get__(self, obj, type=None) -> object:
... return obj.__dict__.get(self.name) or 0
... def __set__(self, obj, value) -> None:
... obj.__dict__[self.name] = value
>>> class NumericValueTwo():
... def __init__(self, name):
... self.name = name
... self.internal_name = '_' + self.name
... def __get__(self, obj, type=None) -> object:
... return getattr(obj, self.internal_name, 0)
... def __set__(self, obj, value) -> None:
... setattr(obj, self.internal_name, value)
>>> class NumericValueThree():
... def __init__(self, name):
... self.name = name
... def __get__(self, obj, type=None) -> object:
... return getattr(obj, self.name, 0)
... def __set__(self, obj, value) -> None:
... setattr(obj, self.name, value)
I then use them in the Foo classes, like below:
>>> class FooOne():
... number = NumericValueOne("number")
>>> class FooTwo():
... number = NumericValueTwo("number")
>>> class FooThree():
... number = NumericValueThree("number")
my_foo_object_one = FooOne()
my_foo_object_two = FooTwo()
my_foo_object_three = FooThree()
my_foo_object_one.number = 3
my_foo_object_two.number = 3
my_foo_object_three.number = 3
While FooOne and FooTwo work as expected when both setting & getting values. FooThree throws the following error:
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "<stdin>", line 7, in __set__
File "<stdin>", line 7, in __set__
File "<stdin>", line 7, in __set__
[Previous line repeated 497 more times]
RecursionError: maximum recursion depth exceeded while calling a Python object
It looks like setattr() is calling the __set__() method? But why should it be doing that if setattr() is modifying the obj __dict__? And why does this work if we use internal_name?
Why is that we NEED to use a private variable in order to use the built-in getattr() and setattr() methods correctly? Also, how is this different from just directly modifying the obj __dict__ like in NumericValueOne?
But why should it be doing that if setattr() is modifying the obj __dict__?
setattr doesn't just modify the __dict__. It sets attributes, exactly like x.y = z would, and for the attribute you're trying to set, "set this attribute" means "call the setter you're already in". Hence, infinite recursion.
And why does this work if we use internal_name?
That name doesn't correspond to a property, so it just gets a __dict__ entry.
Let's say I have a class like this:
class C:
def __init__(self, stuff: int):
self._stuff = stuff
#property
def stuff(self) -> int:
return self._stuff
then stuff is read-only:
c = C(stuff=10)
print(c.stuff) # prints 10
and
c.stuff = 2
fails as expected
AttributeError: can't set attribute
How can I get the identical behavior using a dataclass? If I wanted to also have a setter, I could do:
#dataclass
class DC:
stuff: int
_stuff: int = field(init=False, repr=False)
#property
def stuff(self) -> int:
return self._stuff
#stuff.setter
def stuff(self, stuff: int):
self._stuff = stuff
But how could I do it without the #stuff.setter part?
This answer extends directly from my other post on using descriptor classes, which is a convenient and handy way to define properties, more or less.
Since dataclasses does not offer a field(frozen=True) approach, I think this one can instead work for you.
Here is a straightforward example of usage below:
from dataclasses import dataclass, MISSING
from typing import Generic, TypeVar
_T = TypeVar('_T')
class Frozen(Generic[_T]):
__slots__ = (
'_default',
'_private_name',
)
def __init__(self, default: _T = MISSING):
self._default = default
def __set_name__(self, owner, name):
self._private_name = '_' + name
def __get__(self, obj, objtype=None):
value = getattr(obj, self._private_name, self._default)
return value
def __set__(self, obj, value):
if hasattr(obj, self._private_name):
msg = f'Attribute `{self._private_name[1:]}` is immutable!'
raise TypeError(msg) from None
setattr(obj, self._private_name, value)
#dataclass
class DC:
stuff: int = Frozen()
other_stuff: str = Frozen(default='test')
dc = DC(stuff=10)
# raises a TypeError: Attribute `stuff` is immutable!
# dc.stuff = 2
# raises a TypeError: Attribute `other_stuff` is immutable!
# dc.other_stuff = 'hello'
print(dc)
# raises a TypeError: __init__() missing 1 required positional argument: 'stuff'
# dc = DC()
Another option, is to use a metaclass which automatically applies the #dataclass decorator. This has a few advantages, such as being able to use dataclasses.field(...) for example to set a default value if desired, or to set repr=False for instance.
Note that once #dataclass_transform comes out in PY 3.11, this could potentially be a good use case to apply it here, so that it plays more nicely with IDEs in general.
In any case, here's a working example of this that I was able to put together:
from dataclasses import dataclass, field, fields
class Frozen:
__slots__ = ('private_name', )
def __init__(self, name):
self.private_name = '_' + name
def __get__(self, obj, objtype=None):
value = getattr(obj, self.private_name)
return value
def __set__(self, obj, value):
if hasattr(obj, self.private_name):
msg = f'Attribute `{self.private_name[1:]}` is immutable!'
raise TypeError(msg) from None
setattr(obj, self.private_name, value)
def frozen_field(**kwargs):
return field(**kwargs, metadata={'frozen': True})
def my_meta(name, bases, cls_dict):
cls = dataclass(type(name, bases, cls_dict))
for f in fields(cls):
# if a dataclass field is supposed to be frozen, then set
# the value to a descriptor object accordingly.
if 'frozen' in f.metadata:
setattr(cls, f.name, Frozen(f.name))
return cls
class DC(metaclass=my_meta):
other_stuff: str
stuff: int = frozen_field(default=2)
# DC.stuff = property(lambda self: self._stuff)
dc = DC(other_stuff='test')
print(dc)
# raises TypeError: Attribute `stuff` is immutable!
# dc.stuff = 41
dc.other_stuff = 'hello'
print(dc)
To get the boilerplate reduction that dataclass provides I found the only way to do this is with a descriptor.
In [236]: from dataclasses import dataclass, field
In [237]: class SetOnce:
...: def __init__(self):
...: self.block_set = False
...: def __set_name__(self, owner, attr):
...: self.owner = owner.__name__
...: self.attr = attr
...: def __get__(self, instance, owner):
...: return getattr(instance, f"_{self.attr}")
...: def __set__(self, instance, value):
...: if not self.block_set:
...: self.block_set = True
...: setattr(instance, f"_{self.attr}", value)
...: else:
...: raise AttributeError(f"{self.owner}.{self.attr} cannot be set.")
In [239]: #dataclass
...: class Foo:
...: bar:str = field(default=SetOnce())
In [240]: test = Foo("bar")
In [241]: test.bar
Out[241]: 'bar'
In [242]: test.bar = 1
---------------------------------------------------------------------------
AttributeError Traceback (most recent call last)
<ipython-input-242-9cc7975cd08b> in <module>
----> 1 test.bar = 1
<ipython-input-237-bddce9441c9a> in __set__(self, instance, value)
12 self.value = value
13 else:
---> 14 raise AttributeError(f"{self.owner}.{self.attr} cannot be set.")
15
AttributeError: Foo.bar cannot be set.
In [243]: test
Out[247]: Foo(bar='bar')
from dataclasses import dataclass
#dataclass(frozen=True)
class YourClass:
"""class definition"""
https://docs.python.org/3/library/dataclasses.html#frozen-instances
After instantiation of the class, when trying to change any of its properties, the exception is raised.
Because using the decorator in the class definition essentially triggers the #dataclass decorator to use the property object as a default field, it doesn't play nice. You can set the property outside like:
>>> from dataclasses import dataclass, field
>>> #dataclass
... class DC:
... _stuff: int = field(repr=False)
... stuff: int = field(init=False)
...
>>> DC.stuff = property(lambda self: self._stuff) # dataclass decorator cant see this
>>> dc = DC(42)
>>> dc
DC(stuff=42)
>>> dc.stuff
42
>>> dc.stuff = 99
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
AttributeError: can't set attribute
import operator
#dataclass
class Enum:
name: str = property(operator.attrgetter("_name"))
def __init__(self, name):
self._name = name
You can do it by combining three things:
Set frozen to False (the default);
Use __post_init__, which is called after the auto-generated __init__ finishes, to mark when the initial setting of values is set and the read-only behavior has to start;
Create your own version of __setattr__ to enforce the read-only behavior after the initial assignment.
Example Person class with a read-only ID field and a read-write name field:
from dataclasses import dataclass
#dataclass
class Person(object):
id : str
name : str
def __post_init__(self):
self._initialized = True
def __setattr__(self, key, value):
if "_initialized" not in self.__dict__:
# we are still inside __init__, assign all values
super().__setattr__(key, value)
elif key == 'id':
# __init__ has finished, enforce read-only attributes
raise AttributeError(f'Attribute id is read-only')
else:
# set read-write attributes normally
super().__setattr__(key, value)
p = Person(id="1234", name="John Doe")
p.name = "John Wick" # succeeds
p.id = "3456" # fails
I haven't implemented __delattr__ in this example, but it could follow the same logic we used on __setattr__.
Using a decorator so you don't need to write this much code for each class:
from typing import Optional, Iterable, Callable, Union
from dataclasses import dataclass
def readonlyattr(attrs : Optional[Union[str, Iterable[str]]] = None):
# ensure attrs is a set of strings
if isinstance(attrs, str):
attrs = set([attrs])
elif not isinstance(attrs, set):
attrs = set(attrs)
# return decorator
def wrap_readonly_attributes(cls: type):
# update post_init method
def make_post_init(cls: type, method: Callable):
def post_init(self, *args, **kwargs):
self._initialized = True
if method:
method(self, *args, **kwargs)
else:
for base in cls.__bases__:
try:
getattr(base, "__post_init__")(self, *args, **kwargs)
except AttributeError:
pass
return post_init
setattr(cls, "__post_init__", make_post_init(cls, getattr(cls, "__post_init__", None)))
# update setattr method
def make_setattr(cls: type, method: Callable):
def new_setattr(self, key, value):
if "_initialized" not in self.__dict__:
if method:
method(self, key, value)
else:
super().__setattr__(key, value)
elif key in attrs:
raise AttributeError(f'Attribute {key} is read-only')
else:
if method:
method(self, key, value)
else:
super().__setattr__(key, value)
return new_setattr
setattr(cls, "__setattr__", make_setattr(cls, getattr(cls, "__setattr__", None)))
return cls
return wrap_readonly_attributes
#dataclass
#readonlyattr(["id", "passport_no"])
class Person(object):
id : str
passport_no : str
name : str
p = Person(id="1234", passport_no="AB12345", name="John Doe")
print(p)
p.name = "John Wick" # succeeds
p.id = "3456" # fails
I am new to python. I'm trying to create a configuration class with required validators. In below code snippet, accessing variable 'a' using python class and instance of class is returning a different value. Whether this is a proper design or should i initialise var 'a' only in the class constructor and do validation in the setter method.
class IntField:
def __get__(self, instance, owner):
if instance is None:
return self
return instance.__dict__[self.name]
def __set__(self, instance, value):
if not isinstance(value, int):
raise ValueError('expecting integer')
instance.__dict__[self.name] = value
def __set_name__(self, owner, name):
self.name = name
class DefaultConfig:
a = IntField()
def __init__(self):
self.a = 2
print(DefaultConfig.a)
print(DefaultConfig().a)
output:
<__main__.IntField object at 0x10c34b550>
2
I just want to know whether it's right way to do it
Rather asking for opinion only answer, by I will try to be as objective as I can.
Your code behaves as expected as long as instances attributes are processed:
>>> c = DefaultConfig()
>>> c.a = 'foo'
Traceback (most recent call last):
File "<pyshell#88>", line 1, in <module>
c.a = 'foo'
File "<pyshell#83>", line 10, in __set__
raise ValueError('expecting integer')
ValueError: expecting integer
>>> c.a = 4
>>> c.a
4
When inspecting DefaultConfig.a, the __get__ function is still used with instance=None. So you can choose one of 2 possible ways:
be transparent and show what the attribute actually is (what you currently do)
insist on the descriptor magic and return the default value (here 2).
For that latter way, code could become:
class IntField:
def __get__(self, instance, owner):
if instance is None:
return getattr(owner, '_default_' + self.name, self)
return instance.__dict__[self.name]
def __set__(self, instance, value):
if not isinstance(value, int):
raise ValueError('expecting integer')
instance.__dict__[self.name] = value
def __set_name__(self, owner, name):
self.name = name
class DefaultConfig:
a = IntField()
_default_a = 2
def __init__(self):
self.a = self._default_a
The trick here is that by convention, the default value for an attribute x is expected to be _default_x.
In that case, you will get:
print(DefaultConfig.a)
2
I have a property that has an assertion to check if a value is of type str.
To catch this assertionError I have created a decorator according to the examples I have found online.
Decorator:
def catch_assertionerror(function):
def handle_problems(*args, **kwargs):
try:
return function(*args, **kwargs)
except AssertionError:
# log_error(err.args[0])
print "error caught"
return handle_problems
Property:
#catch_assertionerror
#name.setter
def name(self, value):
assert isinstance(value, str), "This value should be a string"
self._name = name
Setting the name property:
self.name = self.parse_name_from_xml()
When I run this code, there is no error shown, so I guess it is caught, but on the other hand, the error message is not printed to the screen.
Then I tried a more simple example I found on Stachoverflow:
def handleError(function):
def handleProblems():
try:
function()
except Exception:
print "Oh noes"
return handleProblems
#handleError
def example():
raise Exception("Boom!")
This also handled the error but did not print the error message to the screen.
Could someone explain to me what I am missing here?
Your latter example works for me, but your main problem lies in that you're not wrapping a function with catch_assertionerror in
#catch_assertionerror
#name.setter
def name(self, value):
assert isinstance(value, str), "This value should be a string"
self._name = name
but a descriptor. To make matters worse, you return a function instead, not a new descriptor wrapping the original. Now when you assign to name attribute you just replace your wrapper function with the assigned value.
Step by step, using your original class definition:
class X(object):
#property
def name(self):
return self._name
#catch_assertionerror
#name.setter
def name(self, value):
assert isinstance(value, str), "This value should be a string"
self._name = value
>>> x = X()
>>> x.name
<unbound method X.handle_problems>
>>> x.__dict__
{}
>>> x.name = 2
>>> x.name
2
>>> x.__dict__
{'name': 2}
What you must do is wrap the method function instead and then pass it to the descriptor handling decorator:
class X(object):
#property
def name(self):
return self._name
#name.setter
#catch_assertionerror
def name(self, value):
assert isinstance(value, str), "This value should be a string"
self._name = value
and so:
>>> x = X()
>>> x.name
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "<stdin>", line 4, in name
AttributeError: 'X' object has no attribute '_name'
>>> x.name = 2
error caught
>>> x.name = "asdf"
>>> x.name
'asdf'
In the future consider using functools.wraps and functools.update_wrapper. Without them your classes and functions are harder to inspect, because your wrappers will hide the original:
>>> #catch_assertionerror
... def this_name_should_show(): pass
...
>>> this_name_should_show
<function handle_problems at 0x7fd3d69e22a8>
Defining your decorator this way:
def catch_assertionerror(function):
#wraps(function)
def handle_problems(*args, **kwargs):
...
return handle_problems
will preserve the original function's information:
>>> #catch_assertionerror
... def this_name_should_show(): pass
...
>>> this_name_should_show
<function this_name_should_show at 0x7fd3d69e21b8>
It would have also indicated to you in your case that there's a problem:
# When trying to define the class
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "<stdin>", line 6, in X
File "<stdin>", line 2, in catch_assertionerror
File "/usr/lib/python2.7/functools.py", line 33, in update_wrapper
setattr(wrapper, attr, getattr(wrapped, attr))
AttributeError: 'property' object has no attribute '__module__'