5 Mart 2025 Çarşamba

Ruby Temelleri 7

https://ujk-ujk.blogspot.com/2025/03/ruby-temelleri-7.html
İçindekiler +

 Selam, Ruby temel tekniklerine kaldığımız yerden devam ediyoruz. 



Singleton Sınıflar

Singleton (tekil) sınıfların sadece bir tane oluşum nesnesi olabilir. 

Ruby'de 3 çeşit nesne vardır

  • Class ve Module sınıflarının oluşumu olan sınıflar ve modüller
  • Sınıfların oluşum nesneleri
  • Singleton sınıflar


Her nesne, onun metodlarını içeren bir sınıfa sahiptir.

class Örnek
end

nesne = Örnek.new

p nesne.class   # => Örnek
p Örnek.class   # => Class
p Class.class   # => Class


Nesneler kendileri metod barındıramaz, sadece sınıfları barındırabilir. Fakat tekil sınıflar yardımıyla sadece o nesneye ait metodlar tanımlayabiliriz (yani metod gene nesne içnde değil , bir sınıf içinde tanımlanır). 

def nesne.foo
  :foo
end
p nesne.foo #=> :foo


foo metodu Ruby tarafından nesne nesnesinin singleton sınıfında tanımlanır. Diğer Örnek sınıfı oluşumları bu metod çağrısına cevap vermezler.

Ruby tekil sınıfları ihtiyaç olunca üretir. Onlara erişmeye kalkınca ya da yukarıdaki gibi metod eklemeye kalkınca Ruby otomatik olarak tekil sınıfını oluşturur. 



-- Tekil sınıflarda kalıtım

-- -- Altsınıf üretme aynı zamanda tekil sınıfın alt sınıfını üretir

class Örnek
end

p Örnek.singleton_class #=> #<Class:Örnek>

def Örnek.foo
  :örnek
end

class SubÖrnek < Örnek
end

p SubÖrnek.foo #=> :örnek

p SubÖrnek.singleton_class #=> #<Class:SubÖrnek>
p SubÖrnek.singleton_class.superclass #=> #<Class:Örnek>



-- -- Bir modülü include ya da extend etmek tekil sınıfı genişletmez

module ÖrnekModül
end

def ÖrnekModül.foo
  :foo
end

class Örnek
  extend ÖrnekModül
  include ÖrnekModül
end

Örnek.foo #=> NoMethodError: undefined method




-- Tekil sınıflar

Her nesne bir sınıfın oluşumudur. Ancak , tüm gerçek bu değil , Ruby'de her nesnenin ayrıca bir şekilde tekil sınıfı (singleton class) vardır. 

Bu sayede sadece bir nesneye özel metodlar tanımlanabilir. Tekil sınıf nesne ve onun üretildiği sınıf arasında bir yerdedir. Bu yüzden tekil sınıfta tanımlanan metodlara sadece ve sadce o nesneden erişilebilir 

nesne = Object.new

def nesne.özel_metod
  'Sadece bu nesne bu metoda cevap verir.'
end

p nesne.özel_metod
# => "Sadece bu nesne bu metoda cevap verir."

p Object.new.özel_metod rescue p $!
# => #<NoMethodError: undefined method `özel_metod' for #<Object:0x...>>


Yukarıdaki örnekte metod tanımlamasını define_singleton_method metodunu kullanarak da yapabiliriz. 

nesne.define_singleton_method :özel_metod do
  'Sadece bu nesne bu metoda cevap verir.'
end


Ya da daha da dolambaçlı yoldan, nesnenin tekil sınıfına metod tanımlaması göndererek.

# send kullanılmış çünkü 'define_method' metodu private
nesne.singleton_class.send :define_method, :özel_metod do
  "Şimdi metodu direk nesnenin tekil sınıfında tanımlıyorsunuz"
end


Singleton sınıf öncesinde Ruby'de metaclass adında bir yapı vardı ve ona erişmek için 

class << nesne
  self  # nesnenin singleton_class'ına referans olur
end

deyim yapısı kullanılırdı. 



-- Bir nesnenin tekil sınıfına erişmek

Aslında iki yöntemi de daha önceki örneklerde gördük

# metod kullanarak
nesne.singleton_class

# bu deyim yapısında self ifadesi ile
class << nesne
  self  
end



-- Ana sınıfın değişkenine tekil sınıftan erişmek

Tekil sınıflar bağlı oldukları nesnenin sınıf değişkenlerini sadece nesne içinde paylaşırlar. Bazı örnekler verelim.

class Örnek
  @@foo = :örnek
end

class Örnek2 < Örnek
  @@foo = :örnek2
end

def Örnek.foo
  class_variable_get :@@foo
end

p Örnek.foo #=> :örnek2


class Örnek
  def initialize
    @foo = 1
  end

  def foo
    @foo
  end
end
 
e = Örnek.new
 
e.instance_eval <<-BLOCK
  def self.increase_foo
    @foo += 1
  end
BLOCK
 
p e.increase_foo, e.foo #=> 2 2


Bloklar oluşum/sınıf değişkenlerine yakındır. Başka bir ortam içinde blok kullanarak oluşum veya sınıf değişkenine ulaşılamaz. class_eval'e bir string yollamak veya class_variable_get metodu kullanmak sorunu çözer.

class Foo
  @@foo = :foo
end

class Örnek
  @@foo = :örnek
 
  Foo.define_singleton_method :foo do
    @@foo
  end
end
 
p Foo.foo #=> :örnek


Bu hatalı sonuç veriyor ama bu

class Foo
  @@foo = :foo
end

class Örnek
  @@foo = :örnek
 
  Foo.class_eval <<-BLOCK
    def self.foo
      @@foo
    end
  BLOCK
end
 
p Foo.foo #=> :foo

doğru çalışıyor. Bu da sorunu çözer

class Foo
  @@foo = :foo
end

class Örnek
  @@foo = :örnek
 
  Foo.define_singleton_method :foo do
    class_variable_get :@@foo
  end
end
 
p Foo.foo #=> :foo




-- Tekil sınıflarda mesaj yayılması

Oluşum nesneleri metod barındırmaz onlar sadece veri barındırırlar. Ancak bir sınıf oluşumu olan her nesne bir tekil sınıfa sahiptir. 

Bi,r nesneye bir mesaj yollandığında (yani o nesnede bir metod çağrıldığında) Ruby ilk önce bu nesne için tanımlı bir tekil sınıf var mı? diye ve bu mesajı cevaplıyor mu? (yani orada bu metodun tanımı var mı) diye bakar. Bulamazsa nesnenin oluşturulduğu sınıf ve onun atalarına doğru mesaj yayılmaya devam eder. 

class Örnek
  def foo
    :örnek
  end
end

p Örnek.new.foo #=> :örnek

module ÖneEklenenModül
  def foo
    :önde
  end
end

class Örnek
  prepend ÖneEklenenModül
end

p Örnek.ancestors
#=> [ÖneEklenenModül, Örnek, Object, Kernel, BasicObject]
e = Örnek.new
p e.foo #=> :önde

def e.foo
  :tekil
end

p e.foo #=> :tekil 



-- Tekil sınıflarda monkey patching

Monkey patching bir kodun davranışını dinamik olarak değiştirmek anlamına geliyor. Bir tekil sınıfı tekrar açıp müdahale etmenin 3 yolu var:

  • Tekil sınıf üzerinde class_eval kullanmak
  • class << blok kullanmak
  • Nesnede metod tanımlamak için direk def ifadesi ile tekil sınıfa erişmek


class Örnek
end

Örnek.singleton_class.class_eval do
  def foo
    :foo
  end
end

p Örnek.foo #=> :foo


class Örnek
end

class << Örnek
  def bar
    :bar
  end
end
 
p Örnek.bar #=> :bar


class Örnek
end

def Örnek.baz
  :baz
end

p Örnek.baz #=> :baz


Her nesnenin kendi tekil sınıf vardır.

class Örnek
end

örn1 = Örnek.new
def örn1.foobar
  :foobar
end
p örn1.foobar #=> :foobar

örn2 = Örnek.new
örn2.foobar #=> NoMethodError





File I/O


-- Dosya ve giriş/çıkış işlemleri


-- -- Parametreler

FlagAnlamı
"r" Read-only, sadece okunabilir, dosyanın başından başlar (default mod)
"r+" Read-write, okuma-yazma, dosyanın başından başlar
"w" Write-only, sadece yazma, mevcut dosyayı sıfır uzunluğa çevirir, olmayanı yeni üretir
ve yazmak için açar
"w+" Read-write, okuma yazma, mevcut dosyayı sıfır uzunluğa çevirir, olmayanı yeni üretir
ve okumak-yazmak için açar
"a" Write-only, sadece yazma, mevcut dosyanın sonundan başlar, olmayanı yeni üretir
ve yazmak için açar
"a+" Read-write, okuma yazma, mevcut dosyanın sonundan başlar, olmayanı yeni üretir
ve okumak-yazmak için açar
"b" Binary dosya modu, Windows'ta EOL <-> CRLF dönüşmesini zorlar ve aksi
belirtilmedikçe encoding'i ASCII-8bit yapar.
(Bu flag sadece yukarıdaki flag'larla kullanılabilir.
Örneğin File.new("test.txt", "rb") ile test.txt dosyası sadece okunabilir modda
binary dosya olarak açılır.)
"t" Text dosya modu.
(Bu flag sadece yukarıdaki flag'larla kullanılabilir.
Örneğin File.new("test.txt", "wt") ile test.txt dosyası sadece yazılabilir modda
text dosyası olarak açılır.)



-- Bir dosya açmak, kapatmak

Dosyayı elle açıp kapatmak.

# new metodu ile
f = File.new("test.txt", "r") # okuma
f = File.new("test.txt", "w") # yazma
f = File.new("test.txt", "a") # ekleme (appending)

# open metodu ile
f = open("test.txt", "r")

# Çıkmadan dosyaları kapat
f.close


Blok kullanırsak dosya otomatik kapanır.

f = File.open("test.txt", "r") do |f|
  # f dosyası ile birşeyler yap
  puts f.read # örneğin içindekileri oku
end



-- Dosya içine yazı yazmak

Bir string dosya içine File sınıfı oluşumu nesne kullanarak yazılabilir.

file = File.new('tmp.txt', 'w')
file.write("NaNaNaNa\n")
file.write('Batman!\n')
file.close


File sınıfı ayrıca new ve close operasyonlarını open sınıf metoduyla birleştirir. 

File.open('tmp.txt', 'w') do |f|
  f.write("NaNaNaNa\n")
  f.write('Batman!\n')
end


Basit işlemlerde yazmak için direk olarak sınıf metodu olan File.write kullanılabilir. Not, bu default olarak dosyadakilerin üzerine yazar. 

File.write('tmp.txt', "NaNaNaNa\n" * 4 + 'Batman!\n')


File.write sınıf metodunda dosya açma modunu belirtmek için mode isimli bir anahtar ile ilave argüman olarak belirtmeliyiz.

File.write 'tmp.txt', "NaNaNaNa\n" * 4 + 'Batman!\n', mode: 'a'



-- Tek karakter bir girdi almak

gerts.chomp ifadesinden farklı olarak bu yöntem Enter basılmasını beklemez. Öncelikle stdlib programımıza dahil edilmelidir.

require 'io/console'


Sonra bir yardımcı metod yazabiliriz.

def get_char
  input = STDIN.getch
  control_c_code = "\u0003"
  exit(1) if input == control_c_code
  input
end
p get_char


Ctrl+c tuş kombinasyonunda programdan çıkmayı eklemezsek program içinde beklerken takılma ihtimali olacağını ve görev yöneticisi falan açmak zorunda olacağımızı unutmayalım.



-- STDIN'den değer okumak

# STDIN'den iki sayı oku, aralarında newline olsun, ve sonucu göster
sayı1 = gets
sayı2 = gets
puts sayı1.to_i + sayı2.to_i
## çalıştırıp değer gir:         $ ruby a_plus_b.rb
## veya değerleri verek çağır    $ echo -e "1\n2" | ruby a_plus_b.rb



-- ARGV ile argümanları okumak

Komut satırında programımızı çalıştırırken verilen argümanları ARGV özel değişkeninde görebiliriz.

sayı1 = ARGV[0]
sayı2 = ARGV[1]
puts sayı1.to_i + sayı2.to_i
## çalıştırmak için konsolda: $ ruby a_plus_b.rb 1 2






Queue nesnesi

Queue nesnesi bir yığın nesnesi içine yerleştirilen nesneler, özellikle paralel işlemlerde veriye sağlıklı erişmek amacıyla kullanılır. Deyimleri şunlar:

q = Queue.new
q.push nesne
q << nesne # 'push' ile aynı
q.pop #=> nesne




-- Çok çalışan , tek gider

Birçok çalışandan gelen veriyi toplamak istiyoruz. Öncelikle bir Queue nesnesi tanımlarız.

gider = Queue.new

Sonra 16 çalışan aynı gidere rastgele sayılar gönderir. 

(1..16).to_a.map do
  Thread.new do
    gider << rand(1..100)
  end
end.map(&:join)

Veriyi almak için Queue nesnesini array'e çeviririz.

veri = [].tap { |a| a << gider.pop until gider.empty? }



-- Bir Queue nesnesini Array nesnesine dönüştürmek


q = Queue.new
q << 1
q << 2

a = Array.new
a << q.pop until q.empty?

Ya da tek satır olarak

[].tap { |array| array << q.pop until q.empty? }



-- Çok çalışan , tek kaynak

Veriyi paralel işlemek istiyoruz. Önce biraz veri oluşturalım.

kaynak = Queue.new
veri = (1..100)
veri.each { |e| kaynak << e }

Sonra veriyi işleyecek bir kısım çalışanlar üretiyoruz.

(1..16).to_a.map do
  Thread.new do
    until kaynak.empty?
      item = kaynak.pop
      sleep 0.5
      puts "İşlenen: #{item}"
    end
  end
end.map(&:join)

Paralel çalışan prosesler veriye rastgele eriştiği halde işlenmemiş hiç bir veri kalmıyor. 



-- Tek kaynak - Çalışanlar hattı - Tek gider

Verileri birçok paralel çalışan ile işleyip bir hedefe koyacağız.

Çalışanlar hem veriyi tüketip hem veri üreteceği için iki Queue nesnemiz olmalıdır. 

ilk_giriş_kaynağı = Queue.new
ilk_çıkış_gideri  = Queue.new
100.times { |i| ilk_giriş_kaynağı << i }

Çalışanların ilk yaptığı ilk_giriş_kaynağı nesnesinden veriyi okumak, sonraki işi ise bu veriyi işleyip ilk_çıkış_gideri  nesnesine koymak. 

(1..16).to_a.map do
  Thread.new do
    loop do
      item = ilk_giriş_kaynağı.pop
      ilk_çıkış_gideri << item ** 2
      ilk_çıkış_gideri << item ** 3
    end
  end
end

İkinci bir grup çalışansa ilk_çıkış_gideri'ni veri kaynağı olarak alıp elemanlarını işler ve ikinci_çıkış_gideri nesnesine kaydeder.

ikinci_giriş_kaynağı = ilk_çıkış_gideri
ikinci_çıkış_gideri  = Queue.new

(1..32).to_a.map do
  Thread.new do
    loop do
      item = ikinci_giriş_kaynağı.pop
      ikinci_çıkış_gideri << item * 2
      ikinci_çıkış_gideri << item * 3
    end
  end
end

Şimdi ikinci_çıkış_gideri en son işlenmiş veriyi saklıyor onu da bir array'e dönüştürelim.

sleep 5 # senkronizasyon için biraz oyalan
gider = ikinci_çıkış_gideri
a = [].tap { |a| a << gider.pop until gider.empty? }



-- Bir Queue nesnesine veri basmak - push


q = Queue.new
q << "başka bir queue dahil herhangi bir nesne"
# veya
q.push :veri

  • Üst sınır yok, Queue nesnesi sonsuza kadar büyüyebilir.
  • #push asla engellenmez



-- Bir Queue nesnesinden veri çekmek - pop


q = Queue.new
q << :data
q.pop #=> :data

  • #pop kullanılabilir herhangi bir veri olana kadar engellenir
  • #pop senkronizasyon için kullanılabilir.

q = Queue.new
q << :data
q.pop #=> :data
q.pop #=> No live threads left. Deadlock? (fatal)

Eğer Thread içinde kullanılırsa pop satırında bekler. 



-- Senkronizasyon - Zamanlama yönetimi

Thread içinde pop metodunun bekletilmesini senkronizasyon amaçlı kullanabiliriz.

senkroncu = Queue.new

a = Thread.new do
  puts "İlk thread girdi"
  senkroncu.pop
  puts "Bu sonradan oluşur"
end

b = Thread.new do
  puts "Bu önce oluşur"
  STDOUT.flush
  senkroncu << :ok
end

[a, b].map(&:join)

Çıktısı

İlk thread girdi
Bu önce oluşur
Bu sonradan oluşur

İlk thread çalışmaya başlıyor ama senkroncu nesnesinde henüz bir veri olmadığı için pop metodunun çağrıldığı yerde bekletiliyor. Bu arada ikinci thread giriyor ve senkroncu nesnesine yeni bir veri ekliyor. Arkasından ilk thread pop satırında artık veri alabildiği için çalışmaya devam ediyor.



-- İki Queue nesnesini birleştirmek

  • Sonsuz engellemeye mani olmak için birleştirme yapılan thread içinde okuma da yapılmamalıdır.
  • Bir queue nesnesinde veri bitince diğerinin de okuması beklemesin diye birleşecek Queue nesnelerinden okumalar farklı Thread içinde yapılmalıdır.

Önce iki Queue nesnesi üreterek içlerine veri koyalım.

q1 = Queue.new
q2 = Queue.new
(1..100).each { |e| q1 << e }
(101..200).each { |e| q2 << e }

Yeni bir Queue nesnesi oluşturup diğerlerinin içindeki verileri ona aktaralım.

birleşik = Queue.new

[q1, q2].map do |q|
  Thread.new do
    until q.empty?
      birleşik << q.pop
    end
  end
end.map(&:join)

Eğer her iki Queue nesnesini de tamamen boşaltabileceğinizden eminseniz (tüketme hızı ekleme hızından yüksektir) daha basit bir yol da var.

birleşik = Queue.new
birleşik << q1.pop until q1.empty?
birleşik << q2.pop until q2.empty?

Eğer paralelde başka thread'lerin q1 veya q2'ye daha hızlı veri yüklemediğinden eminseniz bu yöntem daha kısa .




Küme Dağıtma

Bir küme veriyi dağıtma işleminin çoğu sihirli * operatörü ile yapılır.

ÖrnekSonuç / yorum
a, b = [0,1] a=0, b=1
a, *gerisi = [0,1,2,3] a=0, gerisi=[1,2,3]
a, * = [0,1,2,3] a=0 .first metoduna eşdeğer
*, z = [0,1,2,3] z=3 .last metoduna eşdeğer



-- Blok argümanlarını dağıtmak

Aşağıdaki yöntemlerle blok argümanlarını dağıtabiliriz.

üçlüler = [[1, 2, 3], [4, 5, 6], [7, 8, 9]]

üçlüler.each { |(ilk, ikinci, üçüncü)| puts ikinci }
# 2
# 5
# 8

üçlüler.map { |(ilk, *gerisi)| gerisi.join(' ') }
# => ["2 3", "5 6", "8 9"]





IRB Kullanmak

IRB Interactive Ruby Shell demek. Normal bir terminalin işletim sistemi komutlarını canlı olarak işlemesi gibi IRB de Ruby komutlarını canlı olarak denemenize imkan tanır. 

Ruby API ile çalışırken IRB vazgeçilemez bir araçtır. Küçük Ruby kodlarının nasıl tepki vereceklerini test etmek için idealdir. 

IRB konsolunun en önemli özelliklerinden biri metod isimlerini yazarken size bir liste göstermesi ve listedeki seçenekler arasında Tab tuşu ile seçim yapabilmenizi sağlamasıdır. 


Parametreleri

OpsiyonDetaylar
-f ~/.irbrc dosyasının okunmasını engeller
-m Bc mod (mathn kütüphanesi yüklenir, kesirler ve matrix kullanılabilir)
-d $DEBUG değerini true yapar ('ruby -d' ile aynı)
-r load-module 'ruby -r' ile aynı
-I path $LOAD_PATH klasörünü belirler
-U 'ruby -U' ile aynı
-E enc 'ruby -E' ile aynı
-w 'ruby -w' ile aynı
-W[level=2] 'ruby -W' ile aynı
–-inspect Çıktılar için "inspect" kullan (default - Bc mod harici)
--noinspect Çıktılar için "inspect" kullanma
--readline Readline genişleme modülünü kullan
--noreadline Readline genişleme modülünü kullanma
--prompt prompt-mode Prompt modu değiştir. Ön-tanımlı modlar 'default', 'simple', 'xmp' ve 'inf-ruby'
–-inf-ruby-mode emac'lerde prompt'u uygun olan "inf-ruby-mode" a geçirir --readline'ı baskılar
–-simple-prompt Basit kısa prompt modu
–-noprompt Prompt yok modu
–-tracer Her komut çalıştırılmasında trace göster
–-back-trace-limit n Back-trace gösterirken n tane geri git Default 16
–-irb_debug n Dahili debug seviyesi n yap (popüler bir kullanım değil)
-v, –-version IRB versiyonunu yaz



-- Temel irb kullanımı

Komut satırında irb yazarak programı çalıştırırız. Basit Ruby komutları girebiliriz.

$ irb
irb(main):001:0> 2 + 2
=> 4
irb(main):002:0>

Metodlar gibi karmaşık blokları da girebiliriz.

irb(main):002:1* def metod
irb(main):003:1*   puts "Merhaba Dümya"
irb(main):004:0> end
=> :metod
irb(main):005:0> metod
Merhaba Dümya
=> nil



-- Ruby betiğinde IRB oturumu çalıştırmak

Ruby 2.4.0'dan itibaren Ruby programımız içinde IRB oturumu başlatabiliriz.

require 'irb'
binding.irb

Bu programın bulunulan noktasında bir irb oturumu açar. Burada betiğimizde o andaki değişkenlere erişip kullanabiliriz. Debug işlemleri için çok faydalıdır.

$ ruby run_irb.rb

From: run_irb.rb @ line 2 :

    1: require 'irb'
 => 2: binding.irb
irb(main):001:0> exit

Çıkmak için exit komutu girin ya da Ctrl+D tuşlarına basın. Betik kaldığı yerden devam eder.

Bu bölüme de bu kadar yeter, oldukça uzadı. Sonraki bölümde Enumerator sınıfı üzerinde duracağız inşallah. Şimdilik kalın sağlıcakla..









Hiç yorum yok:

Yorum Gönder