[* A] 'nın aşırı yer değiştirmesine neden olan nedir?


136

Görünüşe göre list(a)değil, overallocate does [x for x in a]bazı noktalarda overallocates ve [*a]overallocates her zaman ?

N = 100'e kadar boyutlar

Burada, n'den 0'a kadar olan boyutlar ve üç yöntem için elde edilen boyutlar bayt cinsinden verilmiştir:

0 56 56 56
1 64 88 88
2 72 88 96
3 80 88 104
4 88 88 112
5 96 120 120
6 104 120 128
7 112 120 136
8 120 120 152
9 128 184 184
10 136 184 192
11 144 184 200
12 152 184 208

Böyle Bilgisayarlı, repl.it de reproducable Python 3. kullanarak, 8 :

from sys import getsizeof

for n in range(13):
    a = [None] * n
    print(n, getsizeof(list(a)),
             getsizeof([x for x in a]),
             getsizeof([*a]))

Peki: Bu nasıl çalışıyor? [*a]Aşırı nasıl konumlanır? Aslında, verilen girdiden sonuç listesi oluşturmak için hangi mekanizmayı kullanıyor? Üzerinde bir yineleyici ave benzeri bir şey kullanıyor list.appendmu? Kaynak kodu nerede?

( Görüntüleri üreten veri ve kodlarla birlikte çalışın .)

Daha küçük n'ye yakınlaştırma:

N = 40'a kadar boyutlar

Daha büyük n'ye yakınlaştırma:

N = 1000'e kadar boyutlar


1
Fwiw, test durumlarınızı genişletirken, liste kavrayışının bir döngü yazma ve her öğeyi listeye ekleme [*a]gibi görünmesine rağmen extend, boş bir listede olduğu gibi görünüyor .
jdehesa

4
Her biri için oluşturulan bayt koduna bakmak yardımcı olabilir. list(a)tamamen C olarak çalışır; dahili tampon düğümü, yinelendiğinde düğüm tarafından tahsis edebilir a. [x for x in a]sadece LIST_APPENDçok kullanır , bu yüzden normal bir listenin normal "biraz fazla konumlandırın, gerektiğinde yeniden tahsis et" şeklini izler. [*a]kullanır BUILD_LIST_UNPACK, hangi ... Görünüşe göre her zaman aşırı tahsis dışında ne olduğunu bilmiyorum :)
chepner

2
Ayrıca, Python 3.7'de , aynı list(a)ve [*a]aynı görünüyor ve her ikisine kıyasla aşırı konumlandırılmış [x for x in a], bu yüzden ... sys.getsizeofburada kullanmak için doğru araç olmayabilir.
chepner

7
@ chepner Bence sys.getsizeofdoğru araç, sadece list(a)overallocate için kullanılan gösterir . Aslında Python 3.8'de Neler Yeni : "Liste yapıcısı fazla [[]] konumlandırmaz" .
Stefan Pochmann

5
@ chepner: Bu 3.8'de düzeltilen bir hataydı ; yapıcı aşırı konumlandırması gerekmiyor.
ShadowRanger

Yanıtlar:


81

[*a] dahili olarak C eşdeğerini yapıyor :

  1. Yeni, boş bir hale getirin list
  2. Aramak newlist.extend(a)
  3. İade list.

Testinizi şu şekilde genişletirseniz:

from sys import getsizeof

for n in range(13):
    a = [None] * n
    l = []
    l.extend(a)
    print(n, getsizeof(list(a)),
             getsizeof([x for x in a]),
             getsizeof([*a]),
             getsizeof(l))

Çevrimiçi deneyin!

için sonuçları görürsünüz getsizeof([*a])ve l = []; l.extend(a); getsizeof(l)aynıdır.

Bu genellikle yapılacak doğru şeydir; ne zaman extendgenelleştirilmiş açma için benzer genellikle daha sonra eklemeyi bekliyoruz ing ve, birden şeyler birbiri ardına eklenecektir olduğu varsayılır. [*a]normal durum değildir; Python, list( [*a, b, c, *d]) öğesine birden fazla öğe veya yinelenebilir öğe eklendiğini varsayar ; bu nedenle genel konum belirleme, genel durumda iş tasarrufu sağlar.

Aksine, listtek, önceden belirlenmiş bir tekrarlanabilir (ile list()) bir yapı, kullanım sırasında büyüyemez veya küçülemez ve aksi kanıtlanana kadar aşırı konum belirleme erken olur; Python kısa süre önce yapıcıyı bilinen boyuttaki girdiler için bile aşırı konumlandırmaya neden olan bir hatayı düzeltti .

Anlamalara gelince list, bunlar tekrarlanan appends'lere etkili bir şekilde eşdeğerdir , bu nedenle bir seferde bir öğe eklerken normal aşırı büyüme büyüme paterninin nihai sonucunu görüyorsunuz.

Açık olmak gerekirse, bunların hiçbiri bir dil garantisi değildir. CPython bunu böyle uygular. Python dil spesifikasyonu genel olarak belirli büyüme örüntüleri ile ilgisizdir list(amortismanları O(1) appendve sonları garanti popetmenin yanı sıra). Yorumlarda belirtildiği gibi, spesifik uygulama 3.9'da tekrar değişir; etkilemezken [*a], eskiden " tuplemünferit eşyaların geçici olarak inşa edilmesi ve daha sonra " extendile tuplebirden fazla uygulama haline geldiği diğer durumları etkileyebilir LIST_APPEND.


4
@ StefanPochmann: Kodu daha önce okumuştum (bu yüzden bunu zaten biliyordum). Bu, bayt kodu işleyicisidirBUILD_LIST_UNPACK_PyList_Extend , çağrının C eşdeğeri olarak kullanır extend(yöntem araması yerine yalnızca doğrudan). Onlar bir tupleaçma ile inşa yolları ile birleştirdiler ; tupleparça parça bina için güzel bir şekilde konumlandırmayın, bu yüzden her zaman bir list(aşırı konumlandırmadan yararlanmak için) paketini açarlar ve tuplesonunda istenen şey bu haline dönüşürler .
ShadowRanger

4
Not Bu o görünüşte 3.9 maddesinde değişiklik yapı (ayrı baytkodlarına ile yapılır, BUILD_LIST, LIST_EXTEND, paketten her şey için LIST_APPENDbütün oluşturmadan önce yığın yüklemek yerine her şeyin tek öğeler için) list(o izin veren bir tek bayt kodu talimat derleyici uygulamak gibi, hepsi bir arada talimat izin vermedi optimizasyonlar için [*a, b, *c]olduğu gibi LIST_EXTEND, LIST_APPEND, LIST_EXTEND/ o sarmak gerek w bbir tek içinde tupleihtiyaçlarını karşılamak için BUILD_LIST_UNPACK).
ShadowRanger

18

Ne olduğunu tam olarak gösteren resim , diğer cevaplar ve yorumlara dayanarak (özellikle ShadowRanger'ın cevabı , bunun neden böyle yapıldığını açıklar ).

BUILD_LIST_UNPACKKullanılan şovların sökülmesi :

>>> import dis
>>> dis.dis('[*a]')
  1           0 LOAD_NAME                0 (a)
              2 BUILD_LIST_UNPACK        1
              4 RETURN_VALUE

Yani işlenir oldu içindeceval.c boş bir liste oluşturur ve (bunu uzandığı, a):

        case TARGET(BUILD_LIST_UNPACK): {
            ...
            PyObject *sum = PyList_New(0);
              ...
                none_val = _PyList_Extend((PyListObject *)sum, PEEK(i));

_PyList_Extend kullanır list_extend :

_PyList_Extend(PyListObject *self, PyObject *iterable)
{
    return list_extend(self, iterable);
}

Hangi çağrılar list_resizeölçülerinin toplamından ile :

list_extend(PyListObject *self, PyObject *iterable)
    ...
        n = PySequence_Fast_GET_SIZE(iterable);
        ...
        m = Py_SIZE(self);
        ...
        if (list_resize(self, m + n) < 0) {

Ve bu genel olarak şu şekilde konumlanır :

list_resize(PyListObject *self, Py_ssize_t newsize)
{
  ...
    new_allocated = (size_t)newsize + (newsize >> 3) + (newsize < 9 ? 3 : 6);

Hadi kontrol edelim. Beklenen nokta sayısını yukarıdaki formülle hesaplayın ve beklenen bayt boyutunu 8 ile çarparak hesaplayın (burada 64 bit Python kullanıyorum) ve boş bir listenin bayt boyutunu (yani, bir liste nesnesinin sabit yükü) :

from sys import getsizeof
for n in range(13):
    a = [None] * n
    expected_spots = n + (n >> 3) + (3 if n < 9 else 6)
    expected_bytesize = getsizeof([]) + expected_spots * 8
    real_bytesize = getsizeof([*a])
    print(n,
          expected_bytesize,
          real_bytesize,
          real_bytesize == expected_bytesize)

Çıktı:

0 80 56 False
1 88 88 True
2 96 96 True
3 104 104 True
4 112 112 True
5 120 120 True
6 128 128 True
7 136 136 True
8 152 152 True
9 184 184 True
10 192 192 True
11 200 200 True
12 208 208 True

Hariç Maçlar n = 0hangi list_extendaslında kısayolları bu maçları da öyle aslında:

        if (n == 0) {
            ...
            Py_RETURN_NONE;
        }
        ...
        if (list_resize(self, m + n) < 0) {

8

Bunlar CPython yorumlayıcısının uygulama ayrıntıları olacaktır ve bu nedenle diğer tercümanlar arasında tutarlı olmayabilir.

Bununla birlikte, anlama ve list(a)davranışların buraya nereden geldiğini görebilirsiniz:

https://github.com/python/cpython/blob/master/Objects/listobject.c#L36

Özellikle anlama için:

 * The growth pattern is:  0, 4, 8, 16, 25, 35, 46, 58, 72, 88, ...
...

new_allocated = (size_t)newsize + (newsize >> 3) + (newsize < 9 ? 3 : 6);

Bu hatların hemen altında, list_preallocate_exactarama yaparken kullanılanlar vardır list(a).


1
[*a]tek tek öğeleri teker teker eklemez. Kendi özel bayt koduna sahiptir, bu da toplu ekleme yapar extend.
ShadowRanger

Yakaladım - sanırım bu kadar yeterince kazmadım. Bölüm kaldırıldı[*a]
Randy
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.