Symbolic Detour¶
PyGlove offers the pg.detour
feature to redirect a class
to another class or function, allowing the object creation behavior of that class to be changed
dynamically at runtime.
Motivation¶
Symbolizing existing classes and functions is straightforward, but in order to use them, the original classes used in existing code must be replaced with the symbolic versions. However, modifying the source code may not always be possible or objects created within a function or class method may not be accessible externally, making it impossible to manipulate them as part of the symbolic tree.
For example:
@pg.symbolize
def foo():
# Object `a` is not a part of `foo`'s interface,
# therefore it cannot be seen from the symbolic tree
# that contains a `foo` object.
a = A(1)
return a.do_something()
Symbolic Detour (SD) is a solution for these scenarios, it redirects the __new__
method
of a class to another class or function when it’s evaluated under a context manager.
It is not dependent on symbolization and can be used to detour any classes, it does not
require the presence of symbolic objects to modify the program.
Usage¶
Redirecting Classes to Classes¶
The code below illustrates class Foo
is detoured to Bar
under the context manager
of pg.detour
:
class Foo:
def __init__(self, x, y):
self.x = x
self.y = y
def __call__(self):
return self.x + self.y
class Bar:
def __init__(self, a, b):
self.a = a
self.b = b
def __call__(self):
return self.a * self.b
def my_fun():
# Parameters of `Foo` is not exposed as any argument of `my_fun`.
return Foo(1, 2)() + 2
# Symbolically detour `Foo` to `Bar` under the context manager,
# which changes the behavior inside `my_fun` while not requiring
# to modify its source code.
with pg.detour([(Foo, Bar)]):
v = my_fun()
assert v == (1 * 2) + 2
# Execute `my_fun` outside the context manager will result in
# the creation of original `Foo` object.
v2 = my_fun()
assert v2 == (1 + 2) + 2
Redirecting Classes to Functions¶
Symbolic detour can redirect classes to functions, but it has a limitation:
if the function returns an object of the same type (or a subtype) as the original
class, the object’s __init__
method will be called again with the original
arguments. This means that using detour to change argument values won’t work.
For example:
def foo_with_incremented_x(cls, x, y):
return cls(x + 1, y)
with pg.detour([(Foo, foo_with_incremented_x)]):
v= my_fun(1, 2)
# Fails: though argument `x` is incremented by the function,
# but Python calls the `__init__` again with the original value 1,
# thus the Foo's value remains
assert v == (2 * 2) + 2
A simple solution is to create an instance of the symbolized class instead of the
original class. Symbolic classes have built-in handling for re-initialization,
which allows them to do nothing when __init__
is called after an object is
already initialized. For example:
SymbolicFoo = pg.symbolize(Foo)
def foo_with_incremented_x(cls, x, y):
return SymbolicFoo(x + 1, y)
with pg.detour([(Foo, foo_with_incremented_x)]):
v= my_fun(1, 2)
# Okay now!
assert v == (2 * 2) + 2
The Nesting Rules¶
Symbolic detour can be nested, with outer scope mappings taking precedence over inner mappings, allowing users to change object creation behaviors from the outside. For example, the following code will detour class A to class C:
with pg.detour([(A, C)]):
with pg.detour([A, B]):
v = A() # v is a C object.
Detour is transitive across the inner and outer scope. For example:
with pg.detour([(B, C)]):
v1 = A() # v1 is an A object.
with pg.detour([A, B]):
v2 = A() # v2 is a C object. (A -> B -> C)
For more details about symbolic detour, see pg.detour
.