Following system colour scheme - Python 增强提案 Selected dark colour scheme - Python 增强提案 Selected light colour scheme - Python 增强提案

Python 增强提案

PEP 359 – “make” 语句

作者:
Steven Bethard <steven.bethard at gmail.com>
状态:
已撤回
类型:
标准跟踪
创建日期:
2006年4月5日
Python 版本:
2.6
发布历史:
2006年4月5日, 2006年4月6日, 2006年4月13日

目录

摘要

本 PEP 提出了类声明语法 make 语句的泛化。提议的语法和语义与类定义的语法并行,因此

make <callable> <name> <tuple>:
    <block>

被翻译成赋值语句

<name> = <callable>("<name>", <tuple>, <namespace>)

其中 <namespace> 是通过执行 <block> 创建的字典。这主要是语法糖,对应于

class <name> <tuple>:
    __metaclass__ = <callable>
    <block>

旨在帮助更清楚地表达创建类以外的其他对象时的语句意图。当然,这种语句的其他语法也是可能的,但希望通过与类语句保持强烈的并行性,将对类和元类工作方式的理解转化为对 make 语句工作方式的理解。

本 PEP 基于 Michele Simionato 在 python-dev 列表中的建议 [1]

撤回通知

本 PEP 应 Guido 的要求 [2] 撤回。Guido 不喜欢它,尤其不喜欢属性用例将属性的实例方法置于与其它实例方法不同的级别,并且需要为属性函数使用固定名称。

动机

类语句为 Python 提供了两个不错的便利:

  1. 它们执行一个语句块,并将结果绑定作为字典提供给元类。
  2. 它们鼓励 DRY(不重复自己),允许正在创建的类知道它被分配的名称。

因此,在一个简单的类语句中,例如

class C(object):
    x = 1
    def foo(self):
        return 'bar'

元类(type)被调用时类似于

C = type('C', (object,), {'x':1, 'foo':<function foo at ...>})

类语句只是上述赋值语句的语法糖,但显然是一种非常有用的语法糖。它不仅避免了 C 的重复,还通过允许将字典表示为一系列语句来简化字典的创建。

历史上,类型实例(又称类对象)是唯一被这种语法支持所“祝福”的对象。make 语句旨在将这种支持扩展到其他类型的对象,在这些对象中这种语法也可能有用。

示例:简单命名空间

假设我模块中有一些属性,我像这样访问它们:

mod.thematic_roletype
mod.opinion_roletype

mod.text_format
mod.html_format

并且由于“命名空间是一个非常棒的想法”,我希望能够像这样访问这些属性:

mod.roletypes.thematic
mod.roletypes.opinion

mod.format.text
mod.format.html

我目前主要有两种选择:

  1. 将模块转换为包,将 roletypesformat 转换为子模块,并将属性移动到子模块中。
  2. 创建 roletypesformat 类,并将属性移动到类中。

前者是一项相当大的重构工作,会生成两个内容不多的微型模块。后者将属性保留在模块本地,但创建了从未打算创建实例的类。

在这种情况下,能够简单地声明一个“命名空间”来保存少数属性会很好。使用新的 make 语句,我可以像这样引入我的新命名空间:

make namespace roletypes:
    thematic = ...
    opinion = ...

make namespace format:
    text = ...
    html = ...

并将我的属性保留在模块本地,而无需创建从未打算实例化的类。一个能够实现这一点的命名空间定义是:

class namespace(object):
    def __init__(self, name, args, kwargs):
        self.__dict__.update(kwargs)

根据这个定义,在上面的 make 语句结束时,roletypesformat 将是命名空间实例。

示例:GUI 对象

在 GUI 工具包中,框架和面板等对象通常与属性和函数相关联。使用 make 语句,类似以下的代码:

root = Tkinter.Tk()
frame = Tkinter.Frame(root)
frame.pack()
def say_hi():
    print "hi there, everyone!"
hi_there = Tkinter.Button(frame, text="Hello", command=say_hi)
hi_there.pack(side=Tkinter.LEFT)
root.mainloop()

可以重写为将按钮的函数与其声明组合在一起:

root = Tkinter.Tk()
frame = Tkinter.Frame(root)
frame.pack()
make Tkinter.Button hi_there(frame):
    text = "Hello"
    def command():
        print "hi there, everyone!"
hi_there.pack(side=Tkinter.LEFT)
root.mainloop()

示例:自定义描述符

由于描述符用于自定义属性的访问,因此知道该属性的名称通常很有用。当前的 Python 没有提供一种简单的方法来查找此名称,因此许多自定义描述符,例如 Ian Bicking 的 setonce 描述符 [3],必须以某种方式绕过此问题。使用 make 语句,您可以创建一个 setonce 属性,例如:

class A(object):
    ...
    make setonce x:
        "A's x attribute"
    ...

其中 setonce 描述符将定义为:

class setonce(object):

    def __init__(self, name, args, kwargs):
        self._name = '_setonce_attr_%s' % name
        self.__doc__ = kwargs.pop('__doc__', None)

    def __get__(self, obj, type=None):
        if obj is None:
            return self
        return getattr(obj, self._name)

    def __set__(self, obj, value):
        try:
            getattr(obj, self._name)
        except AttributeError:
            setattr(obj, self._name, value)
        else:
            raise AttributeError("Attribute already set")

    def set(self, obj, value):
        setattr(obj, self._name, value)

    def __delete__(self, obj):
        delattr(obj, self._name)

请注意,与原始实现不同,私有属性名称是稳定的,因为它使用描述符的名称,因此类 A 的实例是可 pickle 的。

示例:属性命名空间

Python 的 property 类型接受三个函数参数和一个文档字符串参数,这些参数虽然仅与 property 相关,但必须在其之前声明,然后作为参数传递给 property 调用,例如:

class C(object):
    ...
    def get_x(self):
        ...
    def set_x(self):
        ...
    x = property(get_x, set_x, "the x of the frobulation")

这个问题之前已经提出过,Guido [4] 和其他人 [5] 曾短暂地思考过替代的 property 语法,以使声明属性更容易。使用 make 语句,可以支持以下语法:

class C(object):
    ...
    make block_property x:
        '''The x of the frobulation'''
        def fget(self):
            ...
        def fset(self):
            ...

其中 block_property 的定义如下:

def block_property(name, args, block_dict):
    fget = block_dict.pop('fget', None)
    fset = block_dict.pop('fset', None)
    fdel = block_dict.pop('fdel', None)
    doc = block_dict.pop('__doc__', None)
    assert not block_dict
    return property(fget, fset, fdel, doc)

示例:接口

Guido [6] 和其他人偶尔建议在 Python 中引入接口。大多数建议都提供了类似以下的语法:

interface IFoo:
    """Foo blah blah"""

    def fumble(name, count):
        """docstring"""

但由于 Python 目前无法以这种方式声明接口,大多数 Python 接口的实现都使用类对象,例如 Zope 的:

class IFoo(Interface):
    """Foo blah blah"""

    def fumble(name, count):
        """docstring"""

使用新的 make 语句,这些接口可以声明为:

make Interface IFoo:
    """Foo blah blah"""

    def fumble(name, count):
        """docstring"""

这使得意图(这是一个接口,而不是一个类)更加清晰。

规范

Python 会将一个 make 语句

make <callable> <name> <tuple>:
    <block>

翻译成赋值语句

<name> = <callable>("<name>", <tuple>, <namespace>)

其中 <namespace> 是通过执行 <block> 创建的字典。<tuple> 表达式是可选的;如果不存在,则假定为空元组。

有一个补丁实现了这些语义 [7]

make 语句引入了一个新关键字 make。因此,在 Python 2.6 中,make 语句必须使用 from __future__ import make_statement 来启用。

未解决的问题

关键字

make 关键字破坏了太多代码吗?最初,make 语句使用关键字 create(Alyssa Coghlan 的建议)。然而,对标准库 [8] 和 Zope+Plone 代码 [9] 的调查显示,create 会破坏更多代码,因此采用了 make 作为关键字。然而,仍然有一些实例 make 会破坏代码。是否有更好的关键字来表示该语句?

一些可能的关键字及其在标准库(以及一些已安装包)中的出现次数:

  • make - 2 (都在测试中)
  • create - 19 (包括 imaplib 中现有的函数)
  • build - 83 (包括 distutils.command.build 中现有的类)
  • construct - 0
  • produce - 0

make 语句作为备用构造函数

目前,没有多少函数具有签名 (name, args, kwargs)。这意味着类似

make dict params:
    x = 1
    y = 2

目前是不可能的,因为 dict 构造函数具有不同的签名。是否需要支持这种事情?Carl Banks 的一个建议是添加一个 __make__ 魔术方法,如果找到,将调用它而不是 __call__。对于类型,__make__ 方法将与 __call__ 相同,因此没有必要,但 dict 可以通过在 dict 类型上定义一个看起来像这样的 __make__ 方法来支持 make 语句:

def __make__(cls, name, args, kwargs):
    return cls(**kwargs)

当然,与其添加另一个魔术方法,不如让 dict 类型增加一个类似 dict.fromblock 的类方法,可以像这样使用:

make dict.fromblock params:
    x = 1
    y = 2

所以问题是,许多类型会希望使用 make 语句作为备用构造函数吗?如果是这样,那个备用构造函数需要与原始构造函数同名吗?

自定义执行代码块的 dict

make 语句的用户是否应该能够确定代码在哪个字典对象中执行?这将允许 make 语句在普通字典对象不足以应对的情况下使用,例如,如果必须允许顺序和重复名称。允许这种自定义可以允许在不重复元素名称的情况下编写 XML,并且 make 语句的嵌套对应于 XML 元素的嵌套。

make Element html:
    make Element body:
        text('before first h1')
        make Element h1:
            attrib(style='first')
            text('first h1')
            tail('after first h1')
        make Element h1:
            attrib(style='second')
            text('second h1')
            tail('after second h1')

如果 make 语句尝试通过调用可调用对象的 __make_dict__ 方法来获取执行其代码块的字典,则以下代码将允许 make 语句如上所示使用:

class Element(object):

    class __make_dict__(dict):

        def __init__(self, *args, **kwargs):
            self._super = super(Element.__make_dict__, self)
            self._super.__init__(*args, **kwargs)
            self.elements = []
            self.text = None
            self.tail = None
            self.attrib = {}

        def __getitem__(self, name):
            try:
                return self._super.__getitem__(name)
            except KeyError:
                if name in ['attrib', 'text', 'tail']:
                    return getattr(self, 'set_%s' % name)
                else:
                    return globals()[name]

        def __setitem__(self, name, value):
            self._super.__setitem__(name, value)
            self.elements.append(value)

        def set_attrib(self, **kwargs):
            self.attrib = kwargs

        def set_text(self, text):
            self.text = text

        def set_tail(self, text):
            self.tail = text

    def __new__(cls, name, args, edict):
        get_element = etree.ElementTree.Element
        result = get_element(name, attrib=edict.attrib)
        result.text = edict.text
        result.tail = edict.tail
        for element in edict.elements:
            result.append(element)
        return result

然而,请注意,支持此功能的代码有些脆弱——它必须神奇地用 attribtexttail 填充命名空间,并且它假设 make 语句主体内的每个名称绑定都在创建一个 Element。就目前而言,这段代码如果在一个 make 语句主体中引入一个简单的 for 循环,就会崩溃,因为 for 循环会将一个名称绑定到一个非 Element 对象。这可以通过添加某种 isinstance 检查或属性检查来解决,但这仍然导致了一个相当脆弱的解决方案。

还有人指出,with 语句可以提供等效的嵌套,并且语法更明确:

with Element('html') as html:
    with Element('body') as body:
        body.text = 'before first h1'
        with Element('h1', style='first') as h1:
            h1.text = 'first h1'
            h1.tail = 'after first h1'
        with Element('h1', style='second') as h1:
            h1.text = 'second h1'
            h1.tail = 'after second h1'

如果这里的元素名称重复太多违反了 DRY 原则,也可以通过向 Element 添加一些方法来消除除第一个之外的所有 as 子句。[10]

那么,在不同类型的字典中执行代码块是否存在实际用例?如果存在,make 语句是否应该扩展以支持它们?

可选扩展

移除 make 关键字

也许可以移除 make 关键字,以便此类语句以被调用的可调用对象开头,例如:

namespace ns:
    badger = 42
    def spam():
        ...

interface C(...):
    ...

然而,几乎所有其他的 Python 语句都以关键字开头,移除关键字会使在文档中查找此结构变得更加困难。此外,这会增加语法的复杂性,到目前为止我(Steven Bethard)还无法在没有关键字的情况下实现该功能。

在 Python 3000 中移除 __metaclass__

make 语句的普遍性带来了副作用,即它几乎消除了类对象中对 __metaclass__ 属性的需求。因此,在 Python 3000 中,不必像这样:

class <name> <bases-tuple>:
    __metaclass__ = <metaclass>
    <block>

可以通过将元类用作 make 语句中的可调用对象来支持元类:

make <metaclass> <name> <bases-tuple>:
    <block>

移除 __metaclass__ 钩子会稍微简化 BUILD_CLASS 操作码。

在 Python 3000 中移除 class 语句

在 make 语句最极端的应用中,类语句本身可能会被弃用,取而代之的是 make type 语句。

参考资料


来源: https://github.com/python/peps/blob/main/peps/pep-0359.rst

最后修改: 2025-02-01 08:59:27 GMT