diff --git a/changelog.d/995.change.rst b/changelog.d/995.change.rst new file mode 100644 index 000000000..15ddb9f27 --- /dev/null +++ b/changelog.d/995.change.rst @@ -0,0 +1,3 @@ +Class-creation performance improvements by switching performance-sensitive templating operations to f-strings. + +You can expect an improvement of about 5% even for very simple classes. diff --git a/docs/why.rst b/docs/why.rst index 9edae27a2..be57ce6da 100644 --- a/docs/why.rst +++ b/docs/why.rst @@ -218,7 +218,7 @@ is roughly ... self.b = b ... ... def __repr__(self): - ... return "ArtisanalClass(a={}, b={})".format(self.a, self.b) + ... return f"ArtisanalClass(a={self.a}, b={self.b})" ... ... def __eq__(self, other): ... if other.__class__ is self.__class__: diff --git a/src/attr/_cmp.py b/src/attr/_cmp.py index 81b99e4c3..ad1e18c75 100644 --- a/src/attr/_cmp.py +++ b/src/attr/_cmp.py @@ -130,9 +130,9 @@ def method(self, other): return result - method.__name__ = "__%s__" % (name,) - method.__doc__ = "Return a %s b. Computed by attrs." % ( - _operation_names[name], + method.__name__ = f"__{name}__" + method.__doc__ = ( + f"Return a {_operation_names[name]} b. Computed by attrs." ) return method diff --git a/src/attr/_funcs.py b/src/attr/_funcs.py index a982d7cb5..49f241d02 100644 --- a/src/attr/_funcs.py +++ b/src/attr/_funcs.py @@ -331,9 +331,7 @@ def assoc(inst, **changes): a = getattr(attrs, k, NOTHING) if a is NOTHING: raise AttrsAttributeNotFoundError( - "{k} is not an attrs attribute on {cl}.".format( - k=k, cl=new.__class__ - ) + f"{k} is not an attrs attribute on {new.__class__}." ) _obj_setattr(new, k, v) return new diff --git a/src/attr/_make.py b/src/attr/_make.py index 0c2da5edd..730ed60cc 100644 --- a/src/attr/_make.py +++ b/src/attr/_make.py @@ -23,10 +23,7 @@ # This is used at least twice, so cache it here. _obj_setattr = object.__setattr__ _init_converter_pat = "__attr_converter_%s" -_init_factory_pat = "__attr_factory_{}" -_tuple_property_pat = ( - " {attr_name} = _attrs_property(_attrs_itemgetter({index}))" -) +_init_factory_pat = "__attr_factory_%s" _classvar_prefixes = ( "typing.ClassVar", "t.ClassVar", @@ -342,7 +339,7 @@ class MyClassAttributes(tuple): if attr_names: for i, attr_name in enumerate(attr_names): attr_class_template.append( - _tuple_property_pat.format(index=i, attr_name=attr_name) + f" {attr_name} = _attrs_property(_attrs_itemgetter({i}))" ) else: attr_class_template.append(" pass") @@ -559,7 +556,7 @@ def _transform_attrs( if had_default is True and a.default is NOTHING: raise ValueError( "No mandatory attributes allowed after an attribute with a " - "default value or factory. Attribute in question: %r" % (a,) + f"default value or factory. Attribute in question: {a!r}" ) if had_default is False and a.default is not NOTHING: @@ -1062,8 +1059,9 @@ def _add_method_dunders(self, method): pass try: - method.__doc__ = "Method generated by attrs for class %s." % ( - self._cls.__qualname__, + method.__doc__ = ( + "Method generated by attrs for class " + f"{self._cls.__qualname__}." ) except AttributeError: pass @@ -1583,12 +1581,10 @@ def _generate_unique_filename(cls, func_name): """ Create a "filename" suitable for a function being generated. """ - unique_filename = "".format( - func_name, - cls.__module__, - getattr(cls, "__qualname__", cls.__name__), + return ( + f"" ) - return unique_filename def _make_hash(cls, attrs, frozen, cache_hash): @@ -1630,34 +1626,34 @@ def append_hash_computation_lines(prefix, indent): method_lines.extend( [ indent + prefix + hash_func, - indent + " %d," % (type_hash,), + indent + f" {type_hash},", ] ) for a in attrs: if a.eq_key: - cmp_name = "_%s_key" % (a.name,) + cmp_name = f"_{a.name}_key" globs[cmp_name] = a.eq_key method_lines.append( - indent + " %s(self.%s)," % (cmp_name, a.name) + indent + f" {cmp_name}(self.{a.name})," ) else: - method_lines.append(indent + " self.%s," % a.name) + method_lines.append(indent + f" self.{a.name},") method_lines.append(indent + " " + closing_braces) if cache_hash: - method_lines.append(tab + "if self.%s is None:" % _hash_cache_field) + method_lines.append(tab + f"if self.{_hash_cache_field} is None:") if frozen: append_hash_computation_lines( - "object.__setattr__(self, '%s', " % _hash_cache_field, tab * 2 + f"object.__setattr__(self, '{_hash_cache_field}', ", tab * 2 ) method_lines.append(tab * 2 + ")") # close __setattr__ else: append_hash_computation_lines( - "self.%s = " % _hash_cache_field, tab * 2 + f"self.{_hash_cache_field} = ", tab * 2 ) - method_lines.append(tab + "return self.%s" % _hash_cache_field) + method_lines.append(tab + f"return self.{_hash_cache_field}") else: append_hash_computation_lines("return ", tab) @@ -1713,27 +1709,15 @@ def _make_eq(cls, attrs): others = [" ) == ("] for a in attrs: if a.eq_key: - cmp_name = "_%s_key" % (a.name,) + cmp_name = f"_{a.name}_key" # Add the key function to the global namespace # of the evaluated function. globs[cmp_name] = a.eq_key - lines.append( - " %s(self.%s)," - % ( - cmp_name, - a.name, - ) - ) - others.append( - " %s(other.%s)," - % ( - cmp_name, - a.name, - ) - ) + lines.append(f" {cmp_name}(self.{a.name}),") + others.append(f" {cmp_name}(other.{a.name}),") else: - lines.append(" self.%s," % (a.name,)) - others.append(" other.%s," % (a.name,)) + lines.append(f" self.{a.name},") + others.append(f" other.{a.name},") lines += others + [" )"] else: @@ -1860,7 +1844,7 @@ def _make_repr(attrs, ns, cls): " else:", " already_repring.add(id(self))", " try:", - " return f'%s(%s)'" % (cls_name_fragment, repr_fragment), + f" return f'{cls_name_fragment}({repr_fragment})'", " finally:", " already_repring.remove(id(self))", ] @@ -2037,7 +2021,7 @@ def _setattr(attr_name, value_var, has_on_setattr): """ Use the cached object.setattr to set *attr_name* to *value_var*. """ - return "_setattr('%s', %s)" % (attr_name, value_var) + return f"_setattr('{attr_name}', {value_var})" def _setattr_with_converter(attr_name, value_var, has_on_setattr): @@ -2060,7 +2044,7 @@ def _assign(attr_name, value, has_on_setattr): if has_on_setattr: return _setattr(attr_name, value, True) - return "self.%s = %s" % (attr_name, value) + return f"self.{attr_name} = {value}" def _assign_with_converter(attr_name, value_var, has_on_setattr): @@ -2126,7 +2110,7 @@ def fmt_setter(attr_name, value_var, has_on_setattr): if _is_slot_attr(attr_name, base_attr_map): return _setattr(attr_name, value_var, has_on_setattr) - return "_inst_dict['%s'] = %s" % (attr_name, value_var) + return f"_inst_dict['{attr_name}'] = {value_var}" def fmt_setter_with_converter( attr_name, value_var, has_on_setattr @@ -2174,12 +2158,12 @@ def fmt_setter_with_converter( if a.init is False: if has_factory: - init_factory_name = _init_factory_pat.format(a.name) + init_factory_name = _init_factory_pat % (a.name,) if a.converter is not None: lines.append( fmt_setter_with_converter( attr_name, - init_factory_name + "(%s)" % (maybe_self,), + init_factory_name + f"({maybe_self})", has_on_setattr, ) ) @@ -2189,7 +2173,7 @@ def fmt_setter_with_converter( lines.append( fmt_setter( attr_name, - init_factory_name + "(%s)" % (maybe_self,), + init_factory_name + f"({maybe_self})", has_on_setattr, ) ) @@ -2199,7 +2183,7 @@ def fmt_setter_with_converter( lines.append( fmt_setter_with_converter( attr_name, - "attr_dict['%s'].default" % (attr_name,), + f"attr_dict['{attr_name}'].default", has_on_setattr, ) ) @@ -2209,12 +2193,12 @@ def fmt_setter_with_converter( lines.append( fmt_setter( attr_name, - "attr_dict['%s'].default" % (attr_name,), + f"attr_dict['{attr_name}'].default", has_on_setattr, ) ) elif a.default is not NOTHING and not has_factory: - arg = "%s=attr_dict['%s'].default" % (arg_name, attr_name) + arg = f"{arg_name}=attr_dict['{attr_name}'].default" if a.kw_only: kw_only_args.append(arg) else: @@ -2233,14 +2217,14 @@ def fmt_setter_with_converter( lines.append(fmt_setter(attr_name, arg_name, has_on_setattr)) elif has_factory: - arg = "%s=NOTHING" % (arg_name,) + arg = f"{arg_name}=NOTHING" if a.kw_only: kw_only_args.append(arg) else: args.append(arg) - lines.append("if %s is not NOTHING:" % (arg_name,)) + lines.append(f"if {arg_name} is not NOTHING:") - init_factory_name = _init_factory_pat.format(a.name) + init_factory_name = _init_factory_pat % (a.name,) if a.converter is not None: lines.append( " " @@ -2307,9 +2291,7 @@ def fmt_setter_with_converter( for a in attrs_to_validate: val_name = "__attr_validator_" + a.name attr_name = "__attr_" + a.name - lines.append( - " %s(self, %s, self.%s)" % (val_name, attr_name, a.name) - ) + lines.append(f" {val_name}(self, {attr_name}, self.{a.name})") names_for_globals[val_name] = a.validator names_for_globals[attr_name] = a @@ -2336,9 +2318,9 @@ def fmt_setter_with_converter( # For exceptions we rely on BaseException.__init__ for proper # initialization. if is_exc: - vals = ",".join("self." + a.name for a in attrs if a.init) + vals = ",".join(f"self.{a.name}" for a in attrs if a.init) - lines.append("BaseException.__init__(self, %s)" % (vals,)) + lines.append(f"BaseException.__init__(self, {vals})") args = ", ".join(args) if kw_only_args: @@ -2346,14 +2328,13 @@ def fmt_setter_with_converter( ", " if args else "", # leading comma ", ".join(kw_only_args), # kw_only args ) + return ( - """\ -def {init_name}(self, {args}): - {lines} -""".format( - init_name=("__attrs_init__" if attrs_init else "__init__"), - args=args, - lines="\n ".join(lines) if lines else "pass", + "def %s(self, %s):\n %s\n" + % ( + ("__attrs_init__" if attrs_init else "__init__"), + args, + "\n ".join(lines) if lines else "pass", ), names_for_globals, annotations, diff --git a/tests/test_make.py b/tests/test_make.py index fe8c5e613..a68ae1db9 100644 --- a/tests/test_make.py +++ b/tests/test_make.py @@ -1910,9 +1910,7 @@ class A: if hasattr(A, "__qualname__"): method = getattr(A, meth_name) - expected = "Method generated by attrs for class {}.".format( - A.__qualname__ - ) + expected = f"Method generated by attrs for class {A.__qualname__}." assert expected == method.__doc__