How to create a type that is closed under inherited operations?
Clash Royale CLAN TAG#URR8PPP
up vote
6
down vote
favorite
In the mathematical sense, a set (or type) is closed under an operation if the operation always returns a member of the set itself.
This question is about making a class that is closed under all operations inherited from its superclasses.
Consider the following class.
class MyInt(int):
pass
Since __add__
has not been overridden, it is not closed under addition.
x = MyInt(6)
print(type(x + x)) # <class 'int'>
One very tedious way to make the type closed would be to manually cast back the result of every operation that returns an int
to MyInt
.
Here, I automated that process using a metaclass, but this seems like an overly complex solution.
import functools
class ClosedMeta(type):
_register =
def __new__(cls, name, bases, namespace):
# A unique id for the class
uid = max(cls._register) + 1 if cls._register else 0
def tail_cast(f):
@functools.wraps(f)
def wrapper(*args, **kwargs):
out = f(*args, **kwargs)
if type(out) in bases:
# Since the class does not exist yet, we will recover it later
return cls._register[uid](out)
return out
return wrapper
for base in reversed(bases):
for name, attr in base.__dict__.items():
if callable(attr) and name not in namespace:
namespace[name] = tail_cast(attr)
subcls = super().__new__(cls, name, bases, namespace)
cls._register[uid] = subcls
return subcls
class ClosedInt(int, metaclass=ClosedMeta):
pass
This fails on some cornercases such as property
and methods recovered through __getattribute__
. It also fails when the base is not composed only of base types.
By example, this fails:
class MyInt(int):
pass
class ClosedInt(MyInt, metaclass=ClosedMeta):
pass
ClosedInt(1) + ClosedInt(1) # returns the int 2
I attempted to fix this, but it just seems to go deeper and deeper in the rabbit hole.
This seems like a problem that might have some simple pythonic solution. What would be a other, neater ways to achieve such a closed type?
python python-3.x oop containers metaclass
add a comment |Â
up vote
6
down vote
favorite
In the mathematical sense, a set (or type) is closed under an operation if the operation always returns a member of the set itself.
This question is about making a class that is closed under all operations inherited from its superclasses.
Consider the following class.
class MyInt(int):
pass
Since __add__
has not been overridden, it is not closed under addition.
x = MyInt(6)
print(type(x + x)) # <class 'int'>
One very tedious way to make the type closed would be to manually cast back the result of every operation that returns an int
to MyInt
.
Here, I automated that process using a metaclass, but this seems like an overly complex solution.
import functools
class ClosedMeta(type):
_register =
def __new__(cls, name, bases, namespace):
# A unique id for the class
uid = max(cls._register) + 1 if cls._register else 0
def tail_cast(f):
@functools.wraps(f)
def wrapper(*args, **kwargs):
out = f(*args, **kwargs)
if type(out) in bases:
# Since the class does not exist yet, we will recover it later
return cls._register[uid](out)
return out
return wrapper
for base in reversed(bases):
for name, attr in base.__dict__.items():
if callable(attr) and name not in namespace:
namespace[name] = tail_cast(attr)
subcls = super().__new__(cls, name, bases, namespace)
cls._register[uid] = subcls
return subcls
class ClosedInt(int, metaclass=ClosedMeta):
pass
This fails on some cornercases such as property
and methods recovered through __getattribute__
. It also fails when the base is not composed only of base types.
By example, this fails:
class MyInt(int):
pass
class ClosedInt(MyInt, metaclass=ClosedMeta):
pass
ClosedInt(1) + ClosedInt(1) # returns the int 2
I attempted to fix this, but it just seems to go deeper and deeper in the rabbit hole.
This seems like a problem that might have some simple pythonic solution. What would be a other, neater ways to achieve such a closed type?
python python-3.x oop containers metaclass
1
So, this mgiht be nitpicky, butclass BasicIntContainer(int)
is not an int container, it is an int.
â juanpa.arrivillaga
3 hours ago
@juanpa.arrivillaga I might be off in my terminology, how would you call such a construct?
â Olivier Melançon
3 hours ago
2
It is simply inheritance, an "is-a" relationship in the parlance of OOP. If you hover over the containers tag, you'll see that: "A container is a class, a data structure, or an abstract data type whose instances are collections of other objects.". So like alist
or adict
. Here, aBasicIntContainer
instance isn't a collection of other objects, it is a specialized kind ofint
object. I don't want to distract from an interesting question with more terminology nitpicking. Anyway, I would just rephrase as "how to create a type that is closed under inherited operations"
â juanpa.arrivillaga
3 hours ago
You can easily write a simple decorator if you just want to cast return values to type itself, but the problem with that would be that there may be methods that wouldn't be returning values that would be able to cast to the proper type, if that is the case; you would need to distinguish them somehow, if not you can just decorate the class with a decorator that would decorate every method.
â Ià Âñk Kaplan
3 hours ago
@Ià ÂñkKaplan For simplification, let's assume that the__new__
knows how to cast from all of its superclasses. Also, this decorator solution is basicaly what I implemented with a metaclass. I am currently fixing it, but it is getting very complex and I am for sure forgetting a lot of cornercases.
â Olivier Melançon
3 hours ago
add a comment |Â
up vote
6
down vote
favorite
up vote
6
down vote
favorite
In the mathematical sense, a set (or type) is closed under an operation if the operation always returns a member of the set itself.
This question is about making a class that is closed under all operations inherited from its superclasses.
Consider the following class.
class MyInt(int):
pass
Since __add__
has not been overridden, it is not closed under addition.
x = MyInt(6)
print(type(x + x)) # <class 'int'>
One very tedious way to make the type closed would be to manually cast back the result of every operation that returns an int
to MyInt
.
Here, I automated that process using a metaclass, but this seems like an overly complex solution.
import functools
class ClosedMeta(type):
_register =
def __new__(cls, name, bases, namespace):
# A unique id for the class
uid = max(cls._register) + 1 if cls._register else 0
def tail_cast(f):
@functools.wraps(f)
def wrapper(*args, **kwargs):
out = f(*args, **kwargs)
if type(out) in bases:
# Since the class does not exist yet, we will recover it later
return cls._register[uid](out)
return out
return wrapper
for base in reversed(bases):
for name, attr in base.__dict__.items():
if callable(attr) and name not in namespace:
namespace[name] = tail_cast(attr)
subcls = super().__new__(cls, name, bases, namespace)
cls._register[uid] = subcls
return subcls
class ClosedInt(int, metaclass=ClosedMeta):
pass
This fails on some cornercases such as property
and methods recovered through __getattribute__
. It also fails when the base is not composed only of base types.
By example, this fails:
class MyInt(int):
pass
class ClosedInt(MyInt, metaclass=ClosedMeta):
pass
ClosedInt(1) + ClosedInt(1) # returns the int 2
I attempted to fix this, but it just seems to go deeper and deeper in the rabbit hole.
This seems like a problem that might have some simple pythonic solution. What would be a other, neater ways to achieve such a closed type?
python python-3.x oop containers metaclass
In the mathematical sense, a set (or type) is closed under an operation if the operation always returns a member of the set itself.
This question is about making a class that is closed under all operations inherited from its superclasses.
Consider the following class.
class MyInt(int):
pass
Since __add__
has not been overridden, it is not closed under addition.
x = MyInt(6)
print(type(x + x)) # <class 'int'>
One very tedious way to make the type closed would be to manually cast back the result of every operation that returns an int
to MyInt
.
Here, I automated that process using a metaclass, but this seems like an overly complex solution.
import functools
class ClosedMeta(type):
_register =
def __new__(cls, name, bases, namespace):
# A unique id for the class
uid = max(cls._register) + 1 if cls._register else 0
def tail_cast(f):
@functools.wraps(f)
def wrapper(*args, **kwargs):
out = f(*args, **kwargs)
if type(out) in bases:
# Since the class does not exist yet, we will recover it later
return cls._register[uid](out)
return out
return wrapper
for base in reversed(bases):
for name, attr in base.__dict__.items():
if callable(attr) and name not in namespace:
namespace[name] = tail_cast(attr)
subcls = super().__new__(cls, name, bases, namespace)
cls._register[uid] = subcls
return subcls
class ClosedInt(int, metaclass=ClosedMeta):
pass
This fails on some cornercases such as property
and methods recovered through __getattribute__
. It also fails when the base is not composed only of base types.
By example, this fails:
class MyInt(int):
pass
class ClosedInt(MyInt, metaclass=ClosedMeta):
pass
ClosedInt(1) + ClosedInt(1) # returns the int 2
I attempted to fix this, but it just seems to go deeper and deeper in the rabbit hole.
This seems like a problem that might have some simple pythonic solution. What would be a other, neater ways to achieve such a closed type?
python python-3.x oop containers metaclass
python python-3.x oop containers metaclass
edited 2 hours ago
wim
147k42276397
147k42276397
asked 4 hours ago
Olivier Melançon
11.5k11436
11.5k11436
1
So, this mgiht be nitpicky, butclass BasicIntContainer(int)
is not an int container, it is an int.
â juanpa.arrivillaga
3 hours ago
@juanpa.arrivillaga I might be off in my terminology, how would you call such a construct?
â Olivier Melançon
3 hours ago
2
It is simply inheritance, an "is-a" relationship in the parlance of OOP. If you hover over the containers tag, you'll see that: "A container is a class, a data structure, or an abstract data type whose instances are collections of other objects.". So like alist
or adict
. Here, aBasicIntContainer
instance isn't a collection of other objects, it is a specialized kind ofint
object. I don't want to distract from an interesting question with more terminology nitpicking. Anyway, I would just rephrase as "how to create a type that is closed under inherited operations"
â juanpa.arrivillaga
3 hours ago
You can easily write a simple decorator if you just want to cast return values to type itself, but the problem with that would be that there may be methods that wouldn't be returning values that would be able to cast to the proper type, if that is the case; you would need to distinguish them somehow, if not you can just decorate the class with a decorator that would decorate every method.
â Ià Âñk Kaplan
3 hours ago
@Ià ÂñkKaplan For simplification, let's assume that the__new__
knows how to cast from all of its superclasses. Also, this decorator solution is basicaly what I implemented with a metaclass. I am currently fixing it, but it is getting very complex and I am for sure forgetting a lot of cornercases.
â Olivier Melançon
3 hours ago
add a comment |Â
1
So, this mgiht be nitpicky, butclass BasicIntContainer(int)
is not an int container, it is an int.
â juanpa.arrivillaga
3 hours ago
@juanpa.arrivillaga I might be off in my terminology, how would you call such a construct?
â Olivier Melançon
3 hours ago
2
It is simply inheritance, an "is-a" relationship in the parlance of OOP. If you hover over the containers tag, you'll see that: "A container is a class, a data structure, or an abstract data type whose instances are collections of other objects.". So like alist
or adict
. Here, aBasicIntContainer
instance isn't a collection of other objects, it is a specialized kind ofint
object. I don't want to distract from an interesting question with more terminology nitpicking. Anyway, I would just rephrase as "how to create a type that is closed under inherited operations"
â juanpa.arrivillaga
3 hours ago
You can easily write a simple decorator if you just want to cast return values to type itself, but the problem with that would be that there may be methods that wouldn't be returning values that would be able to cast to the proper type, if that is the case; you would need to distinguish them somehow, if not you can just decorate the class with a decorator that would decorate every method.
â Ià Âñk Kaplan
3 hours ago
@Ià ÂñkKaplan For simplification, let's assume that the__new__
knows how to cast from all of its superclasses. Also, this decorator solution is basicaly what I implemented with a metaclass. I am currently fixing it, but it is getting very complex and I am for sure forgetting a lot of cornercases.
â Olivier Melançon
3 hours ago
1
1
So, this mgiht be nitpicky, but
class BasicIntContainer(int)
is not an int container, it is an int.â juanpa.arrivillaga
3 hours ago
So, this mgiht be nitpicky, but
class BasicIntContainer(int)
is not an int container, it is an int.â juanpa.arrivillaga
3 hours ago
@juanpa.arrivillaga I might be off in my terminology, how would you call such a construct?
â Olivier Melançon
3 hours ago
@juanpa.arrivillaga I might be off in my terminology, how would you call such a construct?
â Olivier Melançon
3 hours ago
2
2
It is simply inheritance, an "is-a" relationship in the parlance of OOP. If you hover over the containers tag, you'll see that: "A container is a class, a data structure, or an abstract data type whose instances are collections of other objects.". So like a
list
or a dict
. Here, a BasicIntContainer
instance isn't a collection of other objects, it is a specialized kind of int
object. I don't want to distract from an interesting question with more terminology nitpicking. Anyway, I would just rephrase as "how to create a type that is closed under inherited operations"â juanpa.arrivillaga
3 hours ago
It is simply inheritance, an "is-a" relationship in the parlance of OOP. If you hover over the containers tag, you'll see that: "A container is a class, a data structure, or an abstract data type whose instances are collections of other objects.". So like a
list
or a dict
. Here, a BasicIntContainer
instance isn't a collection of other objects, it is a specialized kind of int
object. I don't want to distract from an interesting question with more terminology nitpicking. Anyway, I would just rephrase as "how to create a type that is closed under inherited operations"â juanpa.arrivillaga
3 hours ago
You can easily write a simple decorator if you just want to cast return values to type itself, but the problem with that would be that there may be methods that wouldn't be returning values that would be able to cast to the proper type, if that is the case; you would need to distinguish them somehow, if not you can just decorate the class with a decorator that would decorate every method.
â Ià Âñk Kaplan
3 hours ago
You can easily write a simple decorator if you just want to cast return values to type itself, but the problem with that would be that there may be methods that wouldn't be returning values that would be able to cast to the proper type, if that is the case; you would need to distinguish them somehow, if not you can just decorate the class with a decorator that would decorate every method.
â Ià Âñk Kaplan
3 hours ago
@Ià ÂñkKaplan For simplification, let's assume that the
__new__
knows how to cast from all of its superclasses. Also, this decorator solution is basicaly what I implemented with a metaclass. I am currently fixing it, but it is getting very complex and I am for sure forgetting a lot of cornercases.â Olivier Melançon
3 hours ago
@Ià ÂñkKaplan For simplification, let's assume that the
__new__
knows how to cast from all of its superclasses. Also, this decorator solution is basicaly what I implemented with a metaclass. I am currently fixing it, but it is getting very complex and I am for sure forgetting a lot of cornercases.â Olivier Melançon
3 hours ago
add a comment |Â
5 Answers
5
active
oldest
votes
up vote
2
down vote
I think using a class decorator with a black list of methods that should not return objects of the same type would be somewhat more Pythonic:
class containerize:
def __call__(self, obj):
if isinstance(obj, type):
return self.decorate_class(obj)
return self.decorate_callable(obj)
def decorate_class(self, cls):
for name in dir(cls):
attr = getattr(cls, name)
if callable(attr) and name not in ('__class__', '__init__', '__new__', '__str__', '__repr__', '__getattribute__'):
setattr(cls, name, self.decorate_callable(attr))
return cls
def decorate_callable(self, func):
def wrapper(obj, *args, **kwargs):
return obj.__class__(func(obj, *args, **kwargs))
return wrapper
so that:
class MyInt(int):
pass
@containerize()
class ClosedIntContainer(MyInt):
pass
i = ClosedIntContainer(3) + ClosedIntContainer(2)
print(i, type(i))
would output:
5 <class '__main__.ClosedIntContainer'>
add a comment |Â
up vote
1
down vote
I still feel there might be a more natural way to accomplish this, but I was able to fix the attempt provided in the question.
Here are the main points that needed ot be fixed.
We must check methods of all classes in the mro, not only the bases;
__getattribute__
and__getattr__
must be treated as special cases;Attributes with
__get__
must be treated separately;We have to write a list of exceptions as methods such as
__int__
or__eq__
obviously should return their expected types.
Code
import functools
def get_mro(bases):
# We omit 'object' as it is the base type
return type('', bases, ).__mro__[1:-1]
class ClosedMeta(type):
_register =
# Some methods return type must not change
_exceptions = ('__int__', '__eq__', ...)
def __new__(cls, name, bases, namespace):
# A unique id for the class
uid = max(cls._register) + 1 if cls._register else 0
mro = get_mro(bases)
def tail_cast(f):
"""Cast the return value of f"""
@functools.wraps(f)
def wrapper(*args, **kwargs):
out = f(*args, **kwargs)
if type(out) in mro:
# Since the class does not exist yet, we will recover it later
return cls._register[uid](out)
return out
return wrapper
def deep_tail_cast(f):
"""Cast the return value of f or the return value of f(...)"""
@functools.wraps(f)
def wrapper(*args, **kwargs):
out = f(*args, **kwargs)
if callable(out):
return tail_cast(out)
elif type(out) in mro:
return cls._register[uid](out)
else:
return out
return wrapper
class PropertyCast:
"""Cast the return value of a property"""
def __init__(self, prop):
self.prop = prop
def __get__(self, instance, owner):
return cls._register[uid](self.prop.__get__(instance, owner))
def __set__(self, instance, value):
return self.prop.__set__(instance, value)
def __delete__(self, instance):
return self.prop.__delete__(instance)
for base in reversed(mro):
for name, attr in base.__dict__.items():
if name in ('__getattr__', '__getattribute__'):
namespace[name] = deep_tail_cast(attr)
elif callable(attr) and name not in namespace and name not in cls._exceptions:
namespace[name] = tail_cast(attr)
elif hasattr(attr, '__get__'):
namespace[name] = PropertyCast(attr)
subcls = super().__new__(cls, name, bases, namespace)
cls._register[uid] = subcls
return subcls
Example
class MyInt(int):
def __getattr__(self, _):
return 1
@property
def foo(self):
return 2
class ClosedInt(MyInt, metaclass=ClosedMeta):
pass
x = ClosedInt(2)
print(type(x * x), x * x)
print(type(x.foo), x.foo)
print(type(x.bar), x.bar)
Output
<class '__main__.ClosedIntContainer'> 4
<class '__main__.ClosedIntContainer'> 2
<class '__main__.ClosedIntContainer'> 1
This still has some issues. By example we still have the tedious task to go through all dunder methods an mark down exceptions to the implemented rules, but unless there is a list of those somewhere, this seems unavoidable.
add a comment |Â
up vote
0
down vote
This cannot be done, the data model forbids it. And I can prove it to you:
>>> class MyClass(ClosedInt, metaclass=type):
... def __add__(self, other):
... return 'potato'
...
>>> MyClass(1) + ClosedInt(2)
'potato'
Addition is handled by the left hand object first, and if the left type handles it (i.e. does not return NotImplemented
singleton) then nothing about other
is considered in this operation. If the right hand type is a subclass of the left-hand type, you could control the result with the reflected method __radd__
- but of course that is impossible in the general case.
That would be fine since by definition, closure means ifx
andy
are of typeT
, thenx + y
is of typeT
. HereMyClass()
is not of typeClosedInt
. So the question really applies only if both elements of the operation are of theClosedInt
type
â Olivier Melançon
2 hours ago
Also can not be done, in general case, and the reason is the same - subclasses can override any behavior of parent.
â wim
2 hours ago
1
@wim. See my attempt. It's not perfect, and of course you can bypass it in a multitude of ways, but I think it'll work if you want to play along.
â Mad Physicist
2 hours ago
I'm sure there are ways to make it almost work (I am not the downvoter on your answer, by the way). I just wanted to show that any attempt can be defeated, so this is impossible in the general case.
â wim
2 hours ago
add a comment |Â
up vote
0
down vote
Everyone is writing short codes and meta classes while I barely write a decorator. (damn, lol) But I'm gonna share it anyways.
from functools import wraps
class CLOSED:
_built_ins = [
'__add__', '__sub__', '__mul__', '__floordiv__',
'__div__', '__truediv__', '__mod__', '__divmod__',
'__pow__', '__lshift__', '__rshift__','__and__',
'__or__', '__xor__',
]
@staticmethod
def register_closed(method): # Or you can use type annotations
method.registered = True # Or you can add the method names as string to closed decorator
return method # In this version you decorate the methods with this
@staticmethod
def closed_method(method, cls):
@wraps(method)
def wrapper(*a, **kw):
return cls(method(*a, **kw))
return wrapper
@classmethod
def closed_class(klass, cls):
for magic in klass._built_ins:
_method = getattr(cls, magic, False)
if _method:
setattr(cls, magic, klass.closed_method(_method, cls))
for method in dir(cls):
c1 = method not in klass._built_ins
c2 = method not in dir(object)
c3 = getattr(getattr(cls, method), 'registered', False)
if all((c1, c2, c3)):
_method = getattr(cls, method)
setattr(cls, method, klass.closed_method(_method, cls))
return cls
Now after you have this long setup, you just decorate the class as you would normally do; I am too sleepy to make it work with inherited classes so for now you have to decorate the class that inherits from a closed class.
@CLOSED.closed_class
class foo(int):
@CLOSED.register_closed # or if you can simply add this to CLOSED.closed_class
def bar(self, other): # if you are certain that every method can be casted to its own class
"""Basically just the __add__ method"""
return self + other
print(type(foo(1) + foo(1))); print(foo(1) + foo(1)) # <class '__main__.foo'> 2
print(type(foo(1).bar(2))); print(foo(1).bar(2)) # <class '__main__.foo'> 3
@CLOSED.closed_class
class baz(foo):
pass
print(type(baz(1) + baz(3))); print(baz(1) + baz(3)) # <class '__main__.baz'> 4
print(type(baz(1).bar(4))); print(baz(1).bar(4)) # <class '__main__.baz'> 5
Feel free to downvote as I'm still not sure if I understood the question correctly.
add a comment |Â
up vote
0
down vote
Here is a possible way, also using a metaclass, but just to process the target
parameter to the class. The actual magic is done by __getattribute__
and __getattr__
, which are generated for the specific target. You will most likely have to register a list of exceptions, which will default to the dunder methods unless you define a exceptions
keyword in the class parameter list. For example, __index__
, if defined, really has to return an int
for your class to work.
class DunderSet:
def __contains__(self, x):
return isinstance(x, string) and x.startswith('__') and x.endswith('__')
class ClosedMeta(type):
def __new__(meta, name, bases, dct, **kwargs):
if 'exceptions' in kwargs:
exceptions = tuple(map(str, kwargs.pop('exceptions'))
else:
exceptions = DunderSet()
target = kwargs.pop('target', bases[0] if bases else object)
if kwargs:
raise ValueError(f'Unknown kwarg(s) ", ".join(kwargs)')
def __getattr__(self, attr):
value = target.__getattr__(self, attr)
if attr in exceptions:
return value
if type(value) == target:
return type(self)(value)
if callable(value):
@functools.wraps(value)
def return_wrapper(*args, **kwargs):
ret = value(*args, **kwargs)
return type(self)(ret) if type(ret) == target else ret
return return_wrapper
return value
# ditto for __getattribute__
dct['__getattr__'] = __getattr__
dct['__getattribute__'] = __getattribute__
return super().__new__(meta, name, bases, dct)
This will override any custom __getattr__
or __getattribute__
methods in the class, and it won't close methods that return methods that return the target type. It will also leave non-target return values and attributes completely alone.
And finally, it assumes that you have a wrapper constructor for the target type of the form Proxy(target)
. You could bypass this assumption by adding or requiring an explicit method (call it __cast__
perhaps).
For a type that extends int
, you should not try to override things like __index__
, __int__
, __trunc__
, etc.:
class MyInt(int): pass
class ClosedInt(MyInt, metaclass=ClosedMeta, target=int, exceptions=['__index__', '__int__', '__trunc__']): pass
I've defined the class check on attributes and return values to be exact (as opposed to using isinstance
), so you don't need to explicitly list the methods returning a boolean value for example.
The optional target
keyword lets you select the target class when it's not the first base.
I'm really curious about the downvote. Is there some coding error I missed? I did write this on mobile after all. Perhaps it's late and I spewed some BS by accident?
â Mad Physicist
2 hours ago
This isn't going to do anything for magic methods, and there's a bunch of small errors likecollections.frozenset
ormap(string, ...)
.
â user2357112
1 hour ago
@user2357112. I've fixed that and a couple of other things. Thanks for the feedback
â Mad Physicist
21 mins ago
@user2357112. I don't have anything approaching a reasonable testing environment at the moment, so I appreciate your time spent proofreading this.
â Mad Physicist
1 min ago
add a comment |Â
5 Answers
5
active
oldest
votes
5 Answers
5
active
oldest
votes
active
oldest
votes
active
oldest
votes
up vote
2
down vote
I think using a class decorator with a black list of methods that should not return objects of the same type would be somewhat more Pythonic:
class containerize:
def __call__(self, obj):
if isinstance(obj, type):
return self.decorate_class(obj)
return self.decorate_callable(obj)
def decorate_class(self, cls):
for name in dir(cls):
attr = getattr(cls, name)
if callable(attr) and name not in ('__class__', '__init__', '__new__', '__str__', '__repr__', '__getattribute__'):
setattr(cls, name, self.decorate_callable(attr))
return cls
def decorate_callable(self, func):
def wrapper(obj, *args, **kwargs):
return obj.__class__(func(obj, *args, **kwargs))
return wrapper
so that:
class MyInt(int):
pass
@containerize()
class ClosedIntContainer(MyInt):
pass
i = ClosedIntContainer(3) + ClosedIntContainer(2)
print(i, type(i))
would output:
5 <class '__main__.ClosedIntContainer'>
add a comment |Â
up vote
2
down vote
I think using a class decorator with a black list of methods that should not return objects of the same type would be somewhat more Pythonic:
class containerize:
def __call__(self, obj):
if isinstance(obj, type):
return self.decorate_class(obj)
return self.decorate_callable(obj)
def decorate_class(self, cls):
for name in dir(cls):
attr = getattr(cls, name)
if callable(attr) and name not in ('__class__', '__init__', '__new__', '__str__', '__repr__', '__getattribute__'):
setattr(cls, name, self.decorate_callable(attr))
return cls
def decorate_callable(self, func):
def wrapper(obj, *args, **kwargs):
return obj.__class__(func(obj, *args, **kwargs))
return wrapper
so that:
class MyInt(int):
pass
@containerize()
class ClosedIntContainer(MyInt):
pass
i = ClosedIntContainer(3) + ClosedIntContainer(2)
print(i, type(i))
would output:
5 <class '__main__.ClosedIntContainer'>
add a comment |Â
up vote
2
down vote
up vote
2
down vote
I think using a class decorator with a black list of methods that should not return objects of the same type would be somewhat more Pythonic:
class containerize:
def __call__(self, obj):
if isinstance(obj, type):
return self.decorate_class(obj)
return self.decorate_callable(obj)
def decorate_class(self, cls):
for name in dir(cls):
attr = getattr(cls, name)
if callable(attr) and name not in ('__class__', '__init__', '__new__', '__str__', '__repr__', '__getattribute__'):
setattr(cls, name, self.decorate_callable(attr))
return cls
def decorate_callable(self, func):
def wrapper(obj, *args, **kwargs):
return obj.__class__(func(obj, *args, **kwargs))
return wrapper
so that:
class MyInt(int):
pass
@containerize()
class ClosedIntContainer(MyInt):
pass
i = ClosedIntContainer(3) + ClosedIntContainer(2)
print(i, type(i))
would output:
5 <class '__main__.ClosedIntContainer'>
I think using a class decorator with a black list of methods that should not return objects of the same type would be somewhat more Pythonic:
class containerize:
def __call__(self, obj):
if isinstance(obj, type):
return self.decorate_class(obj)
return self.decorate_callable(obj)
def decorate_class(self, cls):
for name in dir(cls):
attr = getattr(cls, name)
if callable(attr) and name not in ('__class__', '__init__', '__new__', '__str__', '__repr__', '__getattribute__'):
setattr(cls, name, self.decorate_callable(attr))
return cls
def decorate_callable(self, func):
def wrapper(obj, *args, **kwargs):
return obj.__class__(func(obj, *args, **kwargs))
return wrapper
so that:
class MyInt(int):
pass
@containerize()
class ClosedIntContainer(MyInt):
pass
i = ClosedIntContainer(3) + ClosedIntContainer(2)
print(i, type(i))
would output:
5 <class '__main__.ClosedIntContainer'>
edited 20 mins ago
answered 1 hour ago
blhsing
14.6k2630
14.6k2630
add a comment |Â
add a comment |Â
up vote
1
down vote
I still feel there might be a more natural way to accomplish this, but I was able to fix the attempt provided in the question.
Here are the main points that needed ot be fixed.
We must check methods of all classes in the mro, not only the bases;
__getattribute__
and__getattr__
must be treated as special cases;Attributes with
__get__
must be treated separately;We have to write a list of exceptions as methods such as
__int__
or__eq__
obviously should return their expected types.
Code
import functools
def get_mro(bases):
# We omit 'object' as it is the base type
return type('', bases, ).__mro__[1:-1]
class ClosedMeta(type):
_register =
# Some methods return type must not change
_exceptions = ('__int__', '__eq__', ...)
def __new__(cls, name, bases, namespace):
# A unique id for the class
uid = max(cls._register) + 1 if cls._register else 0
mro = get_mro(bases)
def tail_cast(f):
"""Cast the return value of f"""
@functools.wraps(f)
def wrapper(*args, **kwargs):
out = f(*args, **kwargs)
if type(out) in mro:
# Since the class does not exist yet, we will recover it later
return cls._register[uid](out)
return out
return wrapper
def deep_tail_cast(f):
"""Cast the return value of f or the return value of f(...)"""
@functools.wraps(f)
def wrapper(*args, **kwargs):
out = f(*args, **kwargs)
if callable(out):
return tail_cast(out)
elif type(out) in mro:
return cls._register[uid](out)
else:
return out
return wrapper
class PropertyCast:
"""Cast the return value of a property"""
def __init__(self, prop):
self.prop = prop
def __get__(self, instance, owner):
return cls._register[uid](self.prop.__get__(instance, owner))
def __set__(self, instance, value):
return self.prop.__set__(instance, value)
def __delete__(self, instance):
return self.prop.__delete__(instance)
for base in reversed(mro):
for name, attr in base.__dict__.items():
if name in ('__getattr__', '__getattribute__'):
namespace[name] = deep_tail_cast(attr)
elif callable(attr) and name not in namespace and name not in cls._exceptions:
namespace[name] = tail_cast(attr)
elif hasattr(attr, '__get__'):
namespace[name] = PropertyCast(attr)
subcls = super().__new__(cls, name, bases, namespace)
cls._register[uid] = subcls
return subcls
Example
class MyInt(int):
def __getattr__(self, _):
return 1
@property
def foo(self):
return 2
class ClosedInt(MyInt, metaclass=ClosedMeta):
pass
x = ClosedInt(2)
print(type(x * x), x * x)
print(type(x.foo), x.foo)
print(type(x.bar), x.bar)
Output
<class '__main__.ClosedIntContainer'> 4
<class '__main__.ClosedIntContainer'> 2
<class '__main__.ClosedIntContainer'> 1
This still has some issues. By example we still have the tedious task to go through all dunder methods an mark down exceptions to the implemented rules, but unless there is a list of those somewhere, this seems unavoidable.
add a comment |Â
up vote
1
down vote
I still feel there might be a more natural way to accomplish this, but I was able to fix the attempt provided in the question.
Here are the main points that needed ot be fixed.
We must check methods of all classes in the mro, not only the bases;
__getattribute__
and__getattr__
must be treated as special cases;Attributes with
__get__
must be treated separately;We have to write a list of exceptions as methods such as
__int__
or__eq__
obviously should return their expected types.
Code
import functools
def get_mro(bases):
# We omit 'object' as it is the base type
return type('', bases, ).__mro__[1:-1]
class ClosedMeta(type):
_register =
# Some methods return type must not change
_exceptions = ('__int__', '__eq__', ...)
def __new__(cls, name, bases, namespace):
# A unique id for the class
uid = max(cls._register) + 1 if cls._register else 0
mro = get_mro(bases)
def tail_cast(f):
"""Cast the return value of f"""
@functools.wraps(f)
def wrapper(*args, **kwargs):
out = f(*args, **kwargs)
if type(out) in mro:
# Since the class does not exist yet, we will recover it later
return cls._register[uid](out)
return out
return wrapper
def deep_tail_cast(f):
"""Cast the return value of f or the return value of f(...)"""
@functools.wraps(f)
def wrapper(*args, **kwargs):
out = f(*args, **kwargs)
if callable(out):
return tail_cast(out)
elif type(out) in mro:
return cls._register[uid](out)
else:
return out
return wrapper
class PropertyCast:
"""Cast the return value of a property"""
def __init__(self, prop):
self.prop = prop
def __get__(self, instance, owner):
return cls._register[uid](self.prop.__get__(instance, owner))
def __set__(self, instance, value):
return self.prop.__set__(instance, value)
def __delete__(self, instance):
return self.prop.__delete__(instance)
for base in reversed(mro):
for name, attr in base.__dict__.items():
if name in ('__getattr__', '__getattribute__'):
namespace[name] = deep_tail_cast(attr)
elif callable(attr) and name not in namespace and name not in cls._exceptions:
namespace[name] = tail_cast(attr)
elif hasattr(attr, '__get__'):
namespace[name] = PropertyCast(attr)
subcls = super().__new__(cls, name, bases, namespace)
cls._register[uid] = subcls
return subcls
Example
class MyInt(int):
def __getattr__(self, _):
return 1
@property
def foo(self):
return 2
class ClosedInt(MyInt, metaclass=ClosedMeta):
pass
x = ClosedInt(2)
print(type(x * x), x * x)
print(type(x.foo), x.foo)
print(type(x.bar), x.bar)
Output
<class '__main__.ClosedIntContainer'> 4
<class '__main__.ClosedIntContainer'> 2
<class '__main__.ClosedIntContainer'> 1
This still has some issues. By example we still have the tedious task to go through all dunder methods an mark down exceptions to the implemented rules, but unless there is a list of those somewhere, this seems unavoidable.
add a comment |Â
up vote
1
down vote
up vote
1
down vote
I still feel there might be a more natural way to accomplish this, but I was able to fix the attempt provided in the question.
Here are the main points that needed ot be fixed.
We must check methods of all classes in the mro, not only the bases;
__getattribute__
and__getattr__
must be treated as special cases;Attributes with
__get__
must be treated separately;We have to write a list of exceptions as methods such as
__int__
or__eq__
obviously should return their expected types.
Code
import functools
def get_mro(bases):
# We omit 'object' as it is the base type
return type('', bases, ).__mro__[1:-1]
class ClosedMeta(type):
_register =
# Some methods return type must not change
_exceptions = ('__int__', '__eq__', ...)
def __new__(cls, name, bases, namespace):
# A unique id for the class
uid = max(cls._register) + 1 if cls._register else 0
mro = get_mro(bases)
def tail_cast(f):
"""Cast the return value of f"""
@functools.wraps(f)
def wrapper(*args, **kwargs):
out = f(*args, **kwargs)
if type(out) in mro:
# Since the class does not exist yet, we will recover it later
return cls._register[uid](out)
return out
return wrapper
def deep_tail_cast(f):
"""Cast the return value of f or the return value of f(...)"""
@functools.wraps(f)
def wrapper(*args, **kwargs):
out = f(*args, **kwargs)
if callable(out):
return tail_cast(out)
elif type(out) in mro:
return cls._register[uid](out)
else:
return out
return wrapper
class PropertyCast:
"""Cast the return value of a property"""
def __init__(self, prop):
self.prop = prop
def __get__(self, instance, owner):
return cls._register[uid](self.prop.__get__(instance, owner))
def __set__(self, instance, value):
return self.prop.__set__(instance, value)
def __delete__(self, instance):
return self.prop.__delete__(instance)
for base in reversed(mro):
for name, attr in base.__dict__.items():
if name in ('__getattr__', '__getattribute__'):
namespace[name] = deep_tail_cast(attr)
elif callable(attr) and name not in namespace and name not in cls._exceptions:
namespace[name] = tail_cast(attr)
elif hasattr(attr, '__get__'):
namespace[name] = PropertyCast(attr)
subcls = super().__new__(cls, name, bases, namespace)
cls._register[uid] = subcls
return subcls
Example
class MyInt(int):
def __getattr__(self, _):
return 1
@property
def foo(self):
return 2
class ClosedInt(MyInt, metaclass=ClosedMeta):
pass
x = ClosedInt(2)
print(type(x * x), x * x)
print(type(x.foo), x.foo)
print(type(x.bar), x.bar)
Output
<class '__main__.ClosedIntContainer'> 4
<class '__main__.ClosedIntContainer'> 2
<class '__main__.ClosedIntContainer'> 1
This still has some issues. By example we still have the tedious task to go through all dunder methods an mark down exceptions to the implemented rules, but unless there is a list of those somewhere, this seems unavoidable.
I still feel there might be a more natural way to accomplish this, but I was able to fix the attempt provided in the question.
Here are the main points that needed ot be fixed.
We must check methods of all classes in the mro, not only the bases;
__getattribute__
and__getattr__
must be treated as special cases;Attributes with
__get__
must be treated separately;We have to write a list of exceptions as methods such as
__int__
or__eq__
obviously should return their expected types.
Code
import functools
def get_mro(bases):
# We omit 'object' as it is the base type
return type('', bases, ).__mro__[1:-1]
class ClosedMeta(type):
_register =
# Some methods return type must not change
_exceptions = ('__int__', '__eq__', ...)
def __new__(cls, name, bases, namespace):
# A unique id for the class
uid = max(cls._register) + 1 if cls._register else 0
mro = get_mro(bases)
def tail_cast(f):
"""Cast the return value of f"""
@functools.wraps(f)
def wrapper(*args, **kwargs):
out = f(*args, **kwargs)
if type(out) in mro:
# Since the class does not exist yet, we will recover it later
return cls._register[uid](out)
return out
return wrapper
def deep_tail_cast(f):
"""Cast the return value of f or the return value of f(...)"""
@functools.wraps(f)
def wrapper(*args, **kwargs):
out = f(*args, **kwargs)
if callable(out):
return tail_cast(out)
elif type(out) in mro:
return cls._register[uid](out)
else:
return out
return wrapper
class PropertyCast:
"""Cast the return value of a property"""
def __init__(self, prop):
self.prop = prop
def __get__(self, instance, owner):
return cls._register[uid](self.prop.__get__(instance, owner))
def __set__(self, instance, value):
return self.prop.__set__(instance, value)
def __delete__(self, instance):
return self.prop.__delete__(instance)
for base in reversed(mro):
for name, attr in base.__dict__.items():
if name in ('__getattr__', '__getattribute__'):
namespace[name] = deep_tail_cast(attr)
elif callable(attr) and name not in namespace and name not in cls._exceptions:
namespace[name] = tail_cast(attr)
elif hasattr(attr, '__get__'):
namespace[name] = PropertyCast(attr)
subcls = super().__new__(cls, name, bases, namespace)
cls._register[uid] = subcls
return subcls
Example
class MyInt(int):
def __getattr__(self, _):
return 1
@property
def foo(self):
return 2
class ClosedInt(MyInt, metaclass=ClosedMeta):
pass
x = ClosedInt(2)
print(type(x * x), x * x)
print(type(x.foo), x.foo)
print(type(x.bar), x.bar)
Output
<class '__main__.ClosedIntContainer'> 4
<class '__main__.ClosedIntContainer'> 2
<class '__main__.ClosedIntContainer'> 1
This still has some issues. By example we still have the tedious task to go through all dunder methods an mark down exceptions to the implemented rules, but unless there is a list of those somewhere, this seems unavoidable.
edited 2 hours ago
answered 2 hours ago
Olivier Melançon
11.5k11436
11.5k11436
add a comment |Â
add a comment |Â
up vote
0
down vote
This cannot be done, the data model forbids it. And I can prove it to you:
>>> class MyClass(ClosedInt, metaclass=type):
... def __add__(self, other):
... return 'potato'
...
>>> MyClass(1) + ClosedInt(2)
'potato'
Addition is handled by the left hand object first, and if the left type handles it (i.e. does not return NotImplemented
singleton) then nothing about other
is considered in this operation. If the right hand type is a subclass of the left-hand type, you could control the result with the reflected method __radd__
- but of course that is impossible in the general case.
That would be fine since by definition, closure means ifx
andy
are of typeT
, thenx + y
is of typeT
. HereMyClass()
is not of typeClosedInt
. So the question really applies only if both elements of the operation are of theClosedInt
type
â Olivier Melançon
2 hours ago
Also can not be done, in general case, and the reason is the same - subclasses can override any behavior of parent.
â wim
2 hours ago
1
@wim. See my attempt. It's not perfect, and of course you can bypass it in a multitude of ways, but I think it'll work if you want to play along.
â Mad Physicist
2 hours ago
I'm sure there are ways to make it almost work (I am not the downvoter on your answer, by the way). I just wanted to show that any attempt can be defeated, so this is impossible in the general case.
â wim
2 hours ago
add a comment |Â
up vote
0
down vote
This cannot be done, the data model forbids it. And I can prove it to you:
>>> class MyClass(ClosedInt, metaclass=type):
... def __add__(self, other):
... return 'potato'
...
>>> MyClass(1) + ClosedInt(2)
'potato'
Addition is handled by the left hand object first, and if the left type handles it (i.e. does not return NotImplemented
singleton) then nothing about other
is considered in this operation. If the right hand type is a subclass of the left-hand type, you could control the result with the reflected method __radd__
- but of course that is impossible in the general case.
That would be fine since by definition, closure means ifx
andy
are of typeT
, thenx + y
is of typeT
. HereMyClass()
is not of typeClosedInt
. So the question really applies only if both elements of the operation are of theClosedInt
type
â Olivier Melançon
2 hours ago
Also can not be done, in general case, and the reason is the same - subclasses can override any behavior of parent.
â wim
2 hours ago
1
@wim. See my attempt. It's not perfect, and of course you can bypass it in a multitude of ways, but I think it'll work if you want to play along.
â Mad Physicist
2 hours ago
I'm sure there are ways to make it almost work (I am not the downvoter on your answer, by the way). I just wanted to show that any attempt can be defeated, so this is impossible in the general case.
â wim
2 hours ago
add a comment |Â
up vote
0
down vote
up vote
0
down vote
This cannot be done, the data model forbids it. And I can prove it to you:
>>> class MyClass(ClosedInt, metaclass=type):
... def __add__(self, other):
... return 'potato'
...
>>> MyClass(1) + ClosedInt(2)
'potato'
Addition is handled by the left hand object first, and if the left type handles it (i.e. does not return NotImplemented
singleton) then nothing about other
is considered in this operation. If the right hand type is a subclass of the left-hand type, you could control the result with the reflected method __radd__
- but of course that is impossible in the general case.
This cannot be done, the data model forbids it. And I can prove it to you:
>>> class MyClass(ClosedInt, metaclass=type):
... def __add__(self, other):
... return 'potato'
...
>>> MyClass(1) + ClosedInt(2)
'potato'
Addition is handled by the left hand object first, and if the left type handles it (i.e. does not return NotImplemented
singleton) then nothing about other
is considered in this operation. If the right hand type is a subclass of the left-hand type, you could control the result with the reflected method __radd__
- but of course that is impossible in the general case.
edited 2 hours ago
answered 2 hours ago
wim
147k42276397
147k42276397
That would be fine since by definition, closure means ifx
andy
are of typeT
, thenx + y
is of typeT
. HereMyClass()
is not of typeClosedInt
. So the question really applies only if both elements of the operation are of theClosedInt
type
â Olivier Melançon
2 hours ago
Also can not be done, in general case, and the reason is the same - subclasses can override any behavior of parent.
â wim
2 hours ago
1
@wim. See my attempt. It's not perfect, and of course you can bypass it in a multitude of ways, but I think it'll work if you want to play along.
â Mad Physicist
2 hours ago
I'm sure there are ways to make it almost work (I am not the downvoter on your answer, by the way). I just wanted to show that any attempt can be defeated, so this is impossible in the general case.
â wim
2 hours ago
add a comment |Â
That would be fine since by definition, closure means ifx
andy
are of typeT
, thenx + y
is of typeT
. HereMyClass()
is not of typeClosedInt
. So the question really applies only if both elements of the operation are of theClosedInt
type
â Olivier Melançon
2 hours ago
Also can not be done, in general case, and the reason is the same - subclasses can override any behavior of parent.
â wim
2 hours ago
1
@wim. See my attempt. It's not perfect, and of course you can bypass it in a multitude of ways, but I think it'll work if you want to play along.
â Mad Physicist
2 hours ago
I'm sure there are ways to make it almost work (I am not the downvoter on your answer, by the way). I just wanted to show that any attempt can be defeated, so this is impossible in the general case.
â wim
2 hours ago
That would be fine since by definition, closure means if
x
and y
are of type T
, then x + y
is of type T
. Here MyClass()
is not of type ClosedInt
. So the question really applies only if both elements of the operation are of the ClosedInt
typeâ Olivier Melançon
2 hours ago
That would be fine since by definition, closure means if
x
and y
are of type T
, then x + y
is of type T
. Here MyClass()
is not of type ClosedInt
. So the question really applies only if both elements of the operation are of the ClosedInt
typeâ Olivier Melançon
2 hours ago
Also can not be done, in general case, and the reason is the same - subclasses can override any behavior of parent.
â wim
2 hours ago
Also can not be done, in general case, and the reason is the same - subclasses can override any behavior of parent.
â wim
2 hours ago
1
1
@wim. See my attempt. It's not perfect, and of course you can bypass it in a multitude of ways, but I think it'll work if you want to play along.
â Mad Physicist
2 hours ago
@wim. See my attempt. It's not perfect, and of course you can bypass it in a multitude of ways, but I think it'll work if you want to play along.
â Mad Physicist
2 hours ago
I'm sure there are ways to make it almost work (I am not the downvoter on your answer, by the way). I just wanted to show that any attempt can be defeated, so this is impossible in the general case.
â wim
2 hours ago
I'm sure there are ways to make it almost work (I am not the downvoter on your answer, by the way). I just wanted to show that any attempt can be defeated, so this is impossible in the general case.
â wim
2 hours ago
add a comment |Â
up vote
0
down vote
Everyone is writing short codes and meta classes while I barely write a decorator. (damn, lol) But I'm gonna share it anyways.
from functools import wraps
class CLOSED:
_built_ins = [
'__add__', '__sub__', '__mul__', '__floordiv__',
'__div__', '__truediv__', '__mod__', '__divmod__',
'__pow__', '__lshift__', '__rshift__','__and__',
'__or__', '__xor__',
]
@staticmethod
def register_closed(method): # Or you can use type annotations
method.registered = True # Or you can add the method names as string to closed decorator
return method # In this version you decorate the methods with this
@staticmethod
def closed_method(method, cls):
@wraps(method)
def wrapper(*a, **kw):
return cls(method(*a, **kw))
return wrapper
@classmethod
def closed_class(klass, cls):
for magic in klass._built_ins:
_method = getattr(cls, magic, False)
if _method:
setattr(cls, magic, klass.closed_method(_method, cls))
for method in dir(cls):
c1 = method not in klass._built_ins
c2 = method not in dir(object)
c3 = getattr(getattr(cls, method), 'registered', False)
if all((c1, c2, c3)):
_method = getattr(cls, method)
setattr(cls, method, klass.closed_method(_method, cls))
return cls
Now after you have this long setup, you just decorate the class as you would normally do; I am too sleepy to make it work with inherited classes so for now you have to decorate the class that inherits from a closed class.
@CLOSED.closed_class
class foo(int):
@CLOSED.register_closed # or if you can simply add this to CLOSED.closed_class
def bar(self, other): # if you are certain that every method can be casted to its own class
"""Basically just the __add__ method"""
return self + other
print(type(foo(1) + foo(1))); print(foo(1) + foo(1)) # <class '__main__.foo'> 2
print(type(foo(1).bar(2))); print(foo(1).bar(2)) # <class '__main__.foo'> 3
@CLOSED.closed_class
class baz(foo):
pass
print(type(baz(1) + baz(3))); print(baz(1) + baz(3)) # <class '__main__.baz'> 4
print(type(baz(1).bar(4))); print(baz(1).bar(4)) # <class '__main__.baz'> 5
Feel free to downvote as I'm still not sure if I understood the question correctly.
add a comment |Â
up vote
0
down vote
Everyone is writing short codes and meta classes while I barely write a decorator. (damn, lol) But I'm gonna share it anyways.
from functools import wraps
class CLOSED:
_built_ins = [
'__add__', '__sub__', '__mul__', '__floordiv__',
'__div__', '__truediv__', '__mod__', '__divmod__',
'__pow__', '__lshift__', '__rshift__','__and__',
'__or__', '__xor__',
]
@staticmethod
def register_closed(method): # Or you can use type annotations
method.registered = True # Or you can add the method names as string to closed decorator
return method # In this version you decorate the methods with this
@staticmethod
def closed_method(method, cls):
@wraps(method)
def wrapper(*a, **kw):
return cls(method(*a, **kw))
return wrapper
@classmethod
def closed_class(klass, cls):
for magic in klass._built_ins:
_method = getattr(cls, magic, False)
if _method:
setattr(cls, magic, klass.closed_method(_method, cls))
for method in dir(cls):
c1 = method not in klass._built_ins
c2 = method not in dir(object)
c3 = getattr(getattr(cls, method), 'registered', False)
if all((c1, c2, c3)):
_method = getattr(cls, method)
setattr(cls, method, klass.closed_method(_method, cls))
return cls
Now after you have this long setup, you just decorate the class as you would normally do; I am too sleepy to make it work with inherited classes so for now you have to decorate the class that inherits from a closed class.
@CLOSED.closed_class
class foo(int):
@CLOSED.register_closed # or if you can simply add this to CLOSED.closed_class
def bar(self, other): # if you are certain that every method can be casted to its own class
"""Basically just the __add__ method"""
return self + other
print(type(foo(1) + foo(1))); print(foo(1) + foo(1)) # <class '__main__.foo'> 2
print(type(foo(1).bar(2))); print(foo(1).bar(2)) # <class '__main__.foo'> 3
@CLOSED.closed_class
class baz(foo):
pass
print(type(baz(1) + baz(3))); print(baz(1) + baz(3)) # <class '__main__.baz'> 4
print(type(baz(1).bar(4))); print(baz(1).bar(4)) # <class '__main__.baz'> 5
Feel free to downvote as I'm still not sure if I understood the question correctly.
add a comment |Â
up vote
0
down vote
up vote
0
down vote
Everyone is writing short codes and meta classes while I barely write a decorator. (damn, lol) But I'm gonna share it anyways.
from functools import wraps
class CLOSED:
_built_ins = [
'__add__', '__sub__', '__mul__', '__floordiv__',
'__div__', '__truediv__', '__mod__', '__divmod__',
'__pow__', '__lshift__', '__rshift__','__and__',
'__or__', '__xor__',
]
@staticmethod
def register_closed(method): # Or you can use type annotations
method.registered = True # Or you can add the method names as string to closed decorator
return method # In this version you decorate the methods with this
@staticmethod
def closed_method(method, cls):
@wraps(method)
def wrapper(*a, **kw):
return cls(method(*a, **kw))
return wrapper
@classmethod
def closed_class(klass, cls):
for magic in klass._built_ins:
_method = getattr(cls, magic, False)
if _method:
setattr(cls, magic, klass.closed_method(_method, cls))
for method in dir(cls):
c1 = method not in klass._built_ins
c2 = method not in dir(object)
c3 = getattr(getattr(cls, method), 'registered', False)
if all((c1, c2, c3)):
_method = getattr(cls, method)
setattr(cls, method, klass.closed_method(_method, cls))
return cls
Now after you have this long setup, you just decorate the class as you would normally do; I am too sleepy to make it work with inherited classes so for now you have to decorate the class that inherits from a closed class.
@CLOSED.closed_class
class foo(int):
@CLOSED.register_closed # or if you can simply add this to CLOSED.closed_class
def bar(self, other): # if you are certain that every method can be casted to its own class
"""Basically just the __add__ method"""
return self + other
print(type(foo(1) + foo(1))); print(foo(1) + foo(1)) # <class '__main__.foo'> 2
print(type(foo(1).bar(2))); print(foo(1).bar(2)) # <class '__main__.foo'> 3
@CLOSED.closed_class
class baz(foo):
pass
print(type(baz(1) + baz(3))); print(baz(1) + baz(3)) # <class '__main__.baz'> 4
print(type(baz(1).bar(4))); print(baz(1).bar(4)) # <class '__main__.baz'> 5
Feel free to downvote as I'm still not sure if I understood the question correctly.
Everyone is writing short codes and meta classes while I barely write a decorator. (damn, lol) But I'm gonna share it anyways.
from functools import wraps
class CLOSED:
_built_ins = [
'__add__', '__sub__', '__mul__', '__floordiv__',
'__div__', '__truediv__', '__mod__', '__divmod__',
'__pow__', '__lshift__', '__rshift__','__and__',
'__or__', '__xor__',
]
@staticmethod
def register_closed(method): # Or you can use type annotations
method.registered = True # Or you can add the method names as string to closed decorator
return method # In this version you decorate the methods with this
@staticmethod
def closed_method(method, cls):
@wraps(method)
def wrapper(*a, **kw):
return cls(method(*a, **kw))
return wrapper
@classmethod
def closed_class(klass, cls):
for magic in klass._built_ins:
_method = getattr(cls, magic, False)
if _method:
setattr(cls, magic, klass.closed_method(_method, cls))
for method in dir(cls):
c1 = method not in klass._built_ins
c2 = method not in dir(object)
c3 = getattr(getattr(cls, method), 'registered', False)
if all((c1, c2, c3)):
_method = getattr(cls, method)
setattr(cls, method, klass.closed_method(_method, cls))
return cls
Now after you have this long setup, you just decorate the class as you would normally do; I am too sleepy to make it work with inherited classes so for now you have to decorate the class that inherits from a closed class.
@CLOSED.closed_class
class foo(int):
@CLOSED.register_closed # or if you can simply add this to CLOSED.closed_class
def bar(self, other): # if you are certain that every method can be casted to its own class
"""Basically just the __add__ method"""
return self + other
print(type(foo(1) + foo(1))); print(foo(1) + foo(1)) # <class '__main__.foo'> 2
print(type(foo(1).bar(2))); print(foo(1).bar(2)) # <class '__main__.foo'> 3
@CLOSED.closed_class
class baz(foo):
pass
print(type(baz(1) + baz(3))); print(baz(1) + baz(3)) # <class '__main__.baz'> 4
print(type(baz(1).bar(4))); print(baz(1).bar(4)) # <class '__main__.baz'> 5
Feel free to downvote as I'm still not sure if I understood the question correctly.
answered 2 hours ago
Ià Âñk Kaplan
568212
568212
add a comment |Â
add a comment |Â
up vote
0
down vote
Here is a possible way, also using a metaclass, but just to process the target
parameter to the class. The actual magic is done by __getattribute__
and __getattr__
, which are generated for the specific target. You will most likely have to register a list of exceptions, which will default to the dunder methods unless you define a exceptions
keyword in the class parameter list. For example, __index__
, if defined, really has to return an int
for your class to work.
class DunderSet:
def __contains__(self, x):
return isinstance(x, string) and x.startswith('__') and x.endswith('__')
class ClosedMeta(type):
def __new__(meta, name, bases, dct, **kwargs):
if 'exceptions' in kwargs:
exceptions = tuple(map(str, kwargs.pop('exceptions'))
else:
exceptions = DunderSet()
target = kwargs.pop('target', bases[0] if bases else object)
if kwargs:
raise ValueError(f'Unknown kwarg(s) ", ".join(kwargs)')
def __getattr__(self, attr):
value = target.__getattr__(self, attr)
if attr in exceptions:
return value
if type(value) == target:
return type(self)(value)
if callable(value):
@functools.wraps(value)
def return_wrapper(*args, **kwargs):
ret = value(*args, **kwargs)
return type(self)(ret) if type(ret) == target else ret
return return_wrapper
return value
# ditto for __getattribute__
dct['__getattr__'] = __getattr__
dct['__getattribute__'] = __getattribute__
return super().__new__(meta, name, bases, dct)
This will override any custom __getattr__
or __getattribute__
methods in the class, and it won't close methods that return methods that return the target type. It will also leave non-target return values and attributes completely alone.
And finally, it assumes that you have a wrapper constructor for the target type of the form Proxy(target)
. You could bypass this assumption by adding or requiring an explicit method (call it __cast__
perhaps).
For a type that extends int
, you should not try to override things like __index__
, __int__
, __trunc__
, etc.:
class MyInt(int): pass
class ClosedInt(MyInt, metaclass=ClosedMeta, target=int, exceptions=['__index__', '__int__', '__trunc__']): pass
I've defined the class check on attributes and return values to be exact (as opposed to using isinstance
), so you don't need to explicitly list the methods returning a boolean value for example.
The optional target
keyword lets you select the target class when it's not the first base.
I'm really curious about the downvote. Is there some coding error I missed? I did write this on mobile after all. Perhaps it's late and I spewed some BS by accident?
â Mad Physicist
2 hours ago
This isn't going to do anything for magic methods, and there's a bunch of small errors likecollections.frozenset
ormap(string, ...)
.
â user2357112
1 hour ago
@user2357112. I've fixed that and a couple of other things. Thanks for the feedback
â Mad Physicist
21 mins ago
@user2357112. I don't have anything approaching a reasonable testing environment at the moment, so I appreciate your time spent proofreading this.
â Mad Physicist
1 min ago
add a comment |Â
up vote
0
down vote
Here is a possible way, also using a metaclass, but just to process the target
parameter to the class. The actual magic is done by __getattribute__
and __getattr__
, which are generated for the specific target. You will most likely have to register a list of exceptions, which will default to the dunder methods unless you define a exceptions
keyword in the class parameter list. For example, __index__
, if defined, really has to return an int
for your class to work.
class DunderSet:
def __contains__(self, x):
return isinstance(x, string) and x.startswith('__') and x.endswith('__')
class ClosedMeta(type):
def __new__(meta, name, bases, dct, **kwargs):
if 'exceptions' in kwargs:
exceptions = tuple(map(str, kwargs.pop('exceptions'))
else:
exceptions = DunderSet()
target = kwargs.pop('target', bases[0] if bases else object)
if kwargs:
raise ValueError(f'Unknown kwarg(s) ", ".join(kwargs)')
def __getattr__(self, attr):
value = target.__getattr__(self, attr)
if attr in exceptions:
return value
if type(value) == target:
return type(self)(value)
if callable(value):
@functools.wraps(value)
def return_wrapper(*args, **kwargs):
ret = value(*args, **kwargs)
return type(self)(ret) if type(ret) == target else ret
return return_wrapper
return value
# ditto for __getattribute__
dct['__getattr__'] = __getattr__
dct['__getattribute__'] = __getattribute__
return super().__new__(meta, name, bases, dct)
This will override any custom __getattr__
or __getattribute__
methods in the class, and it won't close methods that return methods that return the target type. It will also leave non-target return values and attributes completely alone.
And finally, it assumes that you have a wrapper constructor for the target type of the form Proxy(target)
. You could bypass this assumption by adding or requiring an explicit method (call it __cast__
perhaps).
For a type that extends int
, you should not try to override things like __index__
, __int__
, __trunc__
, etc.:
class MyInt(int): pass
class ClosedInt(MyInt, metaclass=ClosedMeta, target=int, exceptions=['__index__', '__int__', '__trunc__']): pass
I've defined the class check on attributes and return values to be exact (as opposed to using isinstance
), so you don't need to explicitly list the methods returning a boolean value for example.
The optional target
keyword lets you select the target class when it's not the first base.
I'm really curious about the downvote. Is there some coding error I missed? I did write this on mobile after all. Perhaps it's late and I spewed some BS by accident?
â Mad Physicist
2 hours ago
This isn't going to do anything for magic methods, and there's a bunch of small errors likecollections.frozenset
ormap(string, ...)
.
â user2357112
1 hour ago
@user2357112. I've fixed that and a couple of other things. Thanks for the feedback
â Mad Physicist
21 mins ago
@user2357112. I don't have anything approaching a reasonable testing environment at the moment, so I appreciate your time spent proofreading this.
â Mad Physicist
1 min ago
add a comment |Â
up vote
0
down vote
up vote
0
down vote
Here is a possible way, also using a metaclass, but just to process the target
parameter to the class. The actual magic is done by __getattribute__
and __getattr__
, which are generated for the specific target. You will most likely have to register a list of exceptions, which will default to the dunder methods unless you define a exceptions
keyword in the class parameter list. For example, __index__
, if defined, really has to return an int
for your class to work.
class DunderSet:
def __contains__(self, x):
return isinstance(x, string) and x.startswith('__') and x.endswith('__')
class ClosedMeta(type):
def __new__(meta, name, bases, dct, **kwargs):
if 'exceptions' in kwargs:
exceptions = tuple(map(str, kwargs.pop('exceptions'))
else:
exceptions = DunderSet()
target = kwargs.pop('target', bases[0] if bases else object)
if kwargs:
raise ValueError(f'Unknown kwarg(s) ", ".join(kwargs)')
def __getattr__(self, attr):
value = target.__getattr__(self, attr)
if attr in exceptions:
return value
if type(value) == target:
return type(self)(value)
if callable(value):
@functools.wraps(value)
def return_wrapper(*args, **kwargs):
ret = value(*args, **kwargs)
return type(self)(ret) if type(ret) == target else ret
return return_wrapper
return value
# ditto for __getattribute__
dct['__getattr__'] = __getattr__
dct['__getattribute__'] = __getattribute__
return super().__new__(meta, name, bases, dct)
This will override any custom __getattr__
or __getattribute__
methods in the class, and it won't close methods that return methods that return the target type. It will also leave non-target return values and attributes completely alone.
And finally, it assumes that you have a wrapper constructor for the target type of the form Proxy(target)
. You could bypass this assumption by adding or requiring an explicit method (call it __cast__
perhaps).
For a type that extends int
, you should not try to override things like __index__
, __int__
, __trunc__
, etc.:
class MyInt(int): pass
class ClosedInt(MyInt, metaclass=ClosedMeta, target=int, exceptions=['__index__', '__int__', '__trunc__']): pass
I've defined the class check on attributes and return values to be exact (as opposed to using isinstance
), so you don't need to explicitly list the methods returning a boolean value for example.
The optional target
keyword lets you select the target class when it's not the first base.
Here is a possible way, also using a metaclass, but just to process the target
parameter to the class. The actual magic is done by __getattribute__
and __getattr__
, which are generated for the specific target. You will most likely have to register a list of exceptions, which will default to the dunder methods unless you define a exceptions
keyword in the class parameter list. For example, __index__
, if defined, really has to return an int
for your class to work.
class DunderSet:
def __contains__(self, x):
return isinstance(x, string) and x.startswith('__') and x.endswith('__')
class ClosedMeta(type):
def __new__(meta, name, bases, dct, **kwargs):
if 'exceptions' in kwargs:
exceptions = tuple(map(str, kwargs.pop('exceptions'))
else:
exceptions = DunderSet()
target = kwargs.pop('target', bases[0] if bases else object)
if kwargs:
raise ValueError(f'Unknown kwarg(s) ", ".join(kwargs)')
def __getattr__(self, attr):
value = target.__getattr__(self, attr)
if attr in exceptions:
return value
if type(value) == target:
return type(self)(value)
if callable(value):
@functools.wraps(value)
def return_wrapper(*args, **kwargs):
ret = value(*args, **kwargs)
return type(self)(ret) if type(ret) == target else ret
return return_wrapper
return value
# ditto for __getattribute__
dct['__getattr__'] = __getattr__
dct['__getattribute__'] = __getattribute__
return super().__new__(meta, name, bases, dct)
This will override any custom __getattr__
or __getattribute__
methods in the class, and it won't close methods that return methods that return the target type. It will also leave non-target return values and attributes completely alone.
And finally, it assumes that you have a wrapper constructor for the target type of the form Proxy(target)
. You could bypass this assumption by adding or requiring an explicit method (call it __cast__
perhaps).
For a type that extends int
, you should not try to override things like __index__
, __int__
, __trunc__
, etc.:
class MyInt(int): pass
class ClosedInt(MyInt, metaclass=ClosedMeta, target=int, exceptions=['__index__', '__int__', '__trunc__']): pass
I've defined the class check on attributes and return values to be exact (as opposed to using isinstance
), so you don't need to explicitly list the methods returning a boolean value for example.
The optional target
keyword lets you select the target class when it's not the first base.
edited 3 mins ago
answered 2 hours ago
Mad Physicist
29.1k145982
29.1k145982
I'm really curious about the downvote. Is there some coding error I missed? I did write this on mobile after all. Perhaps it's late and I spewed some BS by accident?
â Mad Physicist
2 hours ago
This isn't going to do anything for magic methods, and there's a bunch of small errors likecollections.frozenset
ormap(string, ...)
.
â user2357112
1 hour ago
@user2357112. I've fixed that and a couple of other things. Thanks for the feedback
â Mad Physicist
21 mins ago
@user2357112. I don't have anything approaching a reasonable testing environment at the moment, so I appreciate your time spent proofreading this.
â Mad Physicist
1 min ago
add a comment |Â
I'm really curious about the downvote. Is there some coding error I missed? I did write this on mobile after all. Perhaps it's late and I spewed some BS by accident?
â Mad Physicist
2 hours ago
This isn't going to do anything for magic methods, and there's a bunch of small errors likecollections.frozenset
ormap(string, ...)
.
â user2357112
1 hour ago
@user2357112. I've fixed that and a couple of other things. Thanks for the feedback
â Mad Physicist
21 mins ago
@user2357112. I don't have anything approaching a reasonable testing environment at the moment, so I appreciate your time spent proofreading this.
â Mad Physicist
1 min ago
I'm really curious about the downvote. Is there some coding error I missed? I did write this on mobile after all. Perhaps it's late and I spewed some BS by accident?
â Mad Physicist
2 hours ago
I'm really curious about the downvote. Is there some coding error I missed? I did write this on mobile after all. Perhaps it's late and I spewed some BS by accident?
â Mad Physicist
2 hours ago
This isn't going to do anything for magic methods, and there's a bunch of small errors like
collections.frozenset
or map(string, ...)
.â user2357112
1 hour ago
This isn't going to do anything for magic methods, and there's a bunch of small errors like
collections.frozenset
or map(string, ...)
.â user2357112
1 hour ago
@user2357112. I've fixed that and a couple of other things. Thanks for the feedback
â Mad Physicist
21 mins ago
@user2357112. I've fixed that and a couple of other things. Thanks for the feedback
â Mad Physicist
21 mins ago
@user2357112. I don't have anything approaching a reasonable testing environment at the moment, so I appreciate your time spent proofreading this.
â Mad Physicist
1 min ago
@user2357112. I don't have anything approaching a reasonable testing environment at the moment, so I appreciate your time spent proofreading this.
â Mad Physicist
1 min ago
add a comment |Â
Sign up or log in
StackExchange.ready(function ()
StackExchange.helpers.onClickDraftSave('#login-link');
);
Sign up using Google
Sign up using Facebook
Sign up using Email and Password
Post as a guest
StackExchange.ready(
function ()
StackExchange.openid.initPostLogin('.new-post-login', 'https%3a%2f%2fstackoverflow.com%2fquestions%2f52341182%2fhow-to-create-a-type-that-is-closed-under-inherited-operations%23new-answer', 'question_page');
);
Post as a guest
Sign up or log in
StackExchange.ready(function ()
StackExchange.helpers.onClickDraftSave('#login-link');
);
Sign up using Google
Sign up using Facebook
Sign up using Email and Password
Post as a guest
Sign up or log in
StackExchange.ready(function ()
StackExchange.helpers.onClickDraftSave('#login-link');
);
Sign up using Google
Sign up using Facebook
Sign up using Email and Password
Post as a guest
Sign up or log in
StackExchange.ready(function ()
StackExchange.helpers.onClickDraftSave('#login-link');
);
Sign up using Google
Sign up using Facebook
Sign up using Email and Password
Sign up using Google
Sign up using Facebook
Sign up using Email and Password
1
So, this mgiht be nitpicky, but
class BasicIntContainer(int)
is not an int container, it is an int.â juanpa.arrivillaga
3 hours ago
@juanpa.arrivillaga I might be off in my terminology, how would you call such a construct?
â Olivier Melançon
3 hours ago
2
It is simply inheritance, an "is-a" relationship in the parlance of OOP. If you hover over the containers tag, you'll see that: "A container is a class, a data structure, or an abstract data type whose instances are collections of other objects.". So like a
list
or adict
. Here, aBasicIntContainer
instance isn't a collection of other objects, it is a specialized kind ofint
object. I don't want to distract from an interesting question with more terminology nitpicking. Anyway, I would just rephrase as "how to create a type that is closed under inherited operations"â juanpa.arrivillaga
3 hours ago
You can easily write a simple decorator if you just want to cast return values to type itself, but the problem with that would be that there may be methods that wouldn't be returning values that would be able to cast to the proper type, if that is the case; you would need to distinguish them somehow, if not you can just decorate the class with a decorator that would decorate every method.
â Ià Âñk Kaplan
3 hours ago
@Ià ÂñkKaplan For simplification, let's assume that the
__new__
knows how to cast from all of its superclasses. Also, this decorator solution is basicaly what I implemented with a metaclass. I am currently fixing it, but it is getting very complex and I am for sure forgetting a lot of cornercases.â Olivier Melançon
3 hours ago