Skip to content

Commit c6c6e41

Browse files
authored
Speed up bind_self() in trivial cases (#19024)
See #18991 for context. We can skip all of logic in `check_self_arg()` and 90% of logic in `bind_self()` for methods with trivial `self`/`cls` (i.e. if first argument has no explicit annotation and there is no `Self` in signature). Locally I see 3-4% performance improvement (for self-check with non-compiled mypy).
1 parent a3aac71 commit c6c6e41

File tree

3 files changed

+123
-16
lines changed

3 files changed

+123
-16
lines changed

mypy/checkmember.py

Lines changed: 93 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
from __future__ import annotations
44

55
from collections.abc import Sequence
6-
from typing import Callable, cast
6+
from typing import Callable, TypeVar, cast
77

88
from mypy import message_registry, state, subtypes
99
from mypy.checker_shared import TypeCheckerSharedApi
@@ -18,6 +18,7 @@
1818
from mypy.nodes import (
1919
ARG_POS,
2020
ARG_STAR,
21+
ARG_STAR2,
2122
EXCLUDED_ENUM_ATTRIBUTES,
2223
SYMBOL_FUNCBASE_TYPES,
2324
Context,
@@ -359,10 +360,13 @@ def analyze_instance_member_access(
359360
signature = method.type
360361
signature = freshen_all_functions_type_vars(signature)
361362
if not method.is_static:
362-
signature = check_self_arg(
363-
signature, mx.self_type, method.is_class, mx.context, name, mx.msg
364-
)
365-
signature = bind_self(signature, mx.self_type, is_classmethod=method.is_class)
363+
if isinstance(method, (FuncDef, OverloadedFuncDef)) and method.is_trivial_self:
364+
signature = bind_self_fast(signature, mx.self_type)
365+
else:
366+
signature = check_self_arg(
367+
signature, mx.self_type, method.is_class, mx.context, name, mx.msg
368+
)
369+
signature = bind_self(signature, mx.self_type, is_classmethod=method.is_class)
366370
# TODO: should we skip these steps for static methods as well?
367371
# Since generic static methods should not be allowed.
368372
typ = map_instance_to_supertype(typ, method.info)
@@ -521,9 +525,11 @@ def analyze_member_var_access(
521525
mx.chk.warn_deprecated(v, mx.context)
522526

523527
vv = v
528+
is_trivial_self = False
524529
if isinstance(vv, Decorator):
525530
# The associated Var node of a decorator contains the type.
526531
v = vv.var
532+
is_trivial_self = vv.func.is_trivial_self and not vv.decorators
527533
if mx.is_super and not mx.suppress_errors:
528534
validate_super_call(vv.func, mx)
529535

@@ -555,7 +561,7 @@ def analyze_member_var_access(
555561
if mx.is_lvalue and not mx.chk.get_final_context():
556562
check_final_member(name, info, mx.msg, mx.context)
557563

558-
return analyze_var(name, v, itype, mx, implicit=implicit)
564+
return analyze_var(name, v, itype, mx, implicit=implicit, is_trivial_self=is_trivial_self)
559565
elif isinstance(v, FuncDef):
560566
assert False, "Did not expect a function"
561567
elif isinstance(v, MypyFile):
@@ -850,14 +856,21 @@ def is_instance_var(var: Var) -> bool:
850856

851857

852858
def analyze_var(
853-
name: str, var: Var, itype: Instance, mx: MemberContext, *, implicit: bool = False
859+
name: str,
860+
var: Var,
861+
itype: Instance,
862+
mx: MemberContext,
863+
*,
864+
implicit: bool = False,
865+
is_trivial_self: bool = False,
854866
) -> Type:
855867
"""Analyze access to an attribute via a Var node.
856868
857869
This is conceptually part of analyze_member_access and the arguments are similar.
858870
itype is the instance type in which attribute should be looked up
859871
original_type is the type of E in the expression E.var
860872
if implicit is True, the original Var was created as an assignment to self
873+
if is_trivial_self is True, we can use fast path for bind_self().
861874
"""
862875
# Found a member variable.
863876
original_itype = itype
@@ -904,7 +917,7 @@ def analyze_var(
904917
for ct in call_type.items if isinstance(call_type, UnionType) else [call_type]:
905918
p_ct = get_proper_type(ct)
906919
if isinstance(p_ct, FunctionLike) and not p_ct.is_type_obj():
907-
item = expand_and_bind_callable(p_ct, var, itype, name, mx)
920+
item = expand_and_bind_callable(p_ct, var, itype, name, mx, is_trivial_self)
908921
else:
909922
item = expand_without_binding(ct, var, itype, original_itype, mx)
910923
bound_items.append(item)
@@ -938,13 +951,21 @@ def expand_without_binding(
938951

939952

940953
def expand_and_bind_callable(
941-
functype: FunctionLike, var: Var, itype: Instance, name: str, mx: MemberContext
954+
functype: FunctionLike,
955+
var: Var,
956+
itype: Instance,
957+
name: str,
958+
mx: MemberContext,
959+
is_trivial_self: bool,
942960
) -> Type:
943961
functype = freshen_all_functions_type_vars(functype)
944962
typ = get_proper_type(expand_self_type(var, functype, mx.original_type))
945963
assert isinstance(typ, FunctionLike)
946-
typ = check_self_arg(typ, mx.self_type, var.is_classmethod, mx.context, name, mx.msg)
947-
typ = bind_self(typ, mx.self_type, var.is_classmethod)
964+
if is_trivial_self:
965+
typ = bind_self_fast(typ, mx.self_type)
966+
else:
967+
typ = check_self_arg(typ, mx.self_type, var.is_classmethod, mx.context, name, mx.msg)
968+
typ = bind_self(typ, mx.self_type, var.is_classmethod)
948969
expanded = expand_type_by_instance(typ, itype)
949970
freeze_all_type_vars(expanded)
950971
if not var.is_property:
@@ -1203,10 +1224,22 @@ def analyze_class_attribute_access(
12031224
isinstance(node.node, SYMBOL_FUNCBASE_TYPES) and node.node.is_static
12041225
)
12051226
t = get_proper_type(t)
1206-
if isinstance(t, FunctionLike) and is_classmethod:
1227+
is_trivial_self = False
1228+
if isinstance(node.node, Decorator):
1229+
# Use fast path if there are trivial decorators like @classmethod or @property
1230+
is_trivial_self = node.node.func.is_trivial_self and not node.node.decorators
1231+
elif isinstance(node.node, (FuncDef, OverloadedFuncDef)):
1232+
is_trivial_self = node.node.is_trivial_self
1233+
if isinstance(t, FunctionLike) and is_classmethod and not is_trivial_self:
12071234
t = check_self_arg(t, mx.self_type, False, mx.context, name, mx.msg)
12081235
result = add_class_tvars(
1209-
t, isuper, is_classmethod, is_staticmethod, mx.self_type, original_vars=original_vars
1236+
t,
1237+
isuper,
1238+
is_classmethod,
1239+
is_staticmethod,
1240+
mx.self_type,
1241+
original_vars=original_vars,
1242+
is_trivial_self=is_trivial_self,
12101243
)
12111244
# __set__ is not called on class objects.
12121245
if not mx.is_lvalue:
@@ -1255,7 +1288,7 @@ def analyze_class_attribute_access(
12551288
# Annotated and/or explicit class methods go through other code paths above, for
12561289
# unannotated implicit class methods we do this here.
12571290
if node.node.is_class:
1258-
typ = bind_self(typ, is_classmethod=True)
1291+
typ = bind_self_fast(typ)
12591292
return apply_class_attr_hook(mx, hook, typ)
12601293

12611294

@@ -1342,6 +1375,7 @@ def add_class_tvars(
13421375
is_staticmethod: bool,
13431376
original_type: Type,
13441377
original_vars: Sequence[TypeVarLikeType] | None = None,
1378+
is_trivial_self: bool = False,
13451379
) -> Type:
13461380
"""Instantiate type variables during analyze_class_attribute_access,
13471381
e.g T and Q in the following:
@@ -1362,6 +1396,7 @@ class B(A[str]): pass
13621396
original_type: The value of the type B in the expression B.foo() or the corresponding
13631397
component in case of a union (this is used to bind the self-types)
13641398
original_vars: Type variables of the class callable on which the method was accessed
1399+
is_trivial_self: if True, we can use fast path for bind_self().
13651400
Returns:
13661401
Expanded method type with added type variables (when needed).
13671402
"""
@@ -1383,7 +1418,10 @@ class B(A[str]): pass
13831418
tvars = original_vars if original_vars is not None else []
13841419
t = freshen_all_functions_type_vars(t)
13851420
if is_classmethod:
1386-
t = bind_self(t, original_type, is_classmethod=True)
1421+
if is_trivial_self:
1422+
t = bind_self_fast(t, original_type)
1423+
else:
1424+
t = bind_self(t, original_type, is_classmethod=True)
13871425
if is_classmethod or is_staticmethod:
13881426
assert isuper is not None
13891427
t = expand_type_by_instance(t, isuper)
@@ -1422,5 +1460,45 @@ def analyze_decorator_or_funcbase_access(
14221460
if isinstance(defn, Decorator):
14231461
return analyze_var(name, defn.var, itype, mx)
14241462
typ = function_type(defn, mx.chk.named_type("builtins.function"))
1463+
is_trivial_self = False
1464+
if isinstance(defn, Decorator):
1465+
# Use fast path if there are trivial decorators like @classmethod or @property
1466+
is_trivial_self = defn.func.is_trivial_self and not defn.decorators
1467+
elif isinstance(defn, (FuncDef, OverloadedFuncDef)):
1468+
is_trivial_self = defn.is_trivial_self
1469+
if is_trivial_self:
1470+
return bind_self_fast(typ, mx.self_type)
14251471
typ = check_self_arg(typ, mx.self_type, defn.is_class, mx.context, name, mx.msg)
14261472
return bind_self(typ, original_type=mx.self_type, is_classmethod=defn.is_class)
1473+
1474+
1475+
F = TypeVar("F", bound=FunctionLike)
1476+
1477+
1478+
def bind_self_fast(method: F, original_type: Type | None = None) -> F:
1479+
"""Return a copy of `method`, with the type of its first parameter (usually
1480+
self or cls) bound to original_type.
1481+
1482+
This is a faster version of mypy.typeops.bind_self() that can be used for methods
1483+
with trivial self/cls annotations.
1484+
"""
1485+
if isinstance(method, Overloaded):
1486+
items = [bind_self_fast(c, original_type) for c in method.items]
1487+
return cast(F, Overloaded(items))
1488+
assert isinstance(method, CallableType)
1489+
if not method.arg_types:
1490+
# Invalid method, return something.
1491+
return cast(F, method)
1492+
if method.arg_kinds[0] in (ARG_STAR, ARG_STAR2):
1493+
# See typeops.py for details.
1494+
return cast(F, method)
1495+
original_type = get_proper_type(original_type)
1496+
if isinstance(original_type, CallableType) and original_type.is_type_obj():
1497+
original_type = TypeType.make_normalized(original_type.ret_type)
1498+
res = method.copy_modified(
1499+
arg_types=method.arg_types[1:],
1500+
arg_kinds=method.arg_kinds[1:],
1501+
arg_names=method.arg_names[1:],
1502+
bound_args=[original_type],
1503+
)
1504+
return cast(F, res)

mypy/nodes.py

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -550,7 +550,7 @@ class OverloadedFuncDef(FuncBase, SymbolNode, Statement):
550550
Overloaded variants must be consecutive in the source file.
551551
"""
552552

553-
__slots__ = ("items", "unanalyzed_items", "impl", "deprecated")
553+
__slots__ = ("items", "unanalyzed_items", "impl", "deprecated", "_is_trivial_self")
554554

555555
items: list[OverloadPart]
556556
unanalyzed_items: list[OverloadPart]
@@ -563,6 +563,7 @@ def __init__(self, items: list[OverloadPart]) -> None:
563563
self.unanalyzed_items = items.copy()
564564
self.impl = None
565565
self.deprecated = None
566+
self._is_trivial_self: bool | None = None
566567
if items:
567568
# TODO: figure out how to reliably set end position (we don't know the impl here).
568569
self.set_line(items[0].line, items[0].column)
@@ -576,6 +577,27 @@ def name(self) -> str:
576577
assert self.impl is not None
577578
return self.impl.name
578579

580+
@property
581+
def is_trivial_self(self) -> bool:
582+
"""Check we can use bind_self() fast path for this overload.
583+
584+
This will return False if at least one overload:
585+
* Has an explicit self annotation, or Self in signature.
586+
* Has a non-trivial decorator.
587+
"""
588+
if self._is_trivial_self is not None:
589+
return self._is_trivial_self
590+
for item in self.items:
591+
if isinstance(item, FuncDef):
592+
if not item.is_trivial_self:
593+
self._is_trivial_self = False
594+
return False
595+
elif item.decorators or not item.func.is_trivial_self:
596+
self._is_trivial_self = False
597+
return False
598+
self._is_trivial_self = True
599+
return True
600+
579601
def accept(self, visitor: StatementVisitor[T]) -> T:
580602
return visitor.visit_overloaded_func_def(self)
581603

@@ -747,6 +769,7 @@ def is_dynamic(self) -> bool:
747769
"is_decorated",
748770
"is_conditional",
749771
"is_trivial_body",
772+
"is_trivial_self",
750773
"is_mypy_only",
751774
]
752775

@@ -771,6 +794,7 @@ class FuncDef(FuncItem, SymbolNode, Statement):
771794
"abstract_status",
772795
"original_def",
773796
"is_trivial_body",
797+
"is_trivial_self",
774798
"is_mypy_only",
775799
# Present only when a function is decorated with @typing.dataclass_transform or similar
776800
"dataclass_transform_spec",
@@ -804,6 +828,10 @@ def __init__(
804828
self.dataclass_transform_spec: DataclassTransformSpec | None = None
805829
self.docstring: str | None = None
806830
self.deprecated: str | None = None
831+
# This is used to simplify bind_self() logic in trivial cases (which are
832+
# the majority). In cases where self is not annotated and there are no Self
833+
# in the signature we can simply drop the first argument.
834+
self.is_trivial_self = False
807835

808836
@property
809837
def name(self) -> str:

mypy/semanal.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1085,6 +1085,7 @@ def prepare_method_signature(self, func: FuncDef, info: TypeInfo, has_self_type:
10851085
assert self.type is not None and self.type.self_type is not None
10861086
leading_type: Type = self.type.self_type
10871087
else:
1088+
func.is_trivial_self = True
10881089
leading_type = fill_typevars(info)
10891090
if func.is_class or func.name == "__new__":
10901091
leading_type = self.class_type(leading_type)

0 commit comments

Comments
 (0)