django Python中条件元组解包的权衡和注意事项:主要是性能,但也考虑到可维护性的风格

jv2fixgn  于 2023-10-21  发布在  Go
关注(0)|答案(1)|浏览(132)

我在Python中有一段逻辑,涉及基于条件的元组解包。
代码示例化两个变量,并将元组的解包值赋给这些变量。
这个元组是有条件定义的:如果conditional_expression为true,则函数执行返回元组,否则提供并分配默认值。
我的理解是有两种简单的方法来实现这一点,如下所示:

# Method 1 #
if conditional_expression:
    processed_data, context = tuple_returner(raw_data)
else:
    processed_data, context = raw_data[0], None

# Method 2 #
processed_data, context = (
    tuple_returner(raw_data)
    if conditional_expression
    else raw_data[0], None
)

现在我已经尝试了两种方法,两者都像预期的那样工作得很好。
我的问题是双重的:
1.这两种方法中的任何一种可能比另一种更性能吗(在标准CPython实现中)?在运行timeit测试时,我没有注意到任何实质性的差异(尽管方法2似乎稍微慢了一点- * 请参阅底部代码片段 *),但我想知道是否有更深层次的事情发生在我不知道的幕后。
1.这两种方法中的哪一种比另一种更Pythonic,更受社区的青睐?我在PEP 8里面看不到任何具体的东西来指导我。两者对我来说都是可读的,但这个代码库将持续很长时间,并由几个贡献者工作,所以希望坚持最佳实践。
FWIW实际代码是一个大型Django项目的一部分,涉及Celery任务中的一些复杂业务逻辑,并且在我们处理数据和维护上下文 * 的任务中经常重复这种范式(有时我们解包的值远远不止两个)。因此,如果有Django特定的风格考虑,那么知道这一点会很有用!
对于那些想看我的timeit评估的人,请看下面:

import timeit
import random

def tuple_returner(data):
    return data * 2, data * 3

def method1():
    conditional_expression = random.choice([True, False])
    raw_data = random.randint(1, 1000)
    if conditional_expression:
        processed_data, context = tuple_returner(raw_data)
    else:
        processed_data, context = raw_data, None

def method2():
    conditional_expression = random.choice([True, False])
    raw_data = random.randint(1, 1000)
    processed_data, context = tuple_returner(raw_data) if conditional_expression else (raw_data, None)

time1_a = timeit.timeit(method1, number=10000000)
time1_b = timeit.timeit(method1, number=10000000)
time1_c = timeit.timeit(method1, number=10000000)
time1_d = timeit.timeit(method1, number=10000000)
time1_e = timeit.timeit(method1, number=10000000)
time1_mean = (time1_a + time1_b + time1_c + time1_d + time1_e) / 5
time1_median = sorted([time1_a, time1_b, time1_c, time1_d, time1_e])[2]
time2_a = timeit.timeit(method2, number=10000000)
time2_b = timeit.timeit(method2, number=10000000)
time2_c = timeit.timeit(method2, number=10000000)
time2_d = timeit.timeit(method2, number=10000000)
time2_e = timeit.timeit(method2, number=10000000)
time2_mean = (time2_a + time2_b + time2_c + time2_d + time2_e) / 5
time2_median = sorted([time2_a, time2_b, time2_c, time2_d, time2_e])[2]

print("Method 1: mean = {0}, median = {1}".format(time1_mean, time1_median))
## -> Output:  Method 1: mean = 10.77958505996503, median = 10.693453999934718

print("Method 2: mean = {0}, median = {1}".format(time2_mean, time2_median))
## -> Output: Method 2: mean = 11.350917899981141, median = 11.356302100000903
yc0p9oo0

yc0p9oo01#

这个问题似乎可以归结为:
是传统的多线if/else倍快于单线等效
其次,
一个比另一个更好。
这里是我的观点,但我更喜欢method2()(单行)而不是method1(),只要意图仍然清楚。
让我看看我是否可以解决第一部分,虽然采取了一些噪音的系统
首先,让我们将raw_data的定义移出方法调用,因为它只会增加噪声。让我们定义第二个tuple_returner()方法并调用它,这样可以确保两个分支所做的“工作”是相同的。
比如:

import timeit
import random

def tuple_returner1(data):
    return data, None

def tuple_returner2(data):
    return data, None

def method1():
    conditional_expression = random.choice([True, False])
    if conditional_expression:
        processed_data, context = tuple_returner1(raw_data)
    else:
        processed_data, context = tuple_returner2(raw_data)

def method2():
    conditional_expression = random.choice([True, False])
    processed_data, context = tuple_returner1(raw_data) if conditional_expression else tuple_returner2(raw_data)

raw_data = random.randint(1, 1000)

for _ in range(10):
    time1_a = timeit.timeit(method1, number=1_000_000)
    time2_a = timeit.timeit(method2, number=1_000_000)
    pct_2_vs_1 = round((time1_a - time2_a) / time1_a, 4)
    print(pct_2_vs_1)

这对我来说产生了一个结果,如:

0.0338
-0.0558
0.0527
-0.0166
0.0073
0.009
0.0198
0.0175
-0.0283
-0.0022

这向我暗示,在没有分支预测的情况下,性能可能没有有效的差异。如果我们搬家呢

conditional_expression = random.choice([True, False])

在允许分支预测的更好可能性的方法中

import timeit
import random

def tuple_returner1(data):
    return data, None

def tuple_returner2(data):
    return data, None

def method1():
    if conditional_expression:
        processed_data, context = tuple_returner1(raw_data)
    else:
        processed_data, context = tuple_returner2(raw_data)

def method2():
    processed_data, context = tuple_returner1(raw_data) if conditional_expression else tuple_returner2(raw_data)

raw_data = random.randint(1, 1000)
for _ in range(20):
    conditional_expression = random.choice([True, False])
    time1_a = timeit.timeit(method1, number=1_000_000)
    time2_a = timeit.timeit(method2, number=1_000_000)
    pct_2_vs_1 = round((time1_a - time2_a) / time1_a, 4)
    print(pct_2_vs_1)

我看到的结果如下:

-0.0567
-0.0068
0.0111
0.0593
0.0043
0.0991
0.0207
-0.1114
0.0159
0.0493

这再次向我表明,这两种策略之间几乎没有有效的性能差异。对于什么是值得的,虽然,你可以看看dissembley,看看你是否看到任何可能暗示的差异

方法一:

12           0 LOAD_GLOBAL              0 (conditional_expression)
              2 POP_JUMP_IF_FALSE       10 (to 20)

 13           4 LOAD_GLOBAL              1 (tuple_returner1)
              6 LOAD_GLOBAL              2 (raw_data)
              8 CALL_FUNCTION            1
             10 UNPACK_SEQUENCE          2
             12 STORE_FAST               0 (processed_data)
             14 STORE_FAST               1 (context)
             16 LOAD_CONST               0 (None)
             18 RETURN_VALUE

 15     >>   20 LOAD_GLOBAL              3 (tuple_returner2)
             22 LOAD_GLOBAL              2 (raw_data)
             24 CALL_FUNCTION            1
             26 UNPACK_SEQUENCE          2
             28 STORE_FAST               0 (processed_data)
             30 STORE_FAST               1 (context)
             32 LOAD_CONST               0 (None)
             34 RETURN_VALUE

方法二:

18           0 LOAD_GLOBAL              0 (conditional_expression)
              2 POP_JUMP_IF_FALSE        6 (to 12)
              4 LOAD_GLOBAL              1 (tuple_returner1)
              6 LOAD_GLOBAL              2 (raw_data)
              8 CALL_FUNCTION            1
             10 JUMP_FORWARD             3 (to 18)
        >>   12 LOAD_GLOBAL              3 (tuple_returner2)
             14 LOAD_GLOBAL              2 (raw_data)
             16 CALL_FUNCTION            1
        >>   18 UNPACK_SEQUENCE          2
             20 STORE_FAST               0 (processed_data)
             22 STORE_FAST               1 (context)
             24 LOAD_CONST               0 (None)
             26 RETURN_VALUE

虽然第一个更长,但两种情况下的最终结果似乎相同,除了第二个中的分支将JUMP_FORWARD作为额外指令执行。我想从技术上讲,第二个更慢。

相关问题