ฉันจะประกาศค่าเริ่มต้นสำหรับตัวแปรอินสแตนซ์ใน Python ได้อย่างไร


90

ฉันควรให้ค่าเริ่มต้นแก่สมาชิกชั้นเรียนเช่นนี้:

class Foo:
    num = 1

หรือแบบนี้?

class Foo:
    def __init__(self):
        self.num = 1

ในคำถามนี้ฉันพบว่าในทั้งสองกรณี

bar = Foo()
bar.num += 1

เป็นการดำเนินการที่กำหนดไว้อย่างชัดเจน

ฉันเข้าใจว่าวิธีแรกจะให้ตัวแปรคลาสในขณะที่วิธีที่สองจะไม่ อย่างไรก็ตามหากฉันไม่ต้องการตัวแปรคลาส แต่เพียงแค่ตั้งค่าเริ่มต้นสำหรับตัวแปรอินสแตนซ์ของฉันทั้งสองวิธีจะดีเท่ากันหรือไม่ หรือหนึ่งในนั้น 'pythonic' มากกว่าอีกตัว?

สิ่งหนึ่งที่ฉันสังเกตเห็นคือในบทแนะนำ Django พวกเขาใช้วิธีที่สองในการประกาศโมเดล โดยส่วนตัวแล้วฉันคิดว่าวิธีที่สองนั้นดีกว่า แต่ฉันอยากรู้ว่าวิธี 'มาตรฐาน' คืออะไร

คำตอบ:


134

การขยายคำตอบของ bp ฉันต้องการแสดงให้คุณเห็นว่าเขาหมายถึงอะไรโดยประเภทที่ไม่เปลี่ยนรูป

ก่อนอื่นไม่เป็นไร:

>>> class TestB():
...     def __init__(self, attr=1):
...         self.attr = attr
...     
>>> a = TestB()
>>> b = TestB()
>>> a.attr = 2
>>> a.attr
2
>>> b.attr
1

อย่างไรก็ตามสิ่งนี้ใช้ได้กับประเภทที่ไม่เปลี่ยนรูป (ไม่เปลี่ยนรูป) เท่านั้น หากค่าเริ่มต้นไม่แน่นอน (หมายความว่าสามารถแทนที่ได้) สิ่งนี้จะเกิดขึ้นแทน:

>>> class Test():
...     def __init__(self, attr=[]):
...         self.attr = attr
...     
>>> a = Test()
>>> b = Test()
>>> a.attr.append(1)
>>> a.attr
[1]
>>> b.attr
[1]
>>> 

โปรดทราบว่าทั้งสองaและbมีแอตทริบิวต์ที่ใช้ร่วมกัน สิ่งนี้มักไม่ต้องการ

นี่คือวิธี Pythonic ในการกำหนดค่าเริ่มต้นสำหรับตัวแปรอินสแตนซ์เมื่อประเภทไม่แน่นอน:

>>> class TestC():
...     def __init__(self, attr=None):
...         if attr is None:
...             attr = []
...         self.attr = attr
...     
>>> a = TestC()
>>> b = TestC()
>>> a.attr.append(1)
>>> a.attr
[1]
>>> b.attr
[]

เหตุผลที่ข้อมูลโค้ดแรกของฉันใช้งานได้เนื่องจากด้วยประเภทที่ไม่เปลี่ยนรูป Python จะสร้างอินสแตนซ์ใหม่เมื่อใดก็ตามที่คุณต้องการ หากคุณต้องการเพิ่ม 1 ถึง 1 Python จะสร้าง 2 ใหม่ให้คุณเนื่องจาก 1 เก่าไม่สามารถเปลี่ยนแปลงได้ ฉันเชื่อว่าเหตุผลส่วนใหญ่มาจากการแฮช


2
ฉันเพิ่งเจอวิธีที่ง่ายกว่ามากในการใช้ค่าเริ่มต้นสำหรับแอตทริบิวต์ที่เปลี่ยนแปลงได้ เพียงเขียนself.attr = attr or []ภายใน__init__วิธีการ มันได้ผลลัพธ์เดียวกัน (ฉันคิดว่า) และยังคงชัดเจนและอ่านได้
บิล

5
ขอโทษคน ฉันได้บางวิจัยเพิ่มเติมเกี่ยวกับข้อเสนอแนะดังกล่าวข้างต้นในความคิดเห็นของฉันและเห็นได้ชัดว่ามันไม่ได้ค่อนข้างเหมือนกันและจะไม่แนะนำให้ใช้
บิล

เหตุใดคลาส TestC จึงมีวงเล็บว่าง เป็นมรดกของ Python 2 หรือไม่?
ChrisG

55

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

จัดแสดงนิทรรศการ

class Foo:
  def __init__(self):
    self.num = 1

ผูกนี้numกับฟูอินสแตนซ์ การเปลี่ยนฟิลด์นี้จะไม่แพร่กระจายไปยังอินสแตนซ์อื่น

ดังนั้น:

>>> foo1 = Foo()
>>> foo2 = Foo()
>>> foo1.num = 2
>>> foo2.num
1

นิทรรศการ B

class Bar:
  num = 1

ผูกนี้numไปที่บาร์ชั้น การเปลี่ยนแปลงถูกเผยแพร่!

>>> bar1 = Bar()
>>> bar2 = Bar()
>>> bar1.num = 2 #this creates an INSTANCE variable that HIDES the propagation
>>> bar2.num
1
>>> Bar.num = 3
>>> bar2.num
3
>>> bar1.num
2
>>> bar1.__class__.num
3

คำตอบที่แท้จริง

หากฉันไม่ต้องการตัวแปรคลาส แต่เพียงแค่ตั้งค่าเริ่มต้นสำหรับตัวแปรอินสแตนซ์ของฉันทั้งสองวิธีจะดีเท่ากันหรือไม่ หรือหนึ่งในนั้น 'pythonic' มากกว่าอีกตัว?

รหัสในการจัดแสดง B นั้นไม่ถูกต้องสำหรับสิ่งนี้: เหตุใดคุณจึงต้องการผูกแอตทริบิวต์คลาส (ค่าเริ่มต้นในการสร้างอินสแตนซ์) กับอินสแตนซ์เดียว

รหัสในการจัดแสดง A ไม่เป็นไร

หากคุณต้องการให้ค่าเริ่มต้นสำหรับตัวแปรอินสแตนซ์ในตัวสร้างของคุณฉันจะทำสิ่งนี้:

class Foo:
  def __init__(self, num = None):
    self.num = num if num is not None else 1

...หรือแม้กระทั่ง:

class Foo:
  DEFAULT_NUM = 1
  def __init__(self, num = None):
    self.num = num if num is not None else DEFAULT_NUM

... หรือแม้กระทั่ง: (เป็นที่ต้องการ แต่ถ้าและเฉพาะในกรณีที่คุณกำลังจัดการกับประเภทที่ไม่เปลี่ยนรูป!)

class Foo:
  def __init__(self, num = 1):
    self.num = num

วิธีนี้คุณสามารถทำได้:

foo1 = Foo(4)
foo2 = Foo() #use default

1
เป็นวิธีการเริ่มต้นที่ใช้ในตัวอย่างนี้แตกต่างจาก underscore___init__underscore สำหรับการเริ่มต้น
@badp

ตัวอย่างแรกไม่ควรเป็นdef __init__(self, num = None)
PyWalker2797

6

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

ฉันเคยเห็นเทคนิคนี้ใช้อย่างประสบความสำเร็จในการทำซ้ำซึ่งเป็นกรอบงานที่ทำงานบน Zope ข้อได้เปรียบที่นี่ไม่ได้เป็นเพียงแค่ว่าเมื่อคลาสของคุณยังคงอยู่กับฐานข้อมูลจะต้องบันทึกแอตทริบิวต์ที่ไม่ใช่ค่าเริ่มต้นเท่านั้น แต่เมื่อคุณต้องการเพิ่มฟิลด์ใหม่ลงในสคีมาวัตถุที่มีอยู่ทั้งหมดจะเห็นฟิลด์ใหม่พร้อมค่าเริ่มต้น ค่าโดยไม่จำเป็นต้องเปลี่ยนแปลงข้อมูลที่เก็บไว้

ฉันพบว่ามันใช้งานได้ดีในการเข้ารหัสทั่วไป แต่มันก็เป็นสไตล์ ใช้อะไรก็ได้ที่คุณมีความสุขที่สุด


3

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

>>> class c:
...     l = []
... 
>>> x = c()
>>> y = c()
>>> x.l
[]
>>> y.l
[]
>>> x.l.append(10)
>>> y.l
[10]
>>> c.l
[10]

ยกเว้นคนแรกเท่านั้นที่[]ถูกโคลนรอบ ๆ ที่นั่น x.lและy.lมีทั้งชื่อที่ชั่วร้ายและค่าทันทีที่แตกต่างกันซึ่งเชื่อมโยงไปยังผู้อ้างอิงเดียวกัน (คำศัพท์ภาษาทนายความ)
วลี

1

คุณยังสามารถประกาศตัวแปรคลาสเป็นไม่มีซึ่งจะป้องกันการแพร่กระจาย สิ่งนี้มีประโยชน์เมื่อคุณต้องการคลาสที่กำหนดไว้อย่างดีและต้องการป้องกัน AttributeErrors ตัวอย่างเช่น:

>>> class TestClass(object):
...     t = None
... 
>>> test = TestClass()
>>> test.t
>>> test2 = TestClass()
>>> test.t = 'test'
>>> test.t
'test'
>>> test2.t
>>>

นอกจากนี้หากคุณต้องการค่าเริ่มต้น:

>>> class TestClassDefaults(object):
...    t = None
...    def __init__(self, t=None):
...       self.t = t
... 
>>> test = TestClassDefaults()
>>> test.t
>>> test2 = TestClassDefaults([])
>>> test2.t
[]
>>> test.t
>>>

แน่นอนว่ายังคงเป็นไปตามข้อมูลในคำตอบอื่น ๆ เกี่ยวกับการใช้ประเภทที่ผันแปรและไม่เปลี่ยนรูปเป็นค่าเริ่มต้นใน__init__.


0

ด้วยdataclassesซึ่งเป็นคุณลักษณะที่เพิ่มเข้ามาใน Python 3.7 ขณะนี้ยังมีอีกวิธีหนึ่ง (ค่อนข้างสะดวก) ในการตั้งค่าเริ่มต้นในอินสแตนซ์คลาส มัณฑนากรdataclassจะสร้างวิธีการบางอย่างในชั้นเรียนของคุณโดยอัตโนมัติเช่นตัวสร้าง ตามเอกสารที่เชื่อมโยงบันทึกข้างต้น "[t] ตัวแปรสมาชิกที่จะใช้ในวิธีการที่สร้างขึ้นเหล่านี้ถูกกำหนดโดยใช้คำอธิบายประกอบประเภท PEP 526 "

เมื่อพิจารณาจากตัวอย่างของ OP เราสามารถใช้งานได้ดังนี้:

from dataclasses import dataclass

@dataclass
class Foo:
    num: int = 0

เมื่อสร้างออบเจ็กต์ประเภทของคลาสนี้เราสามารถเลือกที่จะเขียนทับค่าได้

print('Default val: {}'.format(Foo()))
# Default val: Foo(num=0)
print('Custom val: {}'.format(Foo(num=5)))
# Custom val: Foo(num=5)
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.