From cd8890c744c8d78b6e75fbd1d492237d2a6a7e73 Mon Sep 17 00:00:00 2001 From: Hynek Schlawack Date: Mon, 4 Dec 2017 18:48:25 +0100 Subject: [PATCH] Speed up __eq__ by generating code (#306) * Speed up __eq__ by generating code * Add newsfragment --- changelog.d/306.change.rst | 1 + src/attr/_make.py | 74 +++++++++++++++++++++++++++----------- 2 files changed, 55 insertions(+), 20 deletions(-) create mode 100644 changelog.d/306.change.rst diff --git a/changelog.d/306.change.rst b/changelog.d/306.change.rst new file mode 100644 index 00000000..ef2e75e0 --- /dev/null +++ b/changelog.d/306.change.rst @@ -0,0 +1 @@ +Equality tests are *much* faster now. diff --git a/src/attr/_make.py b/src/attr/_make.py index 31c5f94c..3ea05011 100644 --- a/src/attr/_make.py +++ b/src/attr/_make.py @@ -701,7 +701,7 @@ def _make_hash(attrs): if a.hash is True or (a.hash is None and a.cmp is True) ) - # We cache the generated init methods for the same kinds of attributes. + # We cache the generated hash methods for the same kinds of attributes. sha1 = hashlib.sha1() sha1.update(repr(attrs).encode("utf-8")) unique_filename = "" % (sha1.hexdigest(),) @@ -742,34 +742,68 @@ def _add_hash(cls, attrs): return cls +def _ne(self, other): + """ + Check equality and either forward a NotImplemented or return the result + negated. + """ + result = self.__eq__(other) + if result is NotImplemented: + return NotImplemented + + return not result + + def _make_cmp(attrs): attrs = [a for a in attrs if a.cmp] + # We cache the generated eq methods for the same kinds of attributes. + sha1 = hashlib.sha1() + sha1.update(repr(attrs).encode("utf-8")) + unique_filename = "" % (sha1.hexdigest(),) + lines = [ + "def __eq__(self, other):", + " if other.__class__ is not self.__class__:", + " return NotImplemented", + ] + # We can't just do a big self.x = other.x and... clause due to + # irregularities like nan == nan is false but (nan,) == (nan,) is true. + if attrs: + lines.append(" return (") + others = [ + " ) == (", + ] + for a in attrs: + lines.append(" self.%s," % (a.name,)) + others.append(" other.%s," % (a.name,)) + + lines += others + [" )"] + else: + lines.append(" return True") + + script = "\n".join(lines) + globs = {} + locs = {} + bytecode = compile(script, unique_filename, "exec") + eval(bytecode, globs, locs) + + # In order of debuggers like PDB being able to step through the code, + # we add a fake linecache entry. + linecache.cache[unique_filename] = ( + len(script), + None, + script.splitlines(True), + unique_filename, + ) + eq = locs["__eq__"] + ne = _ne + def attrs_to_tuple(obj): """ Save us some typing. """ return _attrs_to_tuple(obj, attrs) - def eq(self, other): - """ - Automatically created by attrs. - """ - if other.__class__ is self.__class__: - return attrs_to_tuple(self) == attrs_to_tuple(other) - else: - return NotImplemented - - def ne(self, other): - """ - Automatically created by attrs. - """ - result = eq(self, other) - if result is NotImplemented: - return NotImplemented - else: - return not result - def lt(self, other): """ Automatically created by attrs.