如何在 Python 中记录类属性?

发布于 2024-09-06 00:17:45 字数 1316 浏览 10 评论 0原文

我正在编写一个轻量级类,其属性旨在可公开访问,并且仅有时在特定实例化中被覆盖。 Python 语言中没有为类属性或任何类型的属性创建文档字符串的规定。记录这些属性的预期和支持的方式是什么(是否应该有一种方式)?目前我正在做这样的事情:

class Albatross(object):
    """A bird with a flight speed exceeding that of an unladen swallow.

    Attributes:
    """

    flight_speed = 691
    __doc__ += """
        flight_speed (691)
          The maximum speed that such a bird can attain.
    """

    nesting_grounds = "Raymond Luxury-Yacht"
    __doc__ += """
        nesting_grounds ("Raymond Luxury-Yacht")
          The locale where these birds congregate to reproduce.
    """

    def __init__(self, **keyargs):
        """Initialize the Albatross from the keyword arguments."""
        self.__dict__.update(keyargs)

这将导致类的文档字符串包含初始标准文档字符串部分,以及通过对 __doc__ 的增强赋值为每个属性添加的行。

尽管文档字符串样式指南中似乎没有明确禁止这种样式,它也没有被提及作为一个选项。这里的优点是它提供了一种记录属性及其定义的方法,同时仍然创建可呈现的类文档字符串,并避免编写注释来重申文档字符串中的信息。我仍然有点恼火,因为我实际上必须将属性写两次;我正在考虑使用文档字符串中值的字符串表示形式,以至少避免默认值的重复。

这是否严重违反了临时社区公约?可以吗?有更好的办法吗?例如,可以创建一个包含属性值和文档字符串的字典,然后将内容添加到类 __dict__ 中,并将文档字符串添加到类声明的末尾;这将减少两次键入属性名称和值的需要。 编辑:我认为,最后一个想法实际上是不可能的,至少在没有从数据动态构建整个类的情况下是不可能的,这似乎是一个非常糟糕的主意,除非有其他原因这样做。

我对 python 还很陌生,仍在研究编码风格的细节,所以也欢迎不相关的批评。

I'm writing a lightweight class whose attributes are intended to be publicly accessible, and only sometimes overridden in specific instantiations. There's no provision in the Python language for creating docstrings for class attributes, or any sort of attributes, for that matter. What is the expected and supported way, should there be one, to document these attributes? Currently I'm doing this sort of thing:

class Albatross(object):
    """A bird with a flight speed exceeding that of an unladen swallow.

    Attributes:
    """

    flight_speed = 691
    __doc__ += """
        flight_speed (691)
          The maximum speed that such a bird can attain.
    """

    nesting_grounds = "Raymond Luxury-Yacht"
    __doc__ += """
        nesting_grounds ("Raymond Luxury-Yacht")
          The locale where these birds congregate to reproduce.
    """

    def __init__(self, **keyargs):
        """Initialize the Albatross from the keyword arguments."""
        self.__dict__.update(keyargs)

This will result in the class's docstring containing the initial standard docstring section, as well as the lines added for each attribute via augmented assignment to __doc__.

Although this style doesn't seem to be expressly forbidden in the docstring style guidelines, it's also not mentioned as an option. The advantage here is that it provides a way to document attributes alongside their definitions, while still creating a presentable class docstring, and avoiding having to write comments that reiterate the information from the docstring. I'm still kind of annoyed that I have to actually write the attributes twice; I'm considering using the string representations of the values in the docstring to at least avoid duplication of the default values.

Is this a heinous breach of the ad hoc community conventions? Is it okay? Is there a better way? For example, it's possible to create a dictionary containing values and docstrings for the attributes and then add the contents to the class __dict__ and docstring towards the end of the class declaration; this would alleviate the need to type the attribute names and values twice. edit: this last idea is, I think, not actually possible, at least not without dynamically building the entire class from data, which seems like a really bad idea unless there's some other reason to do that.

I'm pretty new to python and still working out the details of coding style, so unrelated critiques are also welcome.

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(5

月亮是我掰弯的 2024-09-13 00:17:45

简而言之:类属性不能像类和函数那样具有文档字符串。

为了避免混淆,术语属性具有特定含义 在Python中。您所说的就是我们所说的类属性 。由于它们总是通过类来执行,因此我发现在类的文档字符串中记录它们是有意义的。像这样的事情:

class Albatross(object):
    """A bird with a flight speed exceeding that of an unladen swallow.

    Attributes:
        flight_speed     The maximum speed that such a bird can attain.
        nesting_grounds  The locale where these birds congregate to reproduce.
    """
    flight_speed = 691
    nesting_grounds = "Throatwarbler Man Grove"

我认为这比你的例子中的方法更容易看。如果我确实希望属性值的副本出现在文档字符串中,我会将它们放在每个属性的描述旁边或下方。

请记住,在 Python 中,文档字符串是它们所记录的对象的实际成员,而不仅仅是源代码注释。由于类属性变量本身不是对象而是对象的引用,因此它们无法保存自己的文档字符串。我想您可以为引用上的文档字符串提供一个案例,也许是为了描述“这里应该放什么”而不是“这里实际上是什么”,但我发现在包含类文档字符串中做到这一点很容易。

In short: class attributes cannot have doc strings in the way that classes and functions have.

To avoid confusion, the term property has a specific meaning in python. What you're talking about is what we call class attributes. Since they are always acted upon through their class, I find that it makes sense to document them within the class' doc string. Something like this:

class Albatross(object):
    """A bird with a flight speed exceeding that of an unladen swallow.

    Attributes:
        flight_speed     The maximum speed that such a bird can attain.
        nesting_grounds  The locale where these birds congregate to reproduce.
    """
    flight_speed = 691
    nesting_grounds = "Throatwarbler Man Grove"

I think that's a lot easier on the eyes than the approach in your example. If I really wanted a copy of the attribute values to appear in the doc string, I would put them beside or below the description of each attribute.

Keep in mind that in Python, doc strings are actual members of the objects they document, not merely source code annotations. Since class attribute variables are not objects themselves but references to objects, they have no way of holding doc strings of their own. I guess you could make a case for doc strings on references, perhaps to describe "what should go here" instead of "what is actually here", but I find it easy enough to do that in the containing class doc string.

掩饰不了的爱 2024-09-13 00:17:45

其他答案非常已经过时了。 PEP-257 描述了如何使用属性的文档字符串。奇怪的是,它们出现在属性之后:

Python 代码中其他地方出现的字符串文字也可以充当文档。它们不被Python字节码编译器识别,并且不能作为运行时对象属性访问(即不分配给__doc__),但是软件工具可以提取两种类型的额外文档字符串:

  1. 在模块、类或 __init__ 方法顶层的简单赋值之后立即出现的字符串文字称为“属性文档字符串”。
class C:
    "class C doc-string"

    a = 1
    "attribute C.a doc-string (1)"

    b = 2
    "attribute C.b doc-string (2)"

它也适用于这样的类型注释:

class C:
    "class C doc-string"

    a: int
    "attribute C.a doc-string (1)"

    b: str
    "attribute C.b doc-string (2)"

VSCode 支持显示这些。

The other answers are very outdated. PEP-257 describes how you can use docstrings for attributes. They come after the attribute, weirdly:

String literals occurring elsewhere in Python code may also act as documentation. They are not recognized by the Python bytecode compiler and are not accessible as runtime object attributes (i.e. not assigned to __doc__), but two types of extra docstrings may be extracted by software tools:

  1. String literals occurring immediately after a simple assignment at the top level of a module, class, or __init__ method are called “attribute docstrings”.
class C:
    "class C doc-string"

    a = 1
    "attribute C.a doc-string (1)"

    b = 2
    "attribute C.b doc-string (2)"

It also works for type annotations like this:

class C:
    "class C doc-string"

    a: int
    "attribute C.a doc-string (1)"

    b: str
    "attribute C.b doc-string (2)"

VSCode supports showing these.

乖乖哒 2024-09-13 00:17:45

您在 What 部分引用了 PEP257:文档字符串约定是一个文档字符串,它指出:

Python 代码中其他地方出现的字符串文字也可以充当文档。它们不被Python字节码编译器识别,并且不能作为运行时对象属性访问(即不分配给__doc__),但是软件工具可以提取两种类型的额外文档字符串:

在模块、类或 __init__ 方法顶层进行简单赋值之后立即出现的字符串文字称为“属性文档字符串”。

PEP 258:属性文档字符串部分对此进行了更详细的解释。
如上所述,
属性不是可以拥有 __doc__ 的对象,因此它们不会出现在 help() 或 pydoc 中。这些文档字符串只能用于生成的文档。

它们在 Sphinx 中与 指令自动属性。

Sphinx 可以在赋值之前的行上使用注释,或者在赋值之后使用特殊注释,或者在定义之后使用文档字符串,这些注释将被自动记录。

You cite the PEP257: Docstring Conventions, in the section What is a docstring it is stated:

String literals occurring elsewhere in Python code may also act as documentation. They are not recognized by the Python bytecode compiler and are not accessible as runtime object attributes (i.e. not assigned to __doc__), but two types of extra docstrings may be extracted by software tools:

String literals occurring immediately after a simple assignment at the top level of a module, class, or __init__ method are called "attribute docstrings".

And this is explained in more details in the PEP 258: Attribute Docstrings section.
As explains above,
an attribute is not an object that can own a __doc__ so they won't appear in help() or pydoc. These docstrings can only be used for generated documentation.

They are used in Sphinx with the directive autoattribute.

Sphinx can use comments on a line before an assignment or a special comment following an assignment or a docstring after the definition which will be autodocumented.

陈年往事 2024-09-13 00:17:45

您可以滥用属性来达到此目的。属性包含一个 getter、一个 setter、一个删除器、和一个文档字符串。天真地,这会变得非常冗长:

class C:
    def __init__(self):
        self._x = None

    @property
    def x(self):
        """Docstring goes here."""
        return self._x

    @x.setter
    def x(self, value):
        self._x = value

    @x.deleter
    def x(self):
        del self._x

然后你将拥有一个属于 Cx 的文档字符串:

In [24]: print(C.x.__doc__)
Docstring goes here.

对许多属性执行此操作很麻烦,但你可以设想一个辅助函数 myprop:

def myprop(x, doc):
    def getx(self):
        return getattr(self, '_' + x)

    def setx(self, val):
        setattr(self, '_' + x, val)

    def delx(self):
        delattr(self, '_' + x)

    return property(getx, setx, delx, doc)

class C:
    a = myprop("a", "Hi, I'm A!")
    b = myprop("b", "Hi, I'm B!")

In [44]: c = C()

In [46]: c.b = 42

In [47]: c.b
Out[47]: 42

In [49]: print(C.b.__doc__)
Hi, I'm B!

然后,调用 Python 交互式 help 将给予:

Help on class C in module __main__:

class C
 |  Data descriptors defined here:
 |  
 |  a
 |      Hi, I'm A!
 |  
 |  b
 |      Hi, I'm B!

我认为这应该正是你所追求的。

编辑:我现在意识到,我们也许可以完全避免将第一个参数传递给 myprop ,因为内部名称并不重要。如果 myprop 的后续调用可以以某种方式相互通信,它可以自动决定一个很长且不太可能的内部属性名称。我确信有一些方法可以实现这一点,但我不确定它们是否值得。

You could abuse properties to this effect. Properties contain a getter, a setter, a deleter, and a docstring. Naively, this would get very verbose:

class C:
    def __init__(self):
        self._x = None

    @property
    def x(self):
        """Docstring goes here."""
        return self._x

    @x.setter
    def x(self, value):
        self._x = value

    @x.deleter
    def x(self):
        del self._x

Then you will have a docstring belonging to C.x:

In [24]: print(C.x.__doc__)
Docstring goes here.

To do this for many attributes is cumbersome, but you could envision a helper function myprop:

def myprop(x, doc):
    def getx(self):
        return getattr(self, '_' + x)

    def setx(self, val):
        setattr(self, '_' + x, val)

    def delx(self):
        delattr(self, '_' + x)

    return property(getx, setx, delx, doc)

class C:
    a = myprop("a", "Hi, I'm A!")
    b = myprop("b", "Hi, I'm B!")

In [44]: c = C()

In [46]: c.b = 42

In [47]: c.b
Out[47]: 42

In [49]: print(C.b.__doc__)
Hi, I'm B!

Then, calling Pythons interactive help will give:

Help on class C in module __main__:

class C
 |  Data descriptors defined here:
 |  
 |  a
 |      Hi, I'm A!
 |  
 |  b
 |      Hi, I'm B!

which I think should be pretty much what you're after.

Edit: I realise now that we can perhaps avoid to need to pass the first argument to myprop at all, because the internal name doesn't matter. If subsequent calls of myprop can somehow communicate with each other, it could automatically decide upon a long and unlikely internal attribute name. I'm sure there are ways to implement this, but I'm not sure if they're worth it.

陌伤ぢ 2024-09-13 00:17:45

这是滥用 astinspect 的答案。除了更改文档字符串之外,它对原始类实现没有任何作用。

想法
  1. 循环遍历类主体中的所有表达式,
  2. 检查是否有任何字符串表达式出现在属性之前,
  3. 将此类属性存储为属性文档字符串
  4. 为属性文档字符串创建精美的格式并将其附加到类的现有文档字符串中。
实现
import ast
import inspect
from io import StringIO


def ast_find_classdef(tree):
    for e in ast.walk(tree):
        if isinstance(e, ast.ClassDef):
            return e


def attribute_docs(cls):
    """Enable attribute documentations for (data)classes. Use this function as a decorator.

    ```
    @attribute_docs
    @dataclass
    class TargetCoder:
        "Target coder for object detection"

        "Number of detection classes"
        num_classes: int

        "Normalized minimum bounding box size"
        min_size: flaot
    ```
    """
    # == find the class defining syntax tree ==
    src = inspect.getsource(cls)
    tree = ast.parse(src)
    tree = ast_find_classdef(tree)

    # == gather attribute doc strings ==
    # * We skip the first expr, because it is either a class docstring or something else
    # * The idea is that docstring appears on top of the attribute.
    # * Therefore, we search for any string node, mark that as a docstring.
    # * If a class attribute define node appears after the docstring, we store the docstring
    #    along with the class attribute's information
    attribute_docs = {}
    last_doc: Optional[str] = None
    for expr in tree.body[1:]:
        # When encouter an Expr, check if the expr a string
        if isinstance(expr, ast.Expr):
            # The value is a ast.Value node
            # therefore another access to value is needed
            value = expr.value.value
            if isinstance(value, str):
                last_doc = value.strip()

        # if the last known doc string is not none
        # and this next node is an annotation, that's a docstring
        if isinstance(expr, ast.AnnAssign) and last_doc is not None:
            # expr.target is a ast.Name
            name = ast.unparse(expr.target)
            type_name = ast.unparse(expr.annotation)
            attribute_docs[name] = (type_name, last_doc)
            last_doc = None

    # == Append to the class documentation ==
    # * if there is no attribute docstring, leave it be
    if len(attribute_docs) > 0:
        old_docs = cls.__doc__
        append_docs = build_attibute_docstrings(attribute_docs)
        cls.__doc__ = f"""{old_docs}\n\n{append_docs}"""
    return cls


def build_attibute_docstrings(docs):
    # Create pretty formatting for the attribute docs
    with StringIO() as io:
        io.write("Attributes:\n")
        for var_name, (type_name, docstring) in docs.items():
            # == Multiline vs inline doc format ==
            # * if the doc is inline, simply use the `x (type): docstring`
            # * if the doc is multiline, create a new paragraph
            if "\n" in docstring:
                lines = docstring.split("\n")
                lines = ["\t\t" + line.strip() for line in lines]
                docstring = "\n".join(lines)
                line = f"\t{var_name} ({type_name}):\n{docstring}\n"
            else:
                line = f"\t{var_name} ({type_name}): {docstring}\n"

            # Add the docstring line
            io.write(line)
        io.seek(0)
        docstring = io.read()
    return docstring
示例
@attribute_docs
@dataclass
class DBNetAlignCoder:
    "DBNet target coder for aligned case, i.e. detection targets are axis-aligned"

    "Number of detection classes"
    num_classes: int

    "Input image width"
    image_width: int

    "Input image height"
    image_height: int

    """Shrink rate of bounding boxes, the shrink distance will be computed using
    [A * (1 - r^2) / L], where A is the bounding box area, L is the bounding box
    perimeter, and r is the shrink ratio
    """
    shrink_ratio: float

    "Minimum probability to be considered a positive detection"
    det_threshold: float

    """
    Whether to use a simple threshold map drawing method. If true, the threshold
    map values will be 1, instead of the distance from shrink/expand boxes to the
    actual boxes as described in the DBNet paper.
    """
    simple_threshold: bool = False

help() 的输出:

class DBNetAlignCoder(builtins.object)
 |  DBNetAlignCoder(num_classes: int, image_width: int, image_height: int, shrink_ratio: float, det_threshold: float, simple_threshold: bool = False) -> None
 |  
 |  DBNet target coder for aligned case, i.e. detection targets are axis-aligned
 |  
 |  Attributes:
 |          num_classes (int): Number of detection classes
 |          image_width (int): Input image width
 |          image_height (int): Input image height
 |          shrink_ratio (float):
 |                  Shrink rate of bounding boxes, the shrink distance will be computed using
 |                  [A * (1 - r^2) / L], where A is the bounding box area, L is the bounding box
 |                  perimeter, and r is the shrink ratio
 |          det_threshold (float): Minimum probability to be considered a positive detection
 |          simple_threshold (bool):
 |                  Whether to use a simple threshold map drawing method. If true, the threshold
 |                  map values will be 1, instead of the distance from shrink/expand boxes to the
 |                  actual boxes as described in the DBNet paper.

Here's an answer that abuses ast and inspect. It does nothing to the original class implementation besides changing the docstring.

The idea
  1. Loop through all the expressions in the class body,
  2. Check if any string expression appears right before an attribute,
  3. Store such attribute as an attribute docstring
  4. Create fancy formatting for the attribute docstrings and append it to the class's existing docstring.
The implementation
import ast
import inspect
from io import StringIO


def ast_find_classdef(tree):
    for e in ast.walk(tree):
        if isinstance(e, ast.ClassDef):
            return e


def attribute_docs(cls):
    """Enable attribute documentations for (data)classes. Use this function as a decorator.

    ```
    @attribute_docs
    @dataclass
    class TargetCoder:
        "Target coder for object detection"

        "Number of detection classes"
        num_classes: int

        "Normalized minimum bounding box size"
        min_size: flaot
    ```
    """
    # == find the class defining syntax tree ==
    src = inspect.getsource(cls)
    tree = ast.parse(src)
    tree = ast_find_classdef(tree)

    # == gather attribute doc strings ==
    # * We skip the first expr, because it is either a class docstring or something else
    # * The idea is that docstring appears on top of the attribute.
    # * Therefore, we search for any string node, mark that as a docstring.
    # * If a class attribute define node appears after the docstring, we store the docstring
    #    along with the class attribute's information
    attribute_docs = {}
    last_doc: Optional[str] = None
    for expr in tree.body[1:]:
        # When encouter an Expr, check if the expr a string
        if isinstance(expr, ast.Expr):
            # The value is a ast.Value node
            # therefore another access to value is needed
            value = expr.value.value
            if isinstance(value, str):
                last_doc = value.strip()

        # if the last known doc string is not none
        # and this next node is an annotation, that's a docstring
        if isinstance(expr, ast.AnnAssign) and last_doc is not None:
            # expr.target is a ast.Name
            name = ast.unparse(expr.target)
            type_name = ast.unparse(expr.annotation)
            attribute_docs[name] = (type_name, last_doc)
            last_doc = None

    # == Append to the class documentation ==
    # * if there is no attribute docstring, leave it be
    if len(attribute_docs) > 0:
        old_docs = cls.__doc__
        append_docs = build_attibute_docstrings(attribute_docs)
        cls.__doc__ = f"""{old_docs}\n\n{append_docs}"""
    return cls


def build_attibute_docstrings(docs):
    # Create pretty formatting for the attribute docs
    with StringIO() as io:
        io.write("Attributes:\n")
        for var_name, (type_name, docstring) in docs.items():
            # == Multiline vs inline doc format ==
            # * if the doc is inline, simply use the `x (type): docstring`
            # * if the doc is multiline, create a new paragraph
            if "\n" in docstring:
                lines = docstring.split("\n")
                lines = ["\t\t" + line.strip() for line in lines]
                docstring = "\n".join(lines)
                line = f"\t{var_name} ({type_name}):\n{docstring}\n"
            else:
                line = f"\t{var_name} ({type_name}): {docstring}\n"

            # Add the docstring line
            io.write(line)
        io.seek(0)
        docstring = io.read()
    return docstring
The example
@attribute_docs
@dataclass
class DBNetAlignCoder:
    "DBNet target coder for aligned case, i.e. detection targets are axis-aligned"

    "Number of detection classes"
    num_classes: int

    "Input image width"
    image_width: int

    "Input image height"
    image_height: int

    """Shrink rate of bounding boxes, the shrink distance will be computed using
    [A * (1 - r^2) / L], where A is the bounding box area, L is the bounding box
    perimeter, and r is the shrink ratio
    """
    shrink_ratio: float

    "Minimum probability to be considered a positive detection"
    det_threshold: float

    """
    Whether to use a simple threshold map drawing method. If true, the threshold
    map values will be 1, instead of the distance from shrink/expand boxes to the
    actual boxes as described in the DBNet paper.
    """
    simple_threshold: bool = False

The output of help():

class DBNetAlignCoder(builtins.object)
 |  DBNetAlignCoder(num_classes: int, image_width: int, image_height: int, shrink_ratio: float, det_threshold: float, simple_threshold: bool = False) -> None
 |  
 |  DBNet target coder for aligned case, i.e. detection targets are axis-aligned
 |  
 |  Attributes:
 |          num_classes (int): Number of detection classes
 |          image_width (int): Input image width
 |          image_height (int): Input image height
 |          shrink_ratio (float):
 |                  Shrink rate of bounding boxes, the shrink distance will be computed using
 |                  [A * (1 - r^2) / L], where A is the bounding box area, L is the bounding box
 |                  perimeter, and r is the shrink ratio
 |          det_threshold (float): Minimum probability to be considered a positive detection
 |          simple_threshold (bool):
 |                  Whether to use a simple threshold map drawing method. If true, the threshold
 |                  map values will be 1, instead of the distance from shrink/expand boxes to the
 |                  actual boxes as described in the DBNet paper.
~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文