【问题标题】:Fighting python type annotations对抗python类型注解
【发布时间】:2022-01-21 22:20:52
【问题描述】:

我有一个非常简单的类,它继承自 requests.Session。目前的代码如下:

import requests
import urllib.parse

from typing import Any, Optional, Union, cast

default_gutendex_baseurl = "https://gutendex.com/"


class Gutendex(requests.Session):
    def __init__(self, baseurl: Optional[str] = None):
        super().__init__()
        self.baseurl = baseurl or default_gutendex_baseurl

    def search(self, keywords: str) -> Any:
        res = self.get("/books", params={"search": keywords})
        res.raise_for_status()
        return res.json()

    def request(
        self, method: str, url: Union[str, bytes], *args, **kwargs
    ) -> requests.Response:
        if self.baseurl and not url.startswith("http"):
            url = urllib.parse.urljoin(self.baseurl, url)

        return super().request(method, url, *args, **kwargs)

我很难让mypyrequest 方法感到满意。

第一个挑战是获取要验证的参数;环境 url: Union[str, bytes] 是匹配类型注释所必需的 types-requests。我刚刚举手获得*args**kwargs 正确,因为唯一的解决方案似乎是 重现单个参数注释,但我很高兴 就这样吧。

处理完函数签名后,mypy 现在开始抱怨 关于致电startswith

example.py:23:错误:“bytes”的“startswith”的参数 1 具有不兼容的类型“str”;预期“联合[字节,元组[字节,...]]”

我可以通过明确的cast 解决这个问题:

        if not cast(str, url).startswith("http"):
            url = urllib.parse.urljoin(self.baseurl, url)

...但这似乎只是引入了复杂性。

然后它对urllib.parse.urljoin的调用不满意:

example.py:24: 错误:“urljoin”的类型变量“AnyStr”的值不能是“Sequence[object]”
example.py:24:错误:赋值中的类型不兼容(表达式的类型为“Sequence[object]”,变量的类型为“Union[str, bytes]”)

我不太确定这些错误是怎么回事。

我现在通过将显式演员表移到顶部来解决问题 方法:

      def request(
          self, method: str, url: Union[str, bytes], *args, **kwargs
      ) -> requests.Response:
          _url = url.decode() if isinstance(url, bytes) else url

          if not _url.startswith("http"):
              _url = urllib.parse.urljoin(self.baseurl, _url)

          return super().request(method, _url, *args, **kwargs)

但这感觉就像一个 hacky 解决方法。

所以:

  • 我认为我的函数签名尽可能正确 它,但是url 上的类型注释是否正确或者它们是 不正确并导致问题?

  • urljoin 周围的错误是怎么回事?


来自 cmets,这是:

        if self.baseurl and not url.startswith(
            "http" if isinstance(url, str) else b"http"
        ):

失败:

example.py:25:错误:“str”的“startswith”的参数 1 具有不兼容的类型“Union[str, bytes]”;预期“Union[str, Tuple[str, ...]]”
example.py:25:错误:“bytes”的“startswith”的参数 1 具有不兼容的类型“Union[str, bytes]”;预期“联合[字节,元组[字节,...]]”

【问题讨论】:

  • 问题是如果_url是一个bytes值,那么_url.startswith("http")是一个实际的runtime错误;它必须是_url.startswith(b"http")。演员表可能会让mypy 开心,但如果_url 真的是bytes 值,它会在运行时失败。
  • 我知道我从来没有用bytes 值调用request,所以我不太担心这种可能性。我想正确的处理方法是扔进isinstance(bytes, url) 并酌情解码?
  • 您可以将参数声明为str。子类方法不必接受与父类相同的所有类型。
  • 您甚至不必解码它,因为request 已经可以处理这两个问题。只需_url.startswith("http" if isinstance(str, url) else b"http") 就足够了。
  • @Grismar 你不能那样做,因为那样你就会以一种与Session 不兼容的方式限制你接受的类型。如果您在运行时确实收到了bytes 值,则引发异常 很好,但从静态类型的角度来看,您仍然必须首先接受它。

标签: python mypy


【解决方案1】:

这解决了整个问题:

import requests
import urllib.parse

from typing import Union, cast

default_gutendex_baseurl = "https://gutendex.com/"


class Gutendex(requests.Session):
    def __init__(self, baseurl: str = None):
        super().__init__()
        self.baseurl = baseurl or default_gutendex_baseurl

    def search(self, keywords: str) -> dict[str, str]:
        res = self.get("/books", params={"search": keywords})
        res.raise_for_status()
        return res.json()

    def request(
        self, method: str, url: Union[str, bytes], *args, **kwargs
    ) -> requests.Response:
        if isinstance(url, str):
            if not url.startswith("http"):
                url = urllib.parse.urljoin(self.baseurl, url)

            return super().request(method, url, *args, **kwargs)
        else:
            raise TypeError('Gutendex does not support bytes type url arguments')

如果你说你接受它,你不能只是不处理bytes。如果bytes 通过,只需引发异常或做一些更好的事情。如果你喜欢危险地生活,甚至只是pass

这段代码在mypy 中验证得很好。

有点令人失望的是,这样的事情无法验证:

        if not url.startswith("http"):
            url = urllib.parse.urljoin(self.baseurl, url if isinstance(url, str) else url.decode())
        return super().request(method, url, *args, **kwargs)

即使url.startswithbytes 时无法获得bytes,反之亦然,但它仍然无法验证。 mypy 无法通过运行时逻辑进行验证,因此您只能执行以下操作:

    def request(
        self, method: str, url: Union[str, bytes], *args, **kwargs
    ) -> requests.Response:
        if isinstance(url, str):
            if not url.startswith("http"):
                url = urllib.parse.urljoin(self.baseurl, url)

            return super().request(method, url, *args, **kwargs)
        else:
            if not url.startswith(b"http"):
                url = urllib.parse.urljoin(self.baseurl, url.decode())

            return super().request(method, url, *args, **kwargs)

两者都支持,但以丑陋的方式重复逻辑。

【讨论】:

  • 问题中的更新代码——显式调用url.decode()bytes 转换为str——已经解决了问题。我不确定这或多或少是正确的。我的问题更多是关于我是否以正确的方式处理事情,这听起来像是考虑到超类方法上的现有类型注释,我不得不显式检查 bytesstr 参数。
  • 这可能是更好的解决方案。在极端情况下,您不一定知道 如何 url 被编码(latin-1、utf-8、cp-1252 或其他?)。最好要求调用者先解码。
  • 你是对的@larsks - 不过这确实有道理。你正在继承Session,它需要能够同时处理这两种情况,因为你的会话在替换它的超类时应该总是有效,即使你不打算这样做,所以它需要优雅地处理@987654339 @
  • ...但正如@chepner 所说,没有办法知道bytes 值的编码,但我想在文档中处理它(“...必须是UTF- 8 个编码字节字符串...").
  • 正确 - 一种强制执行的方法是拥有 bytesUTF8Bytes 子类,但图书馆的设计者决定反对它(我认为这是明智的,因为这将接近以后添加对其他编码的支持没有很多问题的大门)
猜你喜欢
  • 2016-12-06
  • 2016-12-11
  • 2017-06-07
  • 2016-08-16
  • 1970-01-01
  • 2018-10-29
  • 1970-01-01
  • 2018-02-22
  • 2020-11-20
相关资源
最近更新 更多