Python中的类不变式 [英] Class invariants in Python
问题描述
类不变式绝对可以在编码中使用,因为它们可以在清晰的编程时提供即时反馈错误已被检测到,并且由于明确了参数和返回值的含义,它们还提高了代码的可读性.我确定这也适用于Python.
但是,通常在Python中,对参数的测试似乎不是"Pythonic"的处理方式,因为它与鸭嘴式习惯相反.
我的问题是:
-
在代码中使用断言的Pythonic方法是什么?
例如,如果我具有以下功能:
def do_something(name, path, client): assert isinstance(name, str) assert path.endswith('/') assert hasattr(client, "connect")
-
更笼统地说,什么时候断言太多?
很高兴听到您对此的意见!
简短答案:
断言是Pythonic吗?
取决于您如何使用它们.通常,不会.编写通用的,灵活的代码是最Python化的事情,但是当您需要检查不变量时:
-
使用类型提示来帮助您的IDE执行类型推断,从而避免潜在的陷阱.
-
进行强大的单元测试.
-
更喜欢
try
/except
子句,这些子句会引发更具体的异常. -
将属性转换为属性,以便您可以控制其获取器和设置器.
-
仅将
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:
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")
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:
Use type hinting to help your IDE perform type inference so you can avoid potential pitfalls.
Make robust unit tests.
Prefer
try
/except
clauses that raise more specific exceptions.Turn attributes into properties so you can control their getters and setters.
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屋!