个性化阅读
专注于IT技术分析

Python类属性:相当详尽的指南

本文概述

我最近接受了编程采访, 一个电话屏幕上, 我们使用了协作文本编辑器。

我被要求实现某个API, 并选择使用Python来实现。摘录问题陈述, 假设我需要一个其实例存储一些数据和一些other_data的类。

我深吸了一口气, 开始打字。几行之后, 我有如下内容:

class Service(object):
    data = []

    def __init__(self, other_data):
        self.other_data = other_data
    ...

我的面试官阻止了我:

  • 采访者:”那条线:数据= []。我认为这不是有效的Python吗?”
  • 我:”我很确定。只是为实例属性设置默认值。”
  • 采访者:”该代码何时执行?”
  • 我:”我不确定。我将对其进行修复以避免混淆。”

供参考, 并让你了解我要做什么, 以下是我修改代码的方式:

class Service(object):

    def __init__(self, other_data):
        self.data = []
        self.other_data = other_data
    ...

事实证明, 我们俩都是错的。真正的答案在于了解Python类属性和Python实例属性之间的区别。

Python类属性与Python实例属性

注意:如果你对类属性有熟练的技巧, 则可以跳过用例。

Python类属性

我的面试官是错误的, 因为上面的代码在语法上是有效的。

我也错了, 因为它没有为实例属性设置”默认值”。而是将数据定义为具有[[]]值的类属性。

以我的经验, Python类属性是很多人都知道的话题, 但很少有人完全理解。

Python类变量与实例变量:有什么区别?

Python类属性是类的属性(我知道是圆形的), 而不是类实例的属性。

让我们用一个Python类示例来说明差异。在这里, class_var是一个类属性, 而i_var是一个实例属性:

class MyClass(object):
    class_var = 1

    def __init__(self, i_var):
        self.i_var = i_var

请注意, 该类的所有实例都可以访问class_var, 并且也可以将其作为类本身的属性来访问:

foo = MyClass(2)
bar = MyClass(3)

foo.class_var, foo.i_var
## 1, 2
bar.class_var, bar.i_var
## 1, 3
MyClass.class_var ## <— This is key
## 1

对于Java或C ++程序员, class属性与静态成员相似但不相同。稍后我们将介绍它们的不同之处。

类与实例命名空间

要了解此处发生的情况, 让我们简要地谈谈Python名称空间。

名称空间是从名称到对象的映射, 其属性是不同名称空间中名称之间的关系为零。它们通常被实现为Python字典, 尽管这是抽象的。

根据上下文的不同, 你可能需要使用点语法(例如object.name_from_objects_namespace)或作为局部变量(例如object_from_namespace)来访问名称空间。作为一个具体的例子:

class MyClass(object):
    ## No need for dot syntax
    class_var = 1

    def __init__(self, i_var):
        self.i_var = i_var

## Need dot syntax as we've left scope of class namespace
MyClass.class_var
## 1

Python类和类实例各自具有各自不同的命名空间, 分别由预定义属性MyClass .__ dict__和instance_of_MyClass .__ dict__表示。

当你尝试从类的实例访问属性时, 它首先查看其实例名称空间。如果找到该属性, 则返回关联的值。如果没有, 它将在类名称空间中查找并返回属性(如果存在的话, 否则抛出错误)。例如:

foo = MyClass(2)

## Finds i_var in foo's instance namespace
foo.i_var
## 2

## Doesn't find class_var in instance namespace…
## So look's in class namespace (MyClass.__dict__)
foo.class_var
## 1

实例名称空间优先于类名称空间:如果两个名称空间中都具有相同的名称, 则将首先检查该实例名称空间并返回其值。这是用于属性查找的代码(源代码)的简化版本:

def instlookup(inst, name):
    ## simplified algorithm...
    if inst.__dict__.has_key(name):
        return inst.__dict__[name]
    else:
        return inst.__class__.__dict__[name]

并且, 以视觉形式:

可视形式的属性查找

类属性如何处理分配

考虑到这一点, 我们可以理解Python类属性如何处理分配:

如果通过访问该类来设置类属性, 则它将覆盖所有实例的值。例如:

foo = MyClass(2)
foo.class_var
## 1
MyClass.class_var = 2
foo.class_var
## 2

在命名空间级别上, 我们正在设置MyClass .__ dict __ [‘class_var’] =2。(注意:这不是确切的代码(应为setattr(MyClass, ‘class_var’, 2)), 因为__dict__返回dictproxy , 这是一个固定的包装器, 可防止直接分配, 但有助于演示)。然后, 当我们访问foo.class_var时, class_var在类名称空间中具有新值, 因此返回2。

如果通过访问实例设置了Paython类变量, 则它将仅覆盖该实例的值。从本质上讲, 这将覆盖类变量, 并将其转变为仅可用于该实例的直观直观的实例变量。例如:

foo = MyClass(2)
foo.class_var
## 1
foo.class_var = 2
foo.class_var
## 2
MyClass.class_var
## 1

在名称空间级别上…我们将class_var属性添加到foo .__ dict__, 因此当我们查找foo.class_var时, 我们返回2。同时, MyClass的其他实例在其实例名称空间中将没有class_var, 因此它们继续查找class_var。在MyClass .__ dict__中返回1。

变异性

测验问题:如果你的class属性具有可变类型怎么办?你可以通过在特定实例中访问类属性来操纵(残废?)类属性, 然后最终操纵所有实例正在访问的引用对象(蒂莫西·怀斯曼指出)。

这是最好的例子。让我们回到我之前定义的服务, 看看我对类变量的使用如何可能导致问题。

class Service(object):
    data = []

    def __init__(self, other_data):
        self.other_data = other_data
    ...

我的目标是将空列表([])作为数据的默认值, 并使Service的每个实例具有自己的数据, 这些数据将随实例的不同而随时间变化。但是在这种情况下, 我们得到以下行为(回想一下, Service带有一些参数other_data, 在此示例中为任意值):

s1 = Service(['a', 'b'])
s2 = Service(['c', 'd'])

s1.data.append(1)

s1.data
## [1]
s2.data
## [1]

s2.data.append(2)

s1.data
## [1, 2]
s2.data
## [1, 2]

这是不好的-通过一个实例更改class变量会更改所有其他实例的变量!

在名称空间级别上, 所有Service实例都在访问和修改Service .__ dict__中的相同列表, 而没有在其实例名称空间中创建自己的数据属性。

我们可以使用赋值来解决这个问题;也就是说, 除了利用列表的可变性, 我们还可以将Service对象分配为具有自己的列表, 如下所示:

s1 = Service(['a', 'b'])
s2 = Service(['c', 'd'])

s1.data = [1]
s2.data = [2]

s1.data
## [1]
s2.data
## [2]

在这种情况下, 我们要添加s1 .__ dict __ [‘data’] = [1], 因此原始Service .__ dict __ [‘data’]保持不变。

不幸的是, 这要求服务用户对它的变量有深入的了解, 并且肯定容易出错。从某种意义上说, 我们要解决的是症状而不是原因。我们希望从构造上讲是正确的。

我个人的解决方案:如果你仅使用类变量将默认值分配给可能的Python实例变量, 请不要使用可变值。在这种情况下, 每个Service实例最终都将使用其自己的instance属性覆盖Service.data, 因此使用空列表作为默认值会导致一个容易被忽略的小错误。除了上述内容, 我们还可以:

如导言所述, 完全陷入实例属性。

避免将空列表(可变值)用作我们的”默认值”:

class Service(object):
    data = None

    def __init__(self, other_data):
        self.other_data = other_data
    ...

当然, 我们必须适当地处理None案件, 但这是一个很小的代价。

那么什么时候应该使用Python类属性呢?

类属性比较棘手, 但让我们看一下它们何时会派上用场的几种情况:

存储常数。由于可以将类属性作为类本身的属性来访问, 因此使用它们存储类范围的特定于类的常量通常会很不错。例如:

class Circle(object):
    pi = 3.14159

    def __init__(self, radius):
        self.radius = radius

    def area(self):
        return Circle.pi * self.radius * self.radius

Circle.pi
## 3.14159

c = Circle(10)
c.pi
## 3.14159
c.area()
## 314.159

定义默认值。举一个简单的例子, 我们可以创建一个有界列表(即只能容纳一定数量或更少数量元素的列表), 并选择默认上限为10个项目:

class MyClass(object):
    limit = 10

    def __init__(self):
        self.data = []

    def item(self, i):
        return self.data[i]

    def add(self, e):
        if len(self.data) >= self.limit:
            raise Exception("Too many elements")
        self.data.append(e)

MyClass.limit
## 10

然后, 我们也可以通过分配实例的limit属性来创建具有自己特定限制的实例。

foo = MyClass()
foo.limit = 50
## foo can now hold 50 elements—other instances can hold 10

仅当你希望MyClass的典型实例仅包含10个或更少的元素时才有意义-如果你为所有实例赋予不同的限制, 则limit应该是一个实例变量。 (不过请记住:使用可变值作为默认值时要小心。)

跟踪给定类的所有实例中的所有数据。这是一种特定的情况, 但是我可以看到一种情况, 在这种情况下, 你可能希望访问与给定类的每个现有实例相关的数据。

为了使情况更具体, 假设我们有一个Person类, 每个人都有一个名字。我们要跟踪已使用的所有名称。一种方法可能是遍历垃圾收集器的对象列表, 但是使用类变量更简单。

请注意, 在这种情况下, 只能将名称作为类变量进行访问, 因此可变的默认设置是可以接受的。

class Person(object):
    all_names = []

    def __init__(self, name):
        self.name = name
        Person.all_names.append(name)

joe = Person('Joe')
bob = Person('Bob')
print Person.all_names
## ['Joe', 'Bob']

我们甚至可以使用这种设计模式来跟踪给定类的所有现有实例, 而不仅仅是某些关联数据。

class Person(object):
    all_people = []

    def __init__(self, name):
        self.name = name
        Person.all_people.append(self)

joe = Person('Joe')
bob = Person('Bob')
print Person.all_people
## [<__main__.Person object at 0x10e428c50>, <__main__.Person object at 0x10e428c90>]

性能(有点…见下文)。

相关:srcmini开发人员的Python最佳实践和技巧

引擎盖下

注意:如果你担心此级别的性能, 则可能不希望一开始就使用Python, 因为两者之间的差异大约是十分之一毫秒, 但是拨开一点还是很有趣的, 并为插图提供帮助。

回想一下, 在定义类时已创建并填写了一个类的名称空间。这意味着我们永远只对给定的类变量进行一次分配, 而每次创建新实例时都必须分配实例变量。让我们举个例子。

def called_class():
    print "Class assignment"
    return 2

class Bar(object):
    y = called_class()

    def __init__(self, x):
        self.x = x

## "Class assignment"

def called_instance():
    print "Instance assignment"
    return 2

class Foo(object):
    def __init__(self, x):
        self.y = called_instance()
        self.x = x

Bar(1)
Bar(2)
Foo(1)
## "Instance assignment"
Foo(2)
## "Instance assignment"

我们只分配一次给Bar.y, 但是每次调用__init__时都分配给instance_of_Foo.y。

作为进一步的证据, 让我们使用Python反汇编程序:

import dis

class Bar(object):
    y = 2

    def __init__(self, x):
        self.x = x

class Foo(object):
    def __init__(self, x):
        self.y = 2
        self.x = x

dis.dis(Bar)
##  Disassembly of __init__:
##  7           0 LOAD_FAST                1 (x)
##              3 LOAD_FAST                0 (self)
##              6 STORE_ATTR               0 (x)
##              9 LOAD_CONST               0 (None)
##             12 RETURN_VALUE

dis.dis(Foo)
## Disassembly of __init__:
## 11           0 LOAD_CONST               1 (2)
##              3 LOAD_FAST                0 (self)
##              6 STORE_ATTR               0 (y)

## 12           9 LOAD_FAST                1 (x)
##             12 LOAD_FAST                0 (self)
##             15 STORE_ATTR               1 (x)
##             18 LOAD_CONST               0 (None)
##             21 RETURN_VALUE

当我们查看字节码时, 很明显Foo .__ init__必须执行两次分配, 而Bar .__ init__仅执行一次分配。

实际上, 这种收益实际上是什么样的?我将是第一个承认计时测试高度依赖于通常无法控制的因素, 并且它们之间的差异通常很难准确解释。

但是, 我认为这些小片段(与Python timeit模块一起运行)有助于说明类变量和实例变量之间的差异, 因此无论如何我都将它们包括在内。

注意:我使用的是OS X 10.8.5和Python 2.7.2的MacBook Pro。

初始化

10000000 calls to `Bar(2)`: 4.940s
10000000 calls to `Foo(2)`: 6.043s

Bar的初始化速度快一秒以上, 因此此处的差异确实在统计上显着。

那么为什么会这样呢?一种推测性的解释:我们在Foo .__ init__中执行两项任务, 而在Bar .__ init__中仅执行一项任务。

分配

10000000 calls to `Bar(2).y = 15`: 6.232s
10000000 calls to `Foo(2).y = 15`: 6.855s
10000000 `Bar` assignments: 6.232s - 4.940s = 1.292s
10000000 `Foo` assignments: 6.855s - 6.043s = 0.812s

注意:无法在每次使用timeit的时间上重新运行你的设置代码, 因此我们必须在我们的试用版上重新初始化变量。第二行时间代表上述时间, 其中减去了先前计算的初始化时间。

从上面可以看出, Foo只需花费Bar大约60%的时间即可处理任务。

为什么会这样呢?一个推测性的解释:当我们分配给Bar(2).y时, 我们首先查看实例名称空间(Bar(2).__ dict __ [y]), 找不到y, 然后查看类名称空间(Bar .__ dict__ [y]), 然后进行适当的分配。当我们分配给Foo(2).y时, 我们进行的查找次数是立即分配给实例命名空间(Foo(2).__ dict __ [y])的一半。

总而言之, 尽管这些性能提升实际上并不重要, 但这些测试在概念上还是很有趣的。如果有的话, 我希望这些差异有助于说明类变量和实例变量之间的机械区别。

结论

类属性似乎在Python中没有得到充分利用。许多程序员对他们的工作方式以及为什么会有所帮助有不同的印象。

我的观点:Python类变量在良好代码学院中占有一席之地。当谨慎使用时, 它们可以简化事情并提高可读性。但是, 如果不小心丢进了给定的班级, 他们肯定会让你绊倒。

附录:私有实例变量

我想包含的一件事, 但没有自然的入口点…

Python没有可以说的私有变量, 但是类和实例命名之间的另一个有趣的关系是名称修饰。

在Python样式指南中, 有人说伪私有变量应该以双下划线作为前缀:” __”。这不仅向他人表明你的变量将被私下对待, 而且还是一种防止对其进行访问的方式。这是我的意思:

class Bar(object):
    def __init__(self):
    self.__zap = 1

a = Bar()
a.__zap
## Traceback (most recent call last):
##   File "<stdin>", line 1, in <module>
## AttributeError: 'Bar' object has no attribute '__baz'

## Hmm. So what's in the namespace?
a.__dict__
{'_Bar__zap': 1}
a._Bar__zap
## 1

看一下:实例属性__zap自动带有类名前缀以产生_Bar__zap。

尽管仍可以使用a._Bar__zap进行设置和获取, 但此名称修饰是创建”私有”变量的一种方式, 因为它可以防止你和其他人偶然或无知地访问它。

编辑:正如Pedro Werneck所指出的那样, 此行为主要是为了帮助子类化。在PEP 8样式指南中, 他们认为这样做有两个目的:(1)防止子类访问某些属性, 以及(2)防止这些子类中的名称空间冲突。变量整改虽然有用, 但不应被视为邀请编写具有假定的公共-私人区别的代码, 例如Java中的邀请。

相关:变得更高级:避免Python程序员犯的10个最常见的错误

赞(0)
未经允许不得转载:srcmini » Python类属性:相当详尽的指南

评论 抢沙发

评论前必须登录!