属性、描述符与__getattribute__的用途场景

19 投票
2 回答
3785 浏览
提问于 2025-04-17 23:41

这个问题主要是想知道在什么情况下使用哪种方式更好,而不是讨论技术背景。

在Python中,你可以通过属性描述符或者魔法方法来控制属性的访问。哪种方式在特定情况下更符合Python的风格呢?它们似乎都能达到相似的效果(下面有例子)。

我希望得到这样的回答:

  • 属性:在……的情况下应该使用。
  • 描述符:在……的情况下应该用描述符而不是属性。
  • 魔法方法:只有在……的情况下才使用。

例子

一个使用场景是,有一个属性可能在__init__方法中无法设置,比如因为对象还没有在数据库中存在,但在之后的某个时候可以设置。每次访问这个属性时,都应该尝试设置并返回它。

举个例子,有一个类希望在第二次请求这个属性时才展示它。那么,哪种方式最好,或者在不同情况下哪种方式更合适呢?以下是三种实现方式:

使用属性::

class ContactBook(object):
    intents = 0

    def __init__(self):
        self.__first_person = None

    def get_first_person(self):
        ContactBook.intents += 1
        if self.__first_person is None:
            if ContactBook.intents > 1:
                value = 'Mr. First'
                self.__first_person = value
            else:
                return None
        return self.__first_person

    def set_first_person(self, value):
        self.__first_person = value

    first_person = property(get_first_person, set_first_person)

使用__getattribute__::

class ContactBook(object):
    intents = 0

    def __init__(self):
        self.first_person = None

    def __getattribute__(self, name):
        if name == 'first_person' \
                and object.__getattribute__(self, name) is None:
            ContactBook.intents += 1
            if ContactBook.intents > 1:
                value = 'Mr. First'
                self.first_person = value
            else:
                value = None
        else:
            value = object.__getattribute__(self, name)
        return value

描述符::

class FirstPerson(object):
    def __init__(self, value=None):
        self.value = None

    def __get__(self, instance, owner):
        if self.value is None:
            ContactBook.intents += 1
            if ContactBook.intents > 1:
                self.value = 'Mr. First'
            else:
                return None
        return self.value


class ContactBook(object):
    intents = 0
    first_person = FirstPerson()

每种方式都有这样的行为::

book = ContactBook()
print(book.first_person)
# >>None
print(book.first_person)
# >>Mr. First

2 个回答

3

__getattribute__ 是一个特殊的功能,它让 property(还有其他描述符)能够正常工作。每当你访问一个对象的属性时,这个功能都会被调用。可以把它看作是一个比较底层的接口,当 property 或者自定义描述符不能满足你的需求时,就可以用这个。

当你需要动态属性且名字是固定的时候,使用 property;而如果属性的性质更动态,比如一系列属性与某些值之间有算法关系,那么就用 __getattr__

描述符的用途在于你需要把一些任意的对象绑定到一个实例上。比如说,当你需要用更复杂的东西来替代方法对象时;最近的一个例子是一个 基于类的装饰器,它需要支持方法对象上的额外属性和方法。一般来说,如果你还在考虑简单的属性,就不需要用到描述符。

23

简单来说,尽量使用最简单的方法。大致上,复杂程度的顺序是:普通属性、property__getattr____getattribute__/描述符。(__getattribute__和自定义描述符通常不需要经常使用。) 这就形成了一些简单的经验法则:

  • 如果普通属性可以用,就不要用property
  • 如果property可以用,就不要自己写描述符。
  • 如果property可以用,就不要用__getattr__
  • 如果__getattr__可以用,就不要用__getattribute__

更具体地说:当你想定制处理一个或一小部分属性时,使用property;当你想定制处理所有属性,或者除了少数几个属性外的所有属性时,使用__getattr__;如果你希望使用__getattr__但它不太适用,就使用__getattribute__;如果你在做一些非常复杂的事情,就写自己的描述符类。

当你有一个或一小部分属性需要特别处理时,就使用property。也就是说,你希望像obj.propobj.prop = 2这样的操作背后,调用你写的函数来定制发生的事情。

当你想对很多属性进行这样的处理,而不想一个一个定义时,就使用__getattr__。换句话说,你希望能够处理obj.<任何属性名>,而不是单独处理obj.prop1obj.prop2等。

不过,__getattr__并不能让你覆盖那些确实存在的属性的行为,它只是让你能对那些本来会引发AttributeError的属性进行统一处理。使用__getattribute__可以让你处理所有属性,甚至是那些本来不需要干预的普通属性。由于这个原因,使用__getattribute__可能会破坏一些基本行为,所以只有在考虑过使用__getattr__但不够时,才应该使用它。它也可能会对性能产生明显影响。例如,如果你在包装一个定义了一些属性的类,并且希望以自定义的方式包装这些属性,以便在某些情况下正常工作,而在其他情况下则有自定义行为,你可能需要使用__getattribute__

最后,我想说,编写自己的描述符是一项相对高级的任务。property就是一个描述符,在大约95%的情况下,它是你唯一需要的。一个简单的例子是,如果你需要写多个具有相似行为的property,那么写一个描述符可以让你提取出共同的行为,避免代码重复。自定义描述符在像Django和SQLAlchemy这样的系统中被使用。如果你发现自己在写这种复杂度的东西,可能需要写一个自定义描述符。

在你的例子中,property是最佳选择。如果你在__getattribute__中写if name == 'somespecificname',通常(但不是总是)是个红旗。如果你只需要特别处理一个特定的名字,可能不需要使用__getattribute__。同样,如果你在__get__中写的内容只是你可以在property的getter方法中写的,那就没有必要自己写描述符。

撰写回答