如何避免在Python中重复使用kwargs?

3

假设有一个方法:

def train_model(self, out_dir='./out/',
                test_size=0.2, train_size=None,
                random_state=None, shuffle=True, stratify=None,
                epochs=DEFAULT_EPOCHS, batch_size=DEFAULT_BATCH_SIZE):
    ...
    self.model.train(test_size=test_size, train_size=train_size, random_state=random_state, shuffle=shuffle, stratify=stratify, epochs=epochs, batch_size=batch_size)

在这个函数中,还会调用另一个具有相同签名的方法,然后我必须手动传递所有参数。我不想在 train_model 中使用 kwargs,因为它是一个可能被其他人使用的公共方法,所以我希望保留类型信息。我不知道是否有方法可以允许我在外部函数的 kwargs 中保留类型信息。

在 TypeScript 中,可以使用 Parameters 实用类型来实现相同的功能。例如:

function sum(a: int, b: int) {
    return a + b;
}

type SumParamsType = Paramters<typeof sum>

// Then you can use the SumPramsType in other places.

一个Python失败的示例:
from typing import TypeVar
T = TypeVar('T')

def f1(a=1, b=2, c=3):
    return a+b+c

# Is there anything like T=Parameters(type(f1)) in Python?

def f2(z=0, **kwargs: T):
    return z+f1(**kwargs)

# T cannot capture the kwargs of f1 (of course it won't)

这也不起作用:

def f1(a=1, b=2, c=3):
    return a+b+c

def f2(z=0, **kwargs: f1.__annotations__['kwargs']):
    return z + f1(**kwargs)

# kwargs has the type Any

这个回答解决了你的问题吗?为*args和**kwargs添加类型注释 - MEE
我不确定。它提供的解决方案需要Python 3.11。除此之外,我想知道的不是如何注释 **kwargs ,而是在 train_model 中重用 self.model.train 的类型信息。我认为这是不同的。 - link89
为什么你提供的例子失败了?因为 f2 函数没有返回任何值,缺少了 return 语句。 - Jurakin
它是可运行的,我的意思是T无法捕获“f1”的参数类型信息。 - link89
也许f2.__annotations__ 可以回答你的问题,它返回{'kwargs': ~T}请参见文档。我不明白你说的“T无法捕获f1的kwargs”的意思。它是否可以捕获args和kwargs呢? - Jurakin
现在我知道你的意思了。我没有看到其他的方法来做到这一点,除了复制函数(如果需要执行一些操作则无法工作)。self.train_model = self.model.train - Jurakin
3个回答

1

您可以使用带有UnpackTypedDict(在Python < 3.11中通过typing_extensions可用)来实现最接近的效果:

from typing_extensions import Unpack, TypedDict, NotRequired


class Params(TypedDict):
    a: NotRequired[int]
    b: NotRequired[int]
    c: NotRequired[int]


def f1(**kwargs: Unpack[Params]):
    a = kwargs.pop('a', 1)
    b = kwargs.pop('b', 1)
    c = kwargs.pop('c', 1)
    return a + b + c


def f2(z=0, **kwargs: Unpack[Params]):
    return z + f1(**kwargs)


请注意,如果您的IDE不使用mypy --enable-incomplete-feature=Unpack,则可能不支持Unpack。 VSCode已经支持了,但PyCharm可能不支持。
如果您控制两个函数定义,那么您可能会发现更容易将方法更改为接受一个dataclass,该类封装了所有参数及其默认值,而不是单独获取每个参数。

我想那是目前最好的了。我只是在想是否有任何PEP建议一些实用程序类型,就像TypeScript所做的那样:https://www.typescriptlang.org/docs/handbook/utility-types.html - link89
有关在TypedDict中添加默认的catch-all类型和指定每个参数的默认值的讨论可以在这里找到,但似乎还没有取得任何进展。 - MEE

1
你可以创建一个包含训练参数的类,并将其传递给train方法,就像HuggingFace Transformers library中所做的那样。
这是他们在GitHub上的代码:
from dataclasses import asdict, dataclass, field, fields

#...

@dataclass
class TrainingArguments:
    framework = "pt"
    output_dir: str = field(
        metadata={"help": "The output directory where the model predictions and checkpoints will be written."},
    )
    overwrite_output_dir: bool = field(
        default=False,
        metadata={
            "help": (
                "Overwrite the content of the output directory. "
                "Use this to continue training if output_dir points to a checkpoint directory."
            )
        },
    )

    do_train: bool = field(default=False, metadata={"help": "Whether to run training."})
    do_eval: bool = field(default=False, metadata={"help": "Whether to run eval on the dev set."})
    do_predict: bool = field(default=False, metadata={"help": "Whether to run predictions on the test set."})
    evaluation_strategy: Union[IntervalStrategy, str] = field(
        default="no",
        metadata={"help": "The evaluation strategy to use."},
    )
    prediction_loss_only: bool = field(
        default=False,
        metadata={"help": "When performing evaluation and predictions, only returns the loss."},
    )

    per_device_train_batch_size: int = field(
        default=8, metadata={"help": "Batch size per GPU/TPU core/CPU for training."}
    )
    per_device_eval_batch_size: int = field(
        default=8, metadata={"help": "Batch size per GPU/TPU core/CPU for evaluation."}
    )
    # ...

这句话的意思是:“这有点冗长,但非常清晰,并且可以与您的 IDE 类型提示一起使用。”

0
你需要结合使用 locals().__code__.co_varnames
def f2(b=5, c=7):
    return b*c

def f1(a=1, b=2, c=3):
    sss = locals().copy()
    f2_params = f2.__code__.co_varnames
    return f2(**{x:y for x, y in sss.items() if x in f2_params})

print(f1())
>>> 6

编辑

如果您想使用**kwargs,请尝试以下方法:

def f2(b=5, c=7):
    return b*c

def f1(a=1, **kwargs):
    sss = locals()['kwargs'].copy()
    f2_params = f2.__code__.co_varnames
    return f2(**{x:y for x, y in sss.items() if x in f2_params})

print(f1(b=10, c=3))

这是关于类型注释和DRY原则的内容。我希望能够写成def f1(a=1, **kwargs),而不是def f1(a=1, b=2, c=3),并且kwargs应该保留f2输入参数的类型信息。 - link89
这与我的第二个失败示例相同,kwargs的类型为Any。关键是让kwargs携带f2的类型信息。 - link89

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接