Python neden bir listeyi tekrarlarken sadece bireysel elemanların bir kopyasını alıyor?


31

Sadece şunu yazdım, Python'da

for i in a:
    i += 1

Orijinal listenin elemanları aaslında hiçbir şekilde etkilenmeyecektir, çünkü değişken isadece orijinal öğenin bir kopyası olarak ortaya çıkmaktadır a.

Orijinal öğeyi değiştirmek için,

for index, i in enumerate(a):
    a[index] += 1

gerekli olacaktı.

Bu davranıştan gerçekten şaşırdım. Bu, diğer dillerden görünüşte farklı görünmektedir ve kodumda uzun süredir hata ayıklamak zorunda kaldığım hatalarla sonuçlandı.

Daha önce Python Eğitimini okudum. Emin olmak için kitabı şimdi tekrar kontrol ettim ve bu davranıştan hiç bahsetmiyor bile.

Bu tasarımın arkasındaki sebep nedir? Öğreticinin okuyucuların doğal olarak alması gerektiğine inanması için birçok dilde standart bir uygulama olması bekleniyor mu? Başka hangi dillerde yineleme konusundaki davranışları mevcut, gelecekte dikkat etmem gerekiyor?


19
Bu sadece ideğişmez ise veya değişmeyen bir işlem yapıyorsanız geçerlidir . İç içe geçmiş bir liste ile for i in a: a.append(1)farklı davranış olacaktır; Python gelmez iç içe listeleri kopyalayın. Ancak tamsayılar değişmez ve ekleme yeni bir nesne döndürür, eskisini değiştirmez.
jonrsharpe

10
Hiç şaşırtıcı değil. Tamsayı gibi bir dizi temel tür için tam olarak aynı olmayan bir dil düşünemiyorum. Örneğin, javascript'i deneyin a=[1,2,3];a.forEach(i => i+=1);alert(a). C #
edc65'de

7
i = i + 1Etkilemeyi bekler misiniz a?
27'de

7
Bu davranışın diğer dillerde farklı olmadığını unutmayın. C, Javascript, Java vb. Bu şekilde davranır.
Slebetman,

1
"+ =" listeleri için @jonrsharpe eski listeyi değiştirirken, "+" yeni bir tane oluşturuyor
Vasily Alexeev

Yanıtlar:


68

Son zamanlarda benzer bir soruyu çoktan cevapladım ve +=farklı anlamlara gelebileceğini anlamak çok önemli.

  • Veri türü yerinde ekleme yaparsa (yani doğru çalışan bir __iadd__işlevi varsa), o zaman başvuruda bulunan veriler igüncelleştirilir (listede mi yoksa başka bir yerde olması önemli değildir).

  • Veri türü bir __iadd__yöntem uygulamazsa , i += xifade sadece sözdizimsel şekerdir i = i + x, bu nedenle yeni bir değer yaratılır ve değişken adına atanır i.

  • Veri türü uygular __iadd__ancak garip bir şey yaparsa. Güncellenmesi mümkün olabilir ... ya da değil - orada ne uygulandığına bağlı.

Pythons tamsayıları, kayan noktaları, dizeleri uygulamaz, __iadd__böylece bunlar yerinde güncellenmez. Bununla birlikte, numpy.arrayya da diğerleri gibi diğer veri türleri listonu uygular ve beklediğiniz gibi davranır. Bu yüzden, yinelemede kopya ya da kopya meselesi değildir (normalde lists ve tuples kopyaları yapmaz - ama kapların __iter__ve __getitem__yöntemin uygulanmasına da bağlıdır !) - bu daha çok veri tipi meselesidir içinde sakladınız a.


2
Bu, soruda açıklanan davranış için doğru açıklamadır.
pabouk

19

Açıklama - terminoloji

Python, referans ve pointer kavramları arasında ayrım yapmaz . Genellikle sadece referans terimini kullanırlar , ancak C ++ gibi dillerle karşılaştırırsanız, bu farklılığa sahip - bir işaretçiye çok daha yakın .

Asker açıkça C ++ geçmişinden geldiğinden ve açıklama için gerekli olan ayrım Python'da bulunmadığından , C ++ 'ın terminolojisini kullanmayı seçtim:

  • Değer : Bellekte oturan gerçek veriler. değerine görevoid foo(int x); bir tam sayı alan bir fonksiyonun imzasıdır .
  • İşaretçi : Değer olarak kabul edilen bir hafıza adresi. İşaret ettiği hafızaya erişmek için ertelenebilir. işaretçi ilevoid foo(int* x); bir tamsayı alan bir fonksiyonun imzasıdır .
  • Referans : İşaretçilerin etrafında şeker. Sahnelerin arkasında bir işaretçi vardır, ancak yalnızca ertelenen değere erişebilir ve işaret ettiği adresi değiştiremezsiniz. başvurudavoid foo(int& x); tamsayı alan bir fonksiyonun imzasıdır .

"Diğer dillerden farklı" derken ne demek istiyorsunuz? Her bir döngüyü desteklediğini bildiğim çoğu dil, aksi belirtilmedikçe öğeyi kopyalıyor.

Özellikle Python için (bu nedenlerin çoğu benzer mimari veya felsefi kavramlara sahip diğer diller için geçerli olsa da):

  1. Bu davranış, farkında olmayan insanlar için hatalara neden olabilir, ancak alternatif davranış , farkında olanlar için bile hatalara neden olabilir . Bir değişkeni ( i) atadığınızda, genellikle durmaz ve bu nedenle ( a) nedeniyle değiştirilecek diğer tüm değişkenleri göz önünde bulundurursunuz . Üzerinde çalışmakta olduğunuz kapsamın sınırlandırılması, spagetti kodunun önlenmesinde büyük bir faktördür ve bu nedenle kopya ile yineleme, referans olarak yinelemeyi destekleyen dillerde bile varsayılandır.

  2. Python değişkenleri her zaman tek bir işaretçidir, bu nedenle kopya ile yinelemek ucuzdur - referansa göre yinelemekten daha ucuzdur; bu değere her eriştiğinizde ekstra bir erteleme gerektirir.

  3. Python - örneğin - C ++ gibi referans değişkenleri kavramına sahip değildir. Diğer bir deyişle, Python'daki tüm değişkenler aslında referanslardır, ancak bunlar işaretçi oldukları anlamındadır - C ++ type& nameargümanları gibi sahne arkası constat referansları değildir . Bu kavram Python'da bulunmadığından referans alarak yinelemeyi uygulama - onu varsayılan yapalım! - Bayt koduna daha fazla karmaşıklık eklenmesini gerektirecektir.

  4. Python'un forifadesi sadece diziler üzerinde değil, daha genel bir üretici kavramı üzerinde de çalışır. Sahnelerin ardında Python iter, dizilerinizi çağırır - ki onu çağırdığınızda next- bir sonraki öğeyi döndürür ya da raisesa StopIteration. Jeneratörleri Python'da uygulamak için birkaç yol vardır ve referans olarak yineleme için uygulamak çok daha zor olurdu.


Cevap için teşekkürler. Yineleyicilerle ilgili anlayışımın hala yeterince sağlam olmadığı görülüyor. Varsayılan olarak yineleyiciler C ++ başvurusunda değil mi? Yineleyiciyi kaldırırsanız, orijinal kabın öğesinin değerini her zaman hemen değiştirebilirsiniz?
xji,

4
Piton yapar referans ile bu iterate (değeriyle de, ama değeri bir referans). Bunu değiştirilebilir nesneler listesiyle denemek, hiçbir kopyalamanın gerçekleşmediğini çabucak gösterecektir.
jonrsharpe

C ++ 'daki yineleyiciler aslında dizideki değere erişmek için ertelenebilecek nesnelerdir. Orijinal öğeyi değiştirmek için kullanırsınız *it = ...- ancak bu tür bir sözdizimi zaten başka bir yerde bir şey değiştirdiğinizi gösterir - bu da 1 numaralı nedeni daha az sorun yaratır. Sebep # 2 ve # 3 de geçerli değildir, çünkü C ++ 'da kopyalamak pahalıdır ve referans değişkenleri kavramı vardır. Sebep # 4'e gelince - bir referans döndürme yeteneği, tüm durumlar için basit bir uygulamaya izin verir.
Idan Arye,

1
@ jonrsharpe Evet, referans olarak adlandırılır, ancak işaretçiler ve referanslar arasında ayrım yapan herhangi bir dilde, bu tür yineleme, işaretçiyle bir yineleme (ve işaretçiler değer olduğundan - değere göre yineleme) olacaktır. Bir açıklama ekleyeceğim.
Idan Arye,

20
İlk paragrafınız Python'un diğer diller gibi öğeyi for döngüsüne kopyaladığını gösteriyor. Öyle değil. Bu öğede yaptığınız değişikliklerin kapsamını sınırlamaz. OP sadece bu davranışı görür çünkü unsurları değişmezdir; Bu ayrımdan bile söz etmeden cevabınız en iyi eksik ve en kötü yanıltıcıdır.
jonrsharpe

11

Buradaki cevapların hiçbiri, bunun neden Python bölgesinde olduğunu göstermek için üzerinde çalışacağınız herhangi bir kod vermedi. Ve bu daha derin bir yaklaşıma bakmak eğlencelidir, işte burada.

Bunun beklediğiniz gibi çalışmamasının birincil nedeni, Python’da yazmanızdır.

i += 1

yaptığını düşündüğün şeyi yapmıyor. Tamsayılar değişmezdir. Bu, nesnenin Python'da gerçekte ne olduğuna baktığınızda görülebilir:

a = 0
print('ID of the first integer:', id(a))
a += 1
print('ID of the first integer +=1:', id(a))

İd işlevi , kullanım ömrü boyunca bir nesne için benzersiz ve sabit bir değeri temsil eder. Kavramsal olarak, C / C ++ 'da bir hafıza adresine gevşek olarak eşler. Yukarıdaki kodu çalıştırıyor:

ID of the first integer: 140444342529056
ID of the first integer +=1: 140444342529088

Bunun anlamı, birincisinin aartık ikinciyle aynı olmadığı a, çünkü kimlikleri farklı. Etkili bir biçimde bellekteki farklı konumlardalar.

Bununla birlikte, bir nesneyle, işler farklı şekilde çalışır. +=Burada operatörün üzerine yazdım :

class CustomInt:
  def __iadd__(self, other):
    # Override += 1 for this class
    self.value = self.value + other.value
    return self

  def __init__(self, v):
    self.value = v

ints = []
for i in range(5):
  int = CustomInt(i)
  print('ID={}, value={}'.format(id(int), i))
  ints.append(int)


for i in ints:
  i += CustomInt(i.value)

print("######")
for i in ints:
  print('ID={}, value={}'.format(id(i), i.value))

Bu sonuç aşağıdaki çıktıyla sonuçlanır:

ID=140444284275400, value=0
ID=140444284275120, value=1
ID=140444284275064, value=2
ID=140444284310752, value=3
ID=140444284310864, value=4
######
ID=140444284275400, value=0
ID=140444284275120, value=2
ID=140444284275064, value=4
ID=140444284310752, value=6
ID=140444284310864, value=8

Bu durumda id niteliğinin aslında her iki yineleme için aynı olduğuna dikkat edin , nesnenin değeri farklı olsa da ( idnesnenin mutasyona uğradığı için değişecek olan nesnenin tuttuğu int değerini de bulabilirsiniz) değişmez).

Bunu aynı egzersizi değişmez bir nesneyle yaptığınız zaman karşılaştırın:

ints_primitives = []
for i in range(5):
  int = i
  ints_primitives.append(int)
  print('ID={}, value={}'.format(id(int), i))

print("######")
for i in ints_primitives:
  i += 1
  print('ID={}, value={}'.format(id(int), i))


print("######")
for i in ints_primitives:
  print('ID={}, value={}'.format(id(i), i))

Bu çıktılar:

ID=140023258889248, value=0
ID=140023258889280, value=1
ID=140023258889312, value=2
ID=140023258889344, value=3
ID=140023258889376, value=4
######
ID=140023258889280, value=1
ID=140023258889312, value=2
ID=140023258889344, value=3
ID=140023258889376, value=4
ID=140023258889408, value=5
######
ID=140023258889248, value=0
ID=140023258889280, value=1
ID=140023258889312, value=2
ID=140023258889344, value=3
ID=140023258889376, value=4

Burada dikkat edilmesi gereken birkaç şey var. İlk olarak, ile olan döngüde +=, artık orijinal nesneye ekleyemezsiniz. Bu durumda, girişler Python'daki değişmez türler arasında olduğundan , python farklı bir kimlik kullanır. Ayrıca Python'un idaynı değişmez değere sahip birden fazla değişken için aynı temeli kullandığını not etmek ilginç :

a = 1999
b = 1999
c = 1999

print('id a:', id(a))
print('id b:', id(b))
print('id c:', id(c))

id a: 139846953372048
id b: 139846953372048
id c: 139846953372048

tl; dr - Python, gördüğünüz davranışa neden olan bir avuç değişmez tipe sahiptir. Tüm değişken tipler için beklentiniz doğrudur.


6

@ Idan'ın cevabı, Python'un neden döngü değişkenine bir işaretçi olarak davranmadığını açıklamakta iyi bir iş çıkarsa da, Python'da olduğu gibi kod parçacıklarının nasıl açıldığını daha basit bir şekilde açıklamakta fayda var. kod aslında yerleşik yöntemlere çağrılar olacaktır . İlk örneğini almak için

for i in a:
    i += 1

Açılacak iki şey var: for _ in _:sözdizimi ve _ += _sözdizimi. Önce for döngüsünü almak için, diğer dillerdeki gibi Python, for-eachbir yineleyici desen için esasen sözdizimi şekeri olan bir döngüye sahiptir. Python'da bir yineleyici, .__next__(self)geçerli öğeyi dizide döndüren, bir sonrakine ilerleyen ve StopIterationdizide başka öğe olmadığında bir süre yükselten bir yöntem tanımlayan bir nesnedir . Bir iterable bir tanımlayan bir amacı, .__iter__(self)yöntem döndürdüğü bir yineleyici.

(Not: an Iteratoraynı zamanda bir yöntemdir Iterableve .__iter__(self)yönteminden kendini döndürür .)

Python genellikle özel çift alt çizgi yöntemine atanan yerleşik bir işleve sahip olacaktır. Bu yüzden iter(o)çözen o.__iter__()ve next(o)çözen de vardır o.__next__(). Temsil ettikleri yöntem tanımlanmamışsa, bu yerleşik işlevlerin genellikle makul bir varsayılan tanımı deneyeceğini unutmayın. Örneğin, len(o)genellikle çözer, o.__len__()ancak bu yöntem tanımlanmadıysa, dener iter(o).__len__().

Döngü için esas cinsinden tanımlanır next(), iter()ve daha temel kontrol yapıları. Genel olarak kod

for i in %EXPR%:
    %LOOP%

gibi bir şey için açılacak

_a_iter = iter(%EXPR%)
while True:
    try:
        i = next(_a_iter)
    except StopIteration:
        break
    %LOOP%

Yani bu durumda

for i in a:
    i += 1

paketinden çıkar

_a_iter = iter(a) # = a.__iter__()
while True:
    try: 
        i = next(_a_iter) # = _a_iter.__next__()
    except StopIteration:
        break
    i += 1

Bunun diğer yarısı i += 1. Genel olarak %ASSIGN% += %EXPR%ambalajından çıkar %ASSIGN% = %ASSIGN%.__iadd__(%EXPR%). İşte __iadd__(self, other)yerinde ekleme yapar ve kendini döndürür.

(NB Bu, ana yöntem tanımlanmadığı takdirde Python'un bir alternatif seçeceği başka bir durumdur. Nesne uygulamazsa __iadd__, geri düşecektir __add__. Aslında bu intuygulamamakta __iadd__olduğu gibi yapar - çünkü onlar anlamlıdır; değişmez ve bu yüzden yerinde değiştirilemez.)

Yani kodun şuna benziyor

_a_iter = iter(a)
while True:
    try:
        i = next(_a_iter)
    except StopIteration:
        break
    i = iadd(i,1)

nerede tanımlayabiliriz

def iadd(o, v):
    try:
        return o.__iadd__(v)
    except AttributeError:
        return o.__add__(v)

İkinci kodunuzda biraz daha var. Bilmemiz gereken iki yeni şeyler o vardır %ARG%[%KEY%] = %VALUE%paketten alır (%ARG%).__setitem__(%KEY%, %VALUE%)ve %ARG%[%KEY%]paketten alır (%ARG%).__getitem__(%KEY%). Bu bilgiyi koyarak hep birlikte almak a[ix] += 1için ambalajsız a.__setitem__(ix, a.__getitem__(ix).__add__(1))(tekrar: __add__ziyade __iadd__çünkü __iadd__ints uygulanmadı). Son kodumuz şuna benziyor:

_a_iter = iter(enumerate(a))
while True:
    try:
        index, i = next(_a_iter)
    except StopIteration:
        break
    a.__setitem__(index, iadd(a.__getitem__(index), 1))

Aslında ikinci yapar iken ilki biz alıyorsanız ilk parçada listesini değiştirmez neden olarak sorunuza cevap bulmak için igelen next(_a_iter)araçlar hangi ibir olacaktır int. Yana intler yerinde değiştirilemez' i += 1listesine hiçbir şey yapmaz. İkinci örneğimizde yine değiştirmiyoruz intama arayarak listeyi değiştiriyoruz __setitem__.

Bu ayrıntılı alıştırmanın nedeni Python ile ilgili şu dersi verdiğini düşünüyorum:

  1. Python'un okunabilirliğinin bedeli, her zaman bu sihirli çifte skor yöntemlerini çağırmasıdır.
  2. Bu nedenle, herhangi bir Python kodunu gerçekten anlama şansına sahip olmak için, bu çevirilerin ne yaptığını anlamalısınız.

Çift alt çizgi yöntemleri, başlangıçta bir engeldir, ancak Python'un "çalıştırılabilir sözde kodu" ününü desteklemek için çok önemlidir. İyi bir Python programcısı, bu yöntemleri ve nasıl çağrıldıklarını tam olarak anlayacaktır ve bunu yapmanın iyi olduğu her yerde onları tanımlayacaktır.

Düzenleme : @deltab, "koleksiyon" teriminin özensiz kullanımını düzeltti.


2
"yineleyiciler aynı zamanda koleksiyonlar" da pek doğru değil: aynı zamanda yinelenebilirler ancak koleksiyonlar da var __len__ve__contains__
deltab

2

+=Mevcut değerin değişken veya değişken olmasına bağlı olarak farklı şekilde çalışır . Python geliştiricilerinin kafa karıştırıcı olacağından korktuğu için Python'da hayata geçirilmesi için uzun zaman beklemesinin ana nedeni buydu.

Bir iint ise, o zaman inçler değişken olmadığından değiştirilemez ve bu nedenle ideğişikliklerin değeri o zaman mutlaka başka bir nesneye işaret etmelidir:

>>> i=3
>>> id(i)
14336296
>>> i+=1
>>> id(i)
14336272   # Other object

Bununla birlikte, eğer sol taraf değişken ise , + = gerçekten onu değiştirebilir; Bir liste ise:

>>> i=[]
>>> id(i)
140257231883944
>>> i+=[1]
>>> id(i)
140257231883944  # Still the same object!

For döngünüzde, sırasıyla iher bir elemanı ifade eder a. Bunlar tamsayılarsa, ilk durum geçerli olur ve bunun sonucu i += 1başka bir tamsayı nesnesine işaret etmesi gerektiğidir. aElbette listesi hala her zaman olduğu gibi aynı unsurlara sahiptir.


Eğer: Ben değişken ve sabit nesneler arasındaki bu ayrımı anlamıyorum i = 1setleri ideğişmez bir tamsayı nesneye, daha sonra i = []belirlesin ideğişmez bir liste nesnesine. Başka bir deyişle, neden tamsayı nesneler değişken değil ve liste nesneleri değişken? Bunun arkasında hiçbir mantık görmüyorum.
Giorgio,

@Giorgio: nesneler farklı sınıflardandır, listiçeriğini değiştiren yöntemler uygular, intyapmaz. [] olan bir değişken liste nesnesi ve i = []sağlayan ibu nesne bakın.
RemcoGerlich,

@Giorgio, Python'da değişmez bir liste diye bir şey yoktur. Listeler değiştirilebilir. Tamsayılar değil. Bir liste gibi ama değişmez bir şey istiyorsanız, bir demet düşünün. Neden olarak, hangi seviyede cevap vermek istediğinizi belli değil.
jonrsharpe

@RemcoGerlich: Farklı sınıfların farklı davrandıklarını biliyorum, neden bu şekilde uygulandıklarını anlamıyorum, yani bu seçimin arkasındaki mantığı anlamıyorum. Her +=iki tür için de aynı şekilde davranması (en az sürpriz prensibi) olan operatörü / yöntemi uygulardım: ya orjinal nesneyi değiştir ya da hem tam sayılar hem de listeler için değiştirilmiş bir kopya döndür.
Giorgio,

1
@Giorgio: +=Python'da şaşırtıcı olan kesinlikle doğru , ancak bahsettiğiniz diğer seçeneklerin de şaşırtıcı olacağı ya da en azından daha az pratik olduğu hissediliyordu (orijinal nesneyi değiştirmek en yaygın değerle yapılamaz) + = with, ints kullanırsınız ve tüm listeyi kopyalamak, mutasyona göre çok daha pahalıdır, Python açıkça söylenmediği sürece listeleri ve sözlükleri kopyalamaz. O zamanlar çok büyük bir tartışma oldu.
RemcoGerlich,

1

Buradaki döngü biraz alakasız. Fonksiyon parametreleri ya da argümanları gibi, bunun için bir for döngüsü oluşturarak aslında sadece fantezi görünümlü bir atama.

Tamsayılar değişmezdir. Onları değiştirmenin tek yolu yeni bir tamsayı oluşturmak ve onu asıl ile aynı isme atamaktır.

Python'un doğrudan C'lere (şaşırtıcı şekilde verilen CPython'un PyObject * işaretçileri) üzerine harita ataması için semantiği, sadece her şeyin bir işaretçi olduğu ve ancak çift işaretçilere sahip olmanıza izin verilmeyen uyarılar . Aşağıdaki kodu göz önünde bulundurun:

a = 1
b = a
b += 1
print(a)

Ne oluyor? Basar 1. Niye ya? Aslında aşağıdaki C koduna kabaca eşdeğerdir:

i64* a = malloc(sizeof(i64));
*a = 1;
i64* b = a;
i64* tmp = malloc(sizeof(i64));
tmp = *b + 1;
b = tmp;
printf("%d\n", *a);

C kodunda, değerinin atamamen etkilenmediği açıktır .

Neden listelerin işe yaradığı görülüyorsa, cevap temelde sadece aynı adaya atadığınızdır. Listeler değiştirilebilir. Adlandırılmış nesnenin kimliği a[0]değişecek, ancak a[0]yine de geçerli bir ad. Bunu aşağıdaki kodla kontrol edebilirsiniz:

x = 1
a = [x]
print(a[0] is x)
a[0] += 1
print(a[0] is x)

Ancak, bu listeler için özel değildir. a[0]Bu kodla değiştirin yve aynı sonucu elde edin.

Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.