อะไรคือความแตกต่างระหว่างแอตทริบิวต์คลาสและอินสแตนซ์


134

มีความแตกต่างที่มีความหมายระหว่าง:

class A(object):
    foo = 5   # some default value

เทียบกับ

class B(object):
    def __init__(self, foo=5):
        self.foo = foo

หากคุณสร้างอินสแตนซ์จำนวนมากความต้องการด้านประสิทธิภาพหรือพื้นที่สำหรับทั้งสองสไตล์มีความแตกต่างกันหรือไม่ เมื่อคุณอ่านโค้ดคุณคิดว่าความหมายของทั้งสองสไตล์แตกต่างกันอย่างมีนัยสำคัญหรือไม่?


1
ฉันเพิ่งรู้ว่ามีการถามและตอบคำถามที่คล้ายกันที่นี่: stackoverflow.com/questions/206734/… ฉันควรลบคำถามนี้หรือไม่
Dan Homerick

2
เป็นคำถามของคุณอย่าลังเลที่จะลบมัน เพราะเป็นของคุณทำไมต้องถามความคิดเห็นของใคร?
ล็อ

คำตอบ:


148

นอกเหนือจากการพิจารณาด้านประสิทธิภาพแล้วยังมีความแตกต่างอย่างมีนัยสำคัญ ในกรณีแอตทริบิวต์คลาสมีเพียงหนึ่งวัตถุที่อ้างถึง ในอินสแตนซ์ - แอ็ตทริบิวต์ชุดที่อินสแตนซ์อาจมีหลายอ็อบเจ็กต์ที่อ้างถึง ตัวอย่างเช่น

>>> class A: foo = []
>>> a, b = A(), A()
>>> a.foo.append(5)
>>> b.foo
[5]
>>> class A:
...  def __init__(self): self.foo = []
>>> a, b = A(), A()
>>> a.foo.append(5)
>>> b.foo    
[]

4
แชร์เฉพาะประเภทที่เปลี่ยนแปลงได้ ชอบintและstrยังคงยึดติดกับแต่ละอินสแตนซ์มากกว่าคลาส
Babu

11
@Babu: ไม่มีintและstrจะยังใช้ร่วมกันในทางเดียวกันว่า คุณสามารถตรวจสอบว่าได้อย่างง่ายดายด้วยหรือis idหรือเพียงแค่มองในแต่ละกรณีของและของชั้นเรียน__dict__ __dict__โดยปกติจะไม่สำคัญมากนักว่าจะแชร์ประเภทที่ไม่เปลี่ยนรูปหรือไม่
ยกเลิก

18
อย่างไรก็ตามโปรดทราบว่าถ้าคุณทำa.foo = 5แล้วในทั้งสองกรณีคุณจะเห็นผลตอบแทนb.foo []นั่นเป็นเพราะในกรณีแรกคุณกำลังเขียนทับแอตทริบิวต์คลาสa.fooด้วยแอตทริบิวต์อินสแตนซ์ใหม่ที่มีชื่อเดียวกัน
Konstantin Schubert

39

ความแตกต่างคือแอตทริบิวต์ในคลาสจะถูกแชร์โดยอินสแตนซ์ทั้งหมด แอตทริบิวต์บนอินสแตนซ์ไม่ซ้ำกับอินสแตนซ์นั้น

หากมาจาก C ++ แอตทริบิวต์ในคลาสจะเหมือนกับตัวแปรสมาชิกแบบคงที่


1
เป็นเพียงประเภทที่ไม่เปลี่ยนแปลงเท่านั้นที่ใช้ร่วมกัน? คำตอบที่ยอมรับจะแสดงรายการซึ่งใช้งานได้ แต่ถ้าเป็น int ดูเหมือนว่าจะเหมือนกับอินสแตนซ์ attr: >>> class A(object): foo = 5 >>> a, b = A(), A() >>> a.foo = 10 >>> b.foo 5
Rafe

7
@Rafe: ไม่แชร์ทุกประเภท เหตุผลที่คุณกำลังสับสนว่าสิ่งที่a.foo.append(5)เป็นกรรมวิธีค่าที่a.fooหมายถึงในขณะที่a.foo = 5จะทำให้เป็นชื่อใหม่สำหรับค่าa.foo 5ดังนั้นคุณจะได้รับแอตทริบิวต์อินสแตนซ์ที่ซ่อนแอตทริบิวต์คลาส ลองทำเช่นเดียวกันa.foo = 5ในเวอร์ชันของ Alex และคุณจะเห็นว่าb.fooไม่มีการเปลี่ยนแปลง
ยกเลิก

31

นี่เป็นโพสต์ที่ดีมากและสรุปได้ดังนี้

class Bar(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
Bar.class_var
## 1
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 (Bar.__dict__)
foo.class_var
## 1

และในรูปแบบภาพ

ป้อนคำอธิบายภาพที่นี่

การกำหนดแอตทริบิวต์คลาส

  • หากแอตทริบิวต์คลาสถูกตั้งค่าโดยการเข้าถึงคลาสแอททริบิวต์จะแทนที่ค่าสำหรับอินสแตนซ์ทั้งหมด

    foo = Bar(2)
    foo.class_var
    ## 1
    Bar.class_var = 2
    foo.class_var
    ## 2
    
  • หากตัวแปรคลาสถูกตั้งค่าโดยการเข้าถึงอินสแตนซ์ตัวแปรนั้นจะแทนที่ค่าสำหรับอินสแตนซ์นั้นเท่านั้น สิ่งนี้จะแทนที่ตัวแปรคลาสและเปลี่ยนเป็นตัวแปรอินสแตนซ์ที่มีอยู่โดยสัญชาตญาณสำหรับอินสแตนซ์นั้นเท่านั้น

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

คุณจะใช้แอตทริบิวต์คลาสเมื่อใด

  • การจัดเก็บค่าคงที่ เนื่องจากแอตทริบิวต์คลาสสามารถเข้าถึงได้เป็นแอตทริบิวต์ของคลาสเองจึงมักใช้เพื่อจัดเก็บค่าคงที่เฉพาะคลาสสำหรับคลาส

    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
    

20

เนื่องจากคนในความคิดเห็นที่นี่และในสองคำถามอื่น ๆ ทำเครื่องหมายเป็น dups ทั้งหมดดูเหมือนจะสับสนเกี่ยวกับเรื่องนี้ในทางเดียวกันผมคิดว่ามันคุ้มค่าที่เพิ่มคำตอบเพิ่มเติมด้านบนของอเล็กซ์โคเวนทรี

ข้อเท็จจริงที่ว่าอเล็กซ์กำหนดค่าของประเภทที่ไม่แน่นอนเช่นรายการไม่มีส่วนเกี่ยวข้องกับการแชร์สิ่งต่างๆหรือไม่ เราสามารถเห็นสิ่งนี้ด้วยidฟังก์ชันหรือตัวisดำเนินการ:

>>> class A: foo = object()
>>> a, b = A(), A()
>>> a.foo is b.foo
True
>>> class A:
...     def __init__(self): self.foo = object()
>>> a, b = A(), A()
>>> a.foo is b.foo
False

(หากคุณสงสัยว่าทำไมฉันถึงใช้object()แทนพูด5นั่นคือเพื่อหลีกเลี่ยงการพบปัญหาอื่น ๆ อีกสองประเด็นที่ฉันไม่ต้องการเข้าที่นี่ด้วยเหตุผลสองประการที่แตกต่างกันสิ่งที่สร้างขึ้นแยกจากกันทั้งหมด5อาจกลายเป็น อินสแตนซ์เดียวกันของหมายเลข5แต่object()ไม่สามารถสร้างขึ้นแยกต่างหาก)


เหตุใดa.foo.append(5)ในตัวอย่างของ Alex จึงมีผลb.fooแต่a.foo = 5ในตัวอย่างของฉันไม่ ดีพยายามa.foo = 5ในตัวอย่างของอเล็กซ์และแจ้งให้ทราบว่ามันไม่ได้มีผลต่อการb.fooมีอย่างใดอย่างหนึ่ง

a.foo = 5เป็นเพียงการเข้าชื่อเป็นa.foo 5ที่ไม่ส่งผลกระทบb.fooหรือชื่ออื่นสำหรับค่าเก่าที่a.fooใช้อ้างถึง * เป็นเรื่องยุ่งยากเล็กน้อยที่เรากำลังสร้างแอตทริบิวต์อินสแตนซ์ที่ซ่อนแอตทริบิวต์คลาส ** แต่เมื่อคุณได้รับสิ่งนั้นก็ไม่มีอะไรซับซ้อน เกิดขึ้นที่นี่


หวังว่าตอนนี้จะชัดเจนแล้วว่าทำไม Alex ถึงใช้รายการ: การที่คุณสามารถเปลี่ยนรายการได้หมายความว่าง่ายกว่าที่จะแสดงให้เห็นว่าตัวแปรสองตัวตั้งชื่อรายการเดียวกันและยังหมายความว่ารหัสในชีวิตจริงมีความสำคัญมากกว่าที่จะทราบว่าคุณมีสองรายการหรือ สองชื่อสำหรับรายการเดียวกัน


* ความสับสนสำหรับผู้ที่มาจากภาษาเช่น C ++ คือใน Python ค่าจะไม่ถูกเก็บไว้ในตัวแปร ค่าอยู่ในมูลค่าที่ดินตัวแปรเป็นเพียงชื่อของค่าและการกำหนดค่าจะสร้างชื่อใหม่สำหรับค่า ถ้าช่วยได้ให้คิดว่าตัวแปร Python แต่ละshared_ptr<T>ตัวแทน a T.

** บางคนใช้ประโยชน์จากสิ่งนี้โดยใช้แอตทริบิวต์คลาสเป็น "ค่าเริ่มต้น" สำหรับแอตทริบิวต์อินสแตนซ์ที่อินสแตนซ์อาจตั้งค่าหรือไม่ก็ได้ ซึ่งอาจเป็นประโยชน์ในบางกรณี แต่ก็อาจทำให้สับสนได้เช่นกันดังนั้นโปรดใช้ความระมัดระวัง


0

มีอีกหนึ่งสถานการณ์

แอตทริบิวต์การเรียนและอินสแตนซ์อธิบาย

# -*- encoding: utf-8 -*-


class RevealAccess(object):
    def __init__(self, initval=None, name='var'):
        self.val = initval
        self.name = name

    def __get__(self, obj, objtype):
        return self.val


class Base(object):
    attr_1 = RevealAccess(10, 'var "x"')

    def __init__(self):
        self.attr_2 = RevealAccess(10, 'var "x"')


def main():
    b = Base()
    print("Access to class attribute, return: ", Base.attr_1)
    print("Access to instance attribute, return: ", b.attr_2)

if __name__ == '__main__':
    main()

ด้านบนจะแสดงผล:

('Access to class attribute, return: ', 10)
('Access to instance attribute, return: ', <__main__.RevealAccess object at 0x10184eb50>)

การเข้าถึงอินสแตนซ์ประเภทเดียวกันผ่านคลาสหรืออินสแตนซ์ให้ผลลัพธ์ที่แตกต่างกัน!

และผมพบว่าในความหมาย c.PyObject_GenericGetAttrและดีโพสต์

อธิบาย

หากพบแอตทริบิวต์ในพจนานุกรมของคลาสที่ประกอบขึ้น MRO ของออบเจ็กต์จากนั้นตรวจสอบเพื่อดูว่าแอตทริบิวต์ที่กำลังค้นหาชี้ไปที่ Data Descriptor (ซึ่งไม่มีอะไรมากไปกว่าที่คลาสที่ใช้ทั้งสองวิธี__get__และ__set__วิธีการ) ถ้าเป็นเช่นนั้นให้แก้ไขการค้นหาแอตทริบิวต์โดยการเรียกใช้__get__เมธอดของ Data Descriptor (บรรทัดที่ 28–33)

โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.