सूचियों पर अप्रत्याशित रूप से व्यवहार क्यों किया जाता है?


118

+=अजगर में ऑपरेटर सूची में अप्रत्याशित रूप से काम कर रहा है। क्या कोई बता सकता है कि यहां क्या चल रहा है?

class foo:  
     bar = []
     def __init__(self,x):
         self.bar += [x]


class foo2:
     bar = []
     def __init__(self,x):
          self.bar = self.bar + [x]

f = foo(1)
g = foo(2)
print f.bar
print g.bar 

f.bar += [3]
print f.bar
print g.bar

f.bar = f.bar + [4]
print f.bar
print g.bar

f = foo2(1)
g = foo2(2)
print f.bar 
print g.bar 

आउटपुट

[1, 2]
[1, 2]
[1, 2, 3]
[1, 2, 3]
[1, 2, 3, 4]
[1, 2, 3]
[1]
[2]

foo += barलगता है कि कक्षा के हर उदाहरण को प्रभावित करता है, जबकि मुझे foo = foo + barलगता है कि जिस तरह से मैं व्यवहार करने की उम्मीद करूंगा उससे व्यवहार होगा।

+=ऑपरेटर एक "यौगिक असाइनमेंट ऑपरेटर" कहा जाता है।


सूची में 'विस्तार' और 'परिशिष्ट' के बीच का अंतर भी देखें
एन 1.1

3
मुझे नहीं लगता कि यह पायथन के साथ कुछ गलत दिखाता है। अधिकांश भाषाएं आपको +सरणियों पर ऑपरेटर का उपयोग करने की अनुमति भी नहीं देंगी । मुझे लगता है कि यह इस मामले में सही समझ में आता है जो आगे +=बढ़ेगा।
स्किलड्रिक

4
इसे आधिकारिक तौर पर 'संवर्धित असाइनमेंट' कहा जाता है।
मार्टिज़न पीटर

जवाबों:


138

सामान्य उत्तर यह है कि विशेष विधि +=को कॉल करने की कोशिश करता है __iadd__, और यदि वह उपलब्ध नहीं है तो __add__इसके बजाय इसका उपयोग करने की कोशिश करता है । तो मुद्दा इन विशेष तरीकों के बीच अंतर के साथ है।

__iadd__विशेष विधि यह है कि यह mutates उद्देश्य यह है कि इस पर कार्य करता है, एक में जगह इसके लिए है। __add__विशेष विधि एक नया ऑब्जेक्ट और भी मानक के लिए प्रयोग किया जाता है +ऑपरेटर।

इसलिए जब द += ऑपरेटर का उपयोग उस वस्तु पर किया जाता है जिसमें एक __iadd__परिभाषित वस्तु होती है, तो उसे संशोधित किया जाता है। अन्यथा यह बजाय सादे का उपयोग करने __add__और एक नई वस्तु वापस करने की कोशिश करेगा ।

यही कारण है कि सूचियों जैसे परिवर्तनशील प्रकारों के लिए += ऑब्जेक्ट के मान को बदल देती हैं, जबकि अपरिवर्तनीय प्रकारों जैसे ट्यूपल्स, स्ट्रिंग्स और पूर्णांकों के लिए एक नई ऑब्जेक्ट को बदले में लौटाया जाता है ( a += bसमतुल्य हो जाता है a = a + b)।

उन प्रकारों के लिए जो आप दोनों का समर्थन करते हैं __iadd__और __add__इसलिए आपको सावधान रहना होगा कि आप किसका उपयोग करते हैं। a += bकॉल करेगा __iadd__और म्यूट करेगा a, जबकि a = a + bएक नई ऑब्जेक्ट बनाएगा और उसे असाइन करेगाa । वे एक ही ऑपरेशन नहीं हैं!

>>> a1 = a2 = [1, 2]
>>> b1 = b2 = [1, 2]
>>> a1 += [3]          # Uses __iadd__, modifies a1 in-place
>>> b1 = b1 + [3]      # Uses __add__, creates new list, assigns it to b1
>>> a2
[1, 2, 3]              # a1 and a2 are still the same list
>>> b2
[1, 2]                 # whereas only b1 was changed

अपरिवर्तनीय प्रकारों के लिए (जहां आपके पास नहीं है __iadd__) a += bऔर a = a + bसमतुल्य हैं। यह वह है जो आपको +=अपरिवर्तनीय प्रकारों पर उपयोग करने की अनुमति देता है , जो एक अजीब डिजाइन निर्णय लग सकता है जब तक आप विचार नहीं करते हैं अन्यथा आप +=संख्याओं जैसे अपरिवर्तनीय प्रकारों पर उपयोग नहीं कर सकते हैं !


4
ऐसी __radd__विधि भी है जिसे कभी-कभी कहा जा सकता है (यह उन अभिव्यक्तियों के लिए प्रासंगिक है जिनमें ज्यादातर उपवर्ग शामिल हैं)।
JFS

2
परिप्रेक्ष्य में: + = उपयोगी है यदि स्मृति और गति महत्वपूर्ण हैं
नोरफेल्ट

3
यह जानते हुए कि +=वास्तव में फैली एक सूची, यह बताता है कि क्यों x = []; x = x + {}एक देता है TypeError, जबकि x = []; x += {}सिर्फ रिटर्न []
zezollo

96

सामान्य मामले के लिए, स्कॉट ग्रिफिथ का जवाब देखें । जब आप जैसी सूचियों के साथ काम करते हैं, हालांकि, +=ऑपरेटर के लिए एक आशुलिपि है someListObject.extend(iterableObject)विस्तार का प्रलेखन देखें ()

extendसमारोह सूची में पैरामीटर के सभी तत्वों को जोड़ देंगे।

जब foo += somethingआप सूची fooको जगह में संशोधित कर रहे हैं , तो इस प्रकार आप उस संदर्भ को नहीं बदलते हैं जो नाम fooइंगित करता है, लेकिन आप सीधे सूची ऑब्जेक्ट को बदल रहे हैं। के साथ foo = foo + something, आप वास्तव में एक नई सूची बना रहे हैं ।

यह उदाहरण कोड इसे समझाएगा:

>>> l = []
>>> id(l)
13043192
>>> l += [3]
>>> id(l)
13043192
>>> l = l + [3]
>>> id(l)
13059216

ध्यान दें कि जब आप नई सूची को पुन: असाइन करते हैं तो संदर्भ कैसे बदलता है l

जैसा कि barएक उदाहरण चर के बजाय एक वर्ग चर है, जगह में संशोधन करने से उस वर्ग के सभी उदाहरण प्रभावित होंगे। लेकिन जब पुनर्परिभाषित किया जाता है self.bar, तो self.barअन्य वर्ग उदाहरणों को प्रभावित किए बिना आवृत्ति का एक अलग उदाहरण चर होगा ।


7
यह हमेशा सच नहीं होता है: ए = 1; ए + = 1; मान्य पायथन है, लेकिन ints में कोई "विस्तार ()" विधियाँ नहीं हैं। आप इसे सामान्य नहीं कर सकते।
ई-सिटिस

2
कुछ परीक्षण किए, स्कॉट ग्रिफ़िथ ने इसे सही पाया, इसलिए आपके लिए -1।
ई-सिटिस

11
@ ई-स्टेटिस: ओपी स्पष्ट रूप से सूचियों के बारे में बोल रहा था, और मैंने स्पष्ट रूप से कहा कि मैं सूचियों के बारे में भी बात कर रहा हूं। मैं कुछ भी सामान्य नहीं कर रहा हूँ।
एंडीडॉग

-1 निकाला, जवाब काफी अच्छा है। मुझे अब भी लगता है कि ग्रिफ़िथ का जवाब बेहतर है।
ई-सतीस

पहले तो यह सोचना अजीब लगता है कि a += bयह a = a + bदो सूचियों aऔर से अलग है b। लेकिन यह समझ में आता है; extendअधिक बार पूरी सूची की एक नई प्रति बनाने के बजाय सूचियों के साथ करने के लिए इच्छित वस्तु होगी जो उच्चतर समय की जटिलता होगी। यदि डेवलपर्स को सावधान रहने की आवश्यकता है कि वे मूल सूचियों को जगह में संशोधित नहीं करते हैं, तो ट्यूपल्स अपरिवर्तनीय वस्तुओं का एक बेहतर विकल्प हैं। +=tuples के साथ मूल tuple को संशोधित नहीं कर सकता है।
प्रांजल मित्तल

22

यहां समस्या यह है, barएक वर्ग विशेषता के रूप में परिभाषित किया गया है, न कि एक उदाहरण चर।

में foo, वर्ग विशेषता को संशोधित किया जाता हैinit विधि है, यही कारण है कि सभी उदाहरण प्रभावित होते हैं।

foo2उदाहरण में , (खाली) वर्ग विशेषता का उपयोग करके एक चर चर को परिभाषित किया गया है, और हर उदाहरण को अपना बनाया जाता है bar

"सही" कार्यान्वयन होगा:

class foo:
    def __init__(self, x):
        self.bar = [x]

बेशक, वर्ग विशेषताएँ पूरी तरह से कानूनी हैं। वास्तव में, आप इस तरह की कक्षा का एक उदाहरण बनाए बिना उन्हें एक्सेस और संशोधित कर सकते हैं:

class foo:
    bar = []

foo.bar = [x]

8

यहां दो चीजें शामिल हैं:

1. class attributes and instance attributes
2. difference between the operators + and += for lists

+ऑपरेटर __add__किसी सूची पर विधि को कॉल करता है । यह सभी तत्वों को अपने ऑपरेंड से लेता है और एक नई सूची बनाता है जिसमें उन तत्वों को शामिल किया जाता है जो उनके क्रम को बनाए रखते हैं।

+= ऑपरेटर कॉल करता है __iadd__ विधि को सूची में रखता है। यह एक पुनरावृत्ति लेता है और पुनरावृत्त के सभी तत्वों को जगह में सूची में जोड़ देता है। यह एक नई सूची ऑब्जेक्ट नहीं बनाता है।

कक्षा fooमें कथन self.bar += [x]एक असाइनमेंट स्टेटमेंट नहीं है, लेकिन वास्तव में इसका अनुवाद है

self.bar.__iadd__([x])  # modifies the class attribute  

जो सूची में जगह को संशोधित करता है और सूची विधि की तरह कार्य करता है extend

कक्षा में foo2, इसके विपरीत, initविधि में असाइनमेंट स्टेटमेंट

self.bar = self.bar + [x]  

के रूप में deconstructed किया जा सकता है:
उदाहरण के पास कोई विशेषता नहीं है bar(हालांकि एक ही नाम का एक वर्ग गुण है, हालांकि) इसलिए यह वर्ग विशेषता तक पहुंचता है और इसे संलग्न करके barएक नई सूची बनाता xहै। इस कथन का अनुवाद है:

self.bar = self.bar.__add__([x]) # bar on the lhs is the class attribute 

फिर यह एक इंस्टेंस विशेषता बनाता है barऔर इसे नई बनाई गई सूची असाइन करता है। ध्यान दें कि barअसाइनमेंट के rhs barपर lhs से अलग है ।

वर्ग के उदाहरणों के लिए foo, barएक वर्ग विशेषता है और उदाहरण विशेषता नहीं है। इसलिए barसभी उदाहरणों के लिए वर्ग विशेषता में कोई भी परिवर्तन परिलक्षित होगा।

इसके विपरीत, वर्ग के प्रत्येक उदाहरण का foo2अपना उदाहरण गुण barहोता है जो समान नाम के वर्ग गुण से भिन्न होता है bar

f = foo2(4)
print f.bar # accessing the instance attribute. prints [4]  
print f.__class__.bar # accessing the class attribute. prints []  

आशा है कि इससे चीजें साफ होंगी।


5

हालाँकि बहुत समय बीत चुका है और कई सही बातें कही गई हैं, लेकिन इसका कोई जवाब नहीं है जो दोनों प्रभावों को बांधे।

आपके 2 प्रभाव हैं:

  1. एक "विशेष", के साथ सूची की शायद किसी का ध्यान नहीं व्यवहार +=(द्वारा कहा गया के रूप में स्कॉट ग्रीफिथ )
  2. तथ्य यह है कि वर्ग विशेषताओं के साथ-साथ उदाहरण विशेषताएँ शामिल हैं (जैसा कि कैन बर्क ब्यूडर द्वारा कहा गया है )

कक्षा में foo, __init__विधि वर्ग विशेषता को संशोधित करती है। इसका कारण है कि इसका self.bar += [x]अनुवाद self.bar = self.bar.__iadd__([x])__iadd__()inplace modification के लिए है, इसलिए यह सूची को संशोधित करता है और इसका संदर्भ देता है।

ध्यान दें कि उदाहरण के तौर पर तानाशाही को संशोधित किया जाता है, हालांकि यह सामान्य रूप से आवश्यक नहीं होगा क्योंकि कक्षा में पहले से ही समान कार्यभार होता है। तो यह विवरण लगभग किसी का ध्यान नहीं जाता है - सिवाय इसके कि आप foo.bar = []बाद में करते हैं। यहाँ उदाहरणों barमें उक्त तथ्य की तरह ही धन्यवाद है।

कक्षा में foo2, हालांकि, कक्षा का barउपयोग किया जाता है, लेकिन स्पर्श नहीं किया जाता है। इसके बजाय, [x]इसमें एक नया ऑब्जेक्ट बनाया गया है, जैसा self.bar.__add__([x])कि यहां कहा जाता है, जो ऑब्जेक्ट को संशोधित नहीं करता है। परिणाम को उदाहरण के रूप में नई सूची में डाल दिया जाता है, उदाहरण के लिए नई सूची को एक तानाशाह के रूप में रखा जाता है, जबकि कक्षा की विशेषता संशोधित रहती है।

के बीच भेद ... = ... + ...और ... += ...बाद में असाइनमेंट को प्रभावित करता है:

f = foo(1) # adds 1 to the class's bar and assigns f.bar to this as well.
g = foo(2) # adds 2 to the class's bar and assigns g.bar to this as well.
# Here, foo.bar, f.bar and g.bar refer to the same object.
print f.bar # [1, 2]
print g.bar # [1, 2]

f.bar += [3] # adds 3 to this object
print f.bar # As these still refer to the same object,
print g.bar # the output is the same.

f.bar = f.bar + [4] # Construct a new list with the values of the old ones, 4 appended.
print f.bar # Print the new one
print g.bar # Print the old one.

f = foo2(1) # Here a new list is created on every call.
g = foo2(2)
print f.bar # So these all obly have one element.
print g.bar 

आप वस्तुओं की पहचान को सत्यापित कर सकते हैं print id(foo), id(f), id(g)( ()यदि आप पायथन 3 पर हैं तो अतिरिक्त एस को न भूलें )।

BTW: +=ऑपरेटर को "संवर्धित असाइनमेंट" कहा जाता है और आम तौर पर जहां तक ​​संभव हो, इनकम संशोधनों को करने का इरादा है।


5

हालांकि यह हवाले से और की चर्चा करते हुए लायक लगता है अन्य उत्तर, काफी यह कवर करने के लिए प्रतीत होता है संवर्धित कार्य पीईपी 203 :

वे [संवर्धित असाइनमेंट ऑपरेटर] एक ही ऑपरेटर को अपने सामान्य बाइनरी फॉर्म के रूप में लागू करते हैं, सिवाय इसके कि ऑपरेशन को `इन-प्लेस 'किया जाता है जब बाएं हाथ की ओर वस्तु इसका समर्थन करती है, और यह कि बाएं हाथ की तरफ केवल एक बार मूल्यांकन किया जाता है।

...

पायथन में संवर्धित कार्य के पीछे का विचार यह है कि यह बाइनरी ऑपरेशन के परिणाम को अपने बाएं हाथ के ऑपरेंड में संग्रहीत करने के सामान्य अभ्यास को लिखने का एक आसान तरीका नहीं है, लेकिन सवाल में बाएं हाथ के ऑपरेंड के लिए एक तरीका भी है यह जान लें कि इसे स्वयं की संशोधित प्रति बनाने के बजाय, स्वयं पर संचालित करना चाहिए।


1
>>> elements=[[1],[2],[3]]
>>> subset=[]
>>> subset+=elements[0:1]
>>> subset
[[1]]
>>> elements
[[1], [2], [3]]
>>> subset[0][0]='change'
>>> elements
[['change'], [2], [3]]

>>> a=[1,2,3,4]
>>> b=a
>>> a+=[5]
>>> a,b
([1, 2, 3, 4, 5], [1, 2, 3, 4, 5])
>>> a=[1,2,3,4]
>>> b=a
>>> a=a+[5]
>>> a,b
([1, 2, 3, 4, 5], [1, 2, 3, 4])

0
>>> a = 89
>>> id(a)
4434330504
>>> a = 89 + 1
>>> print(a)
90
>>> id(a)
4430689552  # this is different from before!

>>> test = [1, 2, 3]
>>> id(test)
48638344L
>>> test2 = test
>>> id(test)
48638344L
>>> test2 += [4]
>>> id(test)
48638344L
>>> print(test, test2)  # [1, 2, 3, 4] [1, 2, 3, 4]```
([1, 2, 3, 4], [1, 2, 3, 4])
>>> id(test2)
48638344L # ID is different here

हम देखते हैं कि जब हम एक अपरिवर्तनीय वस्तु (इस मामले में पूर्णांक) को संशोधित करने का प्रयास करते हैं, तो पायथन केवल हमें एक अलग वस्तु देता है। दूसरी ओर, हम एक परिवर्तनशील वस्तु (एक सूची) में परिवर्तन करने में सक्षम हैं और यह एक ही वस्तु है।

रेफरी: https://medium.com/@tyastropheus/tricky-python-i-memory-management-for-mutable-immutable-objects-21507d1e5b95

उथलेपन और डीपकोपी को समझने के लिए यूआरएल के नीचे भी देखें

https://www.geeksforgeeks.org/copy-python-deep-copy-shallow-copy/


# आईडी सूची के लिए समान है
रोशन ओके
हमारी साइट का प्रयोग करके, आप स्वीकार करते हैं कि आपने हमारी Cookie Policy और निजता नीति को पढ़ और समझा लिया है।
Licensed under cc by-sa 3.0 with attribution required.