PHP'de verim ne anlama geliyor?


232

Son zamanlarda bu kod üzerinde tökezledim:

function xrange($min, $max) 
{
    for ($i = $min; $i <= $max; $i++) {
        yield $i;
    }
}

Bu yieldanahtar kelimeyi daha önce hiç görmedim . Aldığım kodu çalıştırmaya çalışırken

Ayrıştırma hatası: sözdizimi hatası, x satırında beklenmeyen T_VARIABLE

Peki bu yieldanahtar kelime nedir? PHP bile geçerli mi? Ve eğer öyleyse, nasıl kullanırım?

Yanıtlar:


355

Nedir yield?

yieldAnahtar kelime jeneratör işlevinden döner veriler:

Bir jeneratör fonksiyonunun kalbi verim anahtar kelimesidir. En basit haliyle, bir getiri ifadesi, bir dönüş ifadesine çok benzemektedir, tek fark, fonksiyonun yürütülmesini durdurmak ve geri dönmek yerine, bunun yerine jeneratör üzerinde döngü yapan koda bir değer sağlamak ve jeneratör fonksiyonunun yürütülmesini duraklatmasıdır.

Jeneratör işlevi nedir?

Bir jeneratör işlevi bir Yineleyici yazmak için daha kompakt ve etkili bir yoldur . Siz bir işlev (sizin tanımlamanızı sağlar xrangeolacak) hesaplanması ve geri dönüş değerleri ise sen edilir üzerinde döngü :

foreach (xrange(1, 10) as $key => $value) {
    echo "$key => $value", PHP_EOL;
}

Bu, aşağıdaki çıktıyı oluşturur:

0 => 1
1 => 2

9 => 10

Ayrıca kontrol edebilirsiniz $keyiçinde foreachkullanarak

yield $someKey => $someValue;

Jeneratör işlevinde, $someKeysizin için görünür istediğiniz şey olur $keyve $someValuedeğer olmak $val. Sorunun örneğinde bu $i.

Normal işlevler arasındaki fark nedir?

Şimdi bu çıktıyı elde etmek için neden sadece PHP'nin yerel rangeişlevini kullanmadığımızı merak edebilirsiniz . Ve haklısın. Çıktı aynı olurdu. Aradaki fark oraya nasıl ulaştığımız.

Kullandığımız zaman rangePHP, bunu çalıştırmak bellekte sayıların tüm dizi oluşturmak ve edecek returno tüm dizi için foreacho zaman ve çıkış değerleri üzerinden gidecek döngü. Başka bir deyişle, foreachdizi üzerinde çalışır. rangeFonksiyonu ve foreachsadece bir kez "konuşma". Postaya bir paket almak gibi düşün. Teslimatçı size paketi teslim edecek ve gidecektir. Ve sonra tüm paketi açıp orada ne varsa çıkarırsınız.

Oluşturucu işlevini kullandığımızda, PHP işleve adım atacak ve sonuna veya bir yieldanahtar kelimeye ulaşana kadar yürütecektir . A ile karşılaştığında yield, o zaman değeri ne olursa olsun dış döngüye dönecektir. Daha sonra jeneratör fonksiyonuna geri döner ve verimden devam eder. Senin bu yana xrangebir tutan fordöngü, bu çalıştırır ve kadar verim $maxulaşıldı. Bunu düşünün foreachve ping pong oynayan jeneratör.

Neden buna ihtiyacım var?

Açıkçası, jeneratörler bellek sınırları etrafında çalışmak için kullanılabilir. Ortamınıza bağlı olarak, bir range(1, 1000000)komut dosyası ölümcül olacak, bir jeneratör ile aynı iyi çalışır. Veya Wikipedia'nın belirttiği gibi:

Üreteçler, elde ettikleri değerleri sadece talep üzerine hesapladıkları için, bir kerede hesaplanması pahalı veya imkansız olan dizileri temsil etmek için kullanışlıdırlar. Bunlar, sonsuz dizileri ve canlı veri akışlarını içerir.

Jeneratörlerin de oldukça hızlı olması gerekiyor. Ancak hızlı konuştuğumuzda, genellikle çok az sayıda konuştuğumuzu unutmayın. Bu nedenle, şimdi bitmeden ve tüm kodlarınızı jeneratörleri kullanacak şekilde değiştirmeden önce, nerede mantıklı olduğunu görmek için bir kıyaslama yapın.

Jeneratörler için bir başka Kullanım Örneği asenkron koroutinlerdir. yieldAnahtar kelime yalnızca değerleri dönmez ama aynı zamanda onları kabul eder. Bununla ilgili ayrıntılar için, aşağıda bağlantı verilen iki mükemmel blog yayınına bakın.

Ne zamandan beri kullanabilirim yield?

Jeneratörler PHP 5.5'te kullanılmaya başlandı . yieldBu sürümden önce kullanılmaya çalışılması, anahtar kelimeyi izleyen koda bağlı olarak çeşitli ayrıştırma hatalarına neden olur. Bu koddan ayrıştırma hatası alırsanız PHP'nizi güncelleyin.

Kaynaklar ve ek okuma:


1
Bunun yeildgibi bir çözümün ne işe yaradığını açıklayınız
Mike

1
Ah, ve oluşturduğum bildirimler. Huh. Eh, bir yardımcı sınıf ile PHP> = 5.0.0 için Jeneratörler öykünme denedim ve evet, biraz daha az okunabilir, ama gelecekte kullanabilirsiniz. İlginç konu. Teşekkürler!
Mike

Okunabilir değil hafıza kullanımı! Üzerinden yineleme için kullanılan belleği karşılaştırın return range(1,100000000)ve for ($i=0; $i<100000000; $i++) yield $i
emix

@mike evet, bu benim cevabımda zaten açıklanmıştı. Diğer Mike'ın örnek hafızasında sorun yok çünkü sadece 10 değeri yineliyor.
Gordon

1
@Mike xrange ile ilgili bir sorun, statik sınırların kullanımının örneğin yuvalama için kullanışlılık olmasıdır (örneğin, n boyutlu bir manifold üzerinde arama veya örneğin jeneratörleri kullanarak özyinelemeli bir hızlı sıralama). Xrange döngülerini iç içe yerleştiremezsiniz çünkü sayacının yalnızca tek bir örneği vardır. Verim sürümü bu sorunla karşılaşmaz.
Shayne

43

Bu işlev verimi kullanıyor:

function a($items) {
    foreach ($items as $item) {
        yield $item + 1;
    }
}

neredeyse bu olmadan aynıdır:

function b($items) {
    $result = [];
    foreach ($items as $item) {
        $result[] = $item + 1;
    }
    return $result;
}

Tek fark a()bir jeneratörü döndürmesi ve b()sadece basit bir dizi . Her ikisini de yineleyebilirsiniz.

Ayrıca, birincisi tam bir dizi ayırmaz ve bu nedenle daha az bellek gerektirir.


2
resmi belgelerden notlar ekleyin: PHP 5'de bir jeneratör bir değer döndüremedi: bunu yapmak derleme hatasına neden olacaktır. Boş bir dönüş ifadesi bir jeneratör içindeki geçerli bir sözdizimiydi ve jeneratörü sonlandıracaktır. PHP 7.0'dan beri, bir jeneratör Generator :: getReturn () kullanılarak alınabilen değerleri döndürebilir. php.net/manual/en/language.generators.syntax.php
Programcı Dancuk

Basit ve özlü.
John Miller

24

basit örnek

<?php
echo '#start main# ';
function a(){
    echo '{start[';
    for($i=1; $i<=9; $i++)
        yield $i;
    echo ']end} ';
}
foreach(a() as $v)
    echo $v.',';
echo '#end main#';
?>

çıktı

#start main# {start[1,2,3,4,5,6,7,8,9,]end} #end main#

gelişmiş örnek

<?php
echo '#start main# ';
function a(){
    echo '{start[';
    for($i=1; $i<=9; $i++)
        yield $i;
    echo ']end} ';
}
foreach(a() as $k => $v){
    if($k === 5)
        break;
    echo $k.'=>'.$v.',';
}
echo '#end main#';
?>

çıktı

#start main# {start[0=>1,1=>2,2=>3,3=>4,4=>5,#end main#

Yani, işlevi kesintiye uğratmadan geri döner?
Lucas Bustamante

22

yieldanahtar kelime PHP 5.5 "jeneratör" tanımı için hizmet vermektedir. Tamam, o zaman jeneratör nedir nedir?

Php.net'ten:

Jeneratörler, Iterator arabirimini uygulayan bir sınıfı uygulama yükü veya karmaşıklığı olmadan basit yineleyicileri uygulamak için kolay bir yol sağlar.

Bir jeneratör, bellekte bir dizi oluşturmanıza gerek kalmadan bir dizi veri üzerinde yineleme yapmak için foreach kullanan kodu yazmanıza izin verir, bu da bellek sınırını aşmanıza neden olabilir veya oluşturmak için önemli miktarda işlem süresi gerektirebilir. Bunun yerine, normal bir işlevle aynı olan bir jeneratör işlevi yazabilirsiniz, ancak bir kez geri dönmek yerine, bir jeneratörün yinelenecek değerleri sağlamak için ihtiyaç duyduğu kadar fazla verim verebilmesi dışında.

Buradan: jeneratörler = jeneratörler, diğer fonksiyonlar (sadece basit fonksiyonlar) = fonksiyonlar.

Bu nedenle, şu durumlarda yararlıdır:

  • işleri basit (ya da basit şeyler) yapmanız gerekir;

    jeneratör Iterator arabirimini uygulamaktan çok daha basittir. diğer yandan, jeneratörler daha az işlevseldir. karşılaştırın .

  • BÜYÜK miktarda veri tasarrufu sağlamanız gerekir;

    aslında hafızayı kurtarmak için, her döngü yinelemesi için fonksiyonlar aracılığıyla gerekli verileri üretebiliriz ve yinelemeden sonra çöp kullanırız. burada ana noktalar - net kod ve muhtemelen performans. ihtiyaçlarınız için neyin daha iyi olduğunu görün.

  • ara değerlere bağlı olan bir dizi oluşturmanız gerekir;

    bu önceki düşüncenin kapsamıdır. jeneratörler fonksiyonlara kıyasla işleri kolaylaştırabilir. Fibonacci örneğini kontrol edin ve jeneratör olmadan sekans yapmaya çalışın. Ayrıca jeneratörler, en azından ara değişkenlerin yerel değişkenlerde depolanması nedeniyle daha hızlı çalışabilir;

  • performansı artırmanız gerekir.

    bazı durumlarda işlevlerden daha hızlı çalışabilirler (önceki faydaya bakın);


1
Jeneratörlerin nasıl çalıştığını anlamadım. bu sınıf yineleyici arabirimini uygular. bildiğim kadarıyla yineleyiciler sınıfları bir nesne üzerinde nasıl yineleme yapmak istediğimi yapılandırmama izin veriyor. örneğin ArrayIterator bir dizi veya nesne alır, böylece değerleri ve anahtarları yineleme sırasında değiştirebilirim. eğer yineleyiciler tüm nesne / dizi alırsanız o zaman jeneratör bellekte tüm dizi oluşturmak zorunda değil mi ???
user3021621

7

İle yieldkolayca tek bir işlev birden fazla görev arasındaki kesme noktalarını tanımlayabilirsiniz. Hepsi bu, bu konuda özel bir şey yok.

$closure = function ($injected1, $injected2, ...){
    $returned = array();
    //task1 on $injected1
    $returned[] = $returned1;
//I need a breakpoint here!!!!!!!!!!!!!!!!!!!!!!!!!
    //task2 on $injected2
    $returned[] = $returned2;
    //...
    return $returned;
};
$returned = $closure($injected1, $injected2, ...);

Görev1 ve görev2 oldukça ilişkiliyse, ancak başka bir şey yapmak için aralarında bir kesme noktasına ihtiyacınız vardır:

  • veritabanı satırlarının işlenmesi arasında boş bellek
  • bir sonraki göreve bağımlılık sağlayan, ancak geçerli kodu anlayarak ilgisiz olan diğer görevleri çalıştırın
  • zaman uyumsuz çağrılar yapmak ve sonuçları beklemek
  • ve bunun gibi ...

jeneratörler en iyi çözümdür, çünkü kodunuzu birçok kapanışa bölmeniz veya başka bir kodla karıştırmanız veya geri aramalar kullanmanız gerekmez. yield bir kesme noktası eklemek için ve bundan devam edebilirsiniz. hazırsanız kesme noktası.

Jeneratör olmadan kesme noktası ekleyin:

$closure1 = function ($injected1){
    //task1 on $injected1
    return $returned1;
};
$closure2 = function ($injected2){
    //task2 on $injected2
    return $returned1;
};
//...
$returned1 = $closure1($injected1);
//breakpoint between task1 and task2
$returned2 = $closure2($injected2);
//...

Jeneratörlerle kesme noktası ekleyin

$closure = function (){
    $injected1 = yield;
    //task1 on $injected1
    $injected2 = (yield($returned1));
    //task2 on $injected2
    $injected3 = (yield($returned2));
    //...
    yield($returnedN);
};
$generator = $closure();
$returned1 = $generator->send($injected1);
//breakpoint between task1 and task2
$returned2 = $generator->send($injected2);
//...
$returnedN = $generator->send($injectedN);

not: Jeneratörlerle hata yapmak kolaydır, bu yüzden bunları uygulamadan önce daima birim testleri yazın! note2: Jeneratörleri sonsuz bir döngüde kullanmak sonsuz uzunlukta bir kapak yazmak gibidir ...


4

Yukarıdaki cevapların hiçbiri, sayısal olmayan üyelerin doldurduğu devasa dizileri kullanan somut bir örnek göstermemektedir. explode()Büyük bir .txt dosyasında (kullanım durumumda 262 MB) tarafından oluşturulan bir dizi kullanan bir örnek :

<?php

ini_set('memory_limit','1000M');

echo "Starting memory usage: " . memory_get_usage() . "<br>";

$path = './file.txt';
$content = file_get_contents($path);

foreach(explode("\n", $content) as $ex) {
    $ex = trim($ex);
}

echo "Final memory usage: " . memory_get_usage();

Çıktı şuydu:

Starting memory usage: 415160
Final memory usage: 270948256

Şimdi yieldanahtar kelimeyi kullanarak benzer bir komut dosyasıyla karşılaştırın :

<?php

ini_set('memory_limit','1000M');

echo "Starting memory usage: " . memory_get_usage() . "<br>";

function x() {
    $path = './file.txt';
    $content = file_get_contents($path);
    foreach(explode("\n", $content) as $x) {
        yield $x;
    }
}

foreach(x() as $ex) {
    $ex = trim($ex);
}

echo "Final memory usage: " . memory_get_usage();

Bu komut dosyasının çıktısı şuydu:

Starting memory usage: 415152
Final memory usage: 415616

Açıkça bellek kullanımı tasarrufları dikkate değerdi ( ilk örnekte emMemoryUsage -----> ~ 270.5 MB , ikinci örnekte ~ 450B ).


3

Burada tartışılmaya değer ilginç bir yön referans olarak ortaya çıkmaktadır . Bir parametreyi fonksiyonun dışında yansıtılacak şekilde değiştirmemiz gerektiğinde, bu parametreyi referans olarak geçmeliyiz. Bunu jeneratörlere uygulamak için, jeneratörün &adına ve yinelemede kullanılan değişkene bir ve işareti yerleştiririz:

 <?php 
 /**
 * Yields by reference.
 * @param int $from
 */
function &counter($from) {
    while ($from > 0) {
        yield $from;
    }
}

foreach (counter(100) as &$value) {
    $value--;
    echo $value . '...';
}

// Output: 99...98...97...96...95...

Yukarıdaki örnek, foreachdöngü içindeki yinelenen değerlerin değiştirilmesinin $fromjeneratör içindeki değişkeni nasıl değiştirdiğini gösterir . Bunun nedeni $from, jeneratör adından önceki ve işareti nedeniyle referans ile verilmiş olmasıdır . Bu nedenle $value, foreachdöngü $fromiçindeki değişken üreteç fonksiyonundaki değişkene bir referanstır .


0

Aşağıdaki kod, tam yinelemeden sonra tam bir dizi döndüren geleneksel oluşturucu olmayan yaklaşımın aksine, bir jeneratörün kullanılmasının bir sonucu nasıl tamamladığını gösterir. Aşağıdaki jeneratörle, değerler hazır olduğunda döndürülür, bir dizinin tamamen doldurulmasını beklemeye gerek yoktur:

<?php 

function sleepiterate($length) {
    for ($i=0; $i < $length; $i++) {
        sleep(2);
        yield $i;
    }
}

foreach (sleepiterate(5) as $i) {
    echo $i, PHP_EOL;
}

Yani, php html kodu oluşturmak için verimi kullanmak mümkün değil mi? Gerçek bir ortamda faydaları bilmiyorum
Giuseppe Lodi Rizzini

@GiuseppeLodiRizzini bunu düşündüren nedir?
Brad Kent
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.