本文概述
我最近接受了编程采访, 一个电话屏幕上, 我们使用了协作文本编辑器。
我被要求实现某个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类示例来说明差异。在这里, 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个最常见的错误
评论前必须登录!
注册