Python 可換手机電影的分電視: 時から根因油法 – EXIORAN – 博客园

0
Python 可換手机電影的分電視: 時から根因油法 – EXIORAN – 博客园


要約: コンテンツ電気効果は、根元までの時間に応じてコンテンツ電気効果を追跡することができます。そのため、さらに上は書き込み法であり、Python 関数のデフォルト値を設定する電気効果です。

一、故障現象

一分页拉拉拉拉会电影電影、ロジックが使用されています start=0 毎回起 limitデータが完全に取得されるまで。連続して呼び出すと、インターフェイスに異常が表示されます。

client = Client()
print(client.get_data())   # 第 1 次: [0, 100, 200, 300, 400]
print(client.get_data())   # 第 2 次: []        ← 返回空
print(client.get_data())   # 第 3 次: []        ← 永远为空

最初に完全に正常に呼び出されたとき、2 回目には空のリストに戻ります。

排查電影安全被语说:排查方设计计入 start=500,ただしサービス終了録画リクエストパラメータ表示 start すでに500になっています。「パラメータが空中にある」ように見えます。

2、問題コード

class Client:
    def get_data(self, param: dict = {}) -> list:   # 问题出在这里
        res_data = []

        param["start"] = param.get("start", 0)
        param["limit"] = param.get("limit", 100)

        while param["start"] < 500:
            res_data.append(param["start"])
            param["start"] += param["limit"]

        return res_data

の関数シグネチャ param デフォルト値を提供します {}、会話システムから見て、最良の効果は見られません。

3、根因分析

3.1 Python のデフォルトパラメータの要求時刻机

Python の中の、関数のデフォルトパラメータにあります一度定義された関数、結果は機能にバインドされます __defaults__ 後続の呼び出しごとに、パラメータが渡されない場合、この既存のパラメータが再利用され、この既存のパラメータが再利用されます。ない再作成します。

それは意味します def get_data(self, param={}) 中国 {}

  • 不是「すべての呼び出しが新しい単語を生み出す」
  • 日本語「単一の単語を作成したときの定義機能。すべては互いに共有されません」

3.2 共有オブジェクト ID の検証

メソッド内で印刷する param オブジェクトID:

logger.info("本次拿到的 param 对象 id: %s", id(param))

3 回目の呼び出しの出力:

本次拿到的 param 对象 id: 2350173998720
本次拿到的 param 对象 id: 2350173998720
本次拿到的 param 对象 id: 2350173998720

3 回目の呼び出しでは同じオブジェクトを取得しました。

3.3 障害が発生する仕組み

  1. 最初の電話:param 共有空文字の場合、start 0 から開始、サイクルが 500 まで増加、終了、データを返す start に変更されました 500
  2. 2 回目: 同じ単語を取得start もう500だよ
  3. param["start"] = param.get("start", 0) —— get 0ではなく500を返します
  4. while param["start"] < 500 —— 500 不これらの電影、電影体一不可了
  5. 空のリストに戻る

ループは実行されないので、単語を共有します start 再度変更されることはないため、3 回を超えると永久に停止され、毎回空のリストに戻ります。

3.4 最低収益率

同じメカニズムを純粋関数を使用して再現します。

def append_to(element, target=[]):
    target.append(element)
    return target

print(append_to(1))  # [1]
print(append_to(2))  # [1, 2]    ← 不是 [2]
print(append_to(3))  # [1, 2, 3]

target デフォルトのリストは 3 回共有され、要素は常に蓄積されます。

四、なぜこのバグは見つけにくいのか

  1. 初めてでも常に正しい障害は2回目以降の呼び出しでのみ発生します。
  2. テストユニットは漏れやすいテストで 1 つのメソッドのみを使用する場合、そのメソッドは合格します。
  3. 方向が間違っている現象「2 回目は空を返す」は、後のデータの問題、接続の問題、関数関数ではない場合に読みやすいです。
  4. コードレビュー不易観察param={} は法文法,看冒了安全滑生说

五、 3種類の修理ソリューション

5.1 電影一:デフォルト使用 なし

class FixedClientWithNone:
    """默认值用 None 哨兵,进函数后再创建可变容器。"""

    def get_data(self, param: Optional[dict] = None) -> list:
        if param is None:        # 关键:函数内部再创建新字典
            param = {}
        param.setdefault("start", 0)
        param.setdefault("limit", 100)

        res_data = []
        while param["start"] < 500:
            res_data.append(param["start"])
            param["start"] += param["limit"]
        return res_data

デフォルト値 None(不可变合,最好起安全上海)、本物の字典在電話をかけるたびに機能インターフェイスの形状はまったく変わっていないため、ユーザーは変更する必要がありません。

副作用に注意する必要があります。使用すると、自分自身の副作用が発生します。 param 字典、電影一会这これら setdefault デフォルト値を直接使用し、会は写生方のオブジェクトを変更するそうでない場合は、電気エフェクト2を使用して変更してください。

5.2 計画二:予約字典インターフェース、内部コピー贝

class FixedClientWithCopy:
    """默认值用 None 兜底,并拷贝一份,绝不改写调用方对象。"""

    def get_data(self, param: Optional[dict] = None) -> list:
        param = dict(param or {})   # 拷贝一份,不改写调用方对象

        start = param.get("start", 0)
        limit = param.get("limit", 100)

        res_data = []
        while start < 500:
            res_data.append(start)
            start += limit
        return res_data

デフォルト値 None,機能 dict(param or {}) これは、クロスコールの共有を回避するだけでなく、コールのパススルーのオブジェクトを汚染することもありません。これがまさに、プラン 1 との重要な違いです。

5.3 スキーム 3: 所定の大きさの代器を配置する

class Paginator:
    """把分页游标封装成迭代器,状态不外泄给调用方。"""

    def __init__(self, total: int = 500, page_size: int = 100) -> None:
        self.total = total
        self.page_size = page_size

    def pages(self):
        """逐页产出 (offset, limit),直到 offset >= total。"""
        offset = 0
        while offset < self.total:
            yield offset, self.page_size
            offset += self.page_size


class FixedClientWithPaginator:
    """用分页器替代手工维护游标。"""

    def get_data(self, paginator: Optional[Paginator] = None) -> list:
        paginator = paginator or Paginator()

        res_data = []
        for offset, limit in paginator.pages():
            res_data.append(offset)
        return res_data

梱包状態 Paginator 内部例では、ユーザーが直接変更できないため、この種の問題は設計レベルから排除されました。これは 3 つの解決策の中で最も徹底したものです。

六、概要と仕様

6.1 デフォルトのセキュリティ

デフォルト値 タイプ 安全であれば
param={} dict (変更可能) ❌落とし穴がある
items=[] リスト (オプション) ❌落とし穴がある
seen=set() セット(変更可能) ❌落とし穴がある
param=None なしタイプ ✅セキュリティ
start=0 int (変更不可) ✅セキュリティ
name="abc" 通り(変更不可) ✅セキュリティ
flag=True ブール ✅セキュリティ
data=() タプル(変更不可) ✅セキュリティ

6.2 標準コード

  1. デフォルトのパラメータは適用されません(辞書 / リスト / セット)
  2. デフォルト値を変更する必要がある場合は、それを使用できます None + 初期化関数
  3. 外部パスイン変数オブジェクトを受信する必要があります,先コピー贝再電影(dict(x) / list(x) / copy.copy(x)
  4. 維持する必要があるロジックについては、最初にパッケージ化するか、デフォルトのパラメーターを介して渡す必要があります。
  5. 静的検査ツール (flake8 の B006、pylint の W0102) は、このような問題を自動的にチェックできます。CI を含めることをお勧めします。

6.3 一言結論

param={} 中国 {} 関数の定義時に一度だけ作成され、すべての呼び出しで同じオブジェクトが共有されます。デフォルト値はデフォルトのオブジェクトと同じで、正しい姿勢はデフォルトの写真です None、関数内で再初期化します。

Leave a Reply

Your email address will not be published. Required fields are marked *