Privatlivets fred: C# vs Python

Privatlivets fred: C# vs Python

Siden sidst

Jeg har i den seneste tid arbejdet på et projekt som dykker lidt mere ned i Terraform, Azure, durable functions, Rust og Python. Det stak af i en helt anden retning, da jeg sad og kiggede lidt på forskellen i C# og Pythons sprogets design, og hvordan de, på hver deres måde, hjalp udvikleren med at lave gode snitflader, som andre kan bruge. C# er by design, Python er by convetion. Enkelt og smukt. I samme ombæring begyndte jeg også at kigge på Spacemacs igen, og dertil Omnisharp. Jeg tog Lisp op igen, da jeg gerne vil kunne kalde mit pack script fra Spacemacs. Koden har jeg udgivet her. Jeg er ved at skrive en artikel omkring forskellen på at lave mit pack script, som jeg omtaler her, i

  1. bash

  2. python

  3. powershell

Så ja, min tankerække er som følger: hemmelige projekt –> spacemacs og lisp (ved at lave indlæg) HOV –> python og C#, og et enkelt indlæg (du læser den).

Spacemacs, elisp, bash (og WSL) og Rust er alt sammen noget jeg har dykket mere ned i (eller genoptaget), alene pga et projekt. Et projekt som jeg nok aldrig bliver færdig med, men det gør ikke noget, så længe jeg blot lærer noget nyt. Viden er magt!!

Nok snak - nu kode

Jeg er ved at lave et større skriv omkring tooling, spacemacs, python og elisp, da jeg faldt over noget lettere kuriøs, som jeg egentlig synes er en sjov snak: er private properties egentlig så private, som de udgiver sig for? Du kan starte med at læse med her: https://stackoverflow.com/a/1641236/21199 hvor brugeren Kirk Strauser svarer følgende, til spørgsmålet, om der eksisterer noget som private variabler i Python

It's cultural. In Python, you don't write to other classes' instance or class variables. In Java, nothing prevents you from doing the same if you really want to - after all, you can always edit the source of the class itself to achieve the same effect. Python drops that pretence of security and encourages programmers to be responsible. In practice, this works very nicely.

Det er specielt det sidste, som virkelig fik mine tanker til at flyve

Python drops that pretence of security and encourages programmers to be responsible

Jeg er lige begyndt at kigge mere på Python (en gammel drøm af mine), og jeg må sige jeg elsker det. Python er anderledes, mere kompakt, og mere ligefrem, end andre sprog. Det med at være mere ligefrem (uden dikkedarer og indpakning) kommer derfor ikke kun til udtryk i selve sprogets semantiske opbygning, men også i hele mindsettet og community'et omkring sproget. Specielt når det kommer til private variabler, som er ikke eksisterende i Python:

Use one leading underscore only for non-public methods and instance variables.

Man kan altså som bruger af et API godt kalde, og sætte private variabler /men det gør man, by convetion, IKKE, /og DET SYNES jeg er facinerende:

  1. det fjerner en masse konstruktioner fra sproget (skal denne variabel være private, protected etc) - og overlader det komplette ansvar til kalderen af API'et

Om overstående er godt eller skidt, vil jeg ikke kloge mig i. Jeg synes det er interessant, for som Kirk skriver, så "lader man ikke som om" at noget er privat i Python, hvilket jeg synes er en interessant tankegang, og jeg må indrømme at jeg er en af de udviklere, som for længst har glemt, at man jo fint, og nogenlunde let kan gøre hvad man vil i .NET, hvis man vil ændre i noget, som egentlig er stemplet som privat. Her ligner tankegangene dog meget hinanden ved .NET og Python udviklere

Man kan godt, men man gør det ikke

I Python rører man ikke ved variabler som er prefixet med en eller to underscores, og i .NET piller man ikke ved det som er angivet som private, selvom det er muligt. Det er dog lidt lettere i Python, end i .NET. Lad mig elaborere.

/images/letscode.jpeg

    using System; using System.Reflection;
    namespace MyNamespace
    {
        public class MyClass
        {
            private int _i = 42;
            public int GetI()
            {
                return _i;
            }
        }
        class Program
        {
            static void Main(string[] args)
            {
                var myClass = new MyClass();

                var fields = typeof(MyClass).GetFields(BindingFlags.NonPublic | BindingFlags.Instance);

                foreach (var field in fields)
                {
                    var val = field.GetValue(myClass);

                    if (field.IsPrivate)
                    {
                        Console.WriteLine($"Private før: {val}");
                    }

                    field.SetValue(myClass, 43);
                }

                Console.WriteLine($"Private efter {myClass.GetI()}");
            }
        }
    }

Det vi ser her er noget som man egentlig ikke må, nemlig at pille ved private variabler; det er lidt besværligt i C#, da man som sagt skal bruge reflection, men det kan lade sig gøre! Vi har ændret variablen, selvom det egentlig ikke burde kunne lade sig gøre. Hvis man gerne vil undgå at dette sker, kan man lave den private integer om til en const, da selv ikke reflection vil kunne ændre det (man får en fejl når man prøver at køre programmet).

Om MyClass ligger i samme DLL, eller ej, er sagen ligegyldigt.

Laver vi =_i=om til en static

    using System;
    using System.Reflection;

    namespace MyNamespace
    {
        public class MyClass
        {
            private const int _i = 42;
            public int GetI()
            {
                return _i;
            }
        }
        class Program
        {
            static void Main(string[] args)
            {
                var myClass = new MyClass();

                var fields = typeof(MyClass).GetFields(BindingFlags.NonPublic | BindingFlags.Static);

                foreach (var field in fields)
                {
                    var val = field.GetValue(myClass);

                    if (field.IsPrivate)
                    {
                        Console.WriteLine($"Private før: {val}");
                    }

                    field.SetValue(myClass, 43);
                }

                Console.WriteLine($"Private efter {myClass.GetI()}");
            }
        }
    }

Vil vi få en fejl når vi kører programmet, hvor vi prøver at sætte værdien

/images/setval.png

Husk at kalde GetFields=med =BindingFlags.Static=i stedet for =BindingFlags.Instance, ellers vil =const=feltet ikke blive fundet.

Grunden til overstående er at const=betyder at man på /compile time/ bytter referencen til værdien, =_i ud med den faktiske værdi. Det viser sig egentlig fint hvis man decompiler dll'en

/images/decompile.png

Uden const vil det decompilede se således ud

/images/udenconst.png

Til reference så bruger jeg dnSpy til dekompliering. Den kan også debugge!! Smart.

Men hvad med readonly? readonly=er en "mellemting" mellem =const=og en almindelig =private. Vil en reflektiv =SetValue=på en =readonly=kaste en fejl, eller vil den gå igennem? Umiddelbart skulle man tro at en runtime fejl vil blive kastet, da det som sagt kun er muligt at sætte en =readonly=gennem konstruktøren på en given klasse. Lad os pille det ad med følgende kode

    using System;
    using System.Reflection;

    namespace MyNamespace
    {
        public class MyClass
        {
            private readonly int _i = 42;
            public int GetI()
            {
                return _i;
            }
        }
        class Program
        {
            static void Main(string[] args)
            {
                var myClass = new MyClass();

                var fields = typeof(MyClass).GetFields(BindingFlags.NonPublic | BindingFlags.Instance);

                foreach (var field in fields)
                {
                    var val = field.GetValue(myClass);

                    if (field.IsPrivate)
                    {
                        Console.WriteLine($"Private før: {val}");
                    }

                    field.SetValue(myClass, 43);
                }

                Console.WriteLine($"Private efter {myClass.GetI()}");
            }
        }
    }

Outputtet er drumrole

/images/drumrole.png

Det kunne man!! Men hvorfor er det muligt? Kigger man på den decompilede kode ser man følgende

/images/myclass.png

Altså ikke rigtig noget vildt. Der er ikke tilføjet noget ekstra til =GetI=metoden. Decompiler man det til IL

    // Token: 0x02000002 RID: 2
    .class public auto ansi beforefieldinit MyNamespace.MyClass
        extends [System.Runtime]System.Object
    {
        // Fields
        // Token: 0x04000001 RID: 1
        .field private initonly int32 _i

        // Methods
        // Token: 0x06000001 RID: 1 RVA: 0x00002050 File Offset: 0x00000250
        .method public hidebysig 
            instance int32 GetI () cil managed 
        {
            // Header Size: 12 bytes
            // Code Size: 12 (0xC) bytes
            // LocalVarSig Token: 0x11000001 RID: 1
            .maxstack 1
            .locals init (
                [0] int32
            )

        } // end of method MyClass::GetI

        // Token: 0x06000002 RID: 2 RVA: 0x00002068 File Offset: 0x00000268
        .method public hidebysig specialname rtspecialname 
            instance void .ctor () cil managed 
        {
            // Header Size: 1 byte
            // Code Size: 16 (0x10) bytes
            .maxstack 8
        } // end of method MyClass::.ctor

    } // end of class MyNamespace.MyClass

Hvor vi kan se … hellere ikke noget specielt, sådan… rigtigt. ANDET END readonly bliver oversat til initonly, som betyder

Specifies that the subsequent array address operation performs no type check at run time, and that it returns a managed pointer whose mutability is restricted.

Fra dokumentationen.

Altså at man på runtime får en pegepind til noget hukommelse som er lidt mere skrap med mutability af det den peger på (rent faktisk er den ikke helt immutable, men den er bare meget restriktiv med hvornår værdien af det den peger på må mutere. EN VÆSENTLIG forskel). Man får ikke en gang lov at bygge kode, hvis =_i=bliver sat udenfor konstruktøren

/images/void.png

Den efterfølgende bygge fejl, fortæller det også meget flot

/images/error.png

Det var vist en lille afstikker. Konklusionen er som følger:

  1. Ren private=kan fint ændres selvom man ikke har en officiel indgang til variablen (fx via en =SetI) metode

  2. const kan ikke ændres, overhovedet, også selvom man reflecter sig ud af det

  3. readonly kan omgåes

Vil man være helt sikker på at ens simple værdier ikke ændre sig, er det en god idé at overveje at bruge const.

Men hvad med Python, sssscccchhhhiiiiii?

/images/schi.png

Python er et specielt sprog på mange måder. Den har ingen =const=og sproget ejer hellere ikke skyggen af konceptet omkring private variabler, dog er der meget der er /by convention. /NOgen kalder ligefrem Python for et no bullshit sprog:

  1. vil du lave noget privat? Prefix din variabel med en eller to underscores. En underscore er faktisk synlig udenfor klassen, men … Lad vær med at ændre den. To underscores bruges faktisk til skærme variabler når der nedarves, dog kan man også bruge den til at fake en privat variabel

  2. vil du lave noget konstant, skriv den med UPPER CLASS, og lad være med at ændre den

  3. readonly? Lad være med at ændre variablen andre steder end i klassen konstruktør

Overstående er indforstået i Python: det er en del af designet, og PEP dokumentet, et dokument som fortæller hvordan man koder godt i Python. Overstående er altså by convention, og kræver lidt tilvendig, dog skjuler sproget hellere ikke noget: man kan ændre i private variabler, lige som man så det i C# eksemplerne før (med reflection), men man gør det ikke (lige som man i C# hellere ikke bare ændre private variabler). Man ved man er ude i noget undefined lige så snart man enten ændre i underscore variabler i Python eller i =private=markerede variabler i C#.

Men hvordan ser det ud i Python? Jeg har lavet et eksempel

    class MyClass1:
        def __init__(self):
            self._i = 42

    myClass1 = MyClass1()

    print(myClass1._i)

    class MyClass2:
        def __init__(self):
            self.__i=42

    myClass2 = MyClass2()

    print(myClass2.__i); # vil fejle
    print(myClass2._MyClass2__i)

    myClass2.__i = 43 # virker ikke
    myClass2._MyClass2__i = 43

    print(myClass2._MyClass2__i)

Eksemplet ovenover viser både enkelt underscore og dobbelt underscore:

  1. enkelt underscores, kan med lethed kaldes og ændres på en instans af klassen, dog gør man det ikke i Python

  2. dobbelt underscores, kan med lidt mere møje og besvær, også kaldes. Dobbelt underscore kan ligne at det er Pythons svar på private variabler, dog bruges det til at skjule variabler når man nedarver

Reflektion i Python? I C# er det en kunst i sig selv: man skal refererer de korrekte biblioteker osv, men i Python er det indbygget. Ikke så mange dikkedarer.

/images/pythonconst.png

Konklusion:

  1. By design: C# har mange flere håndtag i sproget: readonly, private, const når det kommer til at definere ens snitflade på API'et, det kræver meget at omgå disse guards, men det kan lade sig gøre

  2. By convention: Python har ikke så mange håndtag i sproget, dog eksisterer de alligevel, da meget er by convention. Det kræver ikke noget særligt at omgå.

Hvis I kunne tænke jer at få at vide hvornår det hemmelige projekt er færdigt, hvad jeg egenligt går og brygger på med spacemacs, python, bash og powershell, så bookmark siden, og følg mig på Twitter, eller LinkedIn.

comments powered by Disqus