Python中的类不变量

时间:2016-11-16 19:48:09

标签: python invariants

Class invariants在编码时肯定是有用的,因为它们可以在检测到明确的编程错误时提供即时反馈,并且还可以提高代码可读性,因为它们可以明确说明哪些参数和返回值。我确信这也适用于Python。

然而,通常在Python中,对参数的测试似乎不是“pythonic”做事的方式,因为它违背了鸭子类型的习惯用法。

我的问题是:

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

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

    def do_something(name, path, client):
        assert isinstance(name, str)
        assert path.endswith('/')
        assert hasattr(client, "connect")
    
  2. 更一般地说,当断言太多时?

  3. 我很高兴听到您的意见!

2 个答案:

答案 0 :(得分:8)

简答:

  

是断言Pythonic吗?

取决于您如何使用它们。一般来说,没有。制作通用,灵活的代码是Pythonic最需要做的事情,但是当你需要检查不变量时:

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

  2. 进行强大的单元测试

  3. 首选 try / except条款,以提出更具体的例外情况。

  4. 将属性转换为属性,以便您可以控制其getter和setter。

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

  6. 有关最佳做法的详细信息,请参阅this Stack Overflow discussion

    长答案

    你是对的。不认为Pythonic具有严格的类不变量,但有一种内置方法可以指定首选类型的参数和返回,称为类型提示,如PEP 484中所定义:

      

    [类型提示] 旨在为类型注释提供标准语法,打开Python代码以更轻松地进行静态分析和重构,潜在的运行时类型检查,以及(可能在某些情况下)代码生成利用类型信息。

    格式如下:

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

    typing库提供了更多功能。然而,这是一个巨大的警告...

      

    虽然这些注释在运行时通过通常的__annotations__属性可用,但 在运行时没有进行类型检查 。相反,该提议假定存在一个单独的离线类型检查器,用户可以自动运行其源代码。从本质上讲,这种类型的检查器就像一个非常强大的linter。

    糟糕。好吧,你可以在测试时使用外部工具来检查不变性何时被破坏,但这并不能真正回答你的问题。

    属性和try / except

    处理错误的最佳方法是确保它始终不会发生。第二种最好的方法是制定计划。举个例如这样的课程:

     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)
    

    如果您希望您的狗的名字是不变的,这实际上不会被self.name覆盖。它也不会阻止可能导致speak()崩溃的参数。但是,如果您将self.name设为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
    

    由于我们的财产没有设定者,self.name基本上是不变的;除非有人知道self._x,否则该值不会发生变化。此外,由于我们已添加try / except条款来处理我们期望的特定错误,因此我们为我们的计划提供了更简洁的控制流程。

    那么什么时候使用断言?

    可能没有100%&#34; Pythonic&#34;执行断言的方法因为您应该在单元测试中执行这些操作。但是,如果在运行时对数据不变是至关重要的,assert语句可用于查明可能的故障点,如Python wiki中所述:

      

    由于Python功能强大且灵活的动态类型系统,断言在Python中特别有用。在同一个例子中,我们可能希望确保id始终是数字的:这样可以防止内部错误,也可以防止有人混淆和调用by_name时它们的含义为by_id。

         

    例如:

    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
    
         

    请注意&#34;类型&#34;模块明确地安全地导入*&#34 ;;它出口的所有东西都以&#34; Type&#34;。

    结束

    负责数据类型检查。对于课程,您使用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()不应该被滥用:如果它像鸭子那样嘎嘎叫,那么也许没有必要深入探究它是否真的存在。有时,传递原始程序员未预料到的值会很有用。

         

    考虑放置断言的地方:

         
        
    • 检查参数类型,类或值
    •   
    • 检查数据结构不变量
    •   
    • 检查&#34;无法发生&#34;情况(列表中的重复,相互矛盾的状态变量。)
    •   
    • 调用函数后,确保其返回合理
    •   

    如果正确使用断言可能会有所帮助,但对于不需要明确不变的数据,您不应该依赖它们。如果你希望它更像Pythonic,你可能需要重构你的代码。

答案 1 :(得分:0)

请查看icontract库。我们开发它的目的是将带有合同的设计和信息错误消息引入Python。这里作为类不变式的示例:

>>> @icontract.inv(lambda self: self.x > 0)
... class SomeClass:
...     def __init__(self) -> None:
...         self.x = 100
...
...     def some_method(self) -> None:
...         self.x = -1
...
...     def __repr__(self) -> str:
...         return "some instance"
...
>>> some_instance = SomeClass()
>>> some_instance.some_method()
Traceback (most recent call last):
 ...
icontract.ViolationError: self.x > 0:
self was some instance
self.x was -1