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

Python 增強提案

PEP 231 – __findattr__()

作者:
Barry Warsaw <barry at python.org>
狀態:
已拒絕
型別:
標準跟蹤
建立日期:
2000年11月30日
Python 版本:
2.1
釋出歷史:


目錄

引言

本PEP描述了對例項屬性查詢和修改機制的擴充套件,它允許純Python實現許多有趣的程式設計模型。本PEP跟蹤此功能的地位和所有權。它包含對該功能的描述,並概述了支援該功能所需的更改。本PEP總結了郵件列表論壇中的討論,並在適當的地方提供了進一步資訊的URL。此檔案的CVS修訂歷史包含確切的歷史記錄。

背景

Python例項的語義允許程式設計師透過特殊方法__getattr__()__setattr__() [1]定製屬性查詢和屬性修改的某些方面。

然而,由於這些方法施加的某些限制,有一些有用的程式設計技術無法僅用Python編寫,例如嚴格的Java Bean式[2]介面和Zope風格的獲取[3]。在後一種情況下,Zope透過包含一個名為ExtensionClass [5]的C擴充套件來解決這個問題,該擴充套件修改了標準類語義,並使用了Python類模型中的一個元類鉤子,該鉤子也稱為“Don Beaudry Hook”或“Don Beaudry Hack” [6]

雖然Zope的方法有效,但它有幾個缺點。首先,它需要一個C擴充套件。其次,它利用了Python機制中一個非常神秘但漏洞很大的地方。第三,其他程式設計師可能難以使用和理解(元類具有眾所周知的讓人“燒腦”的特性)。第四,由於ExtensionClass例項不是“真實”的Python例項,Python執行時系統的某些方面不適用於ExtensionClass例項。

解決此問題的提案通常被歸入“修復類/型別二分法”的範疇;也就是說,消除內建型別和類之間的區別[7]。雖然這是一個值得稱讚的目標,但為了實現上述型別的程式設計構造,修復這種裂痕並非必要。本提案提供了一個80%的解決方案,對Python的類和例項物件進行了最小的修改。它沒有解決型別/類二分法的問題。

提案

本提案添加了一個名為__findattr__()的新特殊方法,其語義如下:

  • 如果在類中定義,它將在所有例項屬性解析時被呼叫,而不是__getattr__()__setattr__()
  • __findattr__()從不遞迴呼叫。也就是說,當特定例項的__findattr__()在呼叫堆疊上時,對該例項的進一步屬性訪問將使用標準的__getattr__()__setattr__()方法。
  • __findattr__()在屬性訪問(“獲取”)和屬性修改(“設定”)時都會被呼叫。它在屬性刪除時不會被呼叫。
  • 當被呼叫進行獲取時,它會傳遞一個引數(不包括“self”):被訪問屬性的名稱。
  • 當被呼叫進行設定時,它會傳遞第三個引數,即要設定的屬性值。
  • __findattr__()方法的快取語義與__getattr__()__setattr__()相同;即,如果它們在類定義時存在於類中,則會被使用,但如果它們隨後被新增到類中,則不會被使用。

與現有協議的主要區別

__findattr__()的語義與現有協議在關鍵方面有所不同

首先,如果屬性在例項的__dict__中找到,則永遠不會呼叫__getattr__()。這樣做是為了效率,並且因為否則,__setattr__()將無法訪問例項的屬性。

其次,__setattr__()不能使用“普通”語法設定例項屬性,例如“self.name = foo”,因為那會導致對__setattr__()的遞迴呼叫。

無論屬性是否在__dict__中,__findattr__()總是被呼叫,並且例項物件中的一個標誌可以防止對__findattr__()的遞迴呼叫。這使得類有機會對每個屬性訪問執行一些操作。而且因為它對獲取和設定都進行呼叫,所以很容易為所有屬性訪問編寫相似的策略。此外,效率不是問題,因為只有在使用擴充套件機制時才需要付出代價。

示例

本提案允許的一種程式設計風格是類似Java Bean的物件介面,其中無修飾的屬性訪問和修改透明地對映到函式介面。例如:

class Bean:
    def __init__(self, x):
        self.__myfoo = x

    def __findattr__(self, name, *args):
        if name.startswith('_'):
            # Private names
            if args: setattr(self, name, args[0])
            else:    return getattr(self, name)
        else:
            # Public names
            if args: name = '_set_' + name
            else:    name = '_get_' + name
            return getattr(self, name)(*args)

    def _set_foo(self, x):
        self.__myfoo = x

    def _get_foo(self):
        return self.__myfoo


b = Bean(3)
print b.foo
b.foo = 9
print b.foo

第二個,更復雜的例子是在純Python中實現隱式和顯式獲取

import types

class MethodWrapper:
    def __init__(self, container, method):
        self.__container = container
        self.__method = method

    def __call__(self, *args, **kws):
        return self.__method.im_func(self.__container, *args, **kws)


class WrapperImplicit:
    def __init__(self, contained, container):
        self.__contained = contained
        self.__container = container

    def __repr__(self):
        return '<Wrapper: [%s | %s]>' % (self.__container,
                                         self.__contained)

    def __findattr__(self, name, *args):
        # Some things are our own
        if name.startswith('_WrapperImplicit__'):
            if args: return setattr(self, name, *args)
            else:    return getattr(self, name)
        # setattr stores the name on the contained object directly
        if args:
            return setattr(self.__contained, name, args[0])
        # Other special names
        if name == 'aq_parent':
            return self.__container
        elif name == 'aq_self':
            return self.__contained
        elif name == 'aq_base':
            base = self.__contained
            try:
                while 1:
                    base = base.aq_self
            except AttributeError:
                return base
        # no acquisition for _ names
        if name.startswith('_'):
            return getattr(self.__contained, name)
        # Everything else gets wrapped
        missing = []
        which = self.__contained
        obj = getattr(which, name, missing)
        if obj is missing:
            which = self.__container
            obj = getattr(which, name, missing)
            if obj is missing:
                raise AttributeError, name
        of = getattr(obj, '__of__', missing)
        if of is not missing:
            return of(self)
        elif type(obj) == types.MethodType:
            return MethodWrapper(self, obj)
        return obj


class WrapperExplicit:
    def __init__(self, contained, container):
        self.__contained = contained
        self.__container = container

    def __repr__(self):
        return '<Wrapper: [%s | %s]>' % (self.__container,
                                         self.__contained)

    def __findattr__(self, name, *args):
        # Some things are our own
        if name.startswith('_WrapperExplicit__'):
            if args: return setattr(self, name, *args)
            else:    return getattr(self, name)
        # setattr stores the name on the contained object directly
        if args:
            return setattr(self.__contained, name, args[0])
        # Other special names
        if name == 'aq_parent':
            return self.__container
        elif name == 'aq_self':
            return self.__contained
        elif name == 'aq_base':
            base = self.__contained
            try:
                while 1:
                    base = base.aq_self
            except AttributeError:
                return base
        elif name == 'aq_acquire':
            return self.aq_acquire
        # explicit acquisition only
        obj = getattr(self.__contained, name)
        if type(obj) == types.MethodType:
            return MethodWrapper(self, obj)
        return obj

    def aq_acquire(self, name):
        # Everything else gets wrapped
        missing = []
        which = self.__contained
        obj = getattr(which, name, missing)
        if obj is missing:
            which = self.__container
            obj = getattr(which, name, missing)
            if obj is missing:
                raise AttributeError, name
        of = getattr(obj, '__of__', missing)
        if of is not missing:
            return of(self)
        elif type(obj) == types.MethodType:
            return MethodWrapper(self, obj)
        return obj


class Implicit:
    def __of__(self, container):
        return WrapperImplicit(self, container)

    def __findattr__(self, name, *args):
        # ignore setattrs
        if args:
            return setattr(self, name, args[0])
        obj = getattr(self, name)
        missing = []
        of = getattr(obj, '__of__', missing)
        if of is not missing:
            return of(self)
        return obj


class Explicit(Implicit):
    def __of__(self, container):
        return WrapperExplicit(self, container)


# tests
class C(Implicit):
    color = 'red'

class A(Implicit):
    def report(self):
        return self.color

# simple implicit acquisition
c = C()
a = A()
c.a = a
assert c.a.report() == 'red'

d = C()
d.color = 'green'
d.a = a
assert d.a.report() == 'green'

try:
    a.report()
except AttributeError:
    pass
else:
    assert 0, 'AttributeError expected'


# special names
assert c.a.aq_parent is c
assert c.a.aq_self is a

c.a.d = d
assert c.a.d.aq_base is d
assert c.a is not a


# no acquisition on _ names
class E(Implicit):
    _color = 'purple'

class F(Implicit):
    def report(self):
        return self._color

e = E()
f = F()
e.f = f
try:
    e.f.report()
except AttributeError:
    pass
else:
    assert 0, 'AttributeError expected'


# explicit
class G(Explicit):
    color = 'pink'

class H(Explicit):
    def report(self):
        return self.aq_acquire('color')

    def barf(self):
        return self.color

g = G()
h = H()
g.h = h
assert g.h.report() == 'pink'

i = G()
i.color = 'cyan'
i.h = h
assert i.h.report() == 'cyan'

try:
    g.i.barf()
except AttributeError:
    pass
else:
    assert 0, 'AttributeError expected'

C++風格的訪問控制也可以實現,儘管由於難以從執行時呼叫堆疊中找出正在呼叫的方法而不太清晰

import sys
import types

PUBLIC = 0
PROTECTED = 1
PRIVATE = 2

try:
    getframe = sys._getframe
except ImportError:
    def getframe(n):
        try: raise Exception
        except Exception:
            frame = sys.exc_info()[2].tb_frame
        while n > 0:
            frame = frame.f_back
            if frame is None:
                raise ValueError, 'call stack is not deep enough'
        return frame


class AccessViolation(Exception):
    pass


class Access:
    def __findattr__(self, name, *args):
        methcache = self.__dict__.setdefault('__cache__', {})
        missing = []
        obj = getattr(self, name, missing)
        # if obj is missing we better be doing a setattr for
        # the first time
        if obj is not missing and type(obj) == types.MethodType:
            # Digusting hack because there's no way to
            # dynamically figure out what the method being
            # called is from the stack frame.
            methcache[obj.im_func.func_code] = obj.im_class
        #
        # What's the access permissions for this name?
        access, klass = getattr(self, '__access__', {}).get(
            name, (PUBLIC, 0))
        if access is not PUBLIC:
            # Now try to see which method is calling us
            frame = getframe(0).f_back
            if frame is None:
                raise AccessViolation
            # Get the class of the method that's accessing
            # this attribute, by using the code object cache
            if frame.f_code.co_name == '__init__':
                # There aren't entries in the cache for ctors,
                # because the calling mechanism doesn't go
                # through __findattr__().  Are there other
                # methods that might have the same behavior?
                # Since we can't know who's __init__ we're in,
                # for now we'll assume that only protected and
                # public attrs can be accessed.
                if access is PRIVATE:
                    raise AccessViolation
            else:
                methclass = self.__cache__.get(frame.f_code)
                if not methclass:
                    raise AccessViolation
                if access is PRIVATE and methclass is not klass:
                    raise AccessViolation
                if access is PROTECTED and not issubclass(methclass,
                                                          klass):
                    raise AccessViolation
        # If we got here, it must be okay to access the attribute
        if args:
            return setattr(self, name, *args)
        return obj

# tests
class A(Access):
    def __init__(self, foo=0, name='A'):
        self._foo = foo
        # can't set private names in __init__
        self.__initprivate(name)

    def __initprivate(self, name):
        self._name = name

    def getfoo(self):
        return self._foo

    def setfoo(self, newfoo):
        self._foo = newfoo

    def getname(self):
        return self._name

A.__access__ = {'_foo'      : (PROTECTED, A),
                '_name'     : (PRIVATE, A),
                '__dict__'  : (PRIVATE, A),
                '__access__': (PRIVATE, A),
                }

class B(A):
    def setfoo(self, newfoo):
        self._foo = newfoo + 3

    def setname(self, name):
        self._name = name

b = B(1)
b.getfoo()

a = A(1)
assert a.getfoo() == 1
a.setfoo(2)
assert a.getfoo() == 2

try:
    a._foo
except AccessViolation:
    pass
else:
    assert 0, 'AccessViolation expected'

try:
    a._foo = 3
except AccessViolation:
    pass
else:
    assert 0, 'AccessViolation expected'

try:
    a.__dict__['_foo']
except AccessViolation:
    pass
else:
    assert 0, 'AccessViolation expected'


b = B()
assert b.getfoo() == 0
b.setfoo(2)
assert b.getfoo() == 5
try:
    b.setname('B')
except AccessViolation:
    pass
else:
    assert 0, 'AccessViolation expected'

assert b.getname() == 'A'

這是PEP 213中描述的屬性鉤子的實現(除了當前參考實現不支援屬性刪除鉤子)。

class Pep213:
    def __findattr__(self, name, *args):
        hookname = '__attr_%s__' % name
        if args:
            op = 'set'
        else:
            op = 'get'
        # XXX: op = 'del' currently not supported
        missing = []
        meth = getattr(self, hookname, missing)
        if meth is missing:
            if op == 'set':
                return setattr(self, name, *args)
            else:
                return getattr(self, name)
        else:
            return meth(op, *args)


def computation(i):
    print 'doing computation:', i
    return i + 3


def rev_computation(i):
    print 'doing rev_computation:', i
    return i - 3


class X(Pep213):
    def __init__(self, foo=0):
        self.__foo = foo

    def __attr_foo__(self, op, val=None):
        if op == 'get':
            return computation(self.__foo)
        elif op == 'set':
            self.__foo = rev_computation(val)
        # XXX: 'del' not yet supported

x = X()
fooval = x.foo
print fooval
x.foo = fooval + 5
print x.foo
# del x.foo

參考實現

作為Python核心補丁的參考實現可以在此URL找到

http://sourceforge.net/patch/?func=detailpatch&patch_id=102613&group_id=5470

參考資料

拒絕

遞迴保護功能存在嚴重問題。如這裡所述,它不是執行緒安全的,並且執行緒安全的解決方案存在其他問題。總的來說,不清楚遞迴保護功能有多大幫助;它使得編寫需要在__findattr__內部和外部都能呼叫的程式碼變得困難。但是如果沒有遞迴保護,要完全實現__findattr__也很困難(因為__findattr__會遞迴呼叫自身來訪問每個它試圖訪問的屬性)。這裡似乎沒有好的解決方案。

支援__findattr__同時用於獲取和設定屬性的用處也值得懷疑——__setattr__在所有情況下都已經被呼叫了。

如果注意不要以其自身的名稱儲存例項變數,所有示例都可以使用__getattr__實現。


來源:https://github.com/python/peps/blob/main/peps/pep-0231.rst

最後修改:2025-02-01 08:55:40 GMT