一日一技:使用裝飾器簡化大量 if 判斷

攝影:產品經理

乾貨來了

在之前的文章:一日一技:使用裝飾器簡化大量 if…elif… 代碼發佈以後,有很多同學說想看後續,如何在裝飾器中表示大於小於。甚至有同學每週來催一次稿:

於是,今天我們就來看看大於小於應該怎麼來判斷。爲了實現我們今天的目標,有兩個前置知識需要掌握,一個是 Python 自帶的operator模塊,另一個是偏函數。

2 > 1 還有另一種寫法?

當我們要表達大於這個意思的時候,你想到的肯定是大於符號>。所以2大於1,肯定寫作2 > 1。這看起來是很正常的事情。現在,如果我讓你不準使用大於符號>,怎麼表示大於?

實際上,在 Python 裏面,除了>外,還有一種寫法,就是使用自帶的operator模塊:

import operator

operator.gt(2, 1)

其中的.gt(參數1, 參數2)就表示參數1 > 參數2。如果成立,返回True,否則返回False

類似的還有:

因此,下面兩個寫法是等價的:

if a <= b:
    print('成功')
if operator.le(a, b):
    print('成功')

偏函數

我在很久以前的公衆號文章裏面已經介紹過偏函數了:偏函數:在 Python 中設定默認參數的另一種辦法。因此本文就不再講它的基礎用法了,大家點擊鏈接去看那篇文章就可以掌握。

爲什麼我們需要偏函數呢?這是因爲我們今天要做的事情,它需要給函數先傳一半的參數,另一半的參數要在未來才能傳入。例如,循環等待用戶輸入數字,如果其中一次輸入的數字大於等於 5,就打印你好世界

如果不知道偏函數,你可能是這樣寫的:

while True:
    num = int(input('請輸入數字:'))
    if num >= 5:
        print('你好世界')

有了偏函數以後,你的寫法是這樣的:

import operator
from functools import partial
ge_5 = partial(operator.le, 5)
while True:
    num = int(input('請輸入數字:'))
    if ge_5(num):
        print('你好世界')

特別注意,這裏我在偏函數中傳入的第一個參數是operator.le:小於。因爲operator.xx表示第一個參數對第二個參數的比較,所以x >= 5 就相當於 5 <= x 也就是operator.le(5, x)

在裝飾器中實現大小比較

前置知識掌握以後,我們就能看如何在裝飾器裏面實現大小比較。在第一篇文章中,我們只實現了參數等於,它的原理是:

def register(value):
        def wrap(func):
            if value in registry:
                raise ValueError(
                    f'@value_dispatch: there is already a handler '
                    f'registered for {value!r}'
                )
            registry[value] = func
            return func
        return wrap

register只接收了一個位置參數value。但實際上,我們還可以通過修改這段註冊的代碼,實現如下的效果:

@get_discount.register(3, op='gt')
def parse_level_gt3(level):
    print('等級大於3')

@get_discount.register(3, op='le')
def parse_level_le3(level):
    print('等級小於等於3')

有同學問,有沒有可能實現這樣的寫法呢:

@get_discount.register(2, le=3)
def parse_level_gt3(level):
    print('等級爲2')

我覺得這樣寫是沒有什麼必要的。因爲register()裏面,多個參數之間的關係是。那麼只有兩種情況,要麼,就等於這個數,例如@get_discount.register(2, le=3),既要等於 2,又要小於等於 3,那顯然就等於 2。不需要寫這個le=3。要麼,就不存在結果,例如@get_discount.register(2, gt=3),既要等於 2,又要大於 3,顯然下面被裝飾的函數永遠不會執行。因爲找不到這個數。

因此,我們的裝飾器函數就可以做如下修改:

import functools
import operator

def value_dispatch(func):

    registry_eq = {}
    registry_other = {}
    key_op_map = {}

    @functools.wraps(func)
    def wrapper(arg0, *args, **kwargs):
        if arg0 in registry_eq:
            delegate = registry_eq[arg0]
            return delegate(arg0, *args, **kwargs)
        else:
            for key, op in key_op_map.items():
                if op(arg0):
                    delegate = registry_other[key]
                    return delegate(arg0, *args, **kwargs)
        return func(arg0, *args, **kwargs)

    def register(value, op='eq'):
        if op == 'eq':
            def wrap(func):
                if value in registry_eq:
                    raise ValueError(
                        f'@value_dispatch: there is already a handler '
                        f'registered for {value!r}'
                    )
                registry_eq[value] = func
                return func
            return wrap
        else:
            if op == 'gt':
                op_func = functools.partial(operator.lt, value)
            elif op == 'ge':
                op_func = functools.partial(operator.le, value)
            elif op == 'lt':
                op_func = functools.partial(operator.gt, value)
            elif op == 'le':
                op_func = functools.partial(operator.ge, value)
            else:
                raise ValueError('op 參數只能是:gt/ge/lt/le之一')
            key = f'{op}_{value}'
            key_op_map[key] = op_func
            def wrap(func):
                if key in registry_other:
                    raise ValueError(
                        f'@value_dispatch: there is already a handler '
                        f'registered for {key!r}'
                    )
                registry_other[key] = func
                return func
            return wrap
            
    
    wrapper.register = register
    return wrapper

它的使用方法還是跟以前一樣,先定義默認的函數邏輯:

@value_dispatch
def get_discount(level):
    return '等級錯誤'

如果定義相等的邏輯,寫法跟以前完全一樣:

@get_discount.register(1)
def parse_level_1(level):
    "大量計算代碼"
    discount = 0.1
    return discount

如果要定義不等於邏輯,就在.register()中添加一個參數op

@get_discount.register(2, op='gt')
def parse_level_gt2(level):
    discount = 1
    return 1

運行效果如下圖所示:

由於我們定義了大於 2 時,始終返回 1,所以可以看到get_discount(6)get_discount(10)返回的都是 1.

由於我們只定義了等於 1 和大於 2 的邏輯,所以當傳入的參數爲 2 時,就返回等級錯誤.

到這裏,本文要講的內容就結束了。但最後還是要考大家 3 個問題:

如果不使用偏函數和 operator 模塊,你會怎麼做

你可以試一試在不實用偏函數和operator的情況下,實現這個需求。

如果定義的條件有重疊怎麼辦?

例如對於下面的兩個函數:

@get_discount.register(2, op='gt')
def parse_level_gt2(level):
    discount = 1
    return discount

@get_discount.register(10, op='gt')
def parse_level_gt2(level):
    discount = 100
    return discount

當 level 的值是 20 的時候,同時滿足兩個條件,應該運行哪一個呢?

如何定義區間?

怎麼實現這樣的功能:

@get_discount.register(ge=2, lt=5)
def parse_level_between2_5(level):
    print('等級2<=level<5')
    discount = 0.5
    return discount

如果區間存在全包含、部分包含應該運行哪個函數?例如:

@get_discount.register(ge=2, lt=00)
...

@get_discount.register(ge=20, lt=50)
...

@get_discount.register(ge=80, lt=200)
...

請大家把你對這兩個問題的答案回答在評論區裏面。提示(想清楚什麼是真需求,什麼是僞需求,再考慮怎麼解決)

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/s5qQGinQz5SJO8IDlRJ5Gg