如何在 python 中编写与智能感知一起使用的 DRY 包装器

How to write a DRY wrapper that works with intellisense in python

我正在编写一个 python 工具,其模块位于不同 'levels':

我希望能够共享从低级模块到高级模块的函数签名,以便 intellisense 与两个模块一起工作。

In the following examples, I'm using the width and height parameters as placeholders for a pretty long list of arguments (around 30).

我可以明确地做任何事情。这行得通,界面就是我想要的,智能感知也行;但它非常乏味,容易出错,维护起来简直就是一场噩梦:

# high level function, wraps/uses the low level one
def create_rectangles(count, width=10, height=10):
  return [create_rectangle(width=width, height=height) for _ in range(count)]

# low level function
def create_rectangle(width=10, height=10):
  print(f"the rectangle is {width} wide and {height} high")

create_rectangles(3, width=10, height=5)

我可以创建一个 class 来保存较低函数的参数。它非常易读,智能感知有效,但界面笨拙:

class RectOptions:
  def __init__(self, width=10, height=10) -> None:
    self.width = width
    self.height = height

def create_rectangles(count, rectangle_options:RectOptions):
  return [create_rectangle(rectangle_options) for _ in range(count)]

def create_rectangle(options:RectOptions):
  print(f"the rectangle is {options.width} wide and {options.height} high")

# needing to create an instance for a function call feels clunky...
create_rectangles(3, RectOptions(width=10, height=3))

我可以简单地使用 **kwargs。它简洁并允许良好的界面,但它破坏了智能感知并且不是很可读:

def create_rectangles(count, **kwargs):
  return [create_rectangle(**kwargs) for _ in range(count)]

def create_rectangle(width, height):
  print(f"the rectangle is {width} wide and {height} high")

create_rectangles(3, width=10, height=3)

我想要的是具有 kwargs 优点但具有更好 readability/typing/intellisense 支持的东西:

# pseudo-python

class RectOptions:
  def __init__(self, width=10, height=10) -> None:
    self.width = width
    self.height = height

# The '**' operator would add properties from rectangle_options to the function signature
# We could even 'inherit' parameters from multiple sources, and error in case of conflict
def create_rectangles(count, **rectangle_options:RectOptions):
  return [create_rectangle(rectangle_options) for idx in range(count)]

def create_rectangle(options:RectOptions):
  print(f"the rectangle is {options.width} wide and {options.height} high")

create_rectangles(3, width=10, height=3)

我可以使用代码生成,但我对此不是很熟悉,而且它似乎会增加很多复杂性。

在寻找解决方案时,我偶然发现了这个 reddit post。据我所知,我正在寻找的东西目前是不可能的,但我真的希望我错了

我试过 docstring_expander pip 包,因为它看起来是为了解决这个问题,但它对我没有任何作用(我可能用错了......)

我认为这无关紧要,但以防万一:我正在使用 vscode 1.59 和 python 3.9.9

目前确实不可能。 Intellisense IDE 可以让您的生活更轻松,但由于它不能 运行 您的代码,所以它是有限的……因此动态命名参数将无法工作。它要么明确接受什么参数def test(param1: str, param2: str),要么完全动态化def test(**kwargs)。你不能同时拥有动态和显式。

只是为了添加到会议中,另一种选择是使用 TypedDict:

from typing import TypedDict

class RectOptions(TypedDict):
    width: int
    height: int

def create_rectangles(count, opts: RectOptions):
  print(count)
  print(opts)

create_rectangles(0, {'width': 1, 'height': 2})

简化了,因为我们只真正关心函数 def。

这样它是一个 dict 但类型,因此如果您尝试传递不属于 IDE 类型的内容,将会发出警告。 (Pycharm 至少)

您有两种可能的方法来实现这一点,这将使您拥有可选参数并继承多个参数“源”,正如您所说的那样。

  • 请注意,这已被多次询问(例如 1, , 3, )。尽管从过去的答案中得出了什么,但我认为实际上可以公平地说这是有可能的。
    我将提供一个我在过去的答案中找不到的建议,以及对你的案例提供一个过去的答案。

1.使用 @dataclass + kw_only 属性(在 Python 3.10 中可用)

dataclasses 3.10 添加了 kw_only 属性。为了让 @dataclass 实现你想要的,你必须使用这个新属性,否则你要么会得到继承错误,要么会得到不完整的 Intellisense 支持(我将在下面展开,但你可以 ).

它的工作原理如下:

类型:

from dataclasses import dataclass

@dataclass
class RequiredProps:
    # all of these must be present
    width: int
    height: int

@dataclass(kw_only=True) # <-- the kw_only attribute is available in python 3.10
class OptionalProps:
    # these can be included or they can be omitted
    colorBackground: str = None
    colorBorder: str = None

@dataclass
class ReqAndOptional(RequiredProps, OptionalProps):
    pass

函数:

选项 #1

def create_rectangles(count, args: ReqAndOptional):
   return [create_rectangle(args.width, args.height) for _ in range(count)]

def create_rectangle(width, height):
   print(f"the rectangle is {width} wide and {height} high")

create_rectangles(1, ReqAndOptional(width=1, height=2))

选项 #2

def create_rectangles(count, args: ReqAndOptional):
   return [create_rectangle(args) for _ in range(count)]

def create_rectangle(args: RequiredProps):
   print(f"the rectangle is {args.width} wide and {args.height} high")

create_rectangles(1, ReqAndOptional(width=1, height=2))

请注意,一旦您键入 ReqAndOptional,Intellisense 将自动完成字典属性:

2。使用 TypedDict

您可以 适应您的情况:

类型:

import typing

class RequiredProps(typing.TypedDict):
    # all of these must be present
    width: int
    height: int

class OptionalProps(typing.TypedDict, total=False):
    # these can be included or they can be omitted
    colorBackground: str
    colorBorder: str

class ReqAndOptional(RequiredProps, OptionalProps):
    pass

函数:

选项 #1

def create_rectangles(count, args: ReqAndOptional):
   return [create_rectangle(args['width'],args['height']) for _ in range(count)]

def create_rectangle(width, height):
   print(f"the rectangle is {width} wide and {height} high")

create_rectangles(1, {'width':1, 'height':2, 'colorBorder': '#FFF'})

选项 #2

def create_rectangles(count, args: ReqAndOptional):
   return [create_rectangle(args) for _ in range(count)]

def create_rectangle(args: RequiredProps):
   print(f"the rectangle is {args['width']} wide and {args['height']} high")

create_rectangles(1, {'width':1, 'height':2, 'colorBorder': '#FFF'})

请注意,您可以在两个地方自动完成:

在调用端,Intellisense 会显示两个可选参数:

并且在被调用方,一旦您键入 args[],Intellisense 将自动完成字典属性:

结束语

  • 如果不使用 Python 3.10 支持的 kw_only 会怎样?如上所述,您将面临继承问题或不完整的 Intellisense 自动完成。让我们看看他们每个人:

    1. 首先,尽量省略kw_only属性,运行程序。你会得到错误:

    non-default argument 'width' follows default argument

    这是因为属性是从 MRO 的底部开始组合的。您可以参考 以更好地理解发生了什么。

    1. 其次,您可能想通过尝试使用 field(default=None, init=False)(如 中所建议的)来解决我们在第 1 部分中遇到的错误,但这会导致不完整的 Intellisense 自动完成,如下图。
  • 如果您想用 typing.NamedTuple 替换 typing.TypedDict 会怎样?再次,您将获得不完整的 Intellisense 自动完成 (see here why):