Python中的self变量与类变量

5
请你解释一下下面这个类中的周长变量。
我知道self.vertices是指特定实例。由于perimeter没有使用self定义,这是否意味着它是一个类变量?那么它是否适用于所有实例?
将周长定义为self.perimeter不是更好的方式吗?这样就可以适当地对每个实例进行声明了。
这段代码来自一本书。

Polygon.py

import math
class Point:
    def __init__(self, x, y):
        self.x = x
        self.y = y
    def distance(self, p2):
        return math.sqrt((self.x-p2.x)**2 + (self.y-p2.y)**2)

class Polygon:
    def __init__(self):
        self.vertices = []
    def add_point(self, point):
        self.vertices.append((point))
    def perimeter(self):
        perimeter = 0
        points = self.vertices + [self.vertices[0]]
        for i in range(len(self.vertices)):
            perimeter += points[i].distance(points[i+1])
        return perimeter

>>> square = Polygon() 
>>> square.add_point(Point(1,1)) 
>>> square.add_point(Point(1,2)) 
>>> square.add_point(Point(2,2)) 
>>> square.add_point(Point(2,1)) 
>>> square.perimeter() 
4.0 

新类型

import math
class Point:
    def __init__(self, x, y):
        self.x = x
        self.y = y
    def distance(self, p2):
        return math.sqrt((self.x-p2.x)**2 + (self.y-p2.y)**2)

class Polygon:
        def __init__(self):
            self.vertices = []
        def add_point(self, point):
            self.vertices.append((point))
        def perimetermethod(self):
            self.perimeter = 0
            points = self.vertices + [self.vertices[0]]
            for i in range(len(self.vertices)):
                self.perimeter += points[i].distance(points[i+1])
            return self.perimeter

if __name__=='__main__':
    p1 = Polygon()
    p1.add_point(Point(1,1))
    p1.add_point(Point(1,2))
    p1.add_point(Point(2,2))
    p1.add_point(Point(2,1))
    print(p1.perimetermethod())
3个回答

11
使用some_name = ...来定义一个新变量时,它会在最近的封闭作用域内创建这个变量(除非使用了globalnonlocal关键字,但在此并不相关)。在对象上定义一个新属性名会在该对象上创建该属性。

因此,self.foo = 1会在当前由self引用的对象上分配一个名为foo的属性。通常,名称self用作方法的第一个参数,该方法接收调用该方法的对象。因此,“定义一个带有self的变量”并没有什么特别之处;这只是关于在现有对象上分配属性的普通规则。任何存在于实例对象本身中的属性显然必须特定于该实例。

Polygon类中的perimeter = 0在最接近的封闭作用域perimeter方法中创建一个变量。因此它创建了一个局部变量。局部变量仅在函数调用期间存在,因此它既不是类变量也不是实例变量。您只能在特定方法的作用域内访问它(每次调用时都具有新的完全独立的值),因此它不能对所有实例共享。但是,您也不能在每个特定实例上访问不同的值,因此它也不是实例变量。

如果您在类块本身以外的位置使用perimeter = 0,则最近的封闭作用域将是类块本身。那么就会创建一个“类变量”,这只是类对象的一个属性。如果一个属性在类上,则显然它不能特定于任何实例,因为只有一个类,但可以有任意数量的实例。顺带一提,这正是Polygon类的__init__add_pointperimeter方法所做的;它们被分配(使用def语句)在类块中,因此它们成为类对象的属性。


总结:

  1. self.foo = 1是在当前由self引用的对象上分配一个属性(通常是“当前实例”)
  2. foo = 1 在类块内部是创建正在定义的类的类属性
  3. foo = 1 在def块内部是创建正在定义的函数的局部变量

但您真的不应该这样记忆。它们只是以下情况的特殊情况:

  1. 对于类似foo.bar.baz = 1这样的点分式名称赋值,是在写入对象的属性。
  2. 对于简单名称如foo = 1的赋值,是在写入最内部封闭作用域中的变量。

谢谢Ben,这是一篇非常好的解释。还有一个问题,计算周长的正确方法是将其作为实例变量(self.perimeter)还是函数变量(perimeter)?这真的很重要吗? - user1050619
一般来说,这取决于你的意图是什么。在这种情况下,看起来你正在使用它作为方法返回值计算过程中的临时变量,因此它不应该是实例变量。此外,它与一个方法同名,所以如果你将其分配为对象的属性,那么在调用一次方法后就无法再次调用它,因为 square.perimeter 将获得 属性 perimeter 而不是方法,然后使用 square.perimeter() 调用它会导致错误。 - Ben
@user1050619:为了提供一些更强烈的建议,是的,本地变量和实例变量确实很重要。它们执行非常不同的功能。如果本地变量可以满足您的要求,那通常应该使用它。本地变量更易于处理,因为您只需查看一个函数的代码就可以弄清楚它如何使用。实例变量可以从其他方法访问,甚至可以从类外部访问。 - Ben
@user1050619 我也希望实例变量是实例“定义”的一部分;这些数据需要确定特定实例。周长听起来不像是特定多边形定义的一部分;相反,它应该从定义多边形的信息(顶点列表)中计算出来。 - Ben
非常感谢你,Ben。有时候问一些愚蠢的问题可以帮助你澄清所有的疑惑。 :) - user1050619

2

不,那意味着它是本地的。此外,您不希望使用self.perimeter,因为它会遮蔽同名方法。


我已经使用了self.perimeter来编码,上述两种方式有什么区别...哪种才是正确的方法? - user1050619
@user1050619:为什么要把它放在对象上?这有什么用处,特别是因为每次都要重新计算? - Ignacio Vazquez-Abrams

0
在你的代码中,有两个名为perimeter的东西。一个是Polygon类上的方法,另一个是该方法内部的局部变量。你的代码中没有类属性。

我已经使用了self.perimeter编码,上述两种类型有什么区别..哪种是正确的方式? - user1050619
拥有一个方法和一个变量同名只会带来麻烦,因为这会让你(以及解释器)难以理解你所指的是哪一个。我强烈建议你更改其中一个名称。 - Kenny Cason

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接