跳转到主要内容

紧凑语法定义简单的“struct-like”(或“record-like”,“bean-like”)类。

项目描述

Build status Coverage Latest Version License

概述

提供紧凑语法以定义简单的“struct-like”(或“record-like”,“bean-like”)类。生成的类与namedtuple非常相似,但可变,语法更优雅,更灵活,并且具有更多功能。

以下是功能摘要

  • 可以为字段定义默认值。

  • 有用的__repr____str__实现。

  • 结构相等,即有用的__eq____ne__实现。

  • copy方法(复制构造函数)。

  • 从/到字典和元组的转换。

  • 使用 __slots__ 声明来提高性能并防止对未知字段进行赋值。

  • 可以定义自定义方法。

  • 支持继承。

请参阅 动机 部分,了解该概念的其它实现,特别是 MacroPy,它是本项目灵感的来源,并采用了一种非常不同的方法。

兼容性

目前仅支持 Python 2.7。

安装

通常

pip install rbco.caseclasses

或者

easy_install rbco.caseclasses

使用

基础知识

让我们从一个简单的案例类开始

>>> from rbco.caseclasses import case
>>>
>>> @case
... class Person(object):
...     """Represent a person."""
...     def __init__(self, name, age=None, gender=None): pass

声明的 __init__ 仅仅是一个占位符。参数定义了类将拥有的字段及其默认值。将用一个新的 __init__ 方法替换它,该方法负责分配字段的值。

构造函数按预期工作,根据提供的 __init__ 占位符

>>> Person('John')
Person(name='John', age=None, gender=None)
>>> Person('John', 30, 'm')
Person(name='John', age=30, gender='m')
>>> Person(name='John', age=30, gender='m')
Person(name='John', age=30, gender='m')
>>> Person('John', gender='m')
Person(name='John', age=None, gender='m')

注意,在字符串表示中,字段与构造函数中定义的顺序相同。

类的文档字符串被保留

>>> Person.__doc__
'Represent a person.'

构造函数的签名没有被保留。生成的 __init__ 方法签名是通用的,只接受 *args**kwargs

>>> from inspect import getargspec
>>> getargspec(Person.__init__)
ArgSpec(args=['self'], varargs='args', keywords='kwargs', defaults=None)

然而,文档字符串包含了原始签名

>>> Person.__init__.__doc__
'Original signature: (self, name, age=None, gender=None)'

无法创建没有构造函数的案例类

>>> from rbco.caseclasses import case
>>>
>>> @case
... class Foo(object): pass
Traceback (most recent call last):
...
RuntimeError: Case class must define a constructor.

可变性和__slots__

实例是可变的

>>> p = Person('John')
>>> p
Person(name='John', age=None, gender=None)
>>> p.name = 'Bob'
>>> p.age = 35
>>> p
Person(name='Bob', age=35, gender=None)

然而,无法分配给未知属性

>>> p.department = 'sales'
Traceback (most recent call last):
...
AttributeError: 'Person' object has no attribute 'department'

这是因为 __slots__ 声明

>>> p.__slots__
['name', 'age', 'gender']

结构相等

支持结构相等性

>>> p1 = Person('John', 30)
>>> p2 = Person('Bob', 25)
>>> p1 == p2
False
>>> p1 != p2
True
>>> p2.name = 'John'
>>> p2.age = 30
>>> p1 == p2
True
>>> p1 != p2
False
>>> p2.gender = 'm'
>>> p1 == p2
False

复制构造函数

提供了复制构造函数

>>> p1 = Person('John', 30)
>>> copy_of_p1 = p1.copy()
>>> p1
Person(name='John', age=30, gender=None)
>>> copy_of_p1
Person(name='John', age=30, gender=None)
>>> p1 is copy_of_p1
False
>>> p2 = p1.copy(name='Bob', gender='m')
>>> p2
Person(name='Bob', age=30, gender='m')

从/到字典和元组的转换

从/到字典的转换很简单。 as_dict 方法返回一个 OrderedDict

>>> p1 = Person('Mary', 33)
>>> p1
Person(name='Mary', age=33, gender=None)
>>> p1.as_dict()
OrderedDict([('name', 'Mary'), ('age', 33), ('gender', None)])
>>> Person(**p1.as_dict())
Person(name='Mary', age=33, gender=None)

从/到元组的转换也是可能的

>>> p1 = Person('John', 30)
>>> p1
Person(name='John', age=30, gender=None)
>>> p1.as_tuple()
('John', 30, None)
>>> Person(*p1.as_tuple())
Person(name='John', age=30, gender=None)

自定义成员

案例类非常类似于常规类。可以定义任何类型的自定义成员。

最常见的案例是添加自定义实例方法

>>> import math
>>> @case
... class Point(object):
...     def __init__(self, x, y): pass
...
...     def distance(self, other):
...         return math.sqrt((self.x - other.x)**2 + (self.y - other.y)**2)
>>> p1 = Point(0, 0)
>>> p2 = Point(10, 0)
>>> p1.distance(p2)
10.0

也支持其他类型的类成员

>>> @case
... class Example(object):
...     class_attribute = 'some value'
...
...     def __init__(self, field1): pass
...
...     @staticmethod
...     def static_method():
...         print 'This is an static method.'
...
...     @classmethod
...     def class_method(cls):
...         print 'This is a class method of the class {}.'.format(cls.__name__)
...
>>> e = Example('example')
>>> Example.class_attribute
'some value'
>>> e.class_attribute
'some value'
>>> Example.static_method()
This is an static method.
>>> Example.class_method()
This is a class method of the class Example.

继承

让我们创建一个基案例类和一个派生案例类

>>> @case
... class Person(object):
...     def __init__(self, name, age=None, gender=None): pass
...
...     def present(self):
...         print "I'm {}, {} years old and my gender is '{}'.".format(
...             self.name,
...             self.age,
...             self.gender
...         )
...
>>> @case
... class Employee(Person):
...     def __init__(self, name, age=None, gender=None, department=None): pass

必须重复基类的字段,但如果你手动实现案例类,你也必须这样做。

基类的方法被继承

>>> p = Person('John', 30, 'm')
>>> p.present()
I'm John, 30 years old and my gender is 'm'.
>>> e = Employee('Mary', 33, 'f', 'sales')
>>> e.present()
I'm Mary, 33 years old and my gender is 'f'.

PersonEmployee 的实例始终被认为是不同的,因为雇员有一个额外的字段

>>> p = Person('John')
>>> e = Employee('John')
>>> p == e
False

覆盖基类方法按预期工作

>>> @case
... class ImprovedEmployee(Employee):
...     def present(self):
...         super(ImprovedEmployee, self).present()
...         print 'I work at the {} department.'.format(self.department)
...
>>> ie = ImprovedEmployee(name='Mary', department='marketing', age=33, gender='f')
>>> ie.present()
I'm Mary, 33 years old and my gender is 'f'.
I work at the marketing department.

覆盖case class行为

可以覆盖标准的案例类方法(如 __repr____eq__ 等)。例如

>>> @case
... class Foo(object):
...     def __init__(self, bar): pass
...
...     def __eq__(self, other):
...         return True  # All `Foo`s are equal.
...
>>> Foo('bar') == Foo('baz')
True

甚至可以在子类方法中调用原始版本

>>> @case
... class Foo(object):
...     def __init__(self, bar):
...         pass
...
...     def __repr__(self):
...         return 'This is my string representation: ' + super(Foo, self).__repr__()
...
>>> Foo('bar')
This is my string representation: Foo(bar='bar')

无法覆盖 __init__ 方法,因为当应用 @case 装饰器时,它会替换。如果需要自定义构造函数,可以使用 CaseClassMixin 作为解决方案。

使用CaseClassMixin以获得更多灵活性

@case 装饰器创建的类继承自 CaseClassMixin

>>> from rbco.caseclasses import CaseClassMixin
>>> issubclass(Person, CaseClassMixin)
True

CaseClassMixin 提供了所有“案例类”行为,除了构造函数。要直接使用 CaseClassMixin,子类必须满足的唯一要求是提供一个 __fields__ 属性,它包含字段名称的序列。

这可能在需要更大灵活性的情况下很有用。在下面的示例中,我们创建了一个具有自定义构造函数的案例类

>>> class Foo(CaseClassMixin):
...     __fields__ = ('field1', 'field2')
...
...     def __init__(self, field1, *args):
...         self.field1 = field1 + '_modified'
...         self.field2 = list(args)
...
>>> Foo('bar', 1, 2)
Foo(field1='bar_modified', field2=[1, 2])

限制

  • 案例类的构造函数无法自定义,因为它在应用 @case 装饰器时被替换。有关替代方案,请参阅有关 CaseClassMixin 的部分。

  • 由于 __slots__ 声明,无法分配给未知字段。

  • 构造函数不能接受 *args**kwargs

    >>> @case
    ... class Foo(object):
    ...     def __init__(self, **kwargs): pass
    Traceback (most recent call last):
    ...
    RuntimeError: Case class constructor cannot take *args or **kwargs.

    有关替代方案的说明,请参阅关于 CaseClassMixin 的章节。

动机、设计决策和其他实现

与MacroPy的比较

这个项目的想法来自 MacroPy。它通过语法宏实现了case类的实现,从而以非常优雅的方式定义了case类。动机是为了在不使用语法宏或字符串评估(这是namedtuple采取的方法)的情况下提供类似的功能。换句话说:在不使用太多魔法的情况下提供最佳的实现。

MacroPy 的比较可以总结如下

优点

  • 没有魔法。

  • 允许任何类型的自定义成员,包括实例方法。

  • 由于case类只是普通类,因此允许任何类型的继承。

缺点

  • MacroPy的语法更优美。与比较起来,__init__存根的东西可能有点丑。

  • 不支持自定义初始化逻辑。这可以通过使用 CaseClassMixin 实现,但程序员需要做更多的工作。

  • 构造函数中不支持 *args**kwargs。同样,这可以通过使用 CaseClassMixin 实现,但需要做更多的工作。

其他实现

Python中存在其他“case类”概念(或类似)的实现

  • 构造函数存根机制的灵感来自 hwiechers 的这个实现

  • Brian Wickman的一个简单实现可以在 这个Gist 中找到。

  • 有关 stackoverflow 上的这个讨论 ,其中包含一些实现想法。

废弃的实现想法

考虑了一些实现想法,但后来被摒弃了。这里讨论其中的一些。

函数式语法

这意味着使用一个函数来生成类。这可能是这样的

Person = case_class('Person', 'name', age=None, gender=None)

这个想法的第一个问题是无法保留字段的顺序。case_class 函数必须定义如下

def case_class(__name__, *args, **kwargs):
    ...

**kwargs 是一个无序字典,因此字段的顺序丢失。

为了克服这个问题,可以使用以下语法

Person = case_class('Person', 'name', 'age', 'gender', age=None, gender=None)

我认为这个语法不够优雅。我不喜欢字段名称的重复,也不喜欢字段名称既表示为字符串又表示为参数名称。

也许这个也会有效

Person = case_class('Person', ['name', 'age', 'gender'], {'age': None, 'gender': None})

但我想这个语法也不够优雅。

此外,使用这种语法可能难以支持一些功能,特别是

  • 自定义成员。这意味着会复杂化 case_class 函数的签名,或者需要在类创建后添加自定义成员。如下所示

    Person = case_class('Person', ...)
    
    def present(self):
        print ...
    
    Person.present = present

    不够优雅。

  • 继承。这需要向 case_class 函数添加一个新参数,以允许传入基类。

将字段规范作为类装饰器的参数

这将结束定义空构造函数的必要性。语法将是这样的

@case(name, age=None, gender=None)
class Person(object):
    'Represent a person.'

与函数语法面临相同的问题:字段顺序未保留,因为 case 函数必须接受一个 **kwargs 参数,它是一个无序字典。

与为函数语法提供的类似的其他语法可以克服字段顺序问题。然而,我认为使用 __init__ 存根定义字段的方法更优雅。

将字段规范作为类属性

语法将是这样的

@case
class Person(object):
    name = NO_DEFAULT_VALUE
    age = None
    gender = None

再次强调,无法保留字段顺序。case 函数需要从 Person.__dic__ 中检索类属性,而这是无序的。

可能像这样会有效

@case
class Person(object):
    __fields__ = (
        ('name', NO_DEFAULT_VALUE),
        ('age', None),
        ('gender', None)
    )

但我认为使用 __init__ 模板来定义字段的方法更加优雅。

贡献

如果您想贡献,请fork此项目并提交一个pull请求。提前感谢!

变更日志

1.0.2 (2020-04-27)

  • 允许使用更近版本的 funcsigs。[ale-rt]

  • 小的文档调整。[rafaelbco]

  • 改进文档字符串。[rafaelbco]

1.0.1 (2014-05-09)

  • 改进文档。

  • 小的重构。

1.0.0 (2014-05-09)

  • 首次发布。

项目详情


下载文件

下载适用于您平台的应用程序文件。如果您不确定选择哪个,请了解更多关于 安装包 的信息。

源代码分发

rbco.caseclasses-1.0.2.zip (28.7 kB 查看哈希值)

上传时间 源代码

支持