modules

Python 3 中的 dataclass

在 Python 中,如果要为一个类添加一些数据成员的话,需要做的事情还挺多,比如说编写 __init__,
__str__ 这些函数,代码都是重复的,没啥意义。在 Python 3.7 中,终于添加了一个语法糖,叫做
dataclass. 下面我们就来看一下吧~

# 注意!包名叫 dataclasses, 多了个 es
from dataclasses import dataclass

@dataclass
class InventoryItem:
    """Class for keeping track of an item in inventory."""
    name: str
    unit_price: float
    quantity_on_hand: int = 0

    def total_cost(self) -> float:
        return self.unit_price * self.quantity_on_hand

上面的代码就相当于以前的:

def __init__(self, name: str, unit_price: float, quantity_on_hand: int=0):
    self.name = name
    self.unit_price = unit_price
    self.quantity_on_hand = quantity_on_hand

def __repr__(self):
    ...

def __eq__(self):
    ...

...

我们知道,在 Python 的默认参数中,使用 mutable(可变) 的对象是一种常见的坑,在 dataclass 中当然也存在
了,还好标准库中给我们提供了一个方法。

# 会报错
@dataclass
class Request:
    headers: dict = {}

# 相当于
class Request:
    def __init__(self, headers={}):
        self.headers = headers

# 正确的写法
from dataclasses import field

@dataclass
class Request:
    headers: dict = field(default_factory=dict)

字典这种类型是比较容易想起来不能直接做参数的,比较坑的是对于其他的自定义对象,Python 解释器并不会提
示有问题,比如说这样:

# 千万别这么做
@dataclass
class Request:
    headers: Headers = Headers()

这时候坑爹的事情就发生了,每次创建新的 Request 对象引用的都是同一个 Headers 对象,也就是在声明这个类
的同时产生的这个 Headers 对象!原因也很简单,就像是上面的 dict 一样,这个 Headers 并不是在 init 函数
中,所以只会产生一次。所以,需要牢记的是:dataclass 中的所有对象默认值就相当于函数的默认参数,永远不
要传递一个 mutable 就好了。

# 上面的例子相当于
class Request:
    def __init__(self, headers=Headers()):
        self.headers = headers

# 正确的做法
@dataclass
class Request:
    headers: Headers = field(default_factory=Headers)

dataclasses 模块中还提供了 asdict 方法,这样就可以方便地转换为 json 对象啦。

from dataclasses import asdict

@dataclass
class Request:
    url: str = ""
    method: str = ""

req = asdict(Request())

参考资料

  1. https://docs.python.org/3/library/dataclasses.html

Python Argparse 库的使用

基本用法

import argparse
parser = argparse.ArgumentParser()
parser.add_argument("--verbosity", help="increase output verbosity")
args = parser.parse_args()
print(args.verbosity)

两种不同的参数模式, positionaloptional arguments,感觉之间的区别有点像 args 和 kwargs

subcommand

Actually, the argparse module is not ok with subcommand, mannually parse the first command and then pass the rest to argparse

add_argument 的参数

name or flags - Either a name or a list of option strings, e.g. foo or `-f`, `--foo`.
action - The basic type of action to be taken when this argument is encountered at the command line. store/store_const/store_true/append/count
nargs - The number of command-line arguments that should be consumed. N/?/*/+
const - A constant value required by some action and nargs selections.
default - The value produced if the argument is absent from the command line.
type -  The type to which the command-line argument should be converted.
choices - A container of the allowable values for the argument. a list 
required - Whether or not the command-line option may be omitted (optionals only).
help - A brief description of what the argument does.
metavar - A name for the argument in usage messages.
dest - The name of the attribute to be added to the object returned by parse_args().

TODO

尝试使用下 fire: https://github.com/google/python-fire

Python 3 中的 Enum

enum 模块是 Python 3 中新引入的一个用于定义枚举常量的模块。

基本使用

>>> from enum import Enum
>>> class Color(Enum):
...     RED = 1
...     GREEN = 2
...     BLUE = 3
...
>>> print(Color.RED)
Color.RED
>>> type(Color.RED)
<enum "Color">
>>> isinstance(Color.GREEN, Color)
True

# 可以遍历
>>> for color in Color:
...     print color
...
Color.RED
Color.GREEN
Color.BLUE

>>> list(Shape)
[<Shape.SQUARE: 2>, <Shape.DIAMOND: 1>, <Shape.CIRCLE: 3>]

# 可以当做字典使用
>>> Color(1)
<Color.RED: 1>
>>> Color["RED"]
<Color.RED: 1>

# 可以访问 Enum 的 name 和 value 属性
>>> red = Color.RED
>>> red.name
"RED"
>>> red.value
1

自动值

如果枚举的值无关紧要,可以使用 auto,不过一般来说还是不要使用 auto,以免后续存储的数据出问题。

>>> from enum import Enum, auto
>>> class Color(Enum):
...     RED = auto()
...     BLUE = auto()
...     GREEN = auto()
...
>>> list(Color)
[<Color.RED: 1>, <Color.BLUE: 2>, <Color.GREEN: 3>]

保证值是唯一的

使用 enum.unique 装饰器

>>> from enum import Enum, unique
>>> @unique
... class Mistake(Enum):
...     ONE = 1
...     TWO = 2
...     THREE = 3
...     FOUR = 3
...
Traceback (most recent call last):
...
ValueError: duplicate values found in <enum "Mistake">: FOUR -> THREE

比较

注意 enum.Enum 并不是 int, 所以并不能比较大小。如果你想要把 enum 当做 int 使用,可以继承 enum.IntEnum

>>> Color.RED is Color.RED
True
>>> Color.RED is Color.BLUE
False
>>> Color.RED < Color.BLUE
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: "<" not supported between instances of "Color" and "Color"

# 使用 IntEnum 可以比较大小
>>> class IntColor(IntEnum):
...     RED = 1
...     GREEN = 2
...     BLUE = 3
...
>>> IntColor.RED < IntColor.GREEN
True

子类

只有没定义值的类才可以被继承。

# 不可以
>>> class MoreColor(Color):
...     PINK = 17
...
Traceback (most recent call last):
...
TypeError: Cannot extend enumerations

# 可以
>>> class Foo(Enum):
...     def some_behavior(self):
...         pass
...
>>> class Bar(Foo):
...     HAPPY = 1
...     SAD = 2
...

利用函数来生成 enum 对象

>>> Color = Enum("Color", "RED GREEN BLUE")

IntFlag 和 Flag

IntFlag 也会 Enum 和 int 的子类,可以设置每一个 bit 位。

>>> from enum import IntFlag
>>> class Perm(IntFlag):
...     R = 4
...     W = 2
...     X = 1
...
>>> Perm.R | Perm.W
<Perm.R|W: 6>
>>> Perm.R + Perm.W
6
>>> RW = Perm.R | Perm.W
>>> Perm.R in RW
True

还可以个给组合起来的变量命名

>>> class Perm(IntFlag):
...     R = 4
...     W = 2
...     X = 1
...     RWX = 7
>>> Perm.RWX
<Perm.RWX: 7>
>>> ~Perm.RWX
<Perm.-8: -8>

另外,如果 IntFlag 每个位都没有设定,那么恰好是 0 ,会被当做 False 看待。

解决 json 序列化的问题

可以直接继承 str 解决

class Color(str, Enum):
  green = "GREEN"
  red = "RED"
  blue = "BLUE"

列出所有值

[e.value for e in MyEnum]

参考

  1. https://stackoverflow.com/questions/24481852/serialising-an-enum-member-to-json/24482131