Working with Python Bridge objects

When using the PythonBridge objects are transferred from Python to GT as proxies. PBProxyObject PBObject subclass: #PBProxyObject instanceVariableNames: 'pythonClass application' classVariableNames: '' package: 'PythonBridge-Core' are GT native objects that refer to a Python object using an identifier. On the Python side there is a registry where those identifiers are mapped to the corresponding Python objects.

Numbers

Let's start with a list of numbers.

import random
numbers = random.sample(range(100), 10)
  

What we get when we Inspect this Python expression is a GT Inspector with a double view: a Remote view and a Proxy view. They look at the same object in GT, a PBProxyObject, referring to some Python list. These two views show different perspectives.

The Remote view is the default and acts so that it looks as if this is normal object, even though it is remote. The underlying PBProxyObject is hidden and we see inspector tabs related to the Python object. There is even a custom tab showing the list's items.

The Proxy view looks at the internals and shows the actual PBProxyObject which is maintaining this illusion: it shows us the internal mechanism. Here we can see the identifier of the Python object in the registry as well as its Python class.

Clicking on a number will inspect an integer, still as a proxy. Now there is a Download local instance action (down arrow) which will convert this primitive type to a matching GT type.

Where things get a little bit confusing is that some composite primitive types like list or dict are also converted, but only when using the API, not using the Python snippet like above. They are automatically mapped to their GT equivalents (Array and Dictionary respectively).

PBApplication do: [ :application |
	application newCommandFactory
		<< #numbers asP3GIdentifier;
		sendAndWait ]
  
PBApplication do: [ :application |
	application newCommandStringFactory
		resultExpression: '{"x":1,"y":2}';
		sendAndWait ]
  

Still, this difference disappears when normal Python objects are involved. These are always tranferred as proxies.

Persons

Let's consider an ultra simple Person object and assign some instances to some variables.

class Person:
	def __init__(self, first, last):
		self.first = first
		self.last = last

	def __repr__(self):
		return f'Person({self.first} {self.last})'

john = Person('John','Doe')
jane = Person('Jane','Doe')
persons = [ john, jane ]
  

You can see the list of Persons, look at one Person and look at the first and last attributes.

When going through the API, the list gets converted but not the elements.

PBApplication do: [ :application |
	application newCommandFactory
		<< #persons asP3GIdentifier;
		sendAndWait ]
  

This is not a problem because we can send messages to the Python object. You can open the contextual playground at the bottom of the Inspector where a Python snippet's self will refer to the Python object under inspection. Try self.first.

jane
  

We can do the same programmatically using PBProxyObject>>#attributeAt: attributeAt: attributeName "Answer the value of the named attribute in the Python object that I represent" ^ self newCommandFactory << (self => attributeName); sendAndWait and PBProxyObject>>#callMethod: callMethod: methodName "Call methodName on the Python object that I represent and return the result" ^ self callMethod: methodName withArgs: #() which will invoke Python side functionality over the bridge. You can experiment with this using the contextual playground of the proxy perspective instead of the remote perspective.

jane := PBApplication do: [ :application |
	application newCommandFactory
		<< #jane asP3GIdentifier;
		sendAndWait ]
  
jane attributeAt: #first
  
jane callMethod: #__repr__
  

Mirrors

We can go one step further and add a Mirror class for our Python class. This is a subclass of PBProxyObject PBObject subclass: #PBProxyObject instanceVariableNames: 'pythonClass application' classVariableNames: '' package: 'PythonBridge-Core' that implements a class side pythonClass method returning the name of the Python class that we want to mirror.

Create an empty PBPerson subclass of of PBProxyObject PBObject subclass: #PBProxyObject instanceVariableNames: 'pythonClass application' classVariableNames: '' package: 'PythonBridge-Core' with a class side pythonClass method returning Person as a string or symbol. From now on, the proxy class on the GT side will change to our new mirror class.

On itself, this does not change anything, yet.

We can now add GT methods to our mirror class so that it acts more and more like a normal GT class even though it is effectively a proxy to a remote Python object.

Try adding first and last accessors that use PBProxyObject>>#attributeAt: attributeAt: attributeName "Answer the value of the named attribute in the Python object that I represent" ^ self newCommandFactory << (self => attributeName); sendAndWait or a repr method that uses PBProxyObject>>#callMethod: callMethod: methodName "Call methodName on the Python object that I represent and return the result" ^ self callMethod: methodName withArgs: #() .

You can now add more code to the mirror object, like complex, graphical Inspector views that are not possible in Python.