"Verim" gibi bir jeneratör dili tesisi olması iyi bir fikir midir?


10

PHP, C #, Python ve muhtemelen birkaç başka dilde yieldoluşturucu işlevleri oluşturmak için kullanılan bir anahtar kelime vardır.

PHP'de: http://php.net/manual/en/language.generators.syntax.php

Python'da: https://www.pythoncentral.io/python-generators-and-yield-keyword/

C # dilinde : https://docs.microsoft.com/tr-tr/dotnet/csharp/language-reference/keywords/yield

Bir dil özelliği / tesisi olarak yieldbazı sözleşmeleri ihlal ettiğinden endişe duyuyorum . Bunlardan biri, bahsettiğim şey "kesinlik" tir. Her aradığınızda farklı bir sonuç döndüren bir yöntemdir. Normal bir jeneratör olmayan fonksiyon ile onu çağırabilirsiniz ve aynı giriş verilirse aynı çıkışı döndürür. Verim ile, dahili durumuna bağlı olarak farklı çıktılar döndürür. Bu nedenle, önceki durumunu bilmeden, rastgele oluşturma işlevini çağırırsanız, belirli bir sonuç döndürmesini bekleyemezsiniz.

Böyle bir işlev dil paradigmasına nasıl uyar? Gerçekten herhangi bir sözleşmeyi bozuyor mu? Bu özelliğe sahip olmak ve kullanmak iyi bir fikir mi? (neyin iyi neyin kötü olduğuna dair bir örnek vermek, bir gotozamanlar birçok dilin bir özelliğiydi ve hala da öyleydi, ancak zararlı olarak kabul edildi ve Java gibi bazı dillerden silindi). Programlama dili derleyicilerinin / tercümanlarının böyle bir özelliği uygulamak için herhangi bir kuralı aşması gerekir mi, örneğin, bir dilin bu özelliğin çalışması için çoklu iş parçacığı uygulaması mı gerekiyor, yoksa diş teknolojisi olmadan yapılabilir mi?


4
yieldaslında bir devlet motorudur. Her seferinde aynı sonucu döndürmek anlamına gelmez. Ne olacak mutlak bir kesinlikle yapmak bir enumerable içinde onu çağıran her zaman bir sonraki öğeyi döndürmek olduğunu. İplikler gerekli değildir; mevcut durumu korumak için bir kapatma işlemine (az ya da çok) ihtiyacınız vardır.
Robert Harvey

1
"Kesinlik" kalitesine gelince, aynı giriş dizisi göz önüne alındığında, yineleyiciye yapılan bir dizi çağrı, tam olarak aynı öğeleri tam olarak aynı sırada verecektir.
Robert Harvey

4
C ++ 'nın Python gibi bir yield anahtar kelimesi olmadığından, sorularınızın çoğunun nereden geldiğinden emin değilim . Statik bir yöntemi var std::this_thread::yield(), ancak bu bir anahtar kelime değil. Bu nedenle, this_threadhemen hemen her çağrıyı önler, bu da genel olarak kontrol akışını sağlama ile ilgili bir dil özelliği değil, sadece iş parçacıkları vermek için bir kütüphane özelliği olduğunu oldukça açık hale getirir.
Ixrec

bağlantı C # olarak güncellendi, biri C ++ için kaldırıldı
Dennis

Yanıtlar:


16

Önce uyarılar - C # en iyi bildiğim dildir ve yielddiğer dillere çok benzeyen bir dil yieldolsa da, farkında olmadığım küçük farklılıklar olabilir.

Bir dil özelliği / tesisi olarak verimin bazı sözleşmeleri ihlal ettiğinden endişe duyuyorum. Bunlardan biri, bahsettiğim şey "kesinlik" dir. Her aradığınızda farklı bir sonuç döndüren bir yöntemdir.

Saçma. Eğer Do gerçekten beklemek Random.Nextya Console.ReadLine aynı sonucu yeniden diyoruz her zaman dönmek için? Dinlenme çağrılarına ne dersiniz? Doğrulama? Öğe bir koleksiyondan alınsın mı? Saf olmayan her türlü (iyi, kullanışlı) fonksiyon vardır.

Böyle bir işlev dil paradigmasına nasıl uyar? Gerçekten herhangi bir sözleşmeyi bozuyor mu?

Evet, yieldgerçekten kötü oynuyor try/catch/finallyve izin verilmiyor ( https://blogs.msdn.microsoft.com/ericlippert/2009/07/16/iterator-blocks-part-three-why-no-yield-in-finally/ for Daha fazla bilgi).

Bu özelliğe sahip olmak ve kullanmak iyi bir fikir mi?

Bu özelliğe sahip olmak kesinlikle iyi bir fikirdir. C # LINQ gibi şeyler gerçekten güzel - koleksiyonları tembel olarak değerlendirmek büyük bir performans avantajı yieldsağlar ve bu tür şeylerin, kodun bir kısmında elle haddelenmiş bir yineleyicinin yaptığı hataların bir kısmıyla yapılmasına izin verir.

Bununla birlikte yield, LINQ stili toplama işleminin dışında bir ton kullanım yok . Doğrulama işlemi, zamanlama oluşturma, rasgeleleştirme ve birkaç başka şey için kullandım, ancak çoğu geliştiricinin bunu hiç kullanmadığını (veya yanlış kullandığını) umuyorum.

Programlama dili derleyicileri / tercümanları böyle bir özelliği uygulamak için herhangi bir kuraldan kurtulmak zorunda mıdır, örneğin, bir dilin bu özelliğin çalışması için çoklu iş parçacığı uygulaması mı gerekiyor, yoksa diş teknolojisi olmadan yapılabilir mi?

Tam olarak değil. Derleyici, bir sonraki çağrıldığında yeniden başlayabilmesi için nerede durduğunu takip eden bir durum makine yineleyicisi oluşturur. Kod oluşturma süreci, Devam Etme Stili'ne benzer bir şey yapar; burada koddan sonraki kod yieldkendi bloğuna çekilir (ve varsa yields, başka bir alt blok vb.). Bu, İşlevsel Programlamada daha sık kullanılan iyi bilinen bir yaklaşımdır ve ayrıca C # 'ın async / await derlemesinde de görülür.

Diş çekme gerekmez, ancak çoğu derleyicide kod oluşturma için farklı bir yaklaşım gerektirir ve diğer dil özellikleriyle bazı çatışmaları vardır.

Sonuçta, yieldbelirli bir sorun alt kümesine gerçekten yardımcı olan nispeten düşük bir etki özelliğidir.


C # 'ı hiç ciddiye kullanmadım ama bu yieldanahtar kelime coroutines'e benziyor, evet veya farklı bir şey mi? Eğer öyleyse keşke C de olsaydı! Böyle bir dil özelliği ile yazmak çok daha kolay olurdu en azından bazı iyi kod bölümleri düşünebilirsiniz.

2
@ DrunkCoder - benzer, ama bazı sınırlamalar ile, anladığım kadarıyla.
telastin

1
Ayrıca verimin yanlış kullanıldığını görmek istemezsiniz. Bir dilin özellikleri ne kadar fazla olursa, o dilde kötü yazılmış bir program bulma olasılığınız o kadar artar. Ulaşılabilir bir dil yazmak için doğru yaklaşımın hepsini size atmak ve hangi sopaların olduğunu görmek olup olmadığından emin değilim.
Neil

1
@ DrunkCoder: Yarı koroutinlerin sınırlı bir sürümüdür. Aslında, derleyici tarafından bir dizi yöntem çağrısı, sınıf ve nesneye genişleyen sözdizimsel bir desen olarak ele alınır. (Temel olarak, derleyici alanlardaki geçerli bağlamı yakalayan bir devam nesnesi oluşturur.) Koleksiyonlar için varsayılan uygulama yarı koroutindir, ancak derleyicinin kullandığı "sihirli" yöntemleri aşırı yükleyerek davranışı özelleştirebilirsiniz. Örneğin , dile eklenmeden async/ awaiteklenmeden önce birisi bunu kullanarak uyguladı yield.
Jörg W Mittag

1
@Neil Genellikle hemen hemen her programlama dili özelliğini kötüye kullanmak mümkündür. Söyledikleriniz doğru olsaydı, C'yi Python veya C # 'dan kötü bir şekilde programlamak çok daha zor olurdu, ancak bu diller programcıları çok kolay olan hataların çoğundan koruyan çok sayıda araca sahip olduğu için durum böyle değil Gerçekte, kötü programların nedeni kötü programcılardır - bu oldukça dil bilincine sahip olmayan bir sorundur.
Ben Cottrell

12

Jeneratör dili olanağına sahip olmak yieldiyi bir fikir mi?

Buna Python perspektifinden vurgulu bir evet ile cevap vermek istiyorum , bu harika bir fikir .

Öncelikle sorunuzdaki bazı soruları ve varsayımları ele alarak başlayacağım, sonra jeneratörlerin yaygınlığını ve daha sonra Python'daki makul olmayan yararlılıklarını göstereceğim.

Normal bir jeneratör olmayan fonksiyon ile onu çağırabilirsiniz ve aynı giriş verilirse aynı çıkışı döndürür. Verim ile, dahili durumuna bağlı olarak farklı çıktılar döndürür.

Bu yanlış. Nesneler üzerindeki yöntemler, kendi iç durumlarıyla birlikte işlevler olarak düşünülebilir. Python'da, her şey bir nesne olduğundan, aslında bir nesneden bir yöntem alabilir ve bu yöntemi (bu nesnenin geldiği nesneye bağlı olduğu için) iletebilirsiniz, böylece durumunu hatırlar).

Diğer örnekler arasında kasıtlı olarak rastgele işlevlerin yanı sıra ağ, dosya sistemi ve terminal gibi giriş yöntemleri yer alır.

Böyle bir işlev dil paradigmasına nasıl uyar?

Dil paradigması birinci sınıf işlevler gibi şeyleri destekliyorsa ve jeneratörler Yinelenebilir protokol gibi diğer dil özelliklerini destekliyorsa, sorunsuz bir şekilde uyuyorlar.

Gerçekten herhangi bir sözleşmeyi bozuyor mu?

Hayır. Dilde pişirildiğinden, sözleşmeler etrafında inşa edilir ve jeneratör kullanımını içerir (veya gerektirir!).

Programlama dili derleyicileri / tercümanları böyle bir özelliği uygulamak için herhangi bir kuraldan kurtulmak zorunda mıdır?

Diğer özelliklerde olduğu gibi, derleyicinin de özelliği destekleyecek şekilde tasarlanması gerekir. Python durumunda, işlevler halihazırda durumu olan nesnelerdir (varsayılan argümanlar ve işlev ek açıklamaları gibi).

bir dilin bu özelliğin çalışması için çoklu iş parçacığı uygulaması gerekiyor mu, yoksa diş teknolojisi olmadan yapılabilir mi?

İlginç gerçek: Varsayılan Python uygulaması diş açmayı hiç desteklemez. Global Tercüman Kilidi (GIL) özelliğine sahiptir, bu nedenle Python'un farklı bir örneğini çalıştırmak için ikinci bir işlem başlatmadıysanız aslında hiçbir şey eşzamanlı olarak çalışmaz.


not: örnekler Python 3'te

Verimin Ötesinde

İken yieldanahtar bir jeneratör haline getirmek için herhangi bir işlevde kullanılabilir, bu biri yapmak için tek yol değildir. Python, bir jeneratörü başka bir yinelenebilir (diğer jeneratörler dahil) açısından açıkça ifade etmenin güçlü bir yolu olan Generator Expressions'a sahiptir.

>>> pairs = ((x,y) for x in range(10) for y in range(10) if y >= x)
>>> pairs
<generator object <genexpr> at 0x0311DC90>
>>> sum(x*y for x,y in pairs)
1155

Gördüğünüz gibi, sadece sözdizimi temiz ve okunabilir değil, aynı zamanda sumkabul üreteçleri gibi yerleşik işlevler de vardır .

İle

İle ifadesi için Python Geliştirme Önerisine bakın . Diğer dillerde bir With ifadesinden beklediğinizden çok farklı. Standart kütüphaneden biraz yardım alarak, Python'un jeneratörleri onlar için bağlam yöneticisi olarak güzel çalışır.

>>> from contextlib import contextmanager
>>> @contextmanager
def debugWith(arg):
        print("preprocessing", arg)
        yield arg
        print("postprocessing", arg)


>>> with debugWith("foobar") as s:
        print(s[::-1])


preprocessing foobar
raboof
postprocessing foobar

Tabii ki, şeyleri yazdırmak burada yapabileceğiniz en sıkıcı şeyle ilgilidir, ancak görünür sonuçlar gösterir. Daha ilginç seçenekler, kaynakların otomatik yönetimi (dosyaları açma / kapatma / akışlar / ağ bağlantıları), eşzamanlılık için kilitleme, bir işlevi geçici olarak sarma veya değiştirme ve verileri açma ve yeniden sıkıştırma işlemlerini içerir. Arama işlevleri kodunuza kod enjekte etmeye benziyorsa, ifadelerle birlikte kodunuzun bazı bölümlerini diğer koda sarmak gibidir. Ne kadar kullanırsanız kullanın, bu bir dil yapısına kolay bir kancanın sağlam bir örneğidir. Verim tabanlı üreticiler, içerik yöneticileri oluşturmanın tek yolu değildir, ancak kesinlikle uygun olanlardır.

Kısmi Tükenme

Python'daki döngüler ilginç bir şekilde çalışır. Aşağıdaki biçime sahiptirler:

for <name> in <iterable>:
    ...

İlk olarak, çağırdığım ifade <iterable>tekrarlanabilir bir nesne elde etmek için değerlendirilir. İkincisi, yinelenebilir onu __iter__çağırdı ve ortaya çıkan yineleyici perde arkasında saklandı. Onaltılık, __next__koyduğunuz isme bağlanacak bir değer elde etmek için yineleyiciye çağrılır <name>. Çağrısı kadar bu adımı tekrar __next__bir atar StopIteration. İstisna, for döngüsü tarafından yutulur ve yürütme oradan devam eder.

Jeneratörlere geri dönme: __iter__bir jeneratörü çağırdığınızda , sadece kendini döndürür.

>>> x = (a for a in "boring generator")
>>> id(x)
51502272
>>> id(x.__iter__())
51502272

Bunun anlamı, bir şeyi yinelemeyi onunla yapmak istediğiniz şeyden ayırabilir ve bu davranışı ortada değiştirebilirsiniz. Aşağıda, aynı jeneratörün iki döngüde nasıl kullanıldığına dikkat edin ve ikincisinde ilkinden kaldığı yerden çalışmaya başlar.

>>> generator = (x for x in 'more boring stuff')
>>> for letter in generator:
        print(ord(letter))
        if letter > 'p':
                break


109
111
114
>>> for letter in generator:
        print(letter)


e

b
o
r
i
n
g

s
t
u
f
f

Tembel Değerlendirme

Jeneratörlere göre listelerle karşılaştırıldığında aşağı taraflardan biri, bir jeneratörde erişebileceğiniz tek şey, çıkan bir sonraki şeydir. Önceki sonuçlarda olduğu gibi geri gidemez veya ara sonuçlardan geçmeden bir sonraki sonuca atlayamazsınız. Bunun üst tarafı, bir jeneratörün eşdeğer listesine kıyasla neredeyse hiç bellek alamayacağıdır.

>>> import sys
>>> sys.getsizeof([x for x in range(10000)])
43816
>>> sys.getsizeof(range(10000000000))
24
>>> sys.getsizeof([x for x in range(10000000000)])
Traceback (most recent call last):
  File "<pyshell#10>", line 1, in <module>
    sys.getsizeof([x for x in range(10000000000)])
  File "<pyshell#10>", line 1, in <listcomp>
    sys.getsizeof([x for x in range(10000000000)])
MemoryError

Jeneratörler de tembel olarak zincirlenebilir.

logfile = open("logs.txt")
lastcolumn = (line.split()[-1] for line in logfile)
numericcolumn = (float(x) for x in lastcolumn)
print(sum(numericcolumn))

Birinci, ikinci ve üçüncü satırlar her biri bir jeneratör tanımlar, ancak gerçek bir iş yapmayın. Son satır çağrıldığında, sum sayısal sütunu bir değer için sorar, sayısal sütun sütununun lastcolumn değerine ihtiyacı vardır, lastcolumn logfile'dan bir değer ister, bu da gerçekte dosyadan bir satır okur. Bu yığın, toplam ilk tamsayısını alana kadar gevşer. Ardından, işlem ikinci satır için tekrarlanır. Bu noktada, toplamın iki tamsayısı vardır ve bunları bir araya getirir. Üçüncü satırın henüz dosyadan okunmadığını unutmayın. Sum daha sonra sayısal kolondan (tamamen zincirin geri kalanından habersiz) değer istemeye ve sayısal sütun tükenene kadar bunları eklemeye devam eder.

Buradaki gerçekten ilginç kısım, satırların ayrı ayrı okunması, tüketilmesi ve atılmasıdır. Hiçbir zaman bellekteki tüm dosya bir kerede yok. Bu günlük dosyası bir terabaytsa ne olur? Sadece çalışır, çünkü her seferinde sadece bir satır okur.

Sonuç

Bu, Python'daki jeneratörlerin tüm kullanımlarının tam bir incelemesi değildir. Özellikle, sonsuz jeneratörleri, devlet makinelerini, değerleri geri aktarmayı ve koroutinlerle ilişkilerini atladım.

Temiz bir şekilde entegre edilmiş, kullanışlı bir dil özelliği olarak jeneratörlere sahip olabileceğinizi kanıtlamanın yeterli olduğuna inanıyorum.


6

Klasik OOP dillerine alışkınsanız, jeneratörler yielddeğişebilir ve değişken durum nesne seviyesinden ziyade fonksiyon seviyesinde yakalandığından.

"Kesinlik" sorunu yine de kırmızı bir ringa balığıdır. Genellikle referans şeffaflığı olarak adlandırılır ve temel olarak işlevin her zaman aynı argümanlar için aynı sonucu döndürdüğü anlamına gelir. Değişebilir duruma gelir gelmez, referans şeffaflığını kaybedersiniz. OOP'de nesneler genellikle değiştirilebilir duruma sahiptir, bu da yöntem çağrısının sonucunun sadece bağımsız değişkenlere değil, aynı zamanda nesnenin dahili durumuna da bağlı olduğu anlamına gelir.

Soru, değişebilir durumu nerede yakalayacağınızdır. Klasik bir OOP'de, değişken seviyesinde nesne düzeyinde bulunur. Ancak bir dil desteği kapanırsa, işlev düzeyinde değiştirilebilir durumunuz olabilir. Örneğin JavaScript'te:

function getCounter() {
   var cnt = 1;
   return function(){ return cnt++; }
}
var counter = getCounter();
counter() --> 1
counter() --> 2

Kısacası, yieldkapanışları destekleyen bir dilde doğaldır, ancak değişebilir durumun yalnızca nesne düzeyinde bulunduğu eski Java sürümü gibi bir dilde yer almaz .


Dil özelliklerinin bir spektrumu olsaydı, verim olabildiğince işlevsel olmaktan uzak olurdu. Bu mutlaka kötü bir şey değildir. OOP bir zamanlar çok moda ve daha sonra fonksiyonel programlama idi. Bunun, programın beklenmedik şekillerde davranmasını sağlayan fonksiyonel bir tasarımla verim gibi özelliklerin karıştırılması ve eşleştirilmesi anlamına geldiğini düşünüyorum.
Neil

0

Bence bu iyi bir özellik değil. Kötü bir özelliktir, çünkü öncelikle çok dikkatli bir şekilde öğretilmesi gerekir ve herkes bunu yanlış öğretir. İnsanlar, jeneratör işlevi ile jeneratör nesnesi arasında bir denge oluşturan "jeneratör" kelimesini kullanırlar. Soru şudur: Gerçek üretimi kim veya ne yapıyor?

Bu sadece benim düşüncem değil. Guido bile, bunu yönettiği PEP bülteninde, jeneratör işlevinin bir jeneratör değil, bir "jeneratör fabrikası" olduğunu kabul ediyor.

Bu biraz önemli, değil mi? Ancak, belgelerin% 99'unu okurken, jeneratör fonksiyonunun gerçek jeneratör olduğu izlenimini edinirsiniz ve ayrıca bir jeneratör nesnesine ihtiyacınız olduğu gerçeğini göz ardı etme eğilimindedirler.

Guido bu işlevler için "gen" yerine "def" yerine geçmeyi düşündü ve Hayır dedi. Ama yine de bunun yeterli olmayacağını savunuyorum. Gerçekten olmalı:

def make_gen(args)
    def_gen foo
        # Put in "yield" and other beahvior
    return_gen foo
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.