It looks like currently extending numeric classes through subclassing is not supported.
For example, suppose I wish to extend Fraction class with analogs of limit_denominator method for directional rounding (call them limit_denominator_above and limit_denominator_below, for example). I can also easily imagine use cases for subclassing other numeric classes.
However, just subclassing does not just work, because unless I overload all class operators in a subclass, they keep returning instances of the base class:
>>> from fractions import Fraction
>>> class F(Fraction): pass
...
>>> x = F(1)
>>> x
F(1, 1)
>>> y = F(2)
>>> y
F(2, 1)
>>> x + y
Fraction(3, 1)
Same problem with int and other numeric classes.
This looks to me like extending numeric classes is actively discouraged. What is the rationale for this?
Most classes are not designed to be subclassed. Designing them for subclassing would impose other constraints e.g. int.__add__ might be slower if it needed to allow subclasses to override its behaviour. It is also ambiguous what binary operators should do if there are multiple subclasses:
class F(Fraction): pass
class G(Fraction): pass
F(1) + G(2) # what type should this be?
I think that you should not subclass these classes and instead you can just make functions that do what you want.
That’s because the behaviour of a subclass is, unless overridden, the behaviour of the parent class.
You’re not trying to make a subclass, you’re really trying to change the parent class - monkeypatch in an additional couple of methods. That’s not the job of subclassing. You’re not trying to create a variant type of fraction, you’re trying to change how fractions work. Instead, make stand-alone functions, and have them return normal fractions.
No. The behavior of such operations is not ambiguous. If the class provides a __add__, it will be F(1).__add__(G(2))if the class however provides no __add__ but instead a __radd__, it will do G(2).__radd__(F(1)). If neither is defined, a TypeError is raised.
This behavior has been like this for a quite long time (not to say forever), and it’s unlikely to change too, as far as I know. The behavior is also pretty well know, and documented quite heavily.
I think that if a class is not intended to be subclassed, this should better be documented somewhere. Maybe even an error should be raised when attempting to subclass.
I see this the other way round: you should assume that no class is intended to be subclassed unless there is some documentation that explains how it can be subclassed. That documentation should specify what the contract is for subclasses which would be something like “subclasses can override method foo and the superclass bar method will use foo to do X”.
In the case of Fraction there would need to be a contract that there is say a class method new() that the superclass promises to use always when constructing new instances and subclasses can override that method to change what type is returned by all other methods.
Of course this is Python though so nothing stops you from subclassing stuff but I would think of subclassing a random class as being in the same realm as monkeypatching. Nothing stops you from doing it and maybe sometimes it is useful but usually you should avoid things like that and just write a function that does whatever you want it to do.
I, and probably most other users of Python, do see it exactly as @alexeymuranov does. There is a reason why class B(A): ... works, even when A has no explicitly exposed __init_subclass__. Then again, there even is a reason __init_subclass__ exists, and doesn’t raise any errors by default.
Perhaps you would like to look I to a few sources (I used ChatGPT with the prompt to find sources for this discussion, tbh I was too lazy to scroll through the PEP index myself). In PEP 253 the subclassing of types is defined. The AI also pointed me to this part of documentation from Python 2.2.
(Edit: Fixed link formatting, mobile discourse decided to break it)
I don’t feel any need to discuss things that I already know with ChatGPT. If you have an interesting point to make then feel free to use your own words to say it here rather than pointing at chatbots.
Neither of your comments in this thread adds any meaningful information as the first misunderstands what it is replying to and the second is only irrelevant things and includes a random reference to ChatGPT.
I have no idea what ChatGPT said to you but I do know that most classes are not designed to be subclassed and that most classes don’t have any subclasses. That is the default position and there is no need to put a note saying “don’t subclass this” because usually (as in the case of Fraction) there isn’t actually any good reason for subclassing.
“this is our long-standing rule” (compatibility break)
For subclasses, we cannot guarantee that constructor has expected signature and semantic.
That’s a good argument for built-in types. But not for Fraction’s and, maybe, even not for Decimal’s.
I think that subclassing support should be discussed on case-by-case basis. What is fine for int’s may look odd for Fraction’s.
I don’t think so. Both classes have all required interfaces of the numbers ABC and it’s well defined, that F(1) + G(2) is F(1).__add__(G(2)) if F(1)knows how to work with G(2) object. This is the case with default support for mixed arithmetic, coming from the base class.
Yes, you will got other subtype with a different order of operands, unless one of subclasses “knows” about other and override the _operator_fallbacks() accordingly. Not much different wrt Fraction+gmpy2.mpq case, in fact.
See referenced issue as an example.
Fractions are special. In some sense it’s a container data type. Same code would work, for example, if numerator/denominator’s are gmpy2’s integers. Any instance of the numbers.Integer ABC is ok, in fact.
But instead people are forced to reinvent the wheel.
For clarity, please remember that these types ARE all subclassable. And the behaviour of subclasses is that, unless you choose otherwise, they do what the parent class did. If you take a string’s length, that’s an integer; if you subclass str and take the length of that, that’s still an integer. That’s what’s going on here.
When you add two ints, or two Fractions, or two floats, you get back a new object that is constructed according to those rules. Unless the subclass changes the behaviour, the parent class is free to construct an instance of the base class.
I think it would be useful to have a class decorator that wraps all the parent’s arithmetic operators and returns an instance the subclass (so it would be like def __add__(self, other): return type(self)(super().__add__(self, other)) for each of the special methods, but just remember that the default behaviour IS correct for subclassing.
class int2(int):
def __mul__(self, other):
# here you, say, fallback to own FFT-multiplication for large operands
and have int2() closed wrt arithemetics.
This will reduce boilerplate for overriding all arithmetic methods in the child class, but I doubt it’s a practical approach. And different numeric types have different sets of dunder methods.
What I want to say: current support for subclassing of numeric types is fine in most cases, but it shouldn’t be same for all stdlib types, just for consistency. IMO, for some types it does make sense to provide more.
I am trying to make a subclass, I am not trying to change the parent class. I am trying to create an extended variant of Fraction type with some added behaviour that does not interfere with the existing behaviour, I am not trying to change how Fraction works.
If instead of subclassing I monkey-patched the class, this would have worked without issues.
Yes. That’s what I mean. You’re doing something that is more closely aligned with monkeypatching than with subclassing.
Subclassing is when you are defining a narrower kind of thing. For example, you might take the existing http.server.BaseHTTPRequestHandler class, which represents all possible HTTP request handlers, and make your own subclass that is just your specific HTTP request handler. Broadly speaking, when you subclass something, there are fewer possible instances of your subclass than there were of the parent class, because some things will be the parent class but won’t be your subclass. [1] You get MORE functionality but FEWER instances.
What you’re doing here is not defining a different kind of Fraction. You aren’t saying “imagine all fractions, but now look at just the ones that…” for some qualifying rule. You want to change the behaviour of ALL Fractions. That’s monkeypatching’s job. That’s not subclassing.
And that’s where the default behaviour comes in. The normal behaviour of subclasses is that they do exactly what the parent did, neither more nor less. ALL request handlers will record the client_address as a tuple of host and port, therefore your subclass of the request handler will do that too. That’s great! That’s what you want with subclasses. And if you took a request handler that only takes care of GET requests, and you subclass it and also handle POST requests, sure! You don’t need to write a do_GET() method, because that already exists, and does what you want. But if you also want to change what do_GET() does, so that maybe it returns a response in Klingon instead of English, then you need to override that.
Trying to shoehorn this into a subclass is fine, and it certainly may be what you end up doing, but don’t mistake the default behaviour for being “impossible to subclass”. It’s just that you’re trying to “subclass with monkeypatch semantics”, where the class is possible to “subclass with subclassing semantics” instead.
I am not doing anything. I am saying that monkeypatching would have worked without issues (if I modified the source code of Fraction).
I am not convinced by your narrow interpretation of subclassing (compare it with LSP, for example), but I understand that it is a complicated subject (square – rectangle problem, for example).
So, let me put is simple: I need a class that is mostly identical to Fraction, or to some other built-in class, but has a few extra methods or properties. What is the “pythonic“ way to get it?
Easy. Subclass. It will behave identically to every other Fraction, other than that. Just be aware that behaving identically to other Fractions includes how it behaves under arithmetic operations - that is to say, it results in a Fraction.
Do you see why what you’re asking for isn’t actually what you’re looking for?
You are insisting that it should instead be f.limit_denominator_below() which requires creating a new class (or monkey-patching) and then you are asking how you can create that class without having to write all the code for it. It seems like subclassing is close to getting what you wanted but it doesn’t work in the way that you wanted because the class was not designed for that.
It could be possible to change the Fraction class so that what you wanted to do would work better but then the question is whether it is reasonable to impose that constraint on the design and implementation of the Fraction class which comes back to whether there are good use cases for doing this. Here I would say that no there are not good use cases and instead there should be more understanding that some classes are just not intended to be subclassed. The extra methods that you wanted can just be functions.
I agree with @skirpichev that in general this is a question to be considered on a case-by-case basis but I think it is also important to see the slippery slope here: does every class need to be designed with the expectation that someone will subclass it even if the author of the class does not think that there are situations where it would be useful to do that?
I would say that the answer to this question is absolutely no. The default position should be that classes are not intended to be subclassed unless otherwise stated and the default way to get a function that operates with instances of someone else’s class is to write a normal function.
I think I know better what I need. If you think that what I am asking for would be a bad design or otherwise suboptimal, that would be a valid objection. But then you should ague that the Fraction class is also poorly designed, and limit_denominator should be a function, rather than a method.
I might also say that when someone says they need OOP, the word “need” is too strong,
Yes this requires a bit of boiler plate because you need to write code for all the methods that you’re going to use.
But it is a correct, robust pattern, of which you can guarantee that it will have the behavior that you need.
When making a class you can choose which methods to add. That is not the same as saying that anyone else should be able to add methods to that class or that the class should support any kind of subclassing use case.
I just realised what you were intending to do by subclassing Fraction which is to have the subclass break the internal invariants of the superclass by changing the type of its internal members. That is exactly the kind of thing that someone authoring a class like Fraction should be able to declare as totally unsupported and illustrates clearly why arbitrary kinds of subclassing should not be considered reasonable by default.
Why should the maintainer of the Fraction class have to take into account the possibility that some random subclass is monkeying the internal state like that?
I also don’t understand why you would want to replace the numerator and denominator with gmpy2.mpz since if you have gmpy2 installed then you have gmpy2.mpq so why use Fraction at all?