Python类属性(相当详尽的指南)

本文概述

  • Python类属性
  • 那么什么时候应该使用Python类属性呢?
  • 引擎盖下
  • 结论
  • 附录:私有实例变量
我最近接受了编程采访, 一个电话屏幕上, 我们使用了协作文本编辑器。
我被要求实现某个API, 并选择使用Python来实现。摘录问题陈述, 假设我需要一个其实例存储一些数据和一些other_data的类。
我深吸了一口气, 开始打字。几行之后, 我有如下内容:
class Service(object): data = http://www.srcmini.com/[]def __init__(self, other_data): self.other_data = other_data ...

我的面试官阻止了我:
  • 采访者:” 那条线:数据= []。我认为这不是有效的Python吗?”
  • 我:” 我很确定。只是为实例属性设置默认值。”
  • 采访者:” 该代码何时执行?”
  • 我:” 我不确定。我将对其进行修复以避免混淆。”
供参考, 并让你了解我要做什么, 以下是我修改代码的方式:
class Service(object):def __init__(self, other_data): self.data = http://www.srcmini.com/[] self.other_data = other_data ...

事实证明, 我们俩都是错的。真正的答案在于了解Python类属性和Python实例属性之间的区别。
Python类属性(相当详尽的指南)

文章图片
注意:如果你对类属性有熟练的技巧, 则可以跳过用例。
Python类属性 我的面试官是错误的, 因为上面的代码在语法上是有效的。
我也错了, 因为它没有为实例属性设置” 默认值” 。而是将数据定义为具有[[]]值的类属性。
以我的经验, Python类属性是很多人都知道的话题, 但很少有人完全理解。
Python类变量与实例变量:有什么区别?
Python类属性是类的属性(我知道是圆形的), 而不是类实例的属性。
让我们用一个Python类示例来说明差异。在这里, class_var是一个类属性, 而i_var是一个实例属性:
class MyClass(object): class_var = 1def __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 = 1def __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类属性(相当详尽的指南)

文章图片
类属性如何处理分配
考虑到这一点, 我们可以理解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 = http://www.srcmini.com/[]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 = http://www.srcmini.com/[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 = http://www.srcmini.com/Nonedef __init__(self, other_data): self.other_data = other_data ...

当然, 我们必须适当地处理None案件, 但这是一个很小的代价。
那么什么时候应该使用Python类属性呢? 类属性比较棘手, 但让我们看一下它们何时会派上用场的几种情况:
存储常数。由于可以将类属性作为类本身的属性来访问, 因此使用它们存储类范围的特定于类的常量通常会很不错。例如:
class Circle(object): pi = 3.14159def __init__(self, radius): self.radius = radiusdef area(self): return Circle.pi * self.radius * self.radiusCircle.pi ## 3.14159c = Circle(10) c.pi ## 3.14159 c.area() ## 314.159

定义默认值。举一个简单的例子, 我们可以创建一个有界列表(即只能容纳一定数量或更少数量元素的列表), 并选择默认上限为10个项目:
class MyClass(object): limit = 10def __init__(self): self.data = http://www.srcmini.com/[]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 2class Bar(object): y = called_class()def __init__(self, x): self.x = x## "Class assignment"def called_instance(): print "Instance assignment" return 2class Foo(object): def __init__(self, x): self.y = called_instance() self.x = xBar(1) Bar(2) Foo(1) ## "Instance assignment" Foo(2) ## "Instance assignment"

我们只分配一次给Bar.y, 但是每次调用__init__时都分配给instance_of_Foo.y。
作为进一步的证据, 让我们使用Python反汇编程序:
import disclass Bar(object): y = 2def __init__(self, x): self.x = xclass Foo(object): def __init__(self, x): self.y = 2 self.x = xdis.dis(Bar) ##Disassembly of __init__: ##70 LOAD_FAST1 (x) ##3 LOAD_FAST0 (self) ##6 STORE_ATTR0 (x) ##9 LOAD_CONST0 (None) ##12 RETURN_VALUEdis.dis(Foo) ## Disassembly of __init__: ## 110 LOAD_CONST1 (2) ##3 LOAD_FAST0 (self) ##6 STORE_ATTR0 (y)## 129 LOAD_FAST1 (x) ##12 LOAD_FAST0 (self) ##15 STORE_ATTR1 (x) ##18 LOAD_CONST0 (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 = 1a = 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类属性(相当详尽的指南)】相关:变得更高级:避免Python程序员犯的10个最常见的错误

    推荐阅读