While making a function to manipulate the TypeVar from the typing module, I came across a problem. My function do work but I wanted to make it faster so I used the timeit module to test some different versions.
However the result aren't what I expected.


Clearing some potential interrogation :

check_type(object , type) is a function meant to test if an object correspond to a type (quite time consuming currently).

isTypeVar(type) test if type is a TypeVar.

NullType is a type that cannot describe an object so when it's tested with check_type it always return False.

restrict_TypeVar_for_object(typeVar , object) is the function that I have made in order to remove all of the constraints of my typeVar that doesn't correspond to object.

How I time my functions :

from timeit import timeit

func_version = """..."""
task="""..."""
timeit(task , setups=func_version , number=10_000_000)

Here is the task that I have timed (as you can see above I have repeated it 10 000 000 times) :

Number = int | float
T = TypeVar("T" , int , str , Number)

restrict_TypeVar_for_object(T , 5)
#   --> TypeVar("T" , int , Number)

My test :

  • 1st version :
def restrict_TypeVar_for_object(ty , obj):
    if not isTypeVar(ty) : #Just to be sure that ty is a TypeVar
        raise ValueError ('Expected a TypeVar as an argument')

    if not ty.__constraints__ :
        return type(obj)

    lCons=[cons for cons in ty.__constraints__ if check_type(obj , cons)]
    return (TypeVar(ty.__name__ , *lCons)
            if len(lCons)>1 else
            lCons[0]
            if lCons else
            NullType)

Takes : 378.5483768 s

  • 2nd version :
def restrict_TypeVar_for_object(ty , obj):
    if not isTypeVar(ty) : #Just to be sure that ty is a TypeVar
        raise ValueError ('Expected a TypeVar as an argument')

    if not ty.__constraints__ :
        return type(obj)

    return (TypeVar(ty.__name__ , *lCons)
            if len(lCons:=[cons for cons in ty.__constraints__ if check_type(obj , cons)])>1 else
            lCons[0]
            if lCons else
            NullType)

Takes : 376.9706139 s

  • 3rd version :
def restrict_TypeVar_for_object(ty , obj):
    if not isTypeVar(ty) : #Just to be sure that ty is a TypeVar
        raise ValueError ('Expected a TypeVar as an argument')

    if not ty.__constraints__ :
        return type(obj)

    return (TypeVar(ty.__name__ , *lCons[:-1])
            if len(lCons:=[c for c in ty.__constraints__ if check_type(obj,c)]+[NullType]) > 2
            else
            lCons[0])

Takes : 391.5145658000001 s


As you can see the 2nd one is the quickest. However I wasn't expecting the 3rd one to be that slow compared to the other. To be frank I was even expecting it to be the quickest one because I did less if statement in this one.

I have 2 hypothesis for why this is the case :

-First, the walrus operator but if it is, why is the 2nd version the quickest.

-Second, the fact that I add [NullType] at the end of lCons but I don't know why it would be this time consuming.

1

There are 1 best solutions below

0
On

First of all, as tdelaney pointed out, the difference in term of their execution time comes from the creation of many list in the 3rd version :

  • [c for c in ty.__constraints__ if check_type(obj,c)] which is common to all version.
  • [NullType], the list resulting from the concatenation lCons + [NullType] and lCons[:-1] which are exclusive to the 3rd version.

Now comes the 2nd part of my question, about the time consumption of the walrus operator.

The walrus operator takes more time than a simple assignation as shown by the test below :

from timeit import timeit

#Assignement statement :
tsk1="""
a = 5
"""

#Assignement expression :
tsk2="""
(a := 5)
"""

print(timeit(tsk1 , number=10_000_000))
print(timeit(tsk2 , number=10_000_000))

tsk1 repeated 10 000 000 takes : 0.7440573999992921 s

tsk2 repeated 10 000 000 takes : 1.0597749000007752 s

I did the test many time and the result are always approximately the same.


Finally, here is how I should implement my restrict_TypeVar_for_object(typeVar , object) function in Python :

def restrict_TypeVar_for_object(ty , obj):
    if not isTypeVar(ty) : #Just to be sure that ty is a TypeVar
        raise ValueError ('Expected a TypeVar as an argument')

    if not ty.__constraints__ :
        return type(obj)
    
    lCons = [cons for cons in ty.__constraints__ if check_type(obj , cons)]
    return (TypeVar(ty.__name__ , *lCons)
            if len(lCons)>1 else
            [*lCons , NullType][0])

Which, repeated 10 000 000 times takes : 383.57044309999765

However as you can see it still takes too much time so I have tried with the walrus operator :

def restrict_by_object(ty , obj):
    if not isTypeVar(ty) : #Just to be sure that ty is a TypeVar
        raise ValueError ('Expected a TypeVar as an argument')

    if not ty.__constraints__ :
        return type(obj)

    return (TypeVar(ty.__name__ , *lCons)
            if len(lCons:=[cons for cons in ty.__constraints__ if check_type(obj , cons)])>1 else
            [*lCons , NullType][0])

This takes : 373.867099199997 s

I still don't know why the walrus operator is better in this case but will update this answer once I understand it.