简体   繁体   English

python 属性 class 命名空间混淆

[英]python property class namespace confusion

I am confused about use of property class with regard to references to the fset/fget/fdel functions and in which namespaces they live.关于对 fset/fget/fdel 函数的引用以及它们所在的名称空间,我对属性 class 的使用感到困惑。 The behavior is different depending on whether I use property as a decorator or a helper function. Why do duplicate vars in class and instance namespaces impact one example but not the other?行为会有所不同,具体取决于我是将属性用作装饰器还是助手 function。为什么 class 和实例名称空间中的重复变量会影响一个示例而不影响另一个示例?

When using property as a decorator shown here I must hide the var name in __dict__ with a leading underscore to prevent preempting the property functions.当使用属性作为此处显示的装饰器时,我必须使用前导下划线隐藏__dict__中的 var 名称,以防止抢占属性函数。 If not I'll see a recursion loop.如果不是,我会看到一个递归循环。

class setget():
    """Play with setters and getters"""
    @property
    def x(self):
        print('getting x')
        return self._x
    @x.setter
    def x(self, x):
        print('setting x')
        self._x = x
    @x.deleter
    def x(self):
        print('deleting x')
        del self._x

and I can see _x as an instance property and x as a class property:我可以将 _x 视为实例属性,将 x 视为 class 属性:

>>> sg = setget()
>>> sg.x = 1
setting x
>>> sg.__dict__
{'_x': 1}
pprint(setget.__dict__)
mappingproxy({'__dict__': <attribute '__dict__' of 'setget' objects>,
              '__doc__': 'Play with setters and getters',
              '__module__': '__main__',
              '__weakref__': <attribute '__weakref__' of 'setget' objects>,
              'x': <property object at 0x000001BF3A0C37C8>})
>>> 

Here's an example of recursion if the instance var name underscore is omitted.下面是一个递归示例,如果省略了实例变量名称下划线。 (code not shown here) This makes sense to me because instance property x does not exist and so we look further to class properties. (此处未显示代码)这对我来说很有意义,因为实例属性 x 不存在,因此我们进一步查看 class 属性。

>>> sg = setget()
>>> sg.x = 1
setting x
setting x
setting x
setting x
...

However if I use property as a helper function as described in one of the answers here: python class attributes vs instance attributes the name hiding underscore is not needed and there is no conflict.但是,如果我使用属性作为助手 function,如此处的一个答案中所述: python class 属性与实例属性,则不需要隐藏下划线的名称,并且没有冲突。

Copy of the example code:示例代码的副本:

class PropertyHelperDemo:
    '''Demonstrates a property definition helper function'''
    def prop_helper(k: str, doc: str):
        print(f'Creating property instance {k}')
        def _get(self):
            print(f'getting {k}')
            return self.__dict__.__getitem__(k) # might use '_'+k, etc.
        def _set(self, v):
            print(f'setting {k}')
            self.__dict__.__setitem__(k, v)
        def _del(self):
            print(f'deleting {k}')
            self.__dict__.__delitem__(k)
        return property(_get, _set, _del, doc)

    X: float = prop_helper('X', doc="X is the best!")
    Y: float = prop_helper('Y', doc="Y do you ask?")
    Z: float = prop_helper('Z', doc="Z plane!")
    # etc...

    def __init__(self, X: float, Y: float, Z: float):
        #super(PropertyHelperDemo, self).__init__()  # not sure why this was here
        (self.X, self.Y, self.Z) = (X, Y, Z)

    # for read-only properties, the built-in technique remains sleek enough already
    @property
    def Total(self) -> float:
        return self.X + self.Y + self.Z

And here I verify that the property fset function is being executed on subsequent calls.在这里,我验证属性 fset function 正在后续调用中执行。

>>> p = PropertyHelperDemo(1, 2, 3)
setting X
setting Y
setting Z
>>> p.X = 11
setting X
>>> p.X = 111
setting X
>>> p.__dict__
{'X': 111, 'Y': 2, 'Z': 3}
>>> pprint(PropertyHelperDemo.__dict__)
mappingproxy({'Total': <property object at 0x000002333A093F98>,
              'X': <property object at 0x000002333A088EF8>,
              'Y': <property object at 0x000002333A093408>,
              'Z': <property object at 0x000002333A093D18>,
              '__annotations__': {'X': <class 'float'>,
                                  'Y': <class 'float'>,
                                  'Z': <class 'float'>},
              '__dict__': <attribute '__dict__' of 'PropertyHelperDemo' objects>,
              '__doc__': 'Demonstrates a property definition helper function',
              '__init__': <function PropertyHelperDemo.__init__ at 0x000002333A0B3AF8>,
              '__module__': '__main__',
              '__weakref__': <attribute '__weakref__' of 'PropertyHelperDemo' objects>,
              'prop_helper': <function PropertyHelperDemo.prop_helper at 0x000002333A052F78>})
>>> 

I can see the class and instance properties with overlapping names X, Y, Z, in the two namespaces.我可以在两个命名空间中看到 class 和具有重叠名称 X、Y、Z 的实例属性。 It is my understanding that the namespace search order begins with local variables so I don't understand why the property fset function is executed here.我的理解是命名空间搜索顺序是从局部变量开始的,所以我不明白为什么要在这里执行属性 fset function。

Any guidance is greatly appreciated.非常感谢任何指导。

I think you're a little astray in construing _x as an "instance property" and x as a "class property" - in fact, both are bound to the instance only, and neither is bound to the other except by the arbitrarily defined behaviour of the method decorated by @property .我认为你在将_x解释为“实例属性”并将x解释为“类属性”时有点误入歧途 - 事实上,两者都只绑定到实例,除了任意定义的行为之外,两者都没有绑定到另一个由@property装饰的方法。

They both occupy the same namespace, which is why, though they may represent the same quantity, they cannot share a name for fear of shadowing/confusing the namespace.它们都占用相同的名称空间,这就是为什么尽管它们可能代表相同的数量,但由于担心隐藏/混淆名称空间,它们不能共享名称。

The issue of namespaces is not directly connected to the use of the @property decorator.名称空间的问题与@property装饰器的使用没有直接关系。 You don't HAVE to "hide" the attribute name - you just need to ensure that the attribute name differs from the name of the method, because once you apply the @property decorator, the method decorated by @property can be accessed just like any other attribute without a typical method call signature including the () .您不必“隐藏”属性名称 - 您只需要确保属性名称与方法名称不同,因为一旦应用了@property装饰器,就可以像访问@property装饰的方法一样访问没有典型方法调用签名的任何其他属性,包括()

Here's an example, adjacent to the one you provided, that may help clarify.这是一个与您提供的示例相邻的示例,可能有助于澄清。 I define a class, PositionVector below, that holds the x , y and z coordinates of a point in space.我在下面定义了一个 class, PositionVector ,它保存空间中一个点的xyz坐标。

When initialising an instance of the class, I also create an attribute length that computes the length of the vector based on the x, y and z values.在初始化 class 的实例时,我还创建了一个属性length ,它根据 x、y 和 z 值计算向量的长度。 Trying this:试试这个:

import numpy as np

class PositionVector:
    
    def __init__(self, x: float, y: float, z: float) -> None:
        self.x = x
        self.y = y
        self.z = z
        self.length = np.sqrt(x**2 + y**2 + z**2)
        
        
p1 = PositionVector(x = 10, y = 0, z = 0)

print (p1.length)
# Result -> 10.0

Only now I want to change the y attribute of the instance.只是现在我想更改实例的y属性。 I do this:我这样做:

p1.y = 10.0
print (f"p1's 'y' value is {p1.y}")
# Result -> p1's 'y' value is 10.0

Except now, if I again access the length of the vector, I get the wrong answer:除了现在,如果我再次访问向量的长度,我会得到错误的答案:

print (f"p1's length is {p1.length}")
# Result -> p1's length is 10.0

This arises because length , which at any given instant depends on the current values of x , y , and z , is never updated and kept consistent.出现这种情况是因为length在任何给定时刻都取决于xyz的当前值,它永远不会更新并保持一致。 We could fix this issue by redefining our class so length is a method that is continuously recalculated every time the user wants to access it, like so:我们可以通过重新定义我们的 class 来解决这个问题,所以 length 是一种每次用户想要访问它时都会不断重新计算的方法,如下所示:

class PositionVector:
    
    def __init__(self, x: float, y: float, z: float) -> None:
        self.x = x
        self.y = y
        self.z = z
        
    def length(self):
        return np.sqrt(self.x**2 + self.y**2 + self.z**2)

Now, I have a way to get the correct length of an instance of this class at all times by calling the instance's length() method:现在,我有办法通过调用实例的length()方法随时获取此 class 实例的正确长度:

p1 = PositionVector(x = 10, y = 0, z = 0)

print (f"p1's length is {p1.length()}")
# Result -> p1's length is 10.0

p1.y = 10.0
print (f"p1's 'y' value is {p1.y}")
# Result -> p1's 'y' value is 10.0

print (f"p1's length is {p1.length()}")
# Result -> p1's length is 14.142135623730951

This is fine, except for two issues:这很好,除了两个问题:

  1. If this class had been in use already, going back and changing length from an attribute to a method would break backward compatibility, forcing any other code that uses this class to need modifications before it could work as before.如果此 class 已被使用,返回并将length从属性更改为方法将破坏向后兼容性,迫使使用此 class 的任何其他代码需要修改才能像以前一样工作。

  2. Though I DO want length to recalculate every time I invoke it, I want to be able to pick it up and "handle" it like it's a "property" of the instance, not a "behaviour" of the instance.尽管我确实希望每次调用它时都重新计算length ,但我希望能够将其拾取并“处理”,就像它是实例的“属性”,而不是实例的“行为”一样。 So using p1.length() to get the instance's length instead of simply p1.length feels unidiomatic.因此,使用p1.length()而不是简单地p1.length来获取实例的长度感觉很不合常理。

I can restore backward compatibility, AND permit length to be accessed like any other attribute by applying the @property decorator to the method.我可以恢复向后兼容性,并允许通过将@property装饰器应用于方法来像访问任何其他属性一样访问length Simply adding @property to the length() method definition allows its call signature to go back to its original form:只需将@property添加到length()方法定义中,即可将其调用签名 go 恢复为原始形式:

    @property
    def length(self):
        return np.sqrt(self.x**2 + self.y**2 + self.z**2)

p1 = PositionVector(x=10, y=0, z=0)

print(f"p1's length is {p1.length}")
# Result -> p1's length is 10.0

p1.y = 10.0
print(f"p1's 'y' value is {p1.y}")
# Result -> p1's 'y' value is 10.0

print(f"p1's length is {p1.length}")
# Result -> p1's length is 14.142135623730951

At this point, there are no shadowed or "underscored" attribute names, I don't need them - I can access x , y and z normally, and access length as though it were any other attribute, and yet be confident that anytime I call it, I get the most current value, correctly reflective of the current values of x , y , and z .此时,没有阴影或“带下划线”的属性名称,我不需要它们 - 我可以正常访问xyz ,并像访问任何其他属性一样访问length ,但我确信我随时调用它,我得到最新的值,正确反映了xyz的当前值。 Calling dict on p1 in this state yields:在此 state 中对 p1 调用dict会产生:

print(p1.__dict__)
# Result -> {'x': 10, 'y': 10.0, 'z': 0}

There could be use cases where you want to not only calculate length , but also save its value as a static attribute of an instance.在某些用例中,您不仅要计算length ,还要将其值保存为实例的 static 属性。 This is where you might want to create an attribute and have it hold the value of length every time its calculated.这是您可能想要创建一个属性并让它在每次计算时都保存length值的地方。 You'd accomplish this like so:你会像这样完成这个:

class PositionVector:
    def __init__(self, x: float, y: float, z: float) -> None:
        self.x = x
        self.y = y
        self.z = z
        self.placeholder_attribute_name = None

    @property
    def length(self):
        self.placeholder_attribute_name = np.sqrt(self.x**2 + self.y**2 + self.z**2)
        return self.placeholder_attribute_name

Doing this has no effect whatsoever on the prior functioning of the class. It simply creates a way to statically hold the value of length, independent of the act of creating it.这样做对 class 的先前功能没有任何影响。它只是创建了一种静态保存长度值的方法,与创建它的行为无关。

You don't HAVE to name that attribute anything in particular.您不必特别为该属性命名。 You can name it anything you want, except for any other name already in use.除了已经使用的任何其他名称外,您可以随意命名。 In the case above, you can't name it x , y , z , or length , because all of those have other meanings.在上面的例子中,您不能将其命名为xyzlength ,因为所有这些都有其他含义。

For readability, however, it does make sense, and it's common practice, to do the following two things:然而,为了可读性,做以下两件事确实有意义,而且这是常见的做法:

  1. Make it obvious that this attribute is not meant to be used directly.明确表示不能直接使用此属性。 In the case above - you don't want someone to get the length of the vector by calling p1.placeholder_attribute_name because this is not guaranteed to yield the correct current length - they should use p1.length instead.在上面的例子中——你不希望有人通过调用p1.placeholder_attribute_name来获取向量的长度,因为这不能保证产生正确的当前长度——他们应该改用p1.length You indicate that this attribute is not for public consumption with a commonly adopted Python convention - the leading underscore:您使用普遍采用的 Python 约定表明此属性不供公众使用 - 前导下划线:
    class PositionVector:
        def __init__(self, x: float, y: float, z: float) -> None:
            self.x = x
            self.y = y
            self.z = z
            self._placeholder_attribute_name = None
    
        @property
        def length(self):
            self._placeholder_attribute_name = np.sqrt(self.x**2 + self.y**2 + self.z**2)
            return self._placeholder_attribute_name

  1. Use the name of the attribute to convey to anyone reading your code what the attribute actually means.使用属性名称向任何阅读您的代码的人传达该属性的实际含义。 If the attribute is meant to shadow the " length " property - putting length in there somewhere instead of the less helpful placeholder_attribute_name would enhance readability.如果该属性旨在隐藏“ length ”属性 - 将length放在某处而不是不太有用的placeholder_attribute_name将提高可读性。 You could indicate that this shadows length by naming it _length .您可以通过将其命名为_length来指示此阴影length

In summary:总之:

  • Employing the @property decorator does not compel you to use "public" and "private" attribute names - you would only do so if, besides computing your attribute's value with the @property decorated method, you also wanted to save the value of that method's return in a persistent attribute bound to every instance of the class.使用@property装饰器不会强制您使用“公共”和“私有”属性名称——只有在除了使用@property装饰方法计算您的属性值之外,您还想保存该方法的值时,您才会这样做返回绑定到 class 的每个实例的持久属性。
  • Even when you DO choose to use propertyname in public and _propertyname in private this is not an absolute rule, it is simply a convention adopted in aid of readability.即使您确实选择在公共中使用propertyname而在私有中使用_propertyname ,这也不是绝对规则,它只是为了提高可读性而采用的约定。

Thanks to Vin for a nice detailed description of property but it doesn't really answer my question - which could have been worded much more clearly.感谢 Vin 对property进行了很好的详细描述,但它并没有真正回答我的问题 - 本来可以更清楚地措辞。 It shows my confusion.它显示了我的困惑。

The fundamental reason for the recursion in setget but not PropertyHelperDemo is that the property methods in setget invoke themselves while the methods in PropertyHelperDemo access the instance __dict__ directly as such: setget递归而不是PropertyHelperDemo的根本原因是setget中的属性方法调用自身,而PropertyHelperDemo中的方法直接访问实例__dict__ ,如下所示:

def _get(self):
        print(f'getting {k}')
        return self.__dict__.__getitem__(k)

This seems rather obvious now.现在这似乎很明显。 It is apparent that conflicting property and __dict__ attribute names are not prevented and that the resolution order is to look for properties before __dict__ entries.很明显,不会阻止冲突的property__dict__属性名称,并且解决顺序是在__dict__条目之前查找属性。

In other experiments I've found that it's possible to replace an instance method by making an entry of the same name in __dict__ .在其他实验中,我发现可以通过在__dict__中创建同名条目来替换实例方法。 So the overall resolution sequence remains less than clear (to me.)所以整体的解决顺序仍然不太清楚(对我来说)。

Another source of confusion for me is that dir returns a list of names of methods plus __dict__ entries and other attributes, and apparently eliminates duplicates.另一个让我感到困惑的来源是dir返回一个方法名称列表加上__dict__条目和其他属性,并且显然消除了重复项。 From the doc :来自文档

If the object does not provide dir (), the function tries its best to gather information from the object’s dict attribute, if defined, and from its type object. The resulting list is not necessarily complete, and may be inaccurate when the object has a custom getattr ().如果 object 没有提供dir (),function 会尽力从对象的dict属性(如果已定义)和它的类型 object 中收集信息。结果列表不一定完整,并且当 object 有一个自定义getattr ()。

... If the object is a type or class object, the list contains the names of its attributes, and recursively of the attributes of its bases. ...如果 object 是一个类型或 class object,则该列表包含其属性的名称,并递归地包含其基属性的名称。

... The resulting list is sorted alphabetically. ... 结果列表按字母顺序排序。

Interestingly, properties appear in the class __dict__ but not in the instance __dict__ .有趣的是,属性出现在 class __dict__中,但没有出现在实例__dict__中。

Another way to explore is using inspect which does not eliminate duplicates.另一种探索方法是使用inspect ,它不会消除重复项。

>>> p = PropertyHelperDemo(1, 2, 3)
setting X
setting Y
setting Z
>>> 
>>> import inspect
>>> pprint(inspect.getmembers(p))
getting X
getting Y
getting Z
getting X
getting Y
getting Z
[('Total', 6),
 ('X', 1),
 ('Y', 2),
 ('Z', 3),
 ('__annotations__',
  {'X': <class 'float'>, 'Y': <class 'float'>, 'Z': <class 'float'>}),
 ('__class__', <class '__main__.PropertyHelperDemo'>),
 ('__delattr__',
  <method-wrapper '__delattr__' of PropertyHelperDemo object at 0x00000181D14C6608>),
 ('__dict__', {'X': 1, 'Y': 2, 'Z': 3}),
 ('__dir__',
  <built-in method __dir__ of PropertyHelperDemo object at 0x00000181D14C6608>),
...
...
...
>>> pprint(inspect.getmembers(p, predicate=inspect.ismethod))
getting X
getting Y
getting Z
getting X
getting Y
getting Z
[('__init__',
  <bound method PropertyHelperDemo.__init__ of <__main__.PropertyHelperDemo object at 0x00000181D14C6608>>),
 ('prop_helper',
  <bound method PropertyHelperDemo.prop_helper of <__main__.PropertyHelperDemo object at 0x00000181D14C6608>>)]
>>> 

In the first listing we can see the property methods as well as the __dict__ attributes.在第一个清单中,我们可以看到property方法以及__dict__属性。 It's interesting (to me) that the property methods are executed by inspect .有趣的是(对我来说) property方法是由inspect执行的。 We see methods X, Y, Z executed twice because Total also calls them.我们看到方法X, Y, Z执行了两次,因为Total也调用了它们。 Properties X, Y, Z and Total are not listed when we filter for methods.当我们筛选方法时,属性X, Y, ZTotal未列出。

Of course it's a great idea to re-use names like this only if you want to drive yourself and everyone else crazy.当然,只有当你想让自己和其他人都发疯时,才重复使用这样的名字是个好主意。

Enough omphaloskepsis, it's time to move on.足够的 omphaloskepsis,是时候继续前进了。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM