patternpythonMinor
Descriptor class with advanced (inspect/metaclass) functionality
Viewed 0 times
inspectdescriptorwithadvancedmetaclassclassfunctionality
Problem
Having answered this question over on Programmers.SE, I found myself wondering how much effort it would be to write a descriptor that can automatically figure out what the 'destination' attribute would be, e.g.:
would redirect access to the
Given that the
I've included some basic doctests and a demo sub-class to enforce attribute types; what do you think? Using
```
from inspect import currentframe, getouterframes
def with_metaclass(meta, *bases):
"""Create a base class with a metaclass.
Source:
https://pypi.python.org/pypi/six
License:
Copyright (c) 2010-2015 Benjamin Peterson
Released under http://opensource.org/licenses/MIT
"""
class metaclass(meta):
def __new__(cls, name, this_bases, d):
return meta(name, bases, d)
return type.__new__(metaclass, 'temporary_class', (), {})
class EnforceDescriptor(type):
"""Ensures that at least one descriptor method is implemented.
Notes:
Requires at least one of the three descriptor protocol methods
(__get__, __set__ and __delete__) to be implemented.
Attributes:
REQUIRED (tuple): The names of the required methods.
Raises:
TypeError: If none of the three REQUIRED methods are implemented.
Example:
>>> class TestClass(with_metaclass(EnforceDescriptor
class Demo(object):
foo = Something()would redirect access to the
foo attribute to _foo by default. To do this, I looked into inspect to determine the name to which the descriptor is being assigned (see BaseDescriptor._get_name).Given that the
BaseDescriptor on its own is pretty pointless, I also created a metaclass to enforce that at least one of the descriptor protocol methods is implemented in any sub-classes of it (which required borrowing part of six to keep it 2.x-and-3.x-compliant).I've included some basic doctests and a demo sub-class to enforce attribute types; what do you think? Using
inspect makes the auto-naming somewhat fragile (it will probably only work with CPython, and assumes that the descriptor is assigned on a single line), but what else haven't I thought of?```
from inspect import currentframe, getouterframes
def with_metaclass(meta, *bases):
"""Create a base class with a metaclass.
Source:
https://pypi.python.org/pypi/six
License:
Copyright (c) 2010-2015 Benjamin Peterson
Released under http://opensource.org/licenses/MIT
"""
class metaclass(meta):
def __new__(cls, name, this_bases, d):
return meta(name, bases, d)
return type.__new__(metaclass, 'temporary_class', (), {})
class EnforceDescriptor(type):
"""Ensures that at least one descriptor method is implemented.
Notes:
Requires at least one of the three descriptor protocol methods
(__get__, __set__ and __delete__) to be implemented.
Attributes:
REQUIRED (tuple): The names of the required methods.
Raises:
TypeError: If none of the three REQUIRED methods are implemented.
Example:
>>> class TestClass(with_metaclass(EnforceDescriptor
Solution
I'll talk about the little stuff first
Name shadowing
Personal preference, I'm not a huge fan of using the same variable name in your generator expression as you did in the parameters to
Code comments
Your method of getting the name is a little confusing - I had to read it about three times and check the
could use a comment indicating that you're finding the first frame that wasn't this function or the
Name unused variables with
I'd rather see
than
My IDE highlights
Now here's the more interesting things, imo.
Better way of naming the descriptor's attribute
Like you said, using inspect is pretty fragile. It'll likely fail on non-CPython implementations, and it's pretty hard to read. I think a better way to do this is with a metaclass that enforces naming of all descriptors, like so.
then just make all classes that are going to use this functionality like so
or even make a mixin class like that so you don't have to do that every time. You could still include the
One possible issue with this is if, for some ungodly reason, someone wanted to do
you might run into some weird issues... but I figure if someone is doing this much weirdness they probably deserve whatever is coming to them.
Other ways of avoiding using inspection
I think that you might also be able to avoid using inspection AND metaclasses if you don't mind a little overhead on the first time access of the attribute's value. If you pull the code from the meta class into the
which would add names to every descriptor the first time you access one. This might get a little unwieldy if there were a ton of these in a given class, but I suspect that this would be okay in most use cases.
Name shadowing
Personal preference, I'm not a huge fan of using the same variable name in your generator expression as you did in the parameters to
__new__ in your EnforceDescriptor metaclass. I'd prefer something likeif all(attrs.get(func_name) is None for func_name in cls.REQUIRED):Code comments
Your method of getting the name is a little confusing - I had to read it about three times and check the
inspect docs to fully understand how it works (of course, anything involving inspection is usually a little tricky). I, and presumably anyone who wanted to touch this in the future, would appreciate some comments as to why things happen why they do. For example,code = next(frame for frame in getouterframes(currentframe())
if frame[3] not in ('_get_name', '__init__'))[4]could use a comment indicating that you're finding the first frame that wasn't this function or the
__init__ function of the descriptor. Then maybe an explanation about why the 4th and 5th items in that frame are important.Name unused variables with
_I'd rather see
def __get__(self, obj, _):
return getattr(obj, self.name)than
def __get__(self, obj, typ=None):
return getattr(obj, self.name)My IDE highlights
typ=None so I know it's not used, and this is a simple function, but using _ makes it even easier to see that parameter can be ignored.Now here's the more interesting things, imo.
Better way of naming the descriptor's attribute
Like you said, using inspect is pretty fragile. It'll likely fail on non-CPython implementations, and it's pretty hard to read. I think a better way to do this is with a metaclass that enforces naming of all descriptors, like so.
class EnforceNamedDescriptors(type):
"""Ensures that every instance of a BaseDescriptor has a name for
its attribute.
"""
def __new__(cls, name, bases, attrs):
for attr_name, attr_value in attrs.items():
if (isinstance(attr_value, BaseDescriptor) and
not hasattr(attr_value, "name")):
attr_value.name = "_{}".format(attr_name)
return super(EnforceNamedDescriptors, cls).__new__(cls, name, bases, attrs)then just make all classes that are going to use this functionality like so
class TestClass(with_metaclass(EnforceNamedDescriptors):
...or even make a mixin class like that so you don't have to do that every time. You could still include the
_get_name function as a fallback, in case you don't want this auto-naming to work every time. Then you'd probably change BaseDescriptor to look sort of likedef __init__(self, name=None):
if name is not None:
self.name = name
# Otherwise fall back to _get_name and EnforceNamedDescriptors
def __get__(self, obj, typ=None):
if not hasattr(self, 'name'):
self.name = self._get_name()
return getattr(obj, self.name)One possible issue with this is if, for some ungodly reason, someone wanted to do
class TestClass(with_metaclass(EnforceNamedDescriptors)):
__mangled = TypedAttribute(type=(int, float))you might run into some weird issues... but I figure if someone is doing this much weirdness they probably deserve whatever is coming to them.
Other ways of avoiding using inspection
I think that you might also be able to avoid using inspection AND metaclasses if you don't mind a little overhead on the first time access of the attribute's value. If you pull the code from the meta class into the
__get__ of BaseDescriptor you could do something likedef __get__(self, obj, typ=None):
if not hasattr(self, 'name'):
for name, value in obj.__dict__.items():
if (isinstance(value, BaseDescriptor) and not
hasattr(value, 'name')):
value.name = "_{}".format(name)
return getattr(obj, self.name)which would add names to every descriptor the first time you access one. This might get a little unwieldy if there were a ton of these in a given class, but I suspect that this would be okay in most use cases.
Code Snippets
if all(attrs.get(func_name) is None for func_name in cls.REQUIRED):code = next(frame for frame in getouterframes(currentframe())
if frame[3] not in ('_get_name', '__init__'))[4]def __get__(self, obj, _):
return getattr(obj, self.name)def __get__(self, obj, typ=None):
return getattr(obj, self.name)class EnforceNamedDescriptors(type):
"""Ensures that every instance of a BaseDescriptor has a name for
its attribute.
"""
def __new__(cls, name, bases, attrs):
for attr_name, attr_value in attrs.items():
if (isinstance(attr_value, BaseDescriptor) and
not hasattr(attr_value, "name")):
attr_value.name = "_{}".format(attr_name)
return super(EnforceNamedDescriptors, cls).__new__(cls, name, bases, attrs)Context
StackExchange Code Review Q#98892, answer score: 5
Revisions (0)
No revisions yet.