If I have a class like below:
class Point(object):
def __init__(self, x, y):
self.x = x
self.y = y
And have 2 objects:
a = Point(1,2)
b = Point(1,2)
How can I modify class Point to make id(a) == id(b)?
class Point(object):
__cache = {}
def __new__(cls, x, y):
if (x, y) in Point.__cache:
return Point.__cache[(x, y)]
else:
o = object.__new__(cls)
o.x = x
o.y = y
Point.__cache[(x, y)] = o
return o
>>> Point(1, 2)
<__main__.Point object at 0xb6f5d24c>
>>> id(Point(1, 2)) == id(Point(1,2))
True
When you need a really simple class like Point, always consider collections.namedtuple
from collections import namedtuple
def Point(x, y, _Point=namedtuple('Point', 'x y'), _cache={}):
return _cache.setdefault((x, y), _Point(x, y))
>>> Point(1, 2)
Point(x=1, y=2)
>>> id(Point(1, 2)) == id(Point(1, 2))
True
I used a function alongside namedtuple because it is simpler IMO but you can easily represent it as a class if needed:
class Point(namedtuple('Point', 'x y')):
__cache = {}
def __new__(cls, x, y):
return Point.__cache.setdefault((x, y),
super(cls, Point).__new__(cls, x, y))
As #PetrViktorin noted in his answer you should consider the use of a weakref.WeakValueDictionary so deleted instances of the class (doesn't work with namedtuple apparently) don't remain in memory since they remain referenced in the dictionary itself.
You need to have a global dictionary of objects, and get them through a factory function (or a custom __new__, see the other answers). Additionally, consider using a WeakValueDictionary so that you don't unnecessarily fill up memory with objects that are no longer needed.
from weakref import WeakValueDictionary
class _Point(object):
def __init__(self, x, y):
self.x = x
self.y = y
# Cache of Point objects the program currently uses
_points = WeakValueDictionary()
def Point(x, y):
"""Create a Point object"""
# Note that this is a function (a "factory function")
# You can also override Point.__new__ instead
try:
return _points[x, y]
except KeyError:
_points[x, y] = point = _Point(x, y)
return point
if __name__ == '__main__':
# A basic demo
print Point(1, 2)
print id(Point(1, 2))
print Point(2, 3) == Point(2, 3)
pt_2_3 = Point(2, 3)
# The Point(1, 2) we created earlier is not needed any more.
# In current CPython, it will have been been garbage collected by now
# (but note that Python makes no guarantees about when objects are deleted)
# If we create a new Point(1, 2), it should get a different id
print id(Point(1, 2))
Note that a namedtuple won't work with WeakValueDictionary.
If you need to compare whether your two objects house the same values, you can implement the eq operator:
>>> class Point(object):
... def __init__(self, x, y):
... self.x = x
... self.y = y
... def __eq__(self, other):
... return self.x == other.x and self.y == other.y
...
>>> a = Point(1,2)
>>> b = Point(1,2)
>>> a == b
True
>>> b = Point(2,2)
>>> a == b
False
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 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 set of Point objects, and I would like to be able to remove these objects from my set. However, Python seems to be comparing these by pointer rather than by value, so I can't remove elements easily, since they don't have the same pointer because they are not the exact same object. This is a problem only with objects, not with primitives.
A simplified example of my problem:
class Point():
def __init__(self, x, y):
self.x = x
self.y = y
a = set()
a.add(Point(0, 0))
b = Point(0, 0)
a.remove(b)
When run, this returns
Traceback (most recent call last):
File "example.py", line 9, in <module>
a.remove(b)
KeyError: <__main__.Point object at 0x7f6292376128>
(obviously, the specific pointer changes on each run).
I would prefer to have the element (0, 0) removed from a, leaving a to be the empty set.
If you tell Python how to compare these objects, this can work. Add two methods like:
Code:
def __eq__(self, other):
return self.x == other.x and self.y == other.y
def __hash__(self):
return hash((self.x, self.y))
Test Code:
class Point():
def __init__(self, x, y):
self.x = x
self.y = y
def __eq__(self, other):
return self.x == other.x and self.y == other.y
def __hash__(self):
return hash((self.x, self.y))
a = set()
a.add(Point(0, 0))
b = Point(0, 0)
a.remove(b)
It does not appear that you have actually added Point b to the set 'a'.
According to the docs:
If the element(argument) passed to the remove() method doesn't exist, keyError exception is thrown.
I made this slight alteration and it didn't error out
class Point():
def __init__(self, x, y):
self.x = x
self.y = y
a = set()
a.add(Point(0, 0))
b = Point(0, 0)
a.add(b)
a.remove(b)
I will provide an example of the problem in question, in case the title was not clear enough.
Let's say that I have a class Point(object) that represent 2d coordinates.
Is it possible to create a "magic" method that will allow the following?
x, y = point
Maybe some hacks with iterators?
you can simply tap into the iterator protocol of the object and accomplish this
class Point(object):
def __init__(self, x,y):
self.x = x
self.y = y
self.points = (x,y)
def __iter__(self):
return iter(self.points)
p = Point(1,5)
x,y = p
print x,y
# 1,5
take a look at http://www.rafekettler.com/magicmethods.html#sequence on more information on how a custom object can be converted into an iterable; or more precisely how one would use an object like an iterable.
Just provide an __iter__ method.
class Point(object):
def __init__(self, x, y):
self.x = x
self.y = y
def __iter__(self):
yield self.x
yield self.y
p = Point(1, 2)
x, y = p
assert (1, 2) == (x, y)
Be careful though. This means your class suddenly becomes safe to use in many other places where it might have previously thrown a type error.
eg.
def add_1(x):
return x + 1
l = list(map(add_1, p)) # works, because the point is iterable
Ergo, you may want to provide a method other than __iter__ that provides the iterator.
eg.
class Point(object):
def __init__(self, x, y):
self.x = x
self.y = y
def coords(self):
yield self.x
yield self.y
p = Point(1, 2)
x, y = p.coords()
assert (1, 2) == (x, y)
Here is an example which creates a point as p=Point(x, y). Assume that I have some array ppp=(x, y) where x and y are numbers and I want to make it of class Point but in the way: p=Point(ppp). I can do either one or another way but not both simultaneously. Is it possible to have both ways?
There are two different ways to acquire the result, the first is to analyse arguments that you pass to __init__ and in dependence of their quantity and type - choose a decision what are you using to instantiate class.
class Point(object):
x = 0
y = 0
def __init__(self, x, y=None):
if y is None:
self.x, self.y = x, x
else:
self.x, self.y = x, y
The other decision is to use classmethods as instantiators:
class Point(object):
x = 0
y = 0
#classmethod
def from_coords(cls, x, y):
inst = cls()
inst.x = x
inst.y = y
return inst
#classmethod
def from_string(cls, x):
inst = cls()
inst.x, inst.y = x, x
return inst
p1 = Point.from_string('1.2 4.6')
p2 = Point.from_coords(1.2, 4.6)
If you know that you have a tuple/list while creating the instance, you can do: p = Point(*ppp), where ppp is the tuple.
class Point:
def __init__(self, x, y=None):
if isinstance(x, tuple):
self.x, self.y = x
else:
self.x = x
self.y = y
Yes:
class Point(object):
def __init__(self, x, y=None):
if y is not None:
self.x, self.y = x, y
else:
self.x, self.y = x
def __str__(self):
return "{}, {}".format(self.x, self.y)
print Point(1,2)
# 1, 2
print Point((1,2))
# 1, 2
I would guess that your looking for a way to overload your constructor, as is common in statically typed languages such as C++ and Java.
This is not possible in Python. What you can do is provide different keyword argument combinations, something like:
class Point(object):
def __init__(self, x=None, y=None, r=None, t=None):
if x is not None and y is not None:
self.x = x
self.y = y
elif r is not None and t is not None:
# set cartesian coordinates from polar ones
Which you would then use as:
p1 = Point(x=1, y=2)
p2 = Point(r=1, t=3.14)