-
Notifications
You must be signed in to change notification settings - Fork 114
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Interoperability subclassing Java classes with Python classes is poor #470
Comments
The MiscUtils class is pretty simple in Java (this is production code):
So, we've got two functions in Java, numIntsNeededToStoreBytes() and extractBits() in the Python we add py_native_test_bit() which is pure Python, py_call_numIntsNeededToStoreBytes() which is pure Python and attempts to call self.numIntsNeededToStoreBytes(), and obviously subclass numIntsNeededToStoreBytes() which tries to attain it's super but emulates the functionality of the super doesn't exist (because I know we can't resolve it). This gives us the results which I think are surprising as it really means subclassing is VERY limited:
What I glean from this is:
Would that be a fair assessment, and if so which are not intended and which are to be Graal behaviors for the 25.x milestone, if it is possible to make that judgment? |
This comment has been minimized.
This comment has been minimized.
Thank you for the feedback. Re: Java exception handling. The problem with calling methods on a Java exception sounds to me like a bug, I would expect this to just work. @eregon could you please have a look (and also fix the docs)? Re: Java subclassing. Subclassing Java classes is notoriously difficult, we'll need more time to evaluate what we can improve. Re: Jython functionality might not be able to be fine-grained enabled/disabled.
I think we can add two options that enable those things individually and then have |
As of 25.0.0-ea.05 this now works:
About subclassing: you are basically right, we create actually a compound from a Java object and a Python object, and they reference each other (using
You are defining a Python class, instances of which are used as a delegate from a Java object. You cannot access these members from Java, but from the Python defined code on the
Yes, the instance is of a generated Java subclass of the Java superclass or interface you specify, and it automatically delegates to an instance of a Python class that you defined with your method. But to get back to Java, you use
Static members are not exposed on the instance. You can use
That
Inside the Python-defined methods, you are in a Python object. From the outside, however, i.e., what the
You can call the Java methods, just not static methods. See above, you need to go via
Yes, the
Yes, this is not governed by that flag. If you want the public abstract class MiscUtils {
public String someInstanceMethod() {
return "an instance method on the Java side";
}
public static int mask(int v) {
return v;
}
public static int numIntsNeededToStoreBytes(int byteCount) {
return (byteCount + 3) / 4;
}
public static long extractBits(long from, int lsb, int width) {
return (from >>> lsb) & mask(width);
}
} import java
java.add_to_classpath(".")
MiscUtils = java.type("MiscUtils")
class Misc(MiscUtils):
java_static_methods = MiscUtils
def __init__(self):
print("Misc.__init__ runs to initialize the Python object, still without a connection to the Java side")
self.misc_value = 42
def __getattr__(self, name):
if hasattr(self.__super__, name):
return getattr(self.__super__, name)
if hasattr(self.java_static_methods, name):
return getattr(self.java_static_methods, name)
raise AttributeError(name)
def numIntsNeededToStoreBytes(self, byteCount):
print(f"== Misc(MiscUtils).numIntsNeededToStoreBytes({byteCount=}) overrides a Java method, so calls to the Java object go here")
return self.java_static_methods.numIntsNeededToStoreBytes(byteCount)
def py_native_test_bit(self, val, bit):
return val & (1 << bit)
def py_call_numIntsNeededToStoreBytes(self, byteCount):
print(f" --> inside py_call_numIntsNeededToStoreBytes, which is not callable from Java at all")
_ = self.misc_value
y = self.numIntsNeededToStoreBytes(10)
x = self.py_native_test_bit(y if y else 7, 1)
return y if x else None
misc = Misc()
print(f"{misc.getClass().static.mask(10)=}")
print(f"{misc.this.mask(10)=}")
print(f"{misc.this.py_call_numIntsNeededToStoreBytes(10)=}")
print(f"{misc.this.py_native_test_bit(7, 1)=}")
print(f"{misc.getClass().static.extractBits(7, 1, 2)=}")
print(f"{misc.this.extractBits(7, 1, 2)=}")
print(f"{misc.this.misc_value=}")
print(f"{misc.someInstanceMethod()=}")
print(f"{type(misc).mro()=}")
print(f"{issubclass(Misc, MiscUtils)=}")
print(f"{isinstance(Misc(), MiscUtils)=}") > python misc.py
Misc.__init__ runs to initialize the Python object, still without a connection to the Java side
misc.getClass().static.mask(10)=10
misc.this.mask(10)=10
--> inside py_call_numIntsNeededToStoreBytes, which is not callable from Java at all
== Misc(MiscUtils).numIntsNeededToStoreBytes(byteCount=10) overrides a Java method, so calls to the Java object go here
misc.this.py_call_numIntsNeededToStoreBytes(10)=3
misc.this.py_native_test_bit(7, 1)=2
misc.getClass().static.extractBits(7, 1, 2)=2
misc.this.extractBits(7, 1, 2)=2
misc.this.misc_value=42
misc.someInstanceMethod()='an instance method on the Java side'
type(misc).mro()=[<class 'polyglot.ForeignObject'>, <class 'object'>]
issubclass(Misc, MiscUtils)=False
Misc.__init__ runs to initialize the Python object, still without a connection to the Java side
isinstance(Misc(), MiscUtils)=True |
We will work to make this nicer for 25.0, but for 24.2 (which is due out next and feature freezes this week) the above may be the best we can do right now. |
I meant I don't think it is broken in that that wouldn't be the case where the Java class had a constructor? I have another class which has three constructors all with different numbers of parameters (there's a 0, 2 and 4 argument version), and calling
It worked perfectly! Where I found the differences here in the first place as the end code I had to write here was confusing in that to access the Java members I could just use {w,x,y}.name_of_attr but to add new members from Python they ended up in {w,x,y}.this.name_of_attr, even if they had the same name. This isn't how it usually works in Python in that you expect 'instance.anything' to "follow the mro and find the first one that responds favorably e.g. to Then I realized I couldn't actually access any of the Java members inside the class definition, e.g. I wanted to provide replacements for p3 and p4 while still calling the second constructor variant and e.g. create a Python function to modify one of the members
I figured okay,
Having this over in For what it's worth, Jython isn't a lot better - first of all it uses the crusty Python 2.x super() semantics in that there is no no-args version of it, you have to be explicit. Historically it hasn't worked well, or been documented differently, so most people have code that looks like this[1]:
And this is fine, while in Python this is wildly inconvenient to hardcode your parent class, in Jython multiple inheritance is restricted (because it isn't possible in Java). As far as I could tell in my code here, this doesn't work either - calling JavaCP.init is an invalid instantiation of foreign object. I do confuse myself sometimes as I am always going back to "super() considered super!"[2] (there are two recipes[3][4] linked at the bottom with the two syntaxes, and then I cross-reference all of the Jython scripts I have to hand), since I can never remember whether I provide self or not to the named class vs. super(CP, self).init(p1...), what the parameter types need to be, but it doesn't work any which way as far as I tested. Please don't let this come across as a disagreement, since I am struggling to keep in mind 3 implementations of a language (CPython 2, CPython 3 and Jython) and now a fourth I'm trying to get some clarity. Your workaround via If you need to literally override What I would love to see is either one of rhettinger's examples working where e.g. Shape, Root were Java and the rest of the Python effectively unchanged. I am not sure I care if it looks like the Python 2 version or the CPython 3.x version, or the referenced dirt-old Jython docs, or the new Jython docs[5], but the resultant code is the same for all of them with no code changes besides that little bit of extra sugar for finding parent classes. As long as the raw meat of the interfacing stays inside the class definition and not in users of the instances? Thank you in advance however it goes :) [1] https://www.jython.org/jython-old-sites/archive/21/docs/subclassing.html |
Tested version: 25.0.0 ea.04 (CE and Oracle JVMs)
(apologies for the length in advance)
We are running an experiment to investigate whether we can replace or augment our embedded Python technology in products with Graal. Our product has significant amounts of legacy Jython code -- a good amount is not written by us and we must maintain some customer compatibility with minimal changes -- and obviously both our customers and our internal teams therefore expect some Jython semantics to be in place. We've accepted there may be significant differences but we are confident that we can work around the vast majority of them with decorators or modules. However the code relies on some behavior that is not present. These all might be the same issue underneath.
Java exception handling
On previous versions (~24.1) this was simply not working - the objects you got back from:
.. or thrown by internal Java classes were not valid objects and had strange behavior in that it was a ForeignObject etc. that had none of the expected BaseException semantics, nor as it would have been in Jython, been a carbon copy of the Java Exception (up to and including any custom methods).
We are very pleased with the changes moiving from 24.1.1 to 25.0.0 ea.04 in that now we do not need to supply --python.EmulateJython=true to get anything to work, and that we get back as 'e' a very Pythonic BaseException-based object.
Unfortunately we have exceptions thrown by our own internal classes which have custom methods (e.g. OurException.getErrorCode() returns a number as encoded in the message string, since we have a library that acts a little bit like strerror() and comes up with a much more productive string. Don't judge.. it is an old bit of code). There seems to be no way to fetch the original Java exception object or access it as we would in Jython here.
Note that the documentation here doesn't seem correct:
https://github.com/oracle/graalpython/blob/c208cfb8984b4c650c86aea92beef7b2ceb36725/docs/user/Python-on-JVM.md#exceptions-from-java
.. in that
a) --python.EmulateJython=true is not necessary at all (we like this behavior, although either way b occurs)
b) e.getMessage() does not work in the example as e is not the original Java exception anymore.
The code in the example in the docs is exactly our litmus test for it.
Given that we want to call random unhealthy methods on Exception classes which is not very standard, Pythonic or Java-ic of us, this is something we would be happy to accept needs a little bit of porting. We did expect that e.super or something of that ilk would give us the original class but it does not.
Summary: Semantics are different from Jython in that the Exception object caught does not have the methods or members of the original Java Exception object. There is no way to walk the MRO or find the original object to access those methods.
Java subclassing
The documentation here (commit-ish is the master branch as of today):
https://github.com/oracle/graalpython/blob/c208cfb8984b4c650c86aea92beef7b2ceb36725/docs/user/Python-on-JVM.md#inheritance-from-java
Has a very confusing example. We have found that while this example works fine, there are two major issues (and one minor) with it.
a) (Minor) This is absolutely not compatible with Jython, although this is stated in the documentation. We also noted that --python.EmulateJython does not change the behavior at all implying this is not a Jython-emulation-specific feature in Graal. In this code:
Does not work. If you attempt to call a method on the Python side, you can't because it is actually
This alteration does work, and in fact we think we could make a decorator to smooth this out a bit. It goes downhill from here.
b) Augmenting the class further we have to use our imagination and say that the original Java class has a simple member "question" and a method "guide_entry":
Essentially, we cannot access any Java members from inside the class, but methods are fine.
Those members are published outside the class definition on the object as constructed.
This feels as if the class is created, and then GraalPy is wrapping the class and adding a few members (graalpython.build_java_class() or .extend()?) but the user (whoever owns the 'dt' instance above) and the class (receiving 'self') are getting two different objects or different wrappings? Whatever the cause, it is inconsistent.
c) Additionally we can only call super methods which match the name of the subclassed function? Understandably having one function call a superclass method for a different method is a little strange but that is a programmer problem and we're not sure it should be enforced by the "language" in that you can do it in CPython.
If we try and run something from the
__init__
method we also find thatself.__super__
is None. Luckily, super() works. This is a deviation from the documentation (which is very good, we like that it works like Python).I have not had time to express all the possibilities here in code to make sure.
Jython functionality might not be able to be fine-grained enabled/disabled
We are very pleased with the direction this is all taking except for the issues above. However we have to express a little fear of progress here, if features like "not having to EmulateJython=true for either of the above to be possible" are not intentional and this is all going to be hidden behind that feature, we really would like to have a more fine-grained enablement. As far as we're aware the Jython emulation has traditionally intended to bring in 3 features:
We do not like the last feature as it confuses the namespace and in general makes our docs look incomplete, and we would like to disable such functionality while keeping others if at all possible. We considered patching Jython to disable this feature about a decade ago but it didn't seem worth the trouble to maintain it. In that sense, we would not like to see it back. We're looking for a very "clean Python-like" environment with implicit Java compatibility and no API changes.
I suppose this is a plea to see if we can find out if there is a goal for this or a JSR or JEP which covers what the API should look like, and what the behavior is. We are happy to work with the team here in figuring this out, so that we can plan accordingly and come back at a future version, or test more in the meantime.
The text was updated successfully, but these errors were encountered: