Python中的类不变式 [英] Class invariants in Python

查看:107
本文介绍了Python中的类不变式的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

类不变式绝对可以在编码中使用,因为它们可以在清晰的编程时提供即时反馈错误已被检测到,并且由于明确了参数和返回值的含义,它们还提高了代码的可读性.我确定这也适用于Python.

但是,通常在Python中,对参数的测试似乎不是"Pythonic"的处理方式,因为它与鸭嘴式习惯相反.

我的问题是:

  1. 在代码中使用断言的Pythonic方法是什么?

    例如,如果我具有以下功能:

    def do_something(name, path, client):
        assert isinstance(name, str)
        assert path.endswith('/')
        assert hasattr(client, "connect")
    

  2. 更笼统地说,什么时候断言太多?

很高兴听到您对此的意见!

解决方案

简短答案:

断言是Pythonic吗?

取决于您如何使用它们.通常,不会.编写通用的,灵活的代码是最Python化的事情,但是当您需要检查不变量时:

  1. 使用类型提示来帮助您的IDE执行类型推断,从而避免潜在的陷阱.

  2. 进行强大的单元测试.

  3. 更喜欢 try/except子句,这些子句会引发更具体的异常.

  4. 将属性转换为属性,以便您可以控制其获取器和设置器.

  5. 仅将 assert 语句用于调试目的.

有关最佳做法的更多信息,请参考此Stack Overflow讨论. >

长答案

您是对的.严格的类不变性不被认为是Pythonic,但是有一种内置的方法来指定参数的首选类型并返回 type hinting ,如 属性 ...

 class Dog(object):
     """Canis lupus familiaris."""
     
     self._name = str()
     """The name on the microchip."""

     self.name = property()
     """The name on the collar."""


     def __init__(self, name: str):
         """What're you gonna name him?"""

         if not name and not name.isalpha():
             raise ValueError("Name must exist and be pronouncable.")

         self._name = name


     def speak(self, repeat=0):
         """Make dog bark. Can optionally be repeated."""

         try:
             print("{dog} stares at you blankly".format(dog=self.name))
             
             if repeat < 0:
                 raise ValueError("Cannot negatively bark.")

             for i in range(repeat):
                 print("{dog} says: 'Woof!'".format(dog=self.name))

         except (ValueError, TypeError) as e:
             raise RuntimeError("Dog unable to speak.") from e


     @property
     def name(self):
         """Gets name."""

         return self._name

由于我们的属性没有二传手,所以self.name本质上是不变的.除非有人意识到self._x,否则该值将无法更改.此外,由于我们添加了try/except子句来处理我们期望的特定错误,因此我们为程序提供了更简洁的控制流程.


那么您何时使用断言?

可能没有100%的"Pythonic"标记,执行断言的方法,因为您应该在单元测试中进行断言.但是,如果在运行时对于数据保持不变至关重要,则assert语句可用于查明可能的问题点,如 Python Wiki :

由于Python强大而灵活的动态键入系统,因此断言在Python中特别有用.在同一示例中,我们可能要确保id始终为数字:这将防止内部错误,还可以防止可能的情况,当有人用by_id表示某人时,他们会感到困惑并调用by_name.

例如:

from types import *
  class MyDB:
  ...
  def add(self, id, name):
    assert type(id) is IntType, "id is not an integer: %r" % id
    assert type(name) is StringType, "name is not a string: %r" % name

请注意,类型"模块明确地是可安全导入*";它导出的所有内容都以类型"结尾.

负责数据类型检查.对于类,您可以使用isinstance(),就像您在示例中所做的那样:

您也可以对类进行此操作,但是语法略有不同:

class PrintQueueList:
  ...
  def add(self, new_queue):
   assert new_queue not in self._list, \
     "%r is already in %r" % (self, new_queue)
   assert isinstance(new_queue, PrintQueue), \
     "%r is not a print queue" % new_queue

我意识到这并不是我们函数的确切工作方式,但是您明白了:我们希望防止被错误调用.您还可以看到打印错误所涉及的对象的字符串表示形式将如何帮助调试.

要获得适当的格式,请像上面的示例一样在您的断言中附加一条消息
(例如:assert <statement>, "<message>")会自动将信息附加到生成的AssertionError中,以帮助您进行调试.它还可以让用户深入了解消费者错误报告,以了解程序为何崩溃.

检查isinstance()不应过度使用:如果它像鸭子一样发出嘎嘎叫声,则可能无需深入询问是否确实如此.有时,传递原始程序员无法预期的值会很有用.

考虑放置断言的地方:

  • 检查参数类型,类或值
  • 检查数据结构不变性
  • 检查不可能发生";情况(列表中重复,状态变量相互矛盾.)
  • 调用函数后,确保其返回合理

如果正确使用了断言,则可能会很有用,但是对于不需要显式不变的数据,您不应该依赖它们.如果您希望代码更具Python风格,则可能需要重构代码.

Class invariants definitely can be useful in coding, as they can give instant feedback when clear programming error has been detected and also they improve code readability as being explicit about what arguments and return value can be. I'm sure this applies to Python too.

However, generally in Python, testing of arguments seems not to be "pythonic" way to do things, as it is against the duck-typing idiom.

My questions are:

  1. What is Pythonic way to use assertions in code?

    For example, if I had following function:

    def do_something(name, path, client):
        assert isinstance(name, str)
        assert path.endswith('/')
        assert hasattr(client, "connect")
    

  2. More generally, when there is too much of assertions?

I'd be happy to hear your opinions on this!

解决方案

Short Answer:

Are assertions Pythonic?

Depends how you use them. Generally, no. Making generalized, flexible code is the most Pythonic thing to do, but when you need to check invariants:

  1. Use type hinting to help your IDE perform type inference so you can avoid potential pitfalls.

  2. Make robust unit tests.

  3. Prefer try/except clauses that raise more specific exceptions.

  4. Turn attributes into properties so you can control their getters and setters.

  5. Use assert statements only for debug purposes.

Refer to this Stack Overflow discussion for more info on best practices.

Long Answer

You're right. It's not considered Pythonic to have strict class invariants, but there is a built-in way to designate the preferred types of parameters and returns called type hinting, as defined in PEP 484:

[Type hinting] aims to provide a standard syntax for type annotations, opening up Python code to easier static analysis and refactoring, potential runtime type checking, and (perhaps, in some contexts) code generation utilizing type information.

The format is this:

def greeting(name: str) -> str:
    return 'Hello ' + name 

The typing library provides even further functionality. However, there's a huge caveat...

While these annotations are available at runtime through the usual __annotations__ attribute, no type checking happens at runtime . Instead, the proposal assumes the existence of a separate off-line type checker which users can run over their source code voluntarily. Essentially, such a type checker acts as a very powerful linter.

Whoops. Well, you could use an external tool while testing to check when invariance is broken, but that doesn't really answer your question.


Properties and try/except

The best way to handle an error is to make sure it never happens in the first place. The second best way is to have a plan when it does. Take, for example, a class like this:

 class Dog(object):
     """Canis lupus familiaris."""

     self.name = str()
     """The name you call it."""


     def __init__(self, name: str):
         """What're you gonna name him?"""

         self.name = name


     def speak(self, repeat=0):
         """Make dog bark. Can optionally be repeated."""

         print("{dog} stares at you blankly.".format(dog=self.name))

         for i in range(repeat):
             print("{dog} says: 'Woof!'".format(dog=self.name)

If you want your dog's name to be an invariant, this won't actually prevent self.name from being overwritten. It also doesn't prevent parameters that could crash speak(). However, if you make self.name a property...

 class Dog(object):
     """Canis lupus familiaris."""
     
     self._name = str()
     """The name on the microchip."""

     self.name = property()
     """The name on the collar."""


     def __init__(self, name: str):
         """What're you gonna name him?"""

         if not name and not name.isalpha():
             raise ValueError("Name must exist and be pronouncable.")

         self._name = name


     def speak(self, repeat=0):
         """Make dog bark. Can optionally be repeated."""

         try:
             print("{dog} stares at you blankly".format(dog=self.name))
             
             if repeat < 0:
                 raise ValueError("Cannot negatively bark.")

             for i in range(repeat):
                 print("{dog} says: 'Woof!'".format(dog=self.name))

         except (ValueError, TypeError) as e:
             raise RuntimeError("Dog unable to speak.") from e


     @property
     def name(self):
         """Gets name."""

         return self._name

Since our property doesn't have a setter, self.name is essentially invariant; that value can't change unless someone is aware of the self._x. Furthermore, since we've added try/except clauses to process the specific errors we're expecting, we've provided a more concise control flow for our program.


So When Do You Use Assertions?

There might not be a 100% "Pythonic" way to perform assertions since you should be doing those in your unit tests. However, if it's critical at runtime for data to be invariant, assert statements can be used to pinpoint possible trouble spots, as explained in the Python wiki:

Assertions are particularly useful in Python because of Python's powerful and flexible dynamic typing system. In the same example, we might want to make sure that ids are always numeric: this will protect against internal bugs, and also against the likely case of somebody getting confused and calling by_name when they meant by_id.

For example:

from types import *
  class MyDB:
  ...
  def add(self, id, name):
    assert type(id) is IntType, "id is not an integer: %r" % id
    assert type(name) is StringType, "name is not a string: %r" % name

Note that the "types" module is explicitly "safe for import *"; everything it exports ends in "Type".

That takes care of data type checking. For classes, you use isinstance(), as you did in your example:

You can also do this for classes, but the syntax is a little different:

class PrintQueueList:
  ...
  def add(self, new_queue):
   assert new_queue not in self._list, \
     "%r is already in %r" % (self, new_queue)
   assert isinstance(new_queue, PrintQueue), \
     "%r is not a print queue" % new_queue

I realize that's not the exact way our function works but you get the idea: we want to protect against being called incorrectly. You can also see how printing the string representation of the objects involved in the error will help with debugging.

For proper form, attaching a message to your assertions like in the examples above
(ex: assert <statement>, "<message>") will automatically attach the info into the resulting AssertionError to assist you with debugging. It could also give some insight into a consumer bug report as to why the program is crashing.

Checking isinstance() should not be overused: if it quacks like a duck, there's perhaps no need to enquire too deeply into whether it really is. Sometimes it can be useful to pass values that were not anticipated by the original programmer.

Places to consider putting assertions:

  • checking parameter types, classes, or values
  • checking data structure invariants
  • checking "can't happen" situations (duplicates in a list, contradictory state variables.)
  • after calling a function, to make sure that its return is reasonable

Assertions can be beneficial if they're properly used, but you shouldn't become dependent on them for data that doesn't need to be explicitly invariant. You might need to refactor your code if you want it to be more Pythonic.

这篇关于Python中的类不变式的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆