Reputation: 22958
With python properties, I can make it such that
obj.y
calls a function rather than just returning a value.
Is there a way to do this with modules? I have a case where I want
module.y
to call a function, rather than just returning the value stored there.
Upvotes: 139
Views: 43870
Reputation: 8196
Google led me to this gist which provided a pair of decorators (mod_property
, cached_mod_property
) with simple implementations. I tried them and they worked for me.
I took the code from that gist, and some of the code from dickens, and combined it all into a single utility module with full demo here.
Manifest:
Upvotes: 0
Reputation: 1220
As PEP 562 has been implemented in Python >= 3.7, now we can do this
file: module.py
def __getattr__(name):
if name == 'y':
return 3
raise AttributeError(f"module '{__name__}' has no attribute '{name}'")
other = 4
demo:
>>> import module
>>> module.y
3
>>> module.other
4
>>> module.nosuch
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "module.py", line 4, in __getattr__
raise AttributeError(f"module '{__name__}' has no attribute '{name}'")
AttributeError: module 'module' has no attribute 'nosuch'
Note that if you omit the raise AttributeError
in the __getattr__
function, it means the function ends with return None
, then the module.nosuch
will get a value of None
.
EDIT
My answer cannot have setters and deleters. If you need, adopt kxr's answer.
Create a subclass of <class 'module'>
, define properties in that class, then change module class into that class.
file: mymodule.py
import sys
class This(sys.__class__): # sys.__class__ is <class 'module'>
_y = 3
@property
def y(self): # do the property things in this class
return self._y
@y.setter
def y(self, value): # setter is also OK
self._y = value
other = 4
sys.modules[__name__].__class__ = This # change module class into This
demo:
>>> import mymodule
>>> mymodule.y
3
>>> mymodule.other
4
>>> mymodule.y = 5
>>> mymodule.y
5
>>> mymodule._y
5 # to prove that setter works
I am too novice to know why it works. So the credit should go to kxr.
Upvotes: 82
Reputation: 5539
In Python 3, since 3.7 at least, the class of modules can be changed to a sub class, so real module properties (or descriptors) are now easy to implement - more solid and powerful than a PEP 562 module __getattr__
.
# mymodule.py
class ThisMod(sys.modules[__name__].__class__):
y = property(lambda self: "Hi this is module %s." % __name__)
const = property(lambda self: _const) # block setting
sys.modules[__name__].__class__ = ThisMod
_const = 77
# rest of module code ...
A typical use case is: enriching a (huge) existing module with some (few) dynamic attributes - without turning all module stuff into a class layout.
Unfortunately a most simple module class patch like sys.modules[__name__].__class__ = MyPropertyModule
fails with TypeError: __class__ assignment: only for heap types
. So module creation needs to be rewired.
This approach does it without Python import hooks, just by having some prolog on top of the module code:
# propertymodule.py
""" Module property example """
if '__orgmod__' not in globals():
# constant prolog for having module properties / supports reload()
print "PropertyModule stub execution", __name__
import sys, types
class PropertyModule(types.ModuleType):
def __str__(self):
return "<PropertyModule %r from %r>" % (self.__name__, self.__file__)
modnew = PropertyModule(__name__, __doc__)
modnew.__modclass__ = PropertyModule
modnew.__file__ = __file__
modnew.__orgmod__ = sys.modules[__name__]
sys.modules[__name__] = modnew
exec sys._getframe().f_code in modnew.__dict__
else:
# normal module code (usually vast) ..
print "regular module execution"
a = 7
def get_dynval(module):
return "property function returns %s in module %r" % (a * 4, module.__name__)
__modclass__.dynval = property(get_dynval)
Usage:
>>> import propertymodule
PropertyModule stub execution propertymodule
regular module execution
>>> propertymodule.dynval
"property function returns 28 in module 'propertymodule'"
>>> reload(propertymodule) # AFTER EDITS
regular module execution
<module 'propertymodule' from 'propertymodule.pyc'>
>>> propertymodule.dynval
"property function returns 36 in module 'propertymodule'"
Note: Something like from propertymodule import dynval
will produce a frozen copy of course - corresponding to dynval = someobject.dynval
Upvotes: 10
Reputation: 121
based on user2124834's answer:
import sys
class AttrGeter:
def __new__(cls, gt):
if isinstance(gt, cls):
return gt
else:
o = super().__new__(cls)
o.oldgetattr = gt
o.funcmap = {}
return o
def __call__(self, name):
name2 = "_" + name
if name2 in self.funcmap:
return self.funcmap[name2]()
else:
return self.oldgetattr(name)
def add(self, func):
self.funcmap[func.__name__] = func
def module_property(func):
"""Decorator to turn module functions into properties.
Function names must be prefixed with an underscore."""
module = sys.modules[func.__module__]
def base_getattr(name):
raise AttributeError(
f"module '{module.__name__}' has no attribute '{name}'")
ag = AttrGeter(getattr(module, '__getattr__', base_getattr))
module.__getattr__ = ag
ag.add(func)
return func
Usage (note the leading underscore), in the_module.py:
@module_property
def _thing():
return 'hello'
Then:
import the_module
print(the_module.thing) # prints 'hello'
I use a dict
instead of nested function
in original solution. That may be more efficient when use the decorator many times in one module.
Upvotes: 0
Reputation: 2157
proxy_tools
The proxy_tools
package attempts to provide @module_property
functionality.
It installs with
pip install proxy_tools
Using a slight modification of @Marein's example, in the_module.py
we put
from proxy_tools import module_property
@module_property
def thing():
print(". ", end='') # Prints ". " on each invocation
return 'hello'
Now from another script, I can do
import the_module
print(the_module.thing)
# . hello
This solution is not without caveats. Namely, the_module.thing
is not a string! It is a proxy_tools.Proxy
object whose special methods have been overridden so that it mimicks a string. Here are some basic tests which illustrate the point:
res = the_module.thing
# [No output!!! Evaluation doesn't occur yet.]
print(type(res))
# <class 'proxy_tools.Proxy'>
print(isinstance(res, str))
# False
print(res)
# . hello
print(res + " there")
# . hello there
print(isinstance(res + "", str))
# . True
print(res.split('e'))
# . ['h', 'llo']
Internally, the original function is stored to the_module.thing._Proxy__local
:
print(res._Proxy__local)
# <function thing at 0x7f729c3bf680>
Honestly, I'm baffled about why modules don't have this functionality built in. I think the crux of the matter is that the_module
is an instance of the types.ModuleType
class. Setting a "module property" amounts to setting a property on an instance of this class, rather than on the types.ModuleType
class itself. For more details, see this answer.
We can actually implement properties on types.ModuleType
as follows, although the results are not great. We can't directly modify built-in types, but we can curse them:
# python -m pip install forbiddenfruit
from forbiddenfruit import curse
from types import ModuleType
# curse has the same signature as setattr.
curse(ModuleType, "thing2", property(lambda module: f'hi from {module.__name__}'))
This gives us a property which exists over all modules. It's a bit unwieldly, since we break the setting behavior across all modules:
import sys
print(sys.thing2)
# hi from sys
sys.thing2 = 5
# AttributeError: can't set attribute
Upvotes: 1
Reputation:
Based on John Lin's answer:
def module_property(func):
"""Decorator to turn module functions into properties.
Function names must be prefixed with an underscore."""
module = sys.modules[func.__module__]
def base_getattr(name):
raise AttributeError(
f"module '{module.__name__}' has no attribute '{name}'")
old_getattr = getattr(module, '__getattr__', base_getattr)
def new_getattr(name):
if f'_{name}' == func.__name__:
return func()
else:
return old_getattr(name)
module.__getattr__ = new_getattr
return func
Usage (note the leading underscore), in the_module.py
:
@module_property
def _thing():
return 'hello'
Then:
import the_module
print(the_module.thing) # prints 'hello'
The leading underscore is necessary to differentiate the property-ized function from the original function. I couldn't think of a way to reassign the identifier, since during the time of the decorator execution, it has not been assigned yet.
Note that IDEs won't know that the property exists and will show red wavies.
Upvotes: 20
Reputation: 882291
Only instances of new-style classes can have properties. You can make Python believe such an instance is a module by stashing it in sys.modules[thename] = theinstance
. So, for example, your m.py module file could be:
import sys
class _M(object):
def __init__(self):
self.c = 0
def afunction(self):
self.c += 1
return self.c
y = property(afunction)
sys.modules[__name__] = _M()
Upvotes: 63
Reputation: 46821
I would do this in order to properly inherit all the attributes of a module, and be correctly identified by isinstance()
import types
class MyModule(types.ModuleType):
@property
def y(self):
return 5
>>> a=MyModule("test")
>>> a
<module 'test' (built-in)>
>>> a.y
5
And then you can insert this into sys.modules:
sys.modules[__name__] = MyModule(__name__) # remember to instantiate the class
Upvotes: 61