I am trying to figure out a way of having the setter of a property of a property trigger some action on the top level class.
As a dummy example, lets say my top level class is a Segment. All is good if I store the coordinates of its endpoints directly as properties of this object x0, y0, x1 and y1, and have the setters of each one trigger the selected action.
But if I want to group them into two Point members as properties p0 and p1, each with properties x and y, whenever one of these coordinates is modified, there is no obvious way of telling the Segment to do something. This is what I would like to be able to do:
>>> segment = Segment(Point(0, 0), Point(3, 3))
>>> segment.p0
Point(0, 0)
>>> segment.p0.x
0
>>> segment.p1.y = 4
Length of segment changed to 5.0! # This can only be printed by segment, not p1!
The problem is that the line segment.p1.y = 4 first calls the getter of p1 on the segment instance, and then the setter of y on the return of the previous call, at which point there is no simple way of letting the segment instance know that a change has been made.
The best I can think of right now is something along the lines of the following:
class Point(object):
def __init__(self, x, y, parent=None, name=None):
self.parent, self.name = parent, name
self._x, self._y = x, y
#property
def x(self):
return self._x
#x.setter
def x(self, value):
self._x = value
if parent is not None:
setattr(self.parent, self.name, self)
# Similar code for y goes here...
class Segment(object):
def __init__(self, p0, p1):
self.p0, self.p1 = p0, p1
#property
def p0(self):
return self._p0
#p0.setter
def p0(self, point):
self._p0 = point
self.p0.parent = self
self.p0.name = 'p0'
if not self._silent:
self.do_something() # This would print the length in the above example
# Similar code for p1 goes here...
While this does what I want it to, I don't quite like having to manually add that link back to the parent, nor how I would either have to make lots of redundant copies of the Point objects, or risk interesting bugs if doing something like:
p0, p1, p2 = Point(0, 0), Point(1, 1), Point(2, 2)
seg0 = Segment(p0, p1)
seg1 = Segment(p0, p2)
# The following line changes the value on both seg0 and seg1, but triggers
# the do_something call on seg1 only!
seg0.p0.x = 6
Is there some ready-made recipe for this? Anyone can come up with a better way of doing it?
Perhaps you are looking for the Observer design pattern:
import math
class Point(object):
def __init__(self, x, y, name=None):
self.name = name
self._x, self._y = x, y
self.observers = []
def observe(self, observer):
self.observers.append(observer)
def __repr__(self):
return 'Point({}, {})'.format(self.x, self.y)
#property
def x(self):
return self._x
#x.setter
def x(self, value):
self._x = value
for o in self.observers:
o.notify()
#property
def y(self):
return self._y
#y.setter
def y(self, value):
self._y = value
for o in self.observers:
o.notify()
class Segment(object):
def __init__(self, p0, p1):
self._p0, self._p1 = p0, p1
p0.observe(self)
p1.observe(self)
def __repr__(self):
return 'Segment({}, {})'.format(self.p0, self.p1)
def notify(self):
print('Length of {} changed to {}'.format(self, self.length()))
def length(self):
return math.sqrt((self.p0.x - self.p1.x)**2
+ (self.p0.y - self.p1.y)**2)
#property
def p0(self):
return self._p0
#p0.setter
def p0(self, point):
self._p0 = point
#property
def p1(self):
return self._p1
#p1.setter
def p1(self, point):
self._p1 = point
segment = Segment(Point(0, 0), Point(3, 3))
print(segment.p0)
# Point(0, 0)
print(segment.p0.x)
# 0
segment.p1.y = 4
yields
Length of Segment(Point(0, 0), Point(3, 4)) changed to 5.0
and
p0, p1, p2 = Point(0, 0), Point(1, 1), Point(2, 2)
seg0 = Segment(p0, p1)
seg1 = Segment(p0, p2)
seg0.p0.x = 6
yields
Length of Segment(Point(6, 0), Point(1, 1)) changed to 5.09901951359
Length of Segment(Point(6, 0), Point(2, 2)) changed to 4.472135955
Related
I'm writing a class called Position, something like Python's Small Integer Constant Pool:
class Position:
"""Takes 2 args: x, and y. If coordinate are equal, make them pointing to the same object"""
def __init__(self, x, y):
self.x = x
self.y = y
def __new__(self, *args, **kwargs):
import gc
for obj in gc.get_objects():
if isinstance(obj, eval(self.__qualname__)): # Won't work cause arg 2 is a string, not a type name
if obj.x == self.x and obj.y == self.y:
return obj
return super(Position, self).__new__(self, *args, **kwargs) # Have no idea what to write
p1 = Position(1, 2)
p2 = Position(2, 3)
p3 = Position(1, 2)
print(p1 is p2, p2 is p3, p1 is p3) # Should print False, False, True
Rewriting hash and eq won't work when using is to compare.
I want my class to be something like:
a = 1
b = 1
print(a is b) # True
You can't rely on the garbage collector list of all existing objects for this, and much less on a linear search of all objects.
Simply keep your objects in a registry where you can find then back.
You can use WeakValues dictionary so that if there is no reference left to anyone of your objects it is discarded. (Just use a plain dictionary if you want each one to remain created, even if it is no longer in use):
from weakref import WeakValueDictionary
class Position:
"""Takes 2 args: x, and y. If coordinate are equal, make them pointing to the same object"""
_registry = WeakValueDictionary()
def __new__(cls, x, y):
if (x,y) in cls._registry:
return cls._registry[x,y]
instance = super().__new__(cls) # don't pass extra *args and **kwargs to obj.__new__
cls._registry[x,y] = instance
return instance
def __init__(self, x, y):
if hasattr(self, "x"): # avoid running init twice if the attribute is already set
return
self.x = x
self.y = y
p1 = Position(1, 2)
p2 = Position(2, 3)
p3 = Position(1, 2)
in this scenario p1 == p3 evaluates to True
I have a class in one file1, which uses variables (that are constantly changing) from file2 but the changes are not being applied in the original class:
class element():
def __init__(self, x, y)
animations = [pic1, pic2, pic3]
self.x = x
self.y = y
self.pos = pos
self.atts = atts
def update()
...
file2:
x = 100
y = 100
pos = 10
atts = ['red', '#122']
el1 = element(x,y)
...
Value types such as the parameters used in x and y are copied when passed as an attribute so if you change them in your second module, the change will not be applied in your object, so you will need to either update them again:
x = 100
y = 100
pos = 10
atts = ['red', '#122']
el1 = element(x, y, pos, atts)
x = 50
el1.x = x
Or you can use a reference type (another object) that you can share between the two files to wrap the value types:
class ElementConfig():
def __init__(self, x, y, pos, atts):
self._x = x
self._y = y
self._pos = pos
self._atts = atts
#property
def x(self):
return self._x
#x.setter
def x(self, value):
self._x = value
#property
def y(self):
return self._y
#y.setter
def y(self, value):
self._y = value
#property
def pos(self):
return self._pos
#pos.setter
def pos(self, value):
self._pos = value
#property
def atts(self):
return self._atts
#atts.setter
def atts(self, value):
self._atts = value
class Element():
def __init__(self, config: ElementConfig):
self._config = config
#property
def config(self):
return self._config
And in the other module:
from file1 import ElementConfig, Element
elementConfig = ElementConfig(100, 100, 10, ['red', '#122'])
el1 = Element(elementConfig)
print(el1.config.x)
The result is 100
elementConfig.x = 200
print(el1.config.x)
Now the result is 200
I have two Point objects and the code looks like this:
class Point:
def __init__(self, x, y):
self.x = x
self.y = y
a = Point(1, 3)
b = Point(4, 2)
max(a, b) # Make this output Point(4, 3)
My question is: "How can I implement a custom max function for the Point class that will return Point(max(self.x, other.x), max(self.y, other.y))?" The max function seems to just look at the __lt__ and return the highest.
max() can't do this, it can only return one of the elements given as input, not produce new instances.
You need to implement your own function:
def max_xy_point(*points):
if not points:
raise ValueError("Need at least 2 points to compare")
if len(points) == 1:
points = points[0]
return Point(
max(p.x for p in points),
max(p.y for p in points)
)
Like the built-in max() function, this can take either a single sequence (max([p1, p2, p3, ...]) or separate arguments (max(p1, p2, p3, ...)).
max(a, b) can return only a or b - it can't create point with new values.
You may add own method to class and use
c = a.max(b)
class Point:
def __init__(self, x, y):
self.x = x
self.y = y
def max(self, other):
return Point(max(self.x, other.x), max(self.y, other.y))
a = Point(1, 3)
b = Point(4, 2)
c = a.max(b)
print(c.x, c.y)
You can go about it like this, to get desired output:
class Point:
def __init__(self, x, y):
self.x = x
self.y = y
def max(self, other):
if not isinstance(other, Point):
return NotImplemented
return Point(max(self.x, other.x), max(self.y, other.y))
def __repr__(self):
return f'Point{self.x, self.y}'
a = Point(1, 3)
b = Point(4, 2)
a.max(b)
# Point(4, 3)
I have a Node class defined as follows (I only copied the relevant code for simplification):
class Node(object):
def __init__(self, x, y):
self.x = x
self.y = y
self.neighbours = []
I also have a dictionary that has Node objects as key, and the key-node's list of neighbours as value for each key.
successorTable = {
Node(0, 1): [Node(1, 0)],
Node(1, 0): [Node(0, 1)],
# and so on ...
}
Now what I would like to do is to get the dictionary's key (Node object) having the maximum value of x and the key having the maximum value of y
So I basically am trying to get the following result:
# maxXNode -> Node(1, 0)
# maxYNode -> Node(0, 1)
Your problem
First, for you problem you can do this.
max_x = max(successorTable.keys(), key=lambda n: n.x)
max_y = max(successorTable.keys(), key=lambda n: n.y)
Other problems
Then a word on your code.
I suggest you be careful with using your Node's as dictionary key as you didn't define __hash__ and __eq__ methods.
d = {}
d[Node(0, 0)] = 0
d[Node(0, 0)] # raises a KeyError
By default, an object is hashed and compared by its id, so two nodes with the same coordinates will not hash to the same value. You might want to fix this like so.
class Node(object):
def __init__(self, x, y):
self._x = x
self._y = y
self.neighbours = []
#property
def x(self):
return self._x
#property
def y(self):
return self._y
def __hash__(self):
return hash((self.x, self.y))
def __eq__(self, other):
return (self.x, self.y) == (other.x, other.y)
We use _x, _y and property to emphasize the fact that those attributes should not be updated as they are used for hashing.
I'm struggling with this easy assignment. I need to create 2 classes, Point and Route. I need to add/remove Points to/from Route and then calculate the sum distance between the Points in the Route.
So far my code is this:
import math
class Point:
"Two-dimensional points"
def __init__(self, x=0.0, y=0.0):
self._x = x
self._y = y
def __str__(self):
result = "\n".join(["x: %f" % self.x(),
"y: %f" % self.y(),
"rho: %f" % self.rho(),
"theta: %f" % self.theta()])
return result
# Queries
def x(self):
"Abscissa"
return self._x
def y(self):
"Ordinate"
return self._y
def rho(self):
"Distance to origin (0, 0)"
return math.sqrt(self.x()**2 + self.y()**2)
def theta(self):
"Angle to horizontal axis"
return math.atan2(self.y(), self.x())
def distance(self, other):
"Distance to other"
return self.vectorTo(other).rho()
def vectorTo(self, other):
"Returns the Point representing the vector from self to other Point"
return Point(other.x() - self.x(), other.y() - self.y())
# Commands
def translate(self, dx, dy):
"Move by dx horizontally, dy vertically"
self._x += dx
self._y += dy
def scale(self, factor):
"Scale by factor"
self._x *= factor
self._y *= factor
def centre_rotate(self, angle):
"Rotate around origin (0, 0) by angle"
temp_x = self.rho() * math.cos(self.theta() + angle)
temp_y = self.rho() * math.sin(self.theta() + angle)
self._x, self._y = temp_x, temp_y
def rotate(self, p, angle):
"Rotate around p by angle"
self.translate(-p.x(), -p.y())
self.centre_rotate(angle)
self.translate(p.x(), p.y())
class Route:
def __init__(self):
self.Point = []
def __add__ (x,y,index):
self.points.insert(Point(x,y), index)
The Point works fine but I can't figure out how to get Route to work.
The error I get is:
>>>
>>> route = Route()
>>> route.add(32, 12, 2)
Traceback (most recent call last):
File "<pyshell#1>", line 1, in <module>
route.add(32, 12, 2)
File "C:\Users\John Wayne\Dropbox\kool\geom.py", line 73, in add
self.points.insert(Point(x, y), index)
TypeError: 'Point' object cannot be interpreted as an integer
>>>
OK I have managed to fix the Route class as following:
class Route:
def __init__(self):
self.points = []
def add_point(self, x, y, index):
self.points.insert(index, Point(x,y))
But now I have problem with my method get_lenght:
def get_lenght(self, Point):
for Point in self.points:
What is wrong with this get_lenght method?
Thank you very much.
It looks like you are trying to insert in to the list self.points using the insert method. Insert takes two arguments:
my_list = []
my_list.insert(index, object)
Which will insert the object before the index supplied. The argument 'index' must be a number.
In your example, you have passed the object first and the index second. Switch the arguments around and it should be working fine. e.g.:
self.points.insert(index, Point(x,y))
Alternatively, if you just want to add the object on to the end of the list, the 'append' method may work better for you:
self.points.append(Point(x,y))
In addition, I have assumed that your class 'Route' is written as follows as the code copied above appears to be incorrect (your method 'add' shouldn't be defined with the underscored before and after, you didn't pass 'self' as the first argument to your add method, and you had inconsistency with self.points):
class Route:
def __init__(self):
self.points = []
def add (self, x, y, index):
self.points.insert(Point(x,y), index)