Python distribution includes such modules for custom override as UserDict, UserList and UserString. The respective classes might be helpful in providing quick behavior of a built-in type with mixed-in user-typing (e.g. exposing the concept of type oriented programming).
But before going into further details of dynamical type declaration, please note that according to [1]
Historically (until release 2.2), Python’s built-in types have differed from user-defined types because it was not possible to use the built-in types as the basis for object-oriented inheritance. This limitation no longer exists.
Which means that one can extend a built-in type in python 2.2+ as in here:
class index(int):
pass
i = index(1)
print type(i) # produces <class '__main__.index'>
and this might seems satisfying. As disguised by dir(index) all the methods with “working” code are inherited. But since the data typing inside the “borrowed” methods operates with built-in ‘int’ type, this causes type-switching in most cases.
i = index(1)
i += 1
print type(i) # produces <type 'int'>
The result on second line is int, since the inherited __radd__() mehtod has no idea of our new user-type called index.
That leaves us with no user-type support and two options of implementing it:
- Re-implement type-depended methods of python data-model, inherited from the built-in type
- Wrap an instance of the the built-in type and reroute the respective python data-model to it
Classes as UserDict go for second option. In the following code snippet I explore a technique allowing dynamical construction of the user type with aggregated (composition-relation) instance of desired type. The idea is to simplify the “routing” the more the better:
class UserValueType(object):
def __init__(self, value):
self.value = value
@borrow_methods(int)
class UserInt(UserValueType):
pass
Obviously, this looks so simple because all the work is done by @borrow_methods() decorator, which proxies the desired type.
def partialmethod(method, *args, **kw):
def call(obj, *more_args, **more_kw):
call_kw = kw.copy()
call_kw.update(more_kw)
return getattr(obj, method)(*(args+more_args), **call_kw)
return call
def borrow_methods(source_type, overwrite=False, exclude=None, include=None):
'''
Decorator for borrowing methods from other classes.
Note: 'include' has priority over 'exclude'.
'''
if not exclude:
exclude = ['__getnewargs__']
if not include:
include = ['__repr__', '__format__', '__str__']
def invoke_method(self, method_name, *args, **keywords):
method = getattr(self.value, method_name)
return method(*args, **keywords)
def decorator(cls):
setattr(cls, '_invoke_method', invoke_method)
for (name, member) in inspect.getmembers(source_type):
if (not overwrite and hasattr(cls, name))
or (name in exclude and not name in include)
or not inspect.ismethoddescriptor(member):
continue
setattr(cls, name, partialmethod('_invoke_method', name))
return cls
return decorator
Decorator proxies all the necessary methods of the built-in type. Arguably an alternative of implementing descriptors to redirect calls can turn out much more sophisticated than “copying” of attributes. Such copying of methods uses partialmethod() function (similar to functools.partial) and expects that proxy _invoke_method redirects call to self.value.
Generally, using decorator is preferable to meta-class, since the first is more refine and simplistic. Finally, one would want to control the list of borrowed methods by exclusion/inclusion or similar.
Presented solution still does not address the issue of typing, i.e.
uint = UserInt(1)
uint += 1
assert type(unit) == UserInt
We add the modification into the decorator function by introducing uncasted parameter – a list of functions the result of which we do not cast. Normally, these are functions like __int__ or __str__.
def borrow_methods(source_type, overwrite=False, exclude=None, include=None,
uncasted=None):
'''
Decorator for borrowing methods from other classes.
Note: 'include' has priority over 'exclude'.
'''
if not exclude:
exclude = ['__getnewargs__']
if not include:
include = ['__repr__', '__format__', '__str__']
if not uncasted:
uncasted = ['__int__', '__str__', '__cmp__']
def invoke_method(self, method_name, *args, **keywords):
method = getattr(self.value, method_name)
result = method(*args, **keywords)
if method_name not in uncasted and type(self) != type(result):
result = self.__class__(result)
return result
def decorator(cls):
setattr(cls, '_invoke_method', invoke_method)
for (name, member) in inspect.getmembers(source_type):
if (not overwrite and hasattr(cls, name))
or (name in exclude and not name in include)
or not inspect.ismethoddescriptor(member):
continue
setattr(cls, name, partialmethod('_invoke_method', name))
return cls
return decorator
Casting happens on this line:
if method_name not in uncasted and type(self) != type(result):
result = self.__class__(result)
- Built-in Types, http://docs.python.org/library/stdtypes.html#