首页 > 解决方案 > python中可能存在的错误。创建装饰器自动生成属性

问题描述

我创建了一个repl.it在线运行代码: https ://repl.it/repls/ShimmeringQuickwittedHarddrive#main.py


我正在尝试创建一个装饰器来将对象属性中的属性“携带”到对象中。
简单示例(伪代码):

class carry:
    ''' Decorator. '''
    class Prop:
        ''' Representation of '@property'. '''
        # impl
    # impl

class A:
    def __init__(self):
        self._val_0 = 'A: val_0'
        self._val_1 = 'A: val_1'
    @property
    def val_0(self):
        return self._val_0
    @property
    def val_1(self):
        return self._val_1

# '_a'              - the name of the attribute to get the @property'ies from.
# 'carry.Prop(...)' - a list of @property'ies to carry.
@carry(
    '_a',
    carry.Prop('val_0'),
    carry.Prop('val_1')
)
class B:
    def __init__(self):
        self._a = A()

我期望得到的:

>>> b = B()
>>> print(b.val_0)
'A: val_0'
>>> print(b.val_1)
'A: val_1'

我得到了什么:(
警告:差异非常微妙且容易错过;b.val_0返回 的值b.val_1

>>> b = B()
>>> print(b.val_0)
'A: val_1'
>>> print(b.val_1)
'A: val_1'

下面的代码是重现错误的“可执行”简约示例。这是很多代码,所以我将其分为三个部分:

import pprint

from copy import copy

pp = pprint.PrettyPrinter(
    indent = 2,
    width = 80, 
    depth = None, 
    compact = False,
    sort_dicts = False
)

##################################################
# Decorator:
##################################################

class carry:
    class Prop:
        ''' Represents a 'property'. '''
        def __init__(self, name:str, fget:bool=True, fset:bool=False, fdel:bool=False):
            self._name = name
            self._fget = fget
            self._fset = fset
            self._fdel = fdel
        @property
        def name(self) -> str:
            return self._name
        @property
        def fget(self) -> bool:
            return self._fget
        @property
        def fset(self) -> bool:
            return self._fset
        @property
        def fdel(self) -> bool:
            return self._fdel

    def __init__(self, obj, *props):
        self._obj = obj
        self._props = props

    def __call__(self, cls):
        class Template(cls):
            pass
        Template.__name__ = cls.__name__
        Template.__doc__  = cls.__doc__

        for prop in self._props:
            print('prop repr: ... {}'.format(prop.__dict__))

            kwargs = {}
            if prop.fget:
                def fget(self):
                    print('fget id: ..... {}'.format(id(fget)))
                    print('fget addr: ... {}'.format(hex(id(fget))))
                    print('fget.obj: .... {}'.format(fget.obj))
                    print('fget.name: ... {}'.format(fget.name))
                    return getattr(getattr(self, fget.obj), fget.name)
                fget.obj  = copy(self._obj)
                fget.name = copy(prop.name)
                print('fget id: ..... {}'.format(id(fget)))
                print('fget addr: ... {}'.format(hex(id(fget))))
                print('fget.obj: .... {}'.format(fget.obj))
                print('fget.name: ... {}'.format(fget.name))
                kwargs['fget'] = fget

            result = property(**kwargs)
            print('kwargs: ...... {}'.format(kwargs))
            print('prop: ........ {}'.format(result))
            print('prop id: ..... {}'.format(id(result)))
            print('prop addr: ... {}'.format(hex(id(result))))
            setattr(Template, prop.name, result)
            print()

        # Return modified class:
        return Template

##################################################
# Tests:
##################################################

class A:
    def __init__(self):
        self._val_0 = 'A: val_0: 0'
        self._val_1 = 'A: val_1: 1'
        self._val_2 = 'A: val_2: 2'
        self._val_3 = 'A: val_3: 3'
    @property
    def val_0(self):
        return self._val_0
    @property
    def val_1(self):
        return self._val_1
    @property
    def val_2(self):
        return self._val_2
    @property
    def val_3(self):
        return self._val_3

print()
print('##################################################')
print()

props = [
    carry.Prop('val_0'),
    carry.Prop('val_1'),
    carry.Prop('val_2'),
    carry.Prop('val_3'),
]

@carry('_a', *props)
class B:
    def __init__(self):
        self._a = A()

print('##################################################')
print()

print(B)
print(B.__name__)
pp.pprint(dir(B))

print()
print('##################################################')
print()

b = B()
print(b.val_0)
print()
print(b.val_1)
print()
print(b.val_2)
print()
print(b.val_3)
print()

标签: pythonpython-3.xdecorator

解决方案


首先,让我重申我的评论:您似乎正在实施“委托”模式。这里有一些代码可以用更少的行和更多的灵活性来做到这一点: https ://www.michaelcho.me/article/method-delegation-in-python

“携带”这个名字并没有告诉我会发生什么。除非您有一些问题域,该名称已经很成熟,否则我会敦促您考虑使用不同的名称。照这样说 ...

我相信你的问题是你试图使用一个闭包而不创建一个闭包。用于迭代所有属性的循环不是正确的闭包,因此存在泄漏——所有引用最终都指向迭代变量的最后一个值。

我修改了您的示例代码,如下所示:

        if prop.fget:
            fget = self.make_fget(self._obj, prop.name)
            #def fget(self):
            #    print('fget id: ..... {}'.format(id(fget)))
            #    print('fget addr: ... {}'.format(hex(id(fget))))
            #    print('fget.obj: .... {}'.format(fget.obj))
            #    print('fget.name: ... {}'.format(fget.name))
            #    return getattr(getattr(self, fget.obj), fget.name)
            #fget.obj  = copy(self._obj)
            #fget.name = copy(prop.name)

并添加了这个方法:

def make_fget(self, attribute_name, property_name):
    """ Construct and return an fget closure. """
    def fget(self):
        print('fget id: ..... {}'.format(id(fget)))
        print('fget addr: ... {}'.format(hex(id(fget))))
        print('fget.obj: .... {}'.format(fget.obj))
        print('fget.name: ... {}'.format(fget.name))
        return getattr(getattr(self, fget.obj), fget.name)

    fget.obj = attribute_name
    fget.name = property_name
    return fget

结果更符合我的预期:

# ... many, many lines elided ...

fget id: ..... 139848106927168
fget addr: ... 0x7f30ecbc6440
fget.obj: .... _a
fget.name: ... val_0
A: val_0: 0

fget id: ..... 139848106463104
fget addr: ... 0x7f30ecb54f80
fget.obj: .... _a
fget.name: ... val_1
A: val_1: 1

fget id: ..... 139848106491984
fget addr: ... 0x7f30ecb5c050
fget.obj: .... _a
fget.name: ... val_2
A: val_2: 2

fget id: ..... 139848106492128
fget addr: ... 0x7f30ecb5c0e0
fget.obj: .... _a
fget.name: ... val_3
A: val_3: 3

推荐阅读